Compare commits

..

136 Commits

Author SHA1 Message Date
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
Spythere 255e07372e Merge wersji dev do produkcji (1.11)
Wersja produkcyjna 1.11.0
2022-12-26 22:58:17 +01:00
Spythere 279bbfa4db fix: responsywność 2022-12-26 20:01:10 +01:00
Spythere a5c829faf5 Fix: wskaźnik ładowania dzienników 2022-12-26 19:37:52 +01:00
Spythere 5fdfaeac5e hotfix 2022-12-26 18:52:31 +01:00
Spythere 9beb30e3d5 Tłumaczenie monitu 2022-12-26 18:51:50 +01:00
Spythere 48582e2eea lock files sync 2022-12-26 18:45:39 +01:00
Spythere 2e721fb8bf PWA: tryb offline 2022-12-26 18:43:15 +01:00
Spythere f93c1fbfec PWA: tryb offline 2022-12-26 18:43:14 +01:00
Spythere c06e7b6468 Poprawka wyświetlania sumy dystansu 2022-12-26 13:54:30 +01:00
Spythere 22a6d266cb Aktualizacja danych z API 2022-12-26 13:50:48 +01:00
Spythere 5f8a16401b Update API 2022-12-25 23:35:10 +01:00
Spythere c9be01aa29 lock files 2022-12-23 20:26:54 +01:00
Spythere 4ec058b33c Konfiguracja PWA 2022-12-23 20:25:02 +01:00
Spythere 27a5d2a406 fix: tłumaczenie komunikatu 2022-12-22 18:50:09 +01:00
Spythere 58169e26f6 Feedback i stylistyka statystyk RJ 2022-12-22 01:45:43 +01:00
Spythere fee1f4bbd5 Usprawienie podpowiedzi filtrów 2022-12-22 01:36:38 +01:00
Spythere 240817acc3 Przekierowanie do strony głównej 2022-12-21 20:32:41 +01:00
Spythere db3be87dd8 Przystosowanie pod update API 2022-12-21 20:24:48 +01:00
Spythere 1665134d6f Fix odznaczenia filtrów pociągów 2022-12-21 19:34:42 +01:00
Spythere df289ab734 Wskaźnik aktywnych filtrów pociągów online 2022-12-21 19:07:23 +01:00
Spythere f74440ba6f Pogrubienie linku dziennika w headerze 2022-12-21 18:39:40 +01:00
Spythere a25dbe9fd5 Usunięcie firebase config z html 2022-12-21 18:27:27 +01:00
Spythere 4fff136d6b Poprawki reaktywności 2022-12-21 18:24:04 +01:00
Spythere d06f2d5d2e Optymalizacja pobierania danych 2022-12-21 18:10:54 +01:00
Spythere 9f68d628d0 Wskaźnik aktywnych filtrów dziennika DR 2022-12-21 15:51:13 +01:00
Spythere d64b906dac Wskaźnik aktywnych filtrów dziennika RJ 2022-12-21 15:45:03 +01:00
Spythere f3e193e68a Cleanup 2022-12-21 15:02:41 +01:00
Spythere 5640ce9f2b Fix routingu w dzienniku RJ 2022-12-21 15:02:25 +01:00
Spythere 50100eb2f9 Nawigacja 2022-12-20 21:51:40 +01:00
Spythere e478c510b2 Fix działania reaktywności linków 2022-12-20 21:31:59 +01:00
Spythere 7ea558642f Stylistyka statystyk 2022-12-20 21:11:47 +01:00
Spythere 493145f7f2 Fix pola daty 2022-12-20 16:59:59 +01:00
Spythere 4f72535365 Setup GitHub Actions & npm 2022-12-20 16:56:12 +01:00
Spythere 8e3bf80715 Fix logiki przycisków 2022-12-20 16:44:15 +01:00
Spythere 6da586d08a Stylistyka komponentów statystyk 2022-12-20 16:41:42 +01:00
Spythere be53b9c7fb Notka o lokacji pociągu nie pojawia się przy jej braku 2022-12-20 01:41:13 +01:00
Spythere 94ed1160a1 Poprawki 2022-12-20 01:38:08 +01:00
Spythere 859d8d2631 Train modal fix 2022-12-20 00:53:03 +01:00
Spythere 5f3abd73c5 Informacja o statystykach 2022-12-19 00:44:46 +01:00
Spythere d71c8bb6f9 Bump wersji 2022-12-18 23:43:23 +01:00
Spythere a3db13d79c Github Actions 2022-12-18 20:01:15 +01:00
Spythere 8cb3da66f2 Statystyki maszynistów 2022-12-18 19:54:13 +01:00
Spythere 6e07897ac0 Fix: bug routingu dzienników 2022-12-18 03:01:13 +01:00
Spythere 726b859f5c Poprawki tabów statystyk 2022-12-18 01:28:11 +01:00
Spythere 651c60707a Rework statystyk RJ 2022-12-17 20:45:59 +01:00
Spythere d4fee84603 Rework statystyk RJ 2022-12-17 20:45:53 +01:00
Spythere 86539cdf23 1.10.10: status scenerii w dzienniku RJ 2022-12-03 09:41:46 +01:00
Spythere 69772460b8 Poprawka w działaniu sortowania wyszukiwarki scenerii 2022-11-01 18:27:27 +01:00
Spythere 6988a83355 Zmiana API 2022-10-30 23:03:47 +01:00
Spythere b6425564c8 Bump wersji 2022-10-28 13:15:28 +02:00
Spythere caf0a9b4c5 Dodano sugestie wyszukiwania istniejących użytkowników w dziennikach 2022-10-28 13:15:07 +02:00
Spythere bd5f433d6e Update paczek 2022-10-26 15:27:28 +02:00
Spythere 8d9cc721d6 Poprawki stylów 2022-10-16 23:09:46 +02:00
Spythere cceeffe49d Świecące nicki i poziomy sponsorów 2022-10-14 23:15:50 +02:00
Spythere fcb8357489 Dodano animację aktywnych RJ; poprawki 2022-10-11 22:16:46 +02:00
Spythere ceffd8e675 Bump wersji 2022-10-11 18:36:49 +02:00
Spythere 5aa53521f7 Ulepszono informacje o szlakach i statusach rozkładów scenerii 2022-10-11 18:33:53 +02:00
Spythere d8b559694b Upgrade paczek 2022-10-10 15:57:04 +02:00
Spythere c82ac04a91 Poprawka tłumaczenia 2022-10-08 20:42:06 +02:00
Spythere 284bdcbf2a Aktualizacja vue-tsc 2022-10-08 20:41:54 +02:00
Spythere 7f4df98349 Bump wersji 2022-10-02 00:56:27 +02:00
Spythere aecbcf62df Aktualizacja odnośnika do changelogu 2022-10-02 00:54:05 +02:00
Spythere 2a817365a6 Tłumaczenie statystyk maszynistów 2022-10-02 00:40:10 +02:00
Spythere ecf3a00cab Statystyki maszynistów 2022-10-01 15:55:10 +02:00
Spythere beb2f3c0d4 Tłumaczenie 2022-10-01 13:16:40 +02:00
Spythere a65b09981b Poprawki responsywności 2022-09-30 14:56:49 +02:00
Spythere 4ec544e8a9 Dodano informację o timeoucie SWDRa 2022-09-30 00:00:36 +02:00
Spythere 7e108c5183 Bump wersji 2022-09-29 19:41:55 +02:00
Spythere 72361b157e Tłumaczenie PL 2022-09-29 19:41:26 +02:00
Spythere 1cc4d76e4d Poprawki filtrów 2022-09-29 19:40:15 +02:00
Spythere 846d4d0547 Filtry scenerii 2022-09-29 19:27:54 +02:00
Spythere 751cadd218 Poprawki stylistyczne 2022-09-28 16:36:26 +02:00
Spythere 3b44adff44 Poprawki responsywności 2022-09-27 19:36:34 +02:00
Spythere 29a02dd98f Poprawki responsywności; dodano wyszukiwanie scenerii 2022-09-27 18:58:46 +02:00
Spythere c5e68c4d03 Bump wersji 2022-09-27 14:52:47 +02:00
Spythere 95f7c2a4d9 Poprawki 2022-09-27 14:52:24 +02:00
Spythere 84412822ff Zmiana hostingu API 2022-09-26 00:31:55 +02:00
Spythere 42bb056e66 Poprawki dostępności searchboxów 2022-09-25 23:30:37 +02:00
Spythere 053e9d2b6a Update package-lock 2022-09-25 19:44:56 +02:00
Spythere c729d75541 Poprawki dostępności (c.d.) 2022-09-23 23:01:09 +02:00
Spythere a9b72d0b7a Poprawki dostępności 2022-09-23 22:58:23 +02:00
Spythere 95a027f284 Filtrowanie po nicku autora RJ w dzienniku 2022-09-23 22:39:38 +02:00
Spythere dbba83b28b Dodano id pociągu jako parametr 2022-09-22 19:09:28 +02:00
Spythere 65abe550f5 Poprawki list dzienników 2022-09-22 17:16:10 +02:00
Spythere 531108c25a Wygląd filtrów 2022-09-22 15:08:22 +02:00
Spythere bcf750d451 Wywoływanie filtrów za pomocą klawisza F 2022-09-22 14:57:03 +02:00
Spythere 0a8bfe4c52 Poprawki; usunięto github workflows 2022-09-22 14:15:53 +02:00
Spythere 0f19bc767a Poprawki wyglądu; cleanup kodu 2022-09-22 13:59:19 +02:00
Spythere 8eb0266874 Merge branch 'development' 2022-09-15 12:38:57 +02:00
Spythere ae5b5ff965 Responsywność i ułożenie opcji filtrów 2022-09-15 12:38:36 +02:00
Spythere 3a0c4bc151 Aktualizacja 1.10.4
Aktualizacja Stacjownika do wersji 1.10.4
2022-09-11 14:06:59 +02:00
Spythere 4f5fcb3189 Bump wersji 2022-09-11 13:59:08 +02:00
Spythere 3a2978bbe3 Usprawniono działanie listy dziennika dyżurnych 2022-09-11 02:00:58 +02:00
Spythere a81cc4559b Poprawki w filtrach i ustawieniach dzienników 2022-09-10 22:49:56 +02:00
Spythere 065143c359 JournalTimetables: dodano resetowanie filtrów 2022-09-10 18:22:00 +02:00
Spythere 1661881127 Poprawki w stylach 2022-09-10 18:12:07 +02:00
Spythere 93aa889414 Cleanup kodu 2022-09-10 17:57:43 +02:00
Spythere 2a131ab1fb Poprawiono tłumaczenie 2022-09-10 15:14:36 +02:00
Spythere 387f42985a Poprawiono filtrowanie datą 2022-09-10 15:10:39 +02:00
Spythere 6c83ce90bf Dodano filtrowanie po dacie w opcjach 2022-09-09 00:23:18 +02:00
Spythere 3d519e874f Opcje filtrów: tłumaczenia 2022-09-08 23:24:58 +02:00
Spythere 99cdb3442a Opcje filtrów: animacja i poprawki 2022-09-08 23:15:54 +02:00
Spythere a6c0fe86c8 Poprawki filtrów 2022-09-08 12:47:30 +02:00
Spythere 828421efe0 Filtry aktywnych pociągów 2022-09-08 12:21:27 +02:00
Spythere 21bacb1c95 Filtry dzienników; poprawki stylistyczne 2022-09-07 20:37:58 +02:00
Spythere 0d9a3f4b4f Rozszerzone opcje filtrów dzienników 2022-09-06 12:44:18 +02:00
Spythere 76b8534d63 Poprawki responsywności selectboxów 2022-09-06 00:26:49 +02:00
Spythere 0821fd708e Stylistyka informacji o składzie 2022-09-05 23:44:36 +02:00
Spythere b0a9939446 Cleanup kodu; poprawki funkcjonalności 2022-09-05 23:32:27 +02:00
Spythere 2a64b8f10d Dodatkowe informacje i poprawki wyglądu dziennika RJ 2022-09-04 17:12:44 +02:00
Spythere dc1c457ea4 Fix: wykrywanie scrolla dzienników 2022-09-04 16:46:44 +02:00
Spythere 1f95bc5230 Tłumaczenie i poprawki do wersji 1.10.3 2022-09-04 01:27:12 +02:00
Spythere 5a06920e5b Dodano tłumaczenie; poprawki 2022-09-04 01:25:27 +02:00
Spythere ee0d9e7ed4 Wersja 1.10.3
Wersja 1.10.3
2022-09-04 01:14:24 +02:00
Spythere 30ad3ad4f2 Bump wersji 2022-09-04 01:12:04 +02:00
Spythere c2bd5a8a1b Poprawiono mobilny scroll bar 2022-09-04 01:10:56 +02:00
Spythere 7101d0972d Przywrócono ikonę pociągu mobilnego widoku aktywnych RJ 2022-09-04 01:06:30 +02:00
Spythere 82bbfcdf70 Dokończenie widoku dziennika RJ 2022-09-04 01:04:04 +02:00
Spythere b90ac6c09e Zmiany w wyglądzie i funkcjonalnościach dziennika RJ 2022-09-03 00:11:42 +02:00
Spythere 76d0ff88f1 Zmiany w designie dziennika rozkładów jazdy 2022-09-01 01:56:16 +02:00
Spythere 951afcedeb Bump wersji 2022-08-29 19:12:56 +02:00
Spythere 96de3f0dcc Scroll lock przy otwartym modalu 2022-08-29 19:12:19 +02:00
Spythere 03950eef66 Bump wersji 2022-08-27 20:19:03 +02:00
Spythere 6dd8cb2dad Cleanup c.d. 2022-08-27 14:05:35 +02:00
Spythere aae51d4139 Hotfix 2022-08-27 14:04:02 +02:00
Spythere 9994a541b1 Cleanup 2022-08-27 14:02:42 +02:00
Spythere bc3a603ba2 Poprawiono sortowanie stacji 2022-08-27 13:44:04 +02:00
Spythere 7857377cab Merge branch 'development' 2022-08-09 00:01:40 +02:00
Spythere 0034f43be4 Fix: zła ikonka przy nieznanej scenerii 2022-08-08 23:59:55 +02:00
91 changed files with 17758 additions and 6828 deletions
@@ -1,3 +1,6 @@
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools
name: Deploy to Firebase Hosting on PR name: Deploy to Firebase Hosting on PR
'on': pull_request 'on': pull_request
jobs: jobs:
@@ -11,4 +14,4 @@ jobs:
with: with:
repoToken: '${{ secrets.GITHUB_TOKEN }}' repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STACJOWNIK_TD2 }}' firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STACJOWNIK_TD2 }}'
projectId: stacjownik-td2 projectId: stacjownik-td2
+1 -1
View File
@@ -1,7 +1,7 @@
.DS_Store .DS_Store
node_modules node_modules
/dist
/dev-dist /dev-dist
/dist
# local env files # local env files
.env.local .env.local
+5 -2
View File
@@ -1,7 +1,11 @@
{ {
"hosting": { "hosting": {
"public": "dist", "public": "dist",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"], "ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [ "rewrites": [
{ {
"source": "**", "source": "**",
@@ -10,4 +14,3 @@
] ]
} }
} }
-14
View File
@@ -25,20 +25,6 @@
<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@400;500;700&display=swap" rel="stylesheet" />
<script src="https://www.gstatic.com/firebasejs/8.1.1/firebase-app.js"></script>
<script>
const firebaseConfig = {
apiKey: 'AIzaSyBI36X2-p7vU1flxoJdCEc0noByyTe1mpw',
authDomain: 'stacjownik-td2.firebaseapp.com',
databaseURL: 'https://stacjownik-td2.firebaseio.com',
projectId: 'stacjownik-td2',
storageBucket: 'stacjownik-td2.appspot.com',
};
firebase.initializeApp(firebaseConfig);
</script>
</head> </head>
<body> <body>
+8609 -1019
View File
File diff suppressed because it is too large Load Diff
+37 -35
View File
@@ -1,35 +1,37 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.10.0", "version": "1.11.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"preview": "vite preview" "deploy": "yarn build && firebase deploy --only hosting",
}, "preview": "yarn build && vite preview"
"dependencies": { },
"core-js": "^3.12.1", "dependencies": {
"dotenv": "^8.6.0", "core-js": "^3.12.1",
"firebase": "^9.8.1", "dotenv": "^16.0.3",
"howler": "^2.2.1", "firebase": "^9.8.1",
"pinia": "^2.0.14", "howler": "^2.2.1",
"sass": "^1.53.0", "pinia": "^2.0.14",
"socket.io-client": "^4.4.1", "sass": "^1.53.0",
"vue": "^3.2.37", "socket.io-client": "^4.4.1",
"vue-i18n": "^9.1.6", "vue": "^3.2.37",
"vue-router": "^4.0.0-0" "vue-i18n": "^9.1.6",
}, "vue-router": "^4.0.0-0"
"devDependencies": { },
"@types/node": "^17.0.35", "devDependencies": {
"@vitejs/plugin-vue": "^3.0.0", "@types/node": "^18.11.17",
"axios": "^0.21.1", "@vitejs/plugin-vue": "^4.0.0",
"typescript": "^4.6.4", "axios": "^1.2.1",
"vite": "^3.0.0", "typescript": "^4.9.4",
"vue-tsc": "^0.38.4" "vite": "^4.0.3",
}, "vite-plugin-pwa": "^0.14.0",
"browserslist": [ "vue-tsc": "^1.0.18"
"> 1%", },
"last 2 versions", "browserslist": [
"not dead" "> 1%",
] "last 2 versions",
} "not dead"
]
}
+3 -158
View File
@@ -33,7 +33,8 @@
.route { .route {
margin: 0 0.2em; margin: 0 0.2em;
&-active { &-active,
&[data-active='true'] {
color: $accentCol; color: $accentCol;
font-weight: bold; font-weight: bold;
} }
@@ -45,7 +46,7 @@
font-size: 1rem; font-size: 1rem;
@include smallScreen() { @include smallScreen() {
font-size: calc(0.4rem + 1.4vw); font-size: calc(0.5rem + 1.3vw);
} }
} }
@@ -81,162 +82,6 @@
border-radius: 0 0 1em 1em; border-radius: 0 0 1em 1em;
} }
// Error icon
.wip-alert {
padding: 0 0.5em;
text-align: center;
}
.icon-error {
width: 13em;
margin: 0.5em 0;
}
// HEADER
.app_header {
display: flex;
justify-content: center;
position: relative;
background-color: $primaryCol;
}
.header {
&_body {
max-width: 21em;
}
&_container {
display: flex;
justify-content: center;
position: relative;
width: 1350px;
padding: 0.5em 0.3em 0 0.3em;
border-radius: 0 0 1em 1em;
}
&_brand {
img {
width: 100%;
}
}
&_info {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
max-width: 100%;
font-size: 1.2em;
}
&_links {
display: flex;
justify-content: center;
border-radius: 0.7em;
font-size: 1.25em;
padding: 0.5em;
}
&_icons {
position: absolute;
right: 0;
top: 0;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
padding: 0.5em 0.5em;
@include smallScreen() {
right: auto;
left: 0.75em;
padding: 0;
align-items: center;
}
}
}
// ICONS
.icons {
position: relative;
&-top {
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;
}
}
}
}
// COUNTER
.info_counter {
display: flex;
justify-content: center;
align-items: center;
span {
margin: 0 0.15em;
}
img {
width: 1.35em;
}
}
// REGION SELECTION
.info_region {
color: white;
font-weight: bold;
display: flex;
justify-content: flex-end;
.select-box_content button {
background-color: transparent;
font-weight: bold;
padding: 0.1em 0.5em;
color: paleturquoise;
}
.options {
font-size: 0.9em;
}
.arrow {
padding: 0;
}
}
// FOOTER // FOOTER
footer.app_footer { footer.app_footer {
max-width: 100%; max-width: 100%;
+56 -98
View File
@@ -1,72 +1,19 @@
<template> <template>
<div class="app_container"> <div class="app_container">
<UpdateModal />
<transition name="modal-anim"> <transition name="modal-anim">
<keep-alive> <keep-alive>
<TrainModal v-if="store.chosenModalTrainId" /> <TrainModal v-if="store.chosenModalTrainId" />
</keep-alive> </keep-alive>
</transition> </transition>
<header class="app_header"> <UpdatePrompt />
<div class="header_container">
<div class="header_icons">
<span class="icons-top">
<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 />
</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"> <AppHeader :current-lang="currentLang" @change-lang="changeLang" />
<img :src="getIcon('discord', 'png')" alt="icon discord" />
</a>
</span>
</div>
<div class="header_body">
<status-indicator />
<span class="header_brand">
<img :src="getImage('stacjownik-header-logo.svg')" alt="Stacjownik" />
</span>
<span class="header_info">
<Clock />
<div class="info_counter">
<img :src="getIcon('dispatcher')" alt="icon dispatcher" />
<span class="text--primary">{{ onlineDispatchers.length }}</span>
<span class="text--grayed"> / </span>
<span class="text--primary">{{ trainList.length }}</span>
<img :src="getIcon('train')" alt="icon train" />
</div>
<span class="info_region">
<SelectBox :itemList="computedRegions" :defaultItemIndex="0" @selected="changeRegion" />
</span>
</span>
<span class="header_links">
<router-link class="route" active-class="route-active" to="/" exact>
{{ $t('app.sceneries') }}
</router-link>
/
<router-link class="route" active-class="route-active" to="/trains">{{ $t('app.trains') }}</router-link>
/
<router-link class="route" active-class="route-active" to="/journal/timetables">
{{ $t('app.journal') }}
</router-link>
</span>
</div>
</div>
</header>
<main class="app_main"> <main class="app_main">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<keep-alive> <keep-alive exclude="JournalView">
<component :is="Component" :key="$route.path" /> <component :is="Component" :key="$route.name" />
</keep-alive> </keep-alive>
</router-view> </router-view>
</main> </main>
@@ -82,28 +29,33 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, provide, ref } from 'vue'; import { computed, defineComponent, KeepAlive, provide, ref, watch } from 'vue';
import Clock from './components/App/Clock.vue'; import Clock from './components/App/Clock.vue';
import packageInfo from '.././package.json'; import packageInfo from '.././package.json';
import options from './data/options.json';
import StatusIndicator from './components/App/StatusIndicator.vue'; import StatusIndicator from './components/App/StatusIndicator.vue';
import SelectBox from './components/Global/SelectBox.vue'; import SelectBox from './components/Global/SelectBox.vue';
import { useStore } from './store/store'; import { useStore } from './store/store';
import UpdateModal from './components/App/UpdateModal.vue';
import TrainModal from './components/Global/TrainModal.vue'; import TrainModal from './components/Global/TrainModal.vue';
import StorageManager from './scripts/managers/storageManager'; import StorageManager from './scripts/managers/storageManager';
import imageMixin from './mixins/imageMixin'; import imageMixin from './mixins/imageMixin';
import AppHeader from './components/App/AppHeader.vue';
import axios from 'axios';
import UpdatePrompt from './components/App/UpdatePrompt.vue';
import { VERSION } from 'vue-i18n';
import { RouterView } from 'vue-router';
import useCustomSW from './mixins/useCustomSW';
export default defineComponent({ export default defineComponent({
components: { components: {
Clock, Clock,
StatusIndicator, StatusIndicator,
SelectBox, SelectBox,
UpdateModal,
TrainModal, TrainModal,
AppHeader,
UpdatePrompt,
}, },
mixins: [imageMixin], mixins: [imageMixin],
@@ -112,6 +64,8 @@ export default defineComponent({
const store = useStore(); const store = useStore();
store.connectToAPI(); store.connectToAPI();
const { offlineReady } = useCustomSW();
const isFilterCardVisible = ref(false); const isFilterCardVisible = ref(false);
provide('isFilterCardVisible', isFilterCardVisible); provide('isFilterCardVisible', isFilterCardVisible);
@@ -127,30 +81,8 @@ export default defineComponent({
}; };
}, },
computed: {
trainList() {
return this.store.trainList.filter((train) => train.online);
},
computedRegions() {
return this.options.regions.map((region) => {
const regionStationCount =
this.store.apiData.stations?.filter((station) => station.region == region.id && station.isOnline).length || 0;
const regionTrainCount =
this.store.apiData.trains?.filter((train) => train.region == region.id && train.online).length || 0;
return {
id: region.id,
value: `${region.value} <div class='text--grayed'>${regionStationCount} / ${regionTrainCount}</div>`,
selectedValue: region.value,
};
});
},
},
data: () => ({ data: () => ({
VERSION: packageInfo.version, VERSION: packageInfo.version,
options,
currentLang: 'pl', currentLang: 'pl',
releaseURL: '', releaseURL: '',
@@ -158,18 +90,44 @@ export default defineComponent({
created() { created() {
this.loadLang(); this.loadLang();
this.store.isOffline = !window.navigator.onLine;
window.addEventListener('offline', () => {
this.store.isOffline = true;
this.store.apiData = {
stations: [],
dispatchers: [],
trains: [],
connectedSocketCount: 0,
};
this.store.setOnlineData();
});
window.addEventListener('online', () => {
this.store.isOffline = false;
});
}, },
async mounted() { async mounted() {
this.updateStorage();
this.setReleaseURL(); this.setReleaseURL();
watch(
() => this.store.blockScroll,
(value) => {
if (value) {
document.body.classList.add('no-scroll');
return;
}
document.body.classList.remove('no-scroll');
}
);
}, },
methods: { methods: {
changeRegion(region: { id: string; value: string }) {
this.store.changeRegion(region);
},
changeLang(lang: string) { changeLang(lang: string) {
this.$i18n.locale = lang; this.$i18n.locale = lang;
this.currentLang = lang; this.currentLang = lang;
@@ -177,18 +135,18 @@ export default defineComponent({
StorageManager.setStringValue('lang', lang); StorageManager.setStringValue('lang', lang);
}, },
setReleaseURL() { async setReleaseURL() {
const releaseURL = StorageManager.getStringValue('releaseURL'); try {
const releaseData = await (
await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest')
).data;
this.releaseURL = releaseURL || ''; if (!releaseData) return;
},
updateStorage() { this.releaseURL = releaseData.html_url;
if (!StorageManager.isRegistered('unavailable-status')) { } catch (error) {
StorageManager.setBooleanValue('unavailable-status', true); console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
StorageManager.setBooleanValue('ending-status', true); return;
StorageManager.setBooleanValue('no-space-status', true);
StorageManager.setBooleanValue('afk-status', true);
} }
}, },
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" ?><svg enable-background="new 0 0 32 32" id="Glyph" version="1.1" viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M27.414,24.586l-5.077-5.077C23.386,17.928,24,16.035,24,14c0-5.514-4.486-10-10-10S4,8.486,4,14 s4.486,10,10,10c2.035,0,3.928-0.614,5.509-1.663l5.077,5.077c0.78,0.781,2.048,0.781,2.828,0 C28.195,26.633,28.195,25.367,27.414,24.586z M7,14c0-3.86,3.14-7,7-7s7,3.14,7,7s-3.14,7-7,7S7,17.86,7,14z" id="XMLID_223_" fill="white" /></svg>

After

Width:  |  Height:  |  Size: 546 B

+275
View File
@@ -0,0 +1,275 @@
<template>
<header class="app_header">
<div class="header_container">
<div class="header_icons">
<span class="icons-top">
<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 />
</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 class="header_body">
<StatusIndicator />
<span class="header_brand">
<router-link to="/">
<img :src="getImage('stacjownik-header-logo.svg')" alt="Stacjownik" />
</router-link>
</span>
<span class="header_info">
<Clock />
<div class="info_counter">
<img :src="getIcon('dispatcher')" alt="icon dispatcher" />
<span class="text--primary">{{ onlineDispatchersCount }}</span>
<span class="text--grayed"> / </span>
<span class="text--primary">{{ onlineTrainsCount }}</span>
<img :src="getIcon('train')" alt="icon train" />
</div>
<span class="info_region">
<SelectBox :itemList="computedRegions" :defaultItemIndex="0" @selected="changeRegion" />
</span>
</span>
<span class="header_links">
<router-link class="route" active-class="route-active" to="/" exact>
{{ $t('app.sceneries') }}
</router-link>
/
<router-link class="route" active-class="route-active" to="/trains">{{ $t('app.trains') }}</router-link>
/
<router-link
class="route"
active-class="route-active"
:data-active="$route.path.startsWith('/journal')"
to="/journal"
>
{{ $t('app.journal') }}
</router-link>
</span>
</div>
</div>
</header>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../../store/store';
import options from '../../data/options.json';
import imageMixin from '../../mixins/imageMixin';
import SelectBox from '../Global/SelectBox.vue';
import StatusIndicator from './StatusIndicator.vue';
import Clock from './Clock.vue';
export default defineComponent({
emits: ['changeLang'],
mixins: [imageMixin],
props: {
currentLang: {
type: String,
required: true,
},
},
setup() {
return {
store: useStore(),
};
},
methods: {
changeRegion(region: { id: string; value: string }) {
this.store.changeRegion(region);
},
changeLang(lang: string) {
this.$emit('changeLang', lang);
},
},
computed: {
onlineTrainsCount() {
return this.store.trainList.filter((train) => train.online).length;
},
onlineDispatchersCount() {
return this.store.stationList.filter(
(station) => station.onlineInfo && station.onlineInfo.region == this.store.region.id
).length;
},
computedRegions() {
return options.regions.map((region) => {
const regionStationCount =
this.store.apiData.stations?.filter((station) => station.region == region.id && station.isOnline).length || 0;
const regionTrainCount =
this.store.apiData.trains?.filter((train) => train.region == region.id && train.online).length || 0;
return {
id: region.id,
value: `${region.value} <div class='text--grayed'>${regionStationCount} / ${regionTrainCount}</div>`,
selectedValue: region.value,
};
});
},
},
components: { SelectBox, StatusIndicator, Clock },
});
</script>
<style lang="scss" scoped>
@import '../../styles/variables.scss';
@import '../../styles/responsive.scss';
// HEADER
.app_header {
display: flex;
justify-content: center;
position: relative;
background-color: $primaryCol;
}
.header {
&_body {
max-width: 21em;
position: relative;
@include smallScreen {
max-width: 18em;
}
}
&_container {
display: flex;
justify-content: center;
position: relative;
width: 1350px;
padding: 0.5em 0.3em 0 0.3em;
border-radius: 0 0 1em 1em;
}
&_brand {
display: flex;
img {
width: 100%;
margin: 0 auto;
}
}
&_info {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
max-width: 100%;
font-size: 1.2em;
}
&_links {
display: flex;
justify-content: center;
border-radius: 0.7em;
font-size: 1.25em;
padding: 0.5em;
}
&_icons {
position: absolute;
right: 0;
top: 0;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
padding: 0.5em 0.5em;
@include smallScreen() {
right: auto;
left: 0.75em;
padding: 0;
align-items: center;
}
}
}
// ICONS
.icons {
position: relative;
&-top {
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;
}
}
}
}
// COUNTER
.info_counter {
display: flex;
justify-content: center;
align-items: center;
span {
margin: 0 0.15em;
}
img {
width: 1.35em;
}
}
// REGION SELECTION
.info_region {
color: white;
font-weight: bold;
display: flex;
justify-content: flex-end;
.select-box_content button {
background-color: transparent;
font-weight: bold;
padding: 0.1em 0.5em;
color: paleturquoise;
}
.options {
font-size: 0.9em;
}
}
</style>
-31
View File
@@ -1,31 +0,0 @@
<template>
<div class="loading">{{message}}</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
props: ["message"],
});
</script>
<style lang="scss" scoped>
.loading {
display: flex;
justify-content: center;
align-items: center;
min-height: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: calc(0.75rem + 1vw);
color: #fdc62f;
}
</style>
+14 -3
View File
@@ -161,7 +161,6 @@
</template> </template>
<script lang="ts"> <script lang="ts">
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';
@@ -172,6 +171,7 @@ export default defineComponent({
return { return {
tooltipActive: false, tooltipActive: false,
indicator: { indicator: {
offline: false,
status: DataStatus.Loading, status: DataStatus.Loading,
message: 'data-status.S3', message: 'data-status.S3',
}, },
@@ -193,6 +193,7 @@ export default defineComponent({
return { return {
dataStatus: store.dataStatuses, dataStatus: store.dataStatuses,
store,
}; };
}, },
@@ -206,6 +207,13 @@ export default defineComponent({
const trainsDataStatus = statuses.trains; const trainsDataStatus = statuses.trains;
const dispatcherDataStatus = statuses.dispatchers; const dispatcherDataStatus = statuses.dispatchers;
if (this.store.isOffline) {
this.setSignalStatus(DataStatus.Initialized);
this.indicator.status = DataStatus.Initialized;
this.indicator.message = 'data-status.S1-offline';
return;
}
if (connectionStatus == DataStatus.Error) { if (connectionStatus == DataStatus.Error) {
this.setSignalStatus(connectionStatus); this.setSignalStatus(connectionStatus);
this.indicator.status = connectionStatus; this.indicator.status = connectionStatus;
@@ -252,6 +260,10 @@ export default defineComponent({
this.orangeLight = false; this.orangeLight = false;
this.redBottomLight = false; this.redBottomLight = false;
if (status == DataStatus.Initialized) {
this.redTopLight = true;
}
if (status == DataStatus.Loaded) { if (status == DataStatus.Loaded) {
this.greenLight = true; this.greenLight = true;
} }
@@ -291,9 +303,8 @@ export default defineComponent({
.status-indicator { .status-indicator {
position: absolute; position: absolute;
left: 50%; left: 110%;
bottom: 0; bottom: 0;
transform: translateX(12em);
z-index: 100; z-index: 100;
} }
+69
View File
@@ -0,0 +1,69 @@
<template>
<div class="update-prompt">
<transition name="prompt-anim">
<div class="prompt_content" v-if="!hidePrompt && needRefresh">
<div>{{ $t('update.title') }}</div>
<div class="prompt_actions">
<button class="btn btn--filled" @click="updateServiceWorker(true)">{{ $t('update.confirm-button') }}</button>
<button class="btn btn--filled" @click="hidePrompt = true">{{ $t('update.later-button') }}</button>
</div>
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import useCustomSW from '../../mixins/useCustomSW';
const hidePrompt = ref(false);
const { needRefresh, updateServiceWorker } = useCustomSW();
</script>
<style lang="scss" scoped>
@import '../../styles/variables.scss';
.update-prompt {
position: fixed;
bottom: 0;
right: 0;
z-index: 200;
}
.prompt_content {
margin: 1em;
padding: 1em;
font-weight: bold;
background-color: black;
box-shadow: 0 0 10px 1px $accentCol;
border-radius: 1em;
}
.prompt_actions {
display: flex;
margin-top: 1em;
gap: 0.5em;
button {
width: 100%;
}
}
// Animation
.prompt-anim {
&-enter-active,
&-leave-active {
transition: all 120ms ease-in;
transform: translateY(0);
}
&-enter-from,
&-leave-to {
transform: translateY(100%);
}
}
</style>
+1 -42
View File
@@ -1,5 +1,5 @@
<template> <template>
<button class="action-btn"> <button class="action-btn btn--filled">
<div class="button_content"> <div class="button_content">
<slot></slot> <slot></slot>
</div> </div>
@@ -16,47 +16,6 @@ export default defineComponent({});
@import "../../styles/variables"; @import "../../styles/variables";
@import "../../styles/responsive"; @import "../../styles/responsive";
.action-btn {
background: #333;
border: none;
color: #bdbdbd;
font-size: 1em;
font-weight: 500;
padding: 0.35em 0.65em;
cursor: pointer;
transition: all 0.3s;
&.outlined {
border: 1px solid white;
}
img {
width: 1.25em;
vertical-align: middle;
margin-right: 0.35em;
}
p {
font-size: 1em;
overflow: hidden;
}
&.open {
color: $accentCol;
border: none;
}
&:hover,
&:focus {
color: $accentCol;
background: #5c5c5c;
}
}
.button_content { .button_content {
display: flex; display: flex;
justify-content: center; justify-content: center;
+1 -1
View File
@@ -20,7 +20,7 @@ export default defineComponent({
.loading { .loading {
position: absolute; position: absolute;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translate(-50%, -50%);
display: flex; display: flex;
justify-content: center; justify-content: center;
+16 -10
View File
@@ -2,7 +2,6 @@
<div class="select-box"> <div class="select-box">
<div class="select-box_content"> <div class="select-box_content">
<button class="selected" @click="toggleBox"> <button class="selected" @click="toggleBox">
<span class="text--primary">{{ prefix }}</span>
<span>{{ computedSelectedItem.selectedValue || computedSelectedItem.value }}</span> <span>{{ computedSelectedItem.selectedValue || computedSelectedItem.value }}</span>
</button> </button>
@@ -131,13 +130,14 @@ export default defineComponent({
.select-box { .select-box {
position: relative; position: relative;
width: auto;
} }
.arrow { .arrow {
position: absolute; position: absolute;
top: 50%; top: 50%;
right: 0; right: 0;
padding: 0.5em; padding: 0;
img { img {
vertical-align: middle; vertical-align: middle;
@@ -150,13 +150,17 @@ export default defineComponent({
} }
button.selected { button.selected {
background: #333; background-color: transparent;
color: white; color: paleturquoise;
font-size: 1em; font-size: 1em;
font-weight: bold;
padding: 0.1em 0.5em;
margin-right: 2em;
display: flex;
padding: 0.35em 0.5em;
margin-right: 1.4em;
width: 100%; width: 100%;
cursor: pointer; cursor: pointer;
@@ -167,7 +171,7 @@ button.selected {
text-align: left; text-align: left;
&:focus { &:focus {
background: #555; background-color: #262626;
} }
} }
@@ -188,8 +192,9 @@ ul.options {
height: auto; height: auto;
z-index: 100; z-index: 100;
width: 100%; width: 100%;
font-size: 0.9em;
} }
li.option { li.option {
@@ -203,6 +208,7 @@ li.option {
appearance: none; appearance: none;
border: none; border: none;
outline: none; outline: none;
background: none;
&:focus + span { &:focus + span {
color: $accentCol; color: $accentCol;
@@ -218,11 +224,11 @@ li.option {
position: relative; position: relative;
display: inline-block; display: inline-block;
background-color: hsla(0, 0%, 15%, 0.95); background-color: #262626f2;
&:hover, &:hover,
&:focus { &:focus {
background-color: hsla(0, 0%, 20%, 0.95); background-color: #333333f2;
} }
padding: 0.5em 0; padding: 0.5em 0;
-3
View File
@@ -144,9 +144,6 @@ export default defineComponent({
} }
@include smallScreen { @include smallScreen {
.train-modal {
font-size: 1.05em;
}
.modal_content { .modal_content {
max-height: 85vh; max-height: 85vh;
+182
View File
@@ -0,0 +1,182 @@
<template>
<section class="daily-stats">
<span :data-active="data.statsStatus">
<b v-if="data.statsStatus == DataStatus.Loading">
{{ $t('app.loading') }}
</b>
<b v-else-if="data.stats.distanceSum == null">
{{ $t('journal.daily-stats-info') }}
</b>
<span>
<div v-if="data.stats.totalTimetables">
&bull;
<i18n-t keypath="journal.timetable-stats-total">
<template #count>
<b class="text--primary">
{{ data.stats.totalTimetables }}
{{ $t('journal.timetable-count', data.stats.totalTimetables) }}
</b>
</template>
<template #distance>
<b class="text--primary"> {{ data.stats.distanceSum?.toFixed(2) }} km </b>
</template>
</i18n-t>
</div>
<div v-if="data.stats.timetableId">
&bull;
<i18n-t keypath="journal.timetable-stats-longest">
<template #id>
<router-link :to="`/journal/timetables?timetableId=${data.stats.timetableId}`">
<b>{{ data.stats.timetableId }}</b>
</router-link>
</template>
<template #author>
<router-link :to="`/journal/dispatchers?dispatcherName=${data.stats.timetableAuthor}`">
<b>{{ data.stats.timetableAuthor }}</b>
</router-link>
</template>
<template #driver>
<b>{{ data.stats.timetableDriver }}</b>
</template>
<template #distance>
<b class="text--primary">{{ data.stats.timetableRouteDistance }} km</b>
</template>
</i18n-t>
</div>
<div v-if="firstPlaceDispatchers.length == 1">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active">
<template #dispatcher>
<router-link :to="`/journal/dispatchers?dispatcherName=${firstPlaceDispatchers[0].name}`">
<b>{{ firstPlaceDispatchers[0].name }}</b>
</router-link>
</template>
<template #count>
<b class="text--primary">
{{ firstPlaceDispatchers[0].count }}
{{ $t('journal.timetable-count', firstPlaceDispatchers[0].count) }}
</b>
</template>
</i18n-t>
</div>
<div v-if="firstPlaceDispatchers.length > 1">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active-many">
<template #dispatchers>
<span v-for="(disp, i) in firstPlaceDispatchers">
<span v-if="i == firstPlaceDispatchers.length - 1"> {{ $t('general.and') }} </span>
<router-link :to="`/journal/dispatchers?dispatcherName=${disp.name}`">
<b>{{ disp.name }}</b>
</router-link>
<span v-if="i < firstPlaceDispatchers.length - 2">, </span>
</span>
</template>
<template #count>
<b class="text--primary">
{{ firstPlaceDispatchers[0].count }}
{{ $t('journal.timetable-count', firstPlaceDispatchers[0].count) }}
</b>
</template>
</i18n-t>
</div>
</span>
</span>
</section>
</template>
<script setup lang="ts">
import axios from 'axios';
import { computed, reactive, ref } from 'vue';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { ITimetablesDailyStats, ITimetablesDailyStatsResponse } from '../../scripts/interfaces/api/StatsAPIData';
import { URLs } from '../../scripts/utils/apiURLs';
const intervalId = ref(-1);
const data = reactive({
statsStatus: DataStatus.Loading,
stats: {
totalTimetables: 0,
distanceSum: 0,
distanceAvg: 0,
timetableAuthor: '',
timetableDriver: '',
timetableId: 0,
timetableRouteDistance: 0,
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?.id || 0,
timetableRouteDistance: maxTimetable?.routeDistance || 0,
mostActiveDispatchers,
};
data.statsStatus = DataStatus.Loaded;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...');
data.statsStatus = DataStatus.Error;
}
}
function startFetchingDailyStats() {
fetchDailyTimetableStats();
intervalId.value = setInterval(fetchDailyTimetableStats, 60000);
}
function stopFetchingDailyStats() {
clearInterval(intervalId.value);
}
defineExpose({
startFetchingDailyStats,
stopFetchingDailyStats,
});
</script>
<style lang="scss" scoped>
.daily-stats {
text-align: left;
}
.daily-stats > span[data-active='0'] {
opacity: 0.75;
}
</style>
+2 -32
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="stats_container" v-click-outside="() => (cardVisible = false)"> <div class="stats_container" v-click-outside="() => (cardVisible = false)">
<button class="stats_button btn btn--option" @click="toggleCard"> <button class="stats_button" @click="toggleCard">
Statystyki dyżurnego {{ store.dispatcherStatsName }} Statystyki dyżurnego {{ store.dispatcherStatsName }}
</button> </button>
@@ -14,6 +14,7 @@
<div v-else> <div v-else>
<h3>STATYSTYKI WYSTAWIONYCH ROZKŁADÓW</h3> <h3>STATYSTYKI WYSTAWIONYCH ROZKŁADÓW</h3>
<div class="info-stats" v-if="store.dispatcherStatsData._count._all"> <div class="info-stats" v-if="store.dispatcherStatsData._count._all">
<span class="stat-badge"> <span class="stat-badge">
<span>LICZBA</span> <span>LICZBA</span>
@@ -162,42 +163,11 @@ h3 {
text-align: center; text-align: center;
} }
.info-stats {
display: flex;
justify-content: center;
flex-wrap: wrap;
margin-top: 1em;
}
.last-timetables { .last-timetables {
overflow-y: auto; overflow-y: auto;
} }
.stat-badge {
margin-right: 0.5em;
padding-bottom: 1em;
span {
padding: 0.25em 0.3em;
}
span:first-child {
background-color: #4d4d4d;
}
span:last-child {
background-color: $accentCol;
color: black;
font-weight: bold;
}
}
@include smallScreen() {
.stats_card {
text-align: center;
left: 50%;
transform: translateX(-50%);
border-radius: 0 0 1em 1em;
}
}
</style> </style>
-141
View File
@@ -1,141 +0,0 @@
<template>
<div class="card-dimmer" @click="closeCard"></div>
<div class="stats-card card">
<div>
<h2 class="card-title">
STATYSTYKI MASZYNISTY <span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
</h2>
<div class="loading" v-if="!store.driverStatsData">Ładowanie...</div>
<div v-else>
<div class="info-stats" v-if="store.driverStatsData._sum.routeDistance != null">
<span class="stat-badge">
<span>PRZEBYTO</span>
<span>{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km</span>
</span>
<span class="stat-badge">
<span>PORZUCONO</span>
<span>
{{ (store.driverStatsData._sum.routeDistance - store.driverStatsData._sum.currentDistance).toFixed(2) }}km
</span>
</span>
<span class="stat-badge">
<span>WYPEŁNIONO</span>
<span>{{ store.driverStatsData._count.fulfilled }} RJ</span>
</span>
<span class="stat-badge">
<span>PORZUCONO</span>
<span>{{ store.driverStatsData._count._all - store.driverStatsData._count.fulfilled }} RJ</span>
</span>
<span class="stat-badge">
<span>ZATWIERDZONO</span>
<span>{{ store.driverStatsData._sum.confirmedStopsCount }} stacji</span>
</span>
<span class="stat-badge">
<span>PORZUCONO</span>
<span>
{{ store.driverStatsData._sum.allStopsCount - store.driverStatsData._sum.confirmedStopsCount }}
stacji
</span>
</span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import axios from 'axios';
import { defineComponent } from 'vue';
import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData';
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
import { URLs } from '../../scripts/utils/apiURLs';
import { useStore } from '../../store/store';
export default defineComponent({
emits: ['closeCard'],
setup() {
const store = useStore();
return {
store,
};
},
data() {
return {
test: Math.random(),
lastDispatcherName: '',
lastTimetables: [] as TimetableHistory[],
};
},
activated() {
this.fetchDispatcherStats();
},
methods: {
async fetchDispatcherStats() {
this.store.driverStatsData = undefined;
const statsData: DriverStatsAPIData = await (
await axios.get(`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`)
).data;
const recentTimetablesData: TimetableHistory[] = await (
await axios.get(`${URLs.stacjownikAPI}/api/getTimetables?driverName=${this.store.driverStatsName}`)
).data;
this.store.driverStatsData = statsData;
this.lastTimetables = recentTimetablesData || [];
},
closeCard() {
this.$emit('closeCard');
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
.timetable-row {
display: grid;
grid-template-columns: 4fr 1fr 1fr 2fr 2fr;
gap: 0.2em;
margin: 0.5em 0;
text-align: center;
span {
min-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
background-color: #4d4d4d;
padding: 0.5em 0.2em;
}
@include smallScreen() {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
span {
padding: 0.2em 0.3em;
}
grid-template-columns: 1fr;
background-color: #4d4d4d;
}
}
</style>
@@ -1,425 +0,0 @@
<template>
<section class="journal-timetables">
<div class="journal-wrapper">
<div class="journal_top-bar">
<JournalOptions
@on-filter-change="search"
@on-input-change="search"
@on-sorter-change="search"
:sorter-option-ids="['timestampFrom', 'duration']"
/>
<!-- <DispatcherStats /> -->
</div>
<div class="journal-list">
<div class="list-wrapper" ref="scrollElement">
<transition name="warning" mode="out-in">
<div :key="historyDataStatus.status">
<Loading v-if="isDataLoading || isDataInit" />
<div v-else-if="isDataError" class="journal_warning error">
{{ $t('app.error') }}
</div>
<div class="journal_warning" v-else-if="historyList.length == 0">
{{ $t('app.no-result') }}
</div>
<ul v-else>
<transition-group name="journal-list-anim">
<li v-for="(doc, i) in computedHistoryList" :key="doc.id">
<div class="journal_day" v-if="isAnotherDay(i - 1, i)">
<span>{{ new Date(doc.timestampFrom).toLocaleDateString('pl-PL') }}</span>
</div>
<div
class="journal_item"
:class="{ online: doc.isOnline }"
@click="navigateToScenery(doc.stationName, doc.isOnline)"
@keydown.enter="navigateToScenery(doc.stationName, doc.isOnline)"
tabindex="0"
>
<span>
<b class="text--primary">{{ doc.dispatcherName }}</b> &bull; <b>{{ doc.stationName }}</b>
<span class="text--grayed">&nbsp;#{{ doc.stationHash }}&nbsp;</span>
<span class="region-badge" :class="doc.region">PL1</span>
</span>
<span>
<span :data-status="doc.isOnline">
{{ doc.isOnline ? $t('journal.online-since') : 'OFFLINE' }}&nbsp;
</span>
<span>
{{ new Date(doc.timestampFrom).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
</span>
<span v-if="doc.currentDuration && doc.isOnline">
({{ calculateDuration(doc.currentDuration) }})
</span>
<span v-if="doc.timestampTo">
&gt;
{{ new Date(doc.timestampTo).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
({{ $t('journal.duty-lasted') }} {{ calculateDuration(doc.currentDuration!) }})
</span>
</span>
</div>
</li>
</transition-group>
</ul>
</div>
</transition>
</div>
</div>
<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>
</section>
</template>
<script lang="ts">
import { computed, defineComponent, JournalFilter, provide, reactive, Ref, ref } from 'vue';
import axios from 'axios';
import ActionButton from '../../components/Global/ActionButton.vue';
import JournalOptions from '../../components/JournalView/JournalOptions.vue';
import DispatcherStats from '../../components/JournalView/DispatcherStats.vue';
import SearchBox from '../Global/SearchBox.vue';
import Loading from '../Global/Loading.vue';
import { URLs } from '../../scripts/utils/apiURLs';
import dateMixin from '../../mixins/dateMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { useStore } from '../../store/store';
const DISPATCHERS_API_URL = `${URLs.stacjownikAPI}/api/getDispatchers`;
interface DispatcherHistoryItem {
id: string;
stationName: string;
stationHash: string;
region: string;
dispatcherName: string;
dispatcherId: number;
timestampFrom: number;
timestampTo?: number;
currentDuration?: number;
lastOnlineTimestamp: number;
isOnline: boolean;
}
type JournalDispatcherSearcher = {
[key in 'search-dispatcher' | 'search-station']: string;
};
export default defineComponent({
components: { SearchBox, ActionButton, JournalOptions, DispatcherStats, Loading },
mixins: [dateMixin],
name: 'JournalDispatchers',
props: {
sceneryName: {
type: String,
required: false,
},
dispatcherName: {
type: String,
required: false,
},
},
data: () => ({
currentQuery: '',
scrollDataLoaded: true,
scrollNoMoreData: false,
showReturnButton: false,
statsCardOpen: false,
}),
setup() {
const historyDataStatus: Ref<{ status: DataStatus; error: string | null }> = ref({
status: DataStatus.Loading,
error: null,
});
const sorterActive = ref({ id: 'timestampFrom', dir: -1 });
const journalFilterActive = ref({});
const searchersValues = reactive({
'search-dispatcher': '',
'search-station': '',
} as JournalDispatcherSearcher);
const countFromIndex = ref(0);
const countLimit = 15;
provide('sorterActive', sorterActive);
provide('journalFilterActive', journalFilterActive);
provide('searchersValues', searchersValues);
const scrollElement: Ref<HTMLElement | null> = ref(null);
return {
store: useStore(),
historyList: ref([]) as Ref<DispatcherHistoryItem[]>,
historyDataStatus,
isDataLoading: computed(() => historyDataStatus.value.status === DataStatus.Loading),
isDataError: computed(() => historyDataStatus.value.status === DataStatus.Error),
isDataInit: computed(() => historyDataStatus.value.status === DataStatus.Initialized),
sorterActive,
searchersValues,
countFromIndex,
countLimit,
scrollElement,
maxCount: ref(15),
};
},
computed: {
computedHistoryList() {
return this.historyList.filter(
(doc) => doc.isOnline || (doc.currentDuration && doc.currentDuration > 10 * 60000)
);
},
},
activated() {
if (this.sceneryName || this.dispatcherName) {
this.searchersValues['search-station'] = this.sceneryName?.toString() || '';
this.searchersValues['search-dispatcher'] = this.dispatcherName?.toString() || '';
this.search();
}
window.addEventListener('scroll', this.handleScroll);
},
mounted() {
if (!this.sceneryName && !this.dispatcherName) {
this.search();
}
},
deactivated() {
window.removeEventListener('scroll', this.handleScroll);
},
methods: {
closeDispatcherStatsCard() {
this.statsCardOpen = false;
},
navigateToScenery(name: string, isOnline: boolean) {
if (!isOnline) return;
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`);
},
isAnotherDay(prevIndex: number, currIndex: number) {
if (currIndex == 0) return true;
return (
new Date(this.computedHistoryList[prevIndex].timestampFrom).getDate() !=
new Date(this.computedHistoryList[currIndex].timestampFrom).getDate()
);
},
handleScroll() {
this.showReturnButton = window.scrollY > window.innerHeight;
const element = this.$refs.scrollElement as HTMLElement;
if (
element.getBoundingClientRect().bottom * 0.85 < window.innerHeight &&
this.scrollDataLoaded &&
!this.scrollNoMoreData &&
this.historyDataStatus.status == DataStatus.Loaded
)
this.addHistoryData();
},
scrollToTop() {
window.scrollTo({ top: 0 });
},
search() {
this.fetchHistoryData({
searchers: this.searchersValues,
});
this.scrollNoMoreData = false;
this.scrollDataLoaded = true;
},
async addHistoryData() {
this.scrollDataLoaded = false;
const countFrom = this.historyList.length;
const responseData: DispatcherHistoryItem[] = await (
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
).data;
if (!responseData) return;
if (responseData.length == 0) {
this.scrollNoMoreData = true;
return;
}
this.historyList.push(...responseData);
this.scrollDataLoaded = true;
},
async fetchHistoryData(
props: {
searchers?: JournalDispatcherSearcher;
filter?: JournalFilter;
} = {}
) {
this.historyDataStatus.status = DataStatus.Loading;
const queries: string[] = [];
// const dispatcher = props.searchers?.find((s) => s.id == 'search-dispatcher')?.value.trim();
// const station = props.searchers?.find((s) => s.id == 'search-station')?.value.trim();
const dispatcher = props.searchers?.['search-dispatcher'].trim();
const station = props.searchers?.['search-station'].trim();
if (dispatcher) queries.push(`dispatcherName=${dispatcher}`);
if (station) queries.push(`stationName=${station}`);
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
if (this.sorterActive.id == 'timestampFrom') queries.push('sortBy=timestampFrom');
else if (this.sorterActive.id == 'duration') queries.push('sortBy=currentDuration');
else queries.push('sortBy=timestampFrom');
queries.push('countLimit=15');
this.currentQuery = queries.join('&');
try {
const responseData: DispatcherHistoryItem[] = await (
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}`)
).data;
if (!responseData) {
this.historyDataStatus.status = DataStatus.Error;
this.historyDataStatus.error = 'Brak danych!';
return;
}
if (!responseData) return;
// Response data exists
this.historyList = responseData;
// Stats display
this.store.dispatcherStatsName =
this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim()
? this.historyList[0].dispatcherName
: '';
this.historyDataStatus.status = DataStatus.Loaded;
} catch (error) {
this.historyDataStatus.status = DataStatus.Error;
this.historyDataStatus.error = 'Ups! Coś poszło nie tak!';
}
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/JournalSection.scss';
@import '../../styles/responsive.scss';
.region-badge {
padding: 0.1em 0.5em;
border-radius: 0.5em;
font-weight: bold;
&.eu {
background-color: forestgreen;
}
}
.list-wrapper {
margin-top: 1em;
}
.journal_item {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
&.online {
cursor: pointer;
}
span[data-status='true'] {
color: springgreen;
}
span[data-status='false'] {
color: salmon;
}
}
.journal_day {
position: relative;
text-align: center;
background-color: #4d4d4d;
span {
position: relative;
background-color: #4d4d4d;
z-index: 10;
padding: 0 0.5em;
}
&::after {
position: absolute;
content: '';
z-index: 0;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
height: 3px;
width: 60%;
min-width: 200px;
background-color: white;
}
}
@include smallScreen() {
.journal_item {
flex-direction: column;
span {
margin-top: 0.25em;
text-align: center;
}
}
}
</style>
@@ -0,0 +1,177 @@
<template>
<ul class="journal-list">
<!-- <transition-group name="journal-list-anim"> -->
<li v-for="item in computedDispatcherHistory" :class="{ sticky: typeof item == 'string' }">
<div v-if="typeof item == 'string'" class="journal_day">
{{ item }}
</div>
<div
v-else
class="journal_item"
:class="{ online: item.isOnline }"
@click="navigateToScenery(item.stationName, item.isOnline)"
@keydown.enter="navigateToScenery(item.stationName, item.isOnline)"
tabindex="0"
>
<span>
<b
v-if="item.dispatcherLevel !== null"
class="dispatcher-level"
:style="calculateExpStyle(item.dispatcherLevel, item.dispatcherIsSupporter)"
>
{{ item.dispatcherLevel >= 2 ? item.dispatcherLevel : 'L' }}
</b>
<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>
<span :data-status="item.isOnline"> {{ item.isOnline ? $t('journal.online-since') : 'OFFLINE' }}&nbsp; </span>
<span>
{{ new Date(item.timestampFrom).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
</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>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
import styleMixin from '../../mixins/styleMixin';
export default defineComponent({
props: {
dispatcherHistory: {
type: Array as PropType<DispatcherHistory[]>,
required: true,
},
},
mixins: [dateMixin, styleMixin],
computed: {
computedDispatcherHistory() {
return this.dispatcherHistory.reduce((acc, historyItem, i) => {
if (this.isAnotherDay(i - 1, i)) acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL'));
acc.push(historyItem);
return acc;
}, [] as (DispatcherHistory | string)[]);
},
},
methods: {
navigateToScenery(name: string, isOnline: boolean) {
if (!isOnline) return;
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`);
},
isAnotherDay(prevIndex: number, currIndex: number) {
if (currIndex == 0) return true;
return (
new Date(this.dispatcherHistory[prevIndex].timestampFrom).getDate() !=
new Date(this.dispatcherHistory[currIndex].timestampFrom).getDate()
);
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
@import '../../styles/JournalSection.scss';
.region-badge {
padding: 0.1em 0.5em;
border-radius: 0.5em;
font-weight: bold;
&.eu {
background-color: forestgreen;
}
}
li.sticky {
position: sticky;
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;
}
span[data-status='false'] {
color: salmon;
}
}
.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;
}
}
.dispatcher-level {
display: inline-block;
text-align: center;
line-height: 150%;
width: 25px;
height: 25px;
margin-right: 0.5em;
border-radius: 0.25em;
}
@include smallScreen() {
.journal_item {
flex-direction: column;
span {
margin-top: 0.25em;
text-align: center;
}
}
}
</style>
@@ -0,0 +1,67 @@
<template>
<div class="journal-stats">
<span v-if="store.driverStatsData">
<h3>
{{ $t('journal.stats-title') }} <span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
</h3>
<div class="info-stats">
<span class="stat-badge">
<span>{{ $t('journal.stats-timetables') }}</span>
<span>{{ store.driverStatsData._count.fulfilled }} / {{ store.driverStatsData._count._all }}</span>
</span>
<span class="stat-badge">
<span>{{ $t('journal.stats-longest-timetable') }}</span>
<span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span>
</span>
<span class="stat-badge">
<span>{{ $t('journal.stats-avg-timetable') }}</span>
<span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
</span>
<span class="stat-badge">
<span>{{ $t('journal.stats-distance') }}</span>
<span>
{{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km
</span>
</span>
<span class="stat-badge">
<span>{{ $t('journal.stats-stations') }}</span>
<span>
{{ store.driverStatsData._sum.confirmedStopsCount }} /
{{ store.driverStatsData._sum.allStopsCount }}
</span>
</span>
</div>
</span>
<b v-else-if="store.driverStatsStatus == DataStatus.Loading">{{ $t('journal.stats-loading') }}</b>
<b v-else-if="store.driverStatsStatus == DataStatus.Error">
{{ $t('journal.stats-error ') }}
</b>
<b v-else>{{ $t('journal.driver-stats-info') }}</b>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { useStore } from '../../store/store';
export default defineComponent({
data() {
return {
store: useStore(),
DataStatus,
};
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/JournalStats.scss';
</style>
@@ -0,0 +1,46 @@
<template>
<section class="journal-header">
<div class="journal-type-options">
<router-link class="router-link" active-class="route-active" to="/journal/timetables" exact>
{{ $t('journal.section-timetables') }}
</router-link>
&nbsp;&bull;&nbsp;
<router-link class="router-link" active-class="route-active" to="/journal/dispatchers">
{{ $t('journal.section-dispatchers') }}
</router-link>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({});
</script>
<style lang="scss" scoped>
.journal-type-options {
display: flex;
justify-content: center;
background-color: #2c2c2c;
max-width: 18em;
font-size: 1.2em;
margin: 0 auto;
border-radius: 0 0 0.5em 0.5em;
padding: 0.1em 0;
}
.journal-section > section {
height: 100%;
display: flex;
justify-content: center;
}
.router-link.active {
color: gold;
}
</style>
+274 -260
View File
@@ -1,260 +1,274 @@
<template> <template>
<div class="journal-options"> <div class="filters-options" @keydown.esc="showOptions = false">
<div class="options_wrapper"> <div class="bg" v-if="showOptions" @click="showOptions = false"></div>
<div class="options_content">
<div class="content_select"> <button class="filter-button btn--filled btn--image" @click="showOptions = !showOptions" ref="button">
<select-box <img :src="getIcon('filter2')" alt="Open filters" />
:itemList="translatedSorterOptions" {{ $t('options.filters') }} [F]
:defaultItemIndex="0" <span class="active-indicator" v-if="currentOptionsActive"></span>
@selected="onSorterChange" </button>
:prefix="$t('journal.sort-prefix')"
/> <datalist id="search-driver">
</div> <option v-for="sugg in driverSuggestions" :value="sugg"></option>
</datalist>
<div class="content_search">
<div class="search-box" v-for="(value, propName) in searchersValues" :key="propName"> <datalist id="search-dispatcher">
<input <option v-for="sugg in dispatcherSuggestions" :value="sugg"></option>
class="search-input" </datalist>
:placeholder="$t(`journal.${propName}`)"
v-model="searchersValues[propName]" <transition name="options-anim">
@keydown.enter="onInputSearch" <div class="options_wrapper" v-if="showOptions">
/> <div class="options_content">
<h1 class="option-title">{{ $t('options.search-title') }}</h1>
<img class="search-exit" :src="getIcon('exit')" alt="exit-icon" @click="onInputClear(propName)" /> <div class="search_content">
</div> <div class="search" v-for="(_, propName) in searchersValues" :key="propName">
<!-- <div class="search-box"> <label v-if="propName == 'search-date'" for="date">{{ $t('options.search-date') }}</label>
<input
class="search-input" <div class="search-box">
v-model="searchedTrain" <input
:placeholder="$t('journal.search-train')" class="search-input"
@keydown.enter="search" v-model="searchersValues[propName]"
/> @keydown.enter="onSearchConfirm"
@focus="preventKeyDown = true"
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="clearTrain" /> @blur="preventKeyDown = false"
</div> :placeholder="$t(`options.${propName}`)"
:type="propName == 'search-date' ? 'date' : 'text'"
<div class="search-box"> :min="propName == 'search-date' ? '2022-02-01' : undefined"
<input :list="propName.toString()"
class="search-input" />
v-model="searchedDriver"
:placeholder="$t('journal.search-driver')" <button class="search-exit" v-if="propName != 'search-date'">
@keydown.enter="search" <img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear(propName)" />
/> </button>
</div>
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="clearDriver" /> </div>
</div> -->
<div class="search_actions">
<action-button class="search-button" @click="onInputSearch"> <button class="btn--action" @click="onResetButtonClick">
{{ $t('journal.search') }} {{ $t('options.reset-button') }}
</action-button> </button>
</div> <button class="btn--action" @click="onSearchButtonConfirm">
</div> {{ $t('options.search-button') }}
</button>
<div class="options_filters"> </div>
<button </div>
v-for="filter in filters"
class="journal-filter-option btn--option" <h1 class="option-title">{{ $t('options.sort-title') }}</h1>
:class="{ checked: journalFilterActive.id === filter.id }" <div class="options_sorters">
:id="filter.id" <div v-for="opt in translatedSorterOptions">
@click="onFilterChange(filter)" <button
> class="sort-option btn--option"
{{ $t(`journal.filter-${filter.id}`) }} :data-selected="opt.id == sorterActive.id"
</button> @click="onSorterChange(opt)"
</div> >
</div> {{ opt.value.toUpperCase() }}
</div> </button>
</template> </div>
</div>
<script lang="ts">
import { defineComponent, inject, JournalFilter, PropType } from 'vue'; <h1 class="option-title" v-if="filters.length != 0">{{ $t('options.filter-title') }}</h1>
import imageMixin from '../../mixins/imageMixin'; <div class="options_filters">
import ActionButton from '../Global/ActionButton.vue'; <button
import SelectBox from '../Global/SelectBox.vue'; v-for="filter in filters"
class="filter-option btn--option"
export default defineComponent({ :class="{ checked: journalFilterActive.id === filter.id }"
components: { SelectBox, ActionButton }, :id="filter.id"
emits: ['onSorterChange', 'onInputChange', 'onFilterChange'], @click="onFilterChange(filter)"
mixins: [imageMixin], >
{{ $t(`options.filter-${filter.id}`) }}
props: { </button>
sorterOptionIds: { </div>
type: Array as PropType<Array<string>>, </div>
required: true, </div>
}, </transition>
</div>
filters: { </template>
type: Array as PropType<JournalFilter[]>,
default: [], <script lang="ts">
}, import axios from 'axios';
}, import { defineComponent, inject, PropType } from 'vue';
import imageMixin from '../../mixins/imageMixin';
import keyMixin from '../../mixins/keyMixin';
setup() { import { DataStatus } from '../../scripts/enums/DataStatus';
return { import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData';
searchersValues: inject('searchersValues') as {[key: string]: string}, import { URLs } from '../../scripts/utils/apiURLs';
sorterActive: inject('sorterActive') as { id: string | number; dir: number }, import { useStore } from '../../store/store';
journalFilterActive: inject('journalFilterActive') as JournalFilter, import { JournalTimetableFilter } from '../../types/Journal/JournalTimetablesTypes';
}; import ActionButton from '../Global/ActionButton.vue';
}, import SelectBox from '../Global/SelectBox.vue';
computed: { export default defineComponent({
translatedSorterOptions() { components: { SelectBox, ActionButton },
return this.$props.sorterOptionIds.map((id) => ({ emits: ['onSearchConfirm', 'onOptionsReset'],
id, mixins: [imageMixin, keyMixin],
value: this.$t(`journal.option-${id}`),
})); props: {
}, sorterOptionIds: {
}, type: Array as PropType<Array<string>>,
required: true,
methods: { },
onSorterChange(item: { id: string | number; value: string }) {
this.sorterActive.id = item.id; filters: {
this.sorterActive.dir = -1; type: Array as PropType<JournalTimetableFilter[]>,
default: [],
this.$emit('onSorterChange'); },
},
dataStatus: {
onFilterChange(filter: JournalFilter) { type: Number as PropType<DataStatus>,
this.journalFilterActive = filter; default: DataStatus.Initialized,
this.$emit('onFilterChange'); },
},
currentOptionsActive: {
onInputSearch() { type: Boolean,
this.$emit('onInputChange'); default: false,
}, },
},
onInputClear(id: any) {
this.searchersValues[id] = ''; data() {
this.onInputSearch(); return {
}, showOptions: false,
},
}); driverSuggestions: [] as string[],
</script> dispatcherSuggestions: [] as string[],
<style lang="scss" scoped> searchTimeout: 0,
@import '../../styles/responsive'; store: useStore(),
@import '../../styles/option.scss';
DataStatus,
.options { };
&_wrapper { },
display: flex;
flex-direction: column; setup() {
} return {
searchersValues: inject('searchersValues') as { [key: string]: string },
&_content { sorterActive: inject('sorterActive') as { id: string | number; dir: number },
display: flex; journalFilterActive: inject('journalFilterActive') as JournalTimetableFilter,
flex-wrap: wrap; };
},
.content_search,
.content_select { computed: {
display: flex; driverStatsName() {
align-items: center; return this.store.driverStatsName;
flex-wrap: wrap; },
padding: 0.25em 0.25em 0 0; translatedSorterOptions() {
} return this.$props.sorterOptionIds.map((id) => ({
} id,
value: this.$t(`options.sort-${id}`),
&_filters { }));
display: flex; },
flex-wrap: wrap; },
margin: 0.5em 0 0 0;
watch: {
.journal-filter-option { async driverStatsName(value: string) {
margin: 0 0.25em 0 0; await this.fetchDriverStats();
this.store.currentStatsTab = value ? 'driver' : 'daily';
&#abandoned { },
color: salmon;
} async 'searchersValues.search-driver'(value: string | undefined) {
clearTimeout(this.searchTimeout);
&#fulfilled {
color: lightgreen; if (!value || value == '') return;
} if (value.length < 3) return;
&#active { this.startSearchTimeout('driver', value);
color: lightblue; },
}
} async 'searchersValues.search-dispatcher'(value: string | undefined) {
} if (!value || value == '') return;
} if (value.length < 3) return;
.search { this.startSearchTimeout('dispatcher', value);
&-box { },
position: relative; },
background: #333; methods: {
border-radius: 0.5em; async fetchDriverStats() {
min-width: 200px; this.store.driverStatsData = undefined;
margin-right: 0.25em;
} if (!this.store.driverStatsName) {
this.store.driverStatsStatus = DataStatus.Initialized;
&-input { return;
border: none; }
min-width: 100%; try {
padding: 0.35em 0.5em; this.store.driverStatsStatus = DataStatus.Loading;
}
const statsData: DriverStatsAPIData = await (
&-exit { await axios.get(`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`)
position: absolute; ).data;
cursor: pointer;
this.store.driverStatsData = statsData;
top: 50%; this.store.driverStatsStatus = DataStatus.Loaded;
right: 10px; } catch (error) {
transform: translateY(-50%); this.store.driverStatsStatus = DataStatus.Error;
console.error('Ups! Wystąpił błąd przy próbie pobrania statystyk maszynisty! :/');
width: 1em; }
} },
}
startSearchTimeout(type: 'driver' | 'dispatcher', value: string) {
@include smallScreen() { if (this[`${type}Suggestions`].includes(value)) return;
.journal-options {
width: 100%; window.clearTimeout(this.searchTimeout);
}
.options { this.searchTimeout = setTimeout(async () => {
&_wrapper { try {
justify-content: center; const suggestions: string[] = await (
align-items: center; await axios.get(`${URLs.stacjownikAPI}/api/get${type}Suggestions?name=${value}`)
} ).data;
&_content { this[`${type}Suggestions`] = suggestions;
padding: 0 1em; } catch (error) {
this[`${type}Suggestions`] = [];
flex-direction: column; }
}, 450);
.content_select { },
margin: 0 auto;
padding: 0; // Override keyMixin function
} onKeyDownFunction() {
this.showOptions = !this.showOptions;
.content_search {
justify-content: center; this.$nextTick(() => {
} if (this.showOptions) (this.$refs['button'] as HTMLButtonElement)?.focus();
} });
},
&_filters {
justify-content: center; focusEnd() {
console.log('focus end');
.journal-filter-option { },
margin: 0.25em 0.25em;
} onSorterChange(item: { id: string | number; value: string }) {
} this.sorterActive.id = item.id;
} this.sorterActive.dir = -1;
this.$emit('onSearchConfirm');
.search { },
&-box,
&-button { onFilterChange(filter: JournalTimetableFilter) {
margin: 0.5em 0 0 0; this.journalFilterActive = filter;
} this.$emit('onSearchConfirm');
},
&-box {
width: 100%; onInputClear(id: any) {
} this.searchersValues[id] = '';
this.$emit('onSearchConfirm');
&-button { },
width: 80%;
max-width: 300px; onSearchConfirm() {
} this.$emit('onSearchConfirm');
} },
}
</style> onSearchButtonConfirm() {
this.showOptions = false;
this.$emit('onSearchConfirm');
},
onResetButtonClick() {
this.$emit('onOptionsReset');
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/filters_options.scss';
</style>
+110
View File
@@ -0,0 +1,110 @@
<template>
<div class="journal-stats" v-show="!store.isOffline">
<div class="tabs">
<button
v-for="tab in data.tabs"
class="btn--filled"
:data-selected="tab.name == store.currentStatsTab && areStatsOpen"
:data-inactive="tab.inactive"
@click="onTabButtonClick(tab.name)"
>
{{ $t(tab.titlePath) }}
</button>
</div>
<div class="stats-tab" v-show="areStatsOpen">
<keep-alive>
<JournalDailyStats v-if="store.currentStatsTab == 'daily'" ref="dailyStatsComp" />
<JournalDriverStats v-else-if="store.currentStatsTab == 'driver'" />
</keep-alive>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, KeepAlive, onActivated, onDeactivated, reactive, Ref, ref, watch } from 'vue';
import { useStore } from '../../store/store';
import JournalDailyStats from './DailyStats.vue';
import JournalDriverStats from './JournalDriverStats.vue';
// Types
type TStatTab = 'daily' | 'driver';
// Variables
const store = useStore();
const dailyStatsComp: Ref<InstanceType<typeof JournalDailyStats> | null> = ref(null);
const areStatsOpen = ref(true);
const lastClickedTab = ref('daily');
let data = reactive({
tabs: [
{
name: 'daily',
titlePath: 'journal.daily-stats-title',
},
{
name: 'driver',
titlePath: 'journal.driver-stats-title',
inactive: true,
},
] as { name: TStatTab; titlePath: string; inactive?: boolean }[],
});
// Methods
function onTabButtonClick(tab: TStatTab) {
if (lastClickedTab.value == tab || !areStatsOpen.value) {
areStatsOpen.value = !areStatsOpen.value;
}
store.currentStatsTab = tab;
lastClickedTab.value = tab;
}
onActivated(() => {
dailyStatsComp.value?.startFetchingDailyStats();
});
onDeactivated(() => {
dailyStatsComp.value?.stopFetchingDailyStats();
});
watch(
computed(() => store.driverStatsData),
(statsData) => {
data.tabs[1].inactive = statsData ? false : true;
lastClickedTab.value = statsData ? 'driver' : 'daily';
if (statsData) areStatsOpen.value = true;
}
);
</script>
<style lang="scss" scoped>
@import '../../styles/JournalStats.scss';
@import '../../styles/variables.scss';
.tabs {
position: relative;
display: flex;
gap: 0.5em;
margin-bottom: 0.5em;
button {
font-weight: bold;
padding: 0.5em 0.75em;
&[data-inactive='true'] {
color: gray;
}
&[data-selected='true'] {
color: $accentCol;
}
}
}
</style>
@@ -1,439 +0,0 @@
<template>
<section class="journal-timetables">
<keep-alive>
<DriverStats v-if="statsCardOpen" @close-card="closeCard" />
</keep-alive>
<div class="journal-wrapper">
<div class="journal_top-bar">
<JournalOptions
@on-input-change="search"
@on-filter-change="search"
@on-sorter-change="search"
:sorter-option-ids="['timetableId', 'beginDate', 'distance', 'total-stops']"
:filters="journalTimetableFilters"
/>
</div>
<div class="journal-list">
<div class="list-wrapper" ref="scrollElement">
<transition name="warning" mode="out-in">
<div :key="historyDataStatus.status">
<Loading v-if="isDataLoading || isDataInit" />
<div v-else-if="isDataError" class="journal_warning error">
{{ $t('app.error') }}
</div>
<div class="journal_warning" v-else-if="historyList.length == 0">
{{ $t('app.no-result') }}
</div>
<ul v-else>
<transition-group name="journal-list-anim">
<li v-for="(item, i) in historyList" class="journal_item" :key="item.timetableId">
<div class="journal_item-top">
<span>
<span
tabindex="0"
@click="navigateToTimetable(item)"
@keydown.enter="navigateToTimetable(item)"
style="cursor: pointer"
>
<b class="text--primary">{{ item.trainCategoryCode }}&nbsp;</b>
<b>{{ item.trainNo }}</b>
| <span>{{ item.driverName }}</span> |
<span class="text--grayed">#{{ item.timetableId }}</span>
</span>
<div>
<b>{{ item.route.replace('|', ' - ') }}</b>
</div>
<hr style="margin: 0.25em 0" />
<div class="scenery-list">
<span
v-for="(scenery, i) in getSceneryList(item)"
:key="scenery.name"
:class="{ confirmed: scenery.confirmed }"
>
{{ i > 0 ? ' > ' : '' }} {{ scenery.name }}
</span>
</div>
<div class="schedule-dates">
<!-- Data odjazdu ze stacji początkowej -->
<b>{{ item.route.split('|')[0] }}:</b>
<s v-if="item.beginDate != item.scheduledBeginDate" class="text--grayed">
{{ localeTime(item.beginDate, $i18n.locale) }}
</s>
<span>{{ localeTime(item.scheduledBeginDate, $i18n.locale) }} </span>&bull;
<!-- Data przyjazdu na stację końcową / porzucenia -->
<b v-if="(item.fulfilled && item.terminated) || !item.terminated">
{{ item.route.split('|').slice(-1)[0] }}:
</b>
<i v-else>{{ $t('journal.timetable-abandoned') }} </i>
<s v-if="item.endDate != item.scheduledEndDate && item.terminated" class="text--grayed">
{{ localeTime(item.fulfilled ? item.endDate : item.scheduledEndDate, $i18n.locale) }}
</s>
<span
>{{ localeTime(item.fulfilled ? item.scheduledEndDate : item.endDate, $i18n.locale) }}
</span>
</div>
</span>
<b
class="journal_item-status"
:class="{
fulfilled: item.fulfilled || item.currentDistance >= item.routeDistance * 0.9,
terminated: item.terminated && !item.fulfilled,
active: !item.terminated,
}"
>
{{
!item.terminated
? $t('journal.timetable-active')
: item.fulfilled || item.currentDistance >= item.routeDistance * 0.9
? $t('journal.timetable-fulfilled')
: $t('journal.timetable-abandoned')
}}
</b>
</div>
<div style="margin-top: 1em">
<div>
{{ $t('journal.timetable-day') }} <b>{{ localeDay(item.beginDate, $i18n.locale) }}</b>
</div>
<!-- Nick dyżurnego -->
<div v-if="item.authorName">
<b class="text--grayed">{{ $t('journal.dispatcher-name') }}&nbsp;</b>
<router-link
class="dispatcher-link"
:to="`/journal/dispatchers?dispatcherName=${item.authorName}`"
>{{ item.authorName }}</router-link
>
</div>
</div>
<div style="margin-top: 1em">
<div>
<b>{{ $t('journal.route-length') }}</b>
{{ !item.fulfilled ? item.currentDistance + ' /' : '' }}
{{ item.routeDistance }} km
</div>
<div>
<b>{{ $t('journal.station-count') }}</b>
{{ item.confirmedStopsCount }} /
{{ item.allStopsCount }}
</div>
</div>
</li>
</transition-group>
</ul>
</div>
</transition>
</div>
</div>
<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>
</section>
</template>
<script lang="ts">
import { computed, defineComponent, JournalFilter, provide, reactive, Ref, ref } from 'vue';
import axios from 'axios';
import DriverStats from './DriverStats.vue';
import Loading from '../Global/Loading.vue';
import { journalTimetableFilters } from '../../data/journalFilters';
import dateMixin from '../../mixins/dateMixin';
import routerMixin from '../../mixins/routerMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { JournalFilterType } from '../../scripts/enums/JournalFilterType';
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
import { URLs } from '../../scripts/utils/apiURLs';
import { useStore } from '../../store/store';
import JournalOptions from './JournalOptions.vue';
const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`;
type JournalTimetableSearcher = {
[key in 'search-driver' | 'search-train']: string;
};
export default defineComponent({
components: { DriverStats, Loading, JournalOptions },
mixins: [dateMixin, routerMixin],
name: 'JournalTimetables',
props: {
timetableId: {
type: String,
},
},
data: () => ({
currentQuery: '',
scrollDataLoaded: true,
scrollNoMoreData: false,
showReturnButton: false,
statsCardOpen: false,
journalTimetableFilters,
}),
setup() {
const historyDataStatus: Ref<{ status: DataStatus; error: string | null }> = ref({
status: DataStatus.Loading,
error: null,
});
const sorterActive = ref({ id: 'timetableId', dir: -1 });
const journalFilterActive = ref(journalTimetableFilters[0]);
const searchersValues = reactive({
'search-train': '',
'search-driver': '',
} as JournalTimetableSearcher);
const countFromIndex = ref(0);
const countLimit = 15;
provide('searchersValues', searchersValues);
provide('sorterActive', sorterActive);
provide('journalFilterActive', journalFilterActive);
const scrollElement: Ref<HTMLElement | null> = ref(null);
return {
historyList: ref([]) as Ref<TimetableHistory[]>,
historyDataStatus,
isDataLoading: computed(() => historyDataStatus.value.status === DataStatus.Loading),
isDataError: computed(() => historyDataStatus.value.status === DataStatus.Error),
isDataInit: computed(() => historyDataStatus.value.status === DataStatus.Initialized),
sorterActive,
journalFilterActive,
searchersValues,
countFromIndex,
countLimit,
scrollElement,
maxCount: ref(15),
store: useStore(),
};
},
activated() {
window.addEventListener('scroll', this.handleScroll);
if (this.timetableId) {
this.searchersValues['search-train'] = `#${this.timetableId}`;
this.search();
}
},
mounted() {
if (!this.timetableId) this.search();
},
deactivated() {
window.removeEventListener('scroll', this.handleScroll);
},
methods: {
navigateToTimetable(historyItem: TimetableHistory) {
if (historyItem.terminated) return;
this.navigateTo('/trains', {
trainNo: historyItem.trainNo,
driverName: historyItem.driverName,
});
},
closeCard() {
this.statsCardOpen = false;
},
getSceneryList(historyItem: TimetableHistory) {
return historyItem.sceneriesString
.split('%')
.map((name, i) => ({ name, confirmed: i < historyItem.confirmedStopsCount }));
},
handleScroll() {
this.showReturnButton = window.scrollY > window.innerHeight;
const element = this.$refs.scrollElement as HTMLElement;
if (
element.getBoundingClientRect().bottom * 0.85 < window.innerHeight &&
this.scrollDataLoaded &&
!this.scrollNoMoreData &&
this.historyDataStatus.status == DataStatus.Loaded
)
this.addHistoryData();
},
scrollToTop() {
window.scrollTo({ top: 0 });
},
search() {
this.fetchHistoryData({
searchers: this.searchersValues,
filter: this.journalFilterActive,
});
this.scrollNoMoreData = false;
this.scrollDataLoaded = true;
},
async addHistoryData() {
this.scrollDataLoaded = false;
const countFrom = this.historyList.length;
const responseData: TimetableHistory[] = await (
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
).data;
if (!responseData) return;
if (responseData.length == 0) {
this.scrollNoMoreData = true;
return;
}
this.historyList.push(...responseData);
this.scrollDataLoaded = true;
},
async fetchHistoryData(
props: {
searchers?: JournalTimetableSearcher;
filter?: JournalFilter;
} = {}
) {
this.historyDataStatus.status = DataStatus.Loading;
const queries: string[] = [];
const driver = props.searchers?.['search-driver'].trim();
const train = props.searchers?.['search-train'].trim();
if (driver) queries.push(`driverName=${driver}`);
if (train) queries.push(train.startsWith('#') ? `timetableId=${train.replace('#', '')}` : `trainNo=${train}`);
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
if (this.sorterActive.id == 'distance') queries.push('sortBy=routeDistance');
else if (this.sorterActive.id == 'total-stops') queries.push('sortBy=allStopsCount');
else if (this.sorterActive.id == 'beginDate') queries.push('sortBy=beginDate');
else queries.push('sortBy=timetableId');
queries.push('countLimit=15');
switch (props.filter?.id) {
case JournalFilterType.abandoned:
queries.push('fulfilled=0', 'terminated=1');
break;
case JournalFilterType.active:
queries.push('terminated=0');
break;
case JournalFilterType.fulfilled:
queries.push('fulfilled=1');
break;
default:
break;
}
this.currentQuery = queries.join('&');
try {
const responseData: TimetableHistory[] = await (
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}`)
).data;
if (!responseData) {
this.historyDataStatus.status = DataStatus.Error;
this.historyDataStatus.error = 'Brak danych!';
return;
}
if (!responseData) return;
// Response data exists
this.historyList = responseData;
// Stats display
this.store.driverStatsName =
this.historyList.length > 0 && this.searchersValues['search-driver'].trim()
? this.historyList[0].driverName
: '';
this.historyDataStatus.status = DataStatus.Loaded;
} catch (error) {
this.historyDataStatus.status = DataStatus.Error;
this.historyDataStatus.error = 'Ups! Coś poszło nie tak!';
console.error(error);
}
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/JournalSection.scss';
.journal_item {
&-top {
display: flex;
justify-content: space-between;
padding: 0.2em 0;
.scenery-list {
span {
color: #adadad;
&.confirmed {
color: #a3eba3;
}
}
}
}
&-status {
&.terminated {
color: salmon;
}
&.fulfilled {
color: lightgreen;
}
&.active {
color: lightblue;
}
}
}
.dispatcher-link {
font-weight: bold;
}
</style>
@@ -0,0 +1,321 @@
<template>
<ul class="journal-list">
<li
v-for="{ timetable, sceneryList, ...item } in computedTimetableHistory"
class="journal_item"
:key="timetable.timetableId"
>
<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.id }}</span>
</span>
<span>
<b class="info-date">{{ localeDay(timetable.beginDate, $i18n.locale) }}</b>
<b
class="info-status"
:class="{
fulfilled: timetable.fulfilled || timetable.currentDistance >= timetable.routeDistance * 0.9,
terminated: timetable.terminated && !timetable.fulfilled,
active: !timetable.terminated,
}"
>
{{
!timetable.terminated
? $t('journal.timetable-active')
: timetable.fulfilled || timetable.currentDistance >= timetable.routeDistance * 0.9
? $t('journal.timetable-fulfilled')
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
}}
</b>
</span>
</div>
<div class="info-route">
<b>{{ timetable.route.replace('|', ' - ') }}</b>
</div>
<hr />
<div class="scenery-list">
<span v-for="(scenery, i) in sceneryList" :key="scenery.name" :class="{ confirmed: scenery.confirmed }">
<span v-if="i > 0"> &gt;</span>
{{ scenery.name }}
<!-- Data odjazdu ze stacji początkowej -->
<span v-if="i == 0" v-html="scenery.beginDateHTML"></span>
<!-- Data przyjazdu do stacji końcowej -->
<span v-if="i == sceneryList.length - 1" v-html="scenery.endDateHTML"> </span>
</span>
</div>
<!-- Status RJ -->
<div style="margin: 0.5em 0">
<span>
<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 -->
<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
v-if="timetable.stockString"
class="btn--option btn--show"
@click="item.showStock.value = !item.showStock.value"
>
{{ $t('journal.stock-info') }}
<img :src="getIcon(`arrow-${item.showStock.value ? 'asc' : 'desc'}`)" alt="Arrow" />
</button>
<div class="info-extended" v-if="timetable.stockString && item.showStock.value">
<hr />
<div>
<span class="badge info-badge">
<span>{{ $t('journal.stock-max-speed') }}</span>
<span>{{ timetable.maxSpeed }}km/h</span>
</span>
<span class="badge info-badge">
<span>{{ $t('journal.stock-length') }}</span>
<span>{{ timetable.stockLength }}m</span>
</span>
<span class="badge info-badge">
<span>{{ $t('journal.stock-mass') }}</span>
<span>{{ Math.floor(timetable.stockMass! / 1000) }}t</span>
</span>
</div>
<ul class="stock-list">
<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>
</li>
</ul>
</div>
</div>
</li>
</ul>
</template>
<script lang="ts">
import { defineComponent, PropType, ref } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import imageMixin from '../../mixins/imageMixin';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
export default defineComponent({
props: {
timetableHistory: {
type: Array as PropType<TimetableHistory[]>,
required: true,
},
},
mixins: [dateMixin, imageMixin, modalTrainMixin],
computed: {
computedTimetableHistory() {
return this.timetableHistory.map((timetable) => ({
timetable,
sceneryList: this.getSceneryList(timetable),
showStock: ref(false),
}));
},
},
methods: {
getSceneryList(timetable: TimetableHistory) {
return timetable.sceneriesString.split('%').map((name, i) => {
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 =
' (p. ' +
(timetable.endDate != timetable.scheduledEndDate && timetable.fulfilled
? `<s class='text--grayed'>${this.localeTime(
timetable.fulfilled ? timetable.endDate : timetable.scheduledEndDate,
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(
timetable.fulfilled ? timetable.scheduledEndDate : timetable.endDate,
this.$i18n.locale
)})`;
return { name, confirmed: i < timetable.confirmedStopsCount, beginDateHTML, endDateHTML, abandonedDateHTML };
});
},
showTimetable(timetable: TimetableHistory) {
if (timetable.terminated) return;
this.selectModalTrain(timetable.driverName + timetable.trainNo.toString());
},
onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = this.getImage('unknown.png');
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/variables.scss';
@import '../../styles/responsive.scss';
@import '../../styles/badge.scss';
@import '../../styles/JournalSection.scss';
hr {
margin: 0.25em 0;
}
.info {
&-date {
margin-right: 0.5em;
}
&-status {
padding: 0.05em 0.35em;
color: black;
&.terminated {
background-color: salmon;
}
&.fulfilled {
background-color: lightgreen;
}
&.active {
background-color: lightblue;
}
}
&-top {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
&-route {
margin: 0.25em 0;
}
&-extended {
margin-top: 0.5em;
}
}
ul.stock-list {
display: flex;
align-items: flex-end;
overflow: auto;
padding-bottom: 0.5em;
margin-top: 1em;
li > div {
text-align: center;
color: #aaa;
font-size: 0.9em;
}
}
.scenery-list {
color: #adadad;
span.confirmed {
color: #a3eba3;
}
}
.btn--show {
display: flex;
margin-top: 1em;
font-weight: bold;
padding: 0.2em 0.45em;
img {
height: 1.3em;
}
}
.info-badge {
span:last-child {
color: black;
background-color: $accentCol;
}
}
@include smallScreen {
.info-top {
flex-direction: column;
span {
margin: 0.1em auto;
}
}
.info-extended {
text-align: center;
}
.info-route {
display: flex;
justify-content: center;
}
.btn--show {
margin: 1em auto 0 auto;
}
}
</style>
@@ -1,116 +1,112 @@
<template> <template>
<section class="scenery-dispatchers-history scenery-section"> <section class="scenery-dispatchers-history scenery-section">
<Loading v-if="dataStatus != 2" /> <Loading v-if="dataStatus != 2" />
<div class="list-warning" v-else-if="dispatcherHistoryList.length == 0">{{ $t('scenery.history-list-empty') }}</div> <div class="list-warning" v-else-if="dispatcherHistoryList.length == 0">{{ $t('scenery.history-list-empty') }}</div>
<ul class="history-list" v-else> <ul class="history-list" v-else>
<li class="list-item" v-for="historyItem in dispatcherHistoryList"> <li class="list-item" v-for="historyItem in dispatcherHistoryList">
<div> <div>
<router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`"> <router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`">
<span class="text--grayed">#{{ historyItem.stationHash }}&nbsp;</span> <span class="text--grayed">#{{ historyItem.stationHash }}&nbsp;</span>
<b>{{ historyItem.dispatcherName }}</b> <b>{{ historyItem.dispatcherName }}</b>
</router-link> </router-link>
</div> </div>
<div v-if="historyItem.timestampTo"> <div v-if="historyItem.timestampTo">
<b>{{ $d(historyItem.timestampFrom) }}</b> <b>{{ $d(historyItem.timestampFrom) }}</b>
{{ timestampToString(historyItem.timestampFrom) }} {{ timestampToString(historyItem.timestampFrom) }}
- {{ timestampToString(historyItem.timestampTo) }} ({{ calculateDuration(historyItem.currentDuration) }}) - {{ timestampToString(historyItem.timestampTo) }} ({{ calculateDuration(historyItem.currentDuration) }})
</div> </div>
<div class="dispatcher-online" v-else> <div class="dispatcher-online" v-else>
{{ $t('journal.online-since') }} {{ $t('journal.online-since') }}
<b>{{ timestampToString(historyItem.timestampFrom) }}</b> <b>{{ timestampToString(historyItem.timestampFrom) }}</b>
({{ calculateDuration(historyItem.currentDuration) }}) ({{ calculateDuration(historyItem.currentDuration) }})
</div> </div>
</li> </li>
</ul> </ul>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import axios from 'axios';
import axios from 'axios'; import { defineComponent, PropType } from 'vue';
import { defineComponent, PropType } from 'vue'; import dateMixin from '../../mixins/dateMixin';
import dateMixin from '../../mixins/dateMixin'; import { DataStatus } from '../../scripts/enums/DataStatus';
import { DataStatus } from '../../scripts/enums/DataStatus'; import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData'; 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';
export default defineComponent({
export default defineComponent({ name: 'SceneryDispatchersHistory',
name: 'SceneryDispatchersHistory', mixins: [dateMixin],
mixins: [dateMixin], 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[],
dispatcherHistoryList: [] as DispatcherHistory[], dataStatus: DataStatus.Loading,
dataStatus: DataStatus.Loading, };
}; },
}, mounted() {
mounted() { this.fetchAPIData();
this.fetchAPIData(); },
}, methods: {
methods: { async fetchAPIData(countFrom = 0, countLimit = 30) {
async fetchAPIData(countFrom = 0, countLimit = 30) { try {
try { 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.dispatcherHistoryList = historyAPIData; this.dataStatus = DataStatus.Loaded;
this.dataStatus = DataStatus.Loaded; } catch (error) {
console.error(error);
console.log(this.dispatcherHistoryList); }
} catch (error) { },
console.error(error); },
} components: { Loading },
}, });
}, </script>
components: { Loading },
}); <style lang="scss" scoped>
</script> @import '../../styles/responsive.scss';
@import '../../styles/SceneryView/styles.scss';
<style lang="scss" scoped>
@import '../../styles/responsive.scss'; .history-list {
@import '../../styles/SceneryView/styles.scss'; padding: 0 0.5em;
}
.history-list {
padding: 0 0.5em; .list-item {
} display: flex;
flex-wrap: wrap;
.list-item { justify-content: space-between;
display: flex;
flex-wrap: wrap; text-align: left;
justify-content: space-between; background-color: #353535;
padding: 0.5em;
text-align: left; margin: 0.5em 0;
background-color: #353535;
padding: 0.5em; line-height: 1.5em;
margin: 0.5em 0; }
line-height: 1.5em; .dispatcher-online {
} color: springgreen;
}
.dispatcher-online {
color: springgreen; @include smallScreen {
} .history-list {
font-size: 1.1em;
@include smallScreen { }
.history-list { .list-item {
font-size: 1.2em; align-items: center;
} flex-direction: column;
.list-item { }
align-items: center; }
flex-direction: column; </style>
}
}
</style>
+3 -10
View File
@@ -1,8 +1,8 @@
<template> <template>
<section class="info-header"> <section class="info-header">
<div class="scenery-name"> <a class="scenery-name" :href="station.generalInfo?.url">
{{ station.name }} {{ station.name }}
</div> </a>
<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>
@@ -12,7 +12,6 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import Station from '../../scripts/interfaces/Station'; import Station from '../../scripts/interfaces/Station';
export default defineComponent({ export default defineComponent({
props: { props: {
station: { station: {
@@ -32,14 +31,9 @@ export default defineComponent({
position: relative; position: relative;
font-size: 3.5em; font-size: 3em;
padding: 0 0.5em;
text-transform: uppercase; text-transform: uppercase;
@include smallScreen() {
font-size: 2.75em;
}
} }
.scenery-hash { .scenery-hash {
@@ -47,4 +41,3 @@ export default defineComponent({
font-size: 1.2em; font-size: 1.2em;
} }
</style> </style>
+3 -5
View File
@@ -9,7 +9,7 @@
<b>{{ $t('availability.title') }}:</b> {{ $t(`availability.${station.generalInfo.availability}`) }} <b>{{ $t('availability.title') }}:</b> {{ $t(`availability.${station.generalInfo.availability}`) }}
<span v-if="station.generalInfo.reqLevel > -1"> <span v-if="station.generalInfo.reqLevel > -1">
- {{ $tc('scenery.req-level', station.generalInfo.reqLevel, { lvl: station.generalInfo.reqLevel }) }} - {{ $t('scenery.req-level', { lvl: station.generalInfo.reqLevel }, station.generalInfo.reqLevel) }}
</span> </span>
</span> </span>
@@ -33,7 +33,7 @@
<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> {{ $tc('scenery.authors-title', 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>
@@ -72,7 +72,6 @@ import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue';
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue'; import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
import Station from '../../scripts/interfaces/Station'; import Station from '../../scripts/interfaces/Station';
export default defineComponent({ export default defineComponent({
components: { components: {
SceneryInfoDispatcher, SceneryInfoDispatcher,
@@ -109,7 +108,7 @@ h3.section-header {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
font-size: 1.5em; font-size: 1.2em;
img { img {
width: 1.1em; width: 1.1em;
@@ -127,7 +126,6 @@ h3.section-header {
.info-general { .info-general {
margin-top: 1em; margin-top: 1em;
font-size: 1.1em;
} }
.general-list { .general-list {
@@ -1,7 +1,10 @@
<template> <template>
<section class="info-dispatcher"> <section class="info-dispatcher">
<div class="dispatcher" v-if="station.onlineInfo"> <div class="dispatcher" v-if="station.onlineInfo">
<span class="dispatcher_level" :style="calculateExpStyle(station.onlineInfo.dispatcherExp)"> <span
class="dispatcher_level"
:style="calculateExpStyle(station.onlineInfo.dispatcherExp, station.onlineInfo.dispatcherIsSupporter)"
>
{{ station.onlineInfo.dispatcherExp > 1 ? station.onlineInfo.dispatcherExp : 'L' }} {{ station.onlineInfo.dispatcherExp > 1 ? station.onlineInfo.dispatcherExp : 'L' }}
</span> </span>
@@ -64,6 +67,7 @@ export default defineComponent({
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5em;
.dispatcher { .dispatcher {
font-size: 2em; font-size: 2em;
@@ -82,17 +86,15 @@ export default defineComponent({
} }
&_name { &_name {
margin-right: 0.4em;
cursor: pointer; cursor: pointer;
margin-right: 0.25em;
} }
&_likes { &_likes {
img { img {
height: 0.7em; height: 0.7em;
margin-right: 0.25em; margin: 0 0.25em;
} }
margin-right: 1.5em;
} }
} }
@@ -68,7 +68,7 @@
<img <img
v-if="!station.generalInfo" v-if="!station.generalInfo"
class="icon-info" class="icon-info"
:src="getImage('unknown.png')" :src="getIcon('unknown')"
alt="icon-unknown" alt="icon-unknown"
:title="$t('desc.unknown')" :title="$t('desc.unknown')"
/> />
+176 -188
View File
@@ -13,21 +13,22 @@
</h3> </h3>
<div class="timetable-checkpoints" v-if="station && station.generalInfo?.checkpoints"> <div class="timetable-checkpoints" v-if="station && station.generalInfo?.checkpoints">
<button <span v-for="(cp, i) in station.generalInfo.checkpoints" :key="i">
v-for="cp in station.generalInfo.checkpoints" {{ (i > 0 && '&bull;') || '' }}
:key="cp.checkpointName"
class="checkpoint_item btn btn--text" <button
:class="{ current: selectedCheckpoint === cp.checkpointName }" :key="cp.checkpointName"
@click="selectCheckpoint(cp)" class="checkpoint_item"
> :class="{ current: selectedCheckpoint === cp.checkpointName }"
{{ cp.checkpointName }} @click="selectCheckpoint(cp)"
</button> >
{{ cp.checkpointName }}
</button>
</span>
</div> </div>
</div> </div>
<div class="timetable-list"> <div class="timetable-list">
<!-- <transition name="scenery-timetable-list-anim" mode="out-in"> -->
<!-- <div :key="store.dataStatuses.trains + selectedCheckpoint" class="scenery-timetable-list"> -->
<div style="padding-bottom: 5em" v-if="store.dataStatuses.trains == 0 && computedScheduledTrains.length == 0"> <div style="padding-bottom: 5em" v-if="store.dataStatuses.trains == 0 && computedScheduledTrains.length == 0">
<Loading /> <Loading />
</div> </div>
@@ -36,123 +37,120 @@
{{ $t('scenery.offline') }} {{ $t('scenery.offline') }}
</span> </span>
<span class="timetable-item empty" v-else-if="computedScheduledTrains.length == 0"> <span class="timetable-item empty" v-else-if="computedScheduledTrains.length == 0">
{{ $t('scenery.no-timetables') }} {{ $t('scenery.no-timetables') }}
</span> </span>
<div <transition-group name="timetables-anim">
class="timetable-item" <div
v-for="(scheduledTrain, i) in computedScheduledTrains" class="timetable-item"
:key="i + 1" v-for="(scheduledTrain, i) in computedScheduledTrains"
tabindex="0" :key="scheduledTrain.trainId"
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId)" tabindex="0"
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId)" @click.prevent.stop="selectModalTrain(scheduledTrain.trainId)"
> @keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId)"
<span class="timetable-general"> >
<span class="general-info"> <span class="timetable-general">
<span class="info-number"> <span class="general-info">
<strong>{{ scheduledTrain.category }}</strong> <span class="info-number">
{{ scheduledTrain.trainNo }} <strong>{{ scheduledTrain.category }}</strong>
{{ scheduledTrain.trainNo }}
<span class="g-tooltip" v-if="scheduledTrain.stopInfo.comments"> <span class="g-tooltip" v-if="scheduledTrain.stopInfo.comments">
<img :src="getIcon('warning')" /> <img :src="getIcon('warning')" />
<span class="content" v-html="scheduledTrain.stopInfo.comments"> </span> <span class="content" v-html="scheduledTrain.stopInfo.comments"> </span>
</span>
</span>
&nbsp;|&nbsp;
<span style="color: white">
{{ scheduledTrain.driverName }}
</span>
&nbsp;|&nbsp;
<span class="general-status">
<span :class="scheduledTrain.stopStatus">
{{ $t(`timetables.${scheduledTrain.stopStatus}`) }}
<span v-if="scheduledTrain.stopStatus == 'arriving'"> {{ scheduledTrain.prevStationName }}</span>
<span v-if="scheduledTrain.stopStatus.startsWith('departed')">{{
scheduledTrain.nextStationName
}}</span>
</span>
</span>
<div class="info-route">
<strong>{{ scheduledTrain.beginsAt }} - {{ scheduledTrain.terminatesAt }}</strong>
</div>
</span>
</span>
<span class="timetable-schedule">
<span class="schedule-arrival">
<span class="arrival-time begins" v-if="scheduledTrain.stopInfo.beginsHere">
{{ $t('timetables.begins') }}
</span>
<span class="arrival-time" v-else>
<div v-if="scheduledTrain.stopInfo.arrivalDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) }}</span>
</div>
<div v-else>
<div>
<s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.arrivalTimestamp)
}}</s>
</div>
<span>
{{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }}
({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : '' }}{{ scheduledTrain.stopInfo.arrivalDelay }})
</span> </span>
</div> </span>
</span> &nbsp;|&nbsp;
</span> <span>
{{ scheduledTrain.driverName }}
<span class="schedule-stop">
<span class="stop-time">
<span v-if="scheduledTrain.stopInfo.stopTime">
{{ scheduledTrain.stopInfo.stopTime }}
{{ scheduledTrain.stopInfo.stopType || 'pt' }}
</span> </span>
<span v-else>&nbsp;</span> <div class="info-route">
</span> <strong>{{ scheduledTrain.beginsAt }} - {{ scheduledTrain.terminatesAt }}</strong>
</div>
<span class="arrow"></span> <ScheduledTrainStatus :scheduledTrain="scheduledTrain" />
<span class="stop-line">
{{ scheduledTrain.arrivingLine }}
{{ scheduledTrain.arrivingLine && scheduledTrain.departureLine && '&gt;' }}
{{ scheduledTrain.departureLine }}
</span> </span>
</span> </span>
<span class="schedule-departure"> <span class="timetable-schedule">
<span class="departure-time terminates" v-if="scheduledTrain.stopInfo.terminatesHere"> <span class="schedule-arrival">
{{ $t('timetables.terminates') }} <span class="arrival-time begins" v-if="scheduledTrain.stopInfo.beginsHere">
</span> {{ $t('timetables.begins') }}
</span>
<span class="departure-time" v-else> <span class="arrival-time" v-else>
<div v-if="scheduledTrain.stopInfo.departureDelay == 0"> <div v-if="scheduledTrain.stopInfo.arrivalDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.departureTimestamp) }}</span> <span>{{ timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) }}</span>
</div>
<div v-else>
<div>
<s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.departureTimestamp)
}}</s>
</div> </div>
<div v-else>
<div>
<s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.arrivalTimestamp)
}}</s>
</div>
<span> <span>
{{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }} {{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }}
({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : '' ({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : ''
}}{{ scheduledTrain.stopInfo.departureDelay }}) }}{{ scheduledTrain.stopInfo.arrivalDelay }})
</span>
</div>
</span>
</span>
<span class="schedule-stop">
<span class="stop-time">
<span v-if="scheduledTrain.stopInfo.stopTime">
{{ scheduledTrain.stopInfo.stopTime }}
{{ scheduledTrain.stopInfo.stopType || 'pt' }}
</span> </span>
</div>
<span v-else>&nbsp;</span>
</span>
<span class="arrow"></span>
<span class="stop-line">
<span>
{{ scheduledTrain.arrivingLine }}
</span>
<span></span>
<span>
{{ scheduledTrain.departureLine }}
</span>
</span>
</span>
<span class="schedule-departure">
<span class="departure-time terminates" v-if="scheduledTrain.stopInfo.terminatesHere">
{{ $t('timetables.terminates') }}
</span>
<span class="departure-time" v-else>
<div v-if="scheduledTrain.stopInfo.departureDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.departureTimestamp) }}</span>
</div>
<div v-else>
<div>
<s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.departureTimestamp)
}}</s>
</div>
<span>
{{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }}
({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : ''
}}{{ scheduledTrain.stopInfo.departureDelay }})
</span>
</div>
</span>
</span> </span>
</span> </span>
</span> </div>
</div> </transition-group>
</div> </div>
<!-- </transition> -->
</section> </section>
</template> </template>
@@ -169,11 +167,13 @@ import Station from '../../scripts/interfaces/Station';
import { useStore } from '../../store/store'; 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 ScheduledTrain from '../../scripts/interfaces/ScheduledTrain';
export default defineComponent({ export default defineComponent({
name: 'SceneryTimetable', name: 'SceneryTimetable',
components: { SelectBox, Loading, TrainModal }, components: { SelectBox, Loading, TrainModal, ScheduledTrainStatus },
mixins: [dateMixin, routerMixin, imageMixin, modalTrainMixin], mixins: [dateMixin, routerMixin, imageMixin, modalTrainMixin],
@@ -182,11 +182,14 @@ export default defineComponent({
type: Object as PropType<Station>, type: Object as PropType<Station>,
required: true, required: true,
}, },
timetableOnly: {
type: Boolean,
},
}, },
data: () => ({ data: () => ({
listOpen: false, listOpen: false,
}), }),
setup(props) { setup(props) {
@@ -250,6 +253,10 @@ export default defineComponent({
selectCheckpoint(cp: { checkpointName: string }) { selectCheckpoint(cp: { checkpointName: string }) {
this.selectedCheckpoint = cp.checkpointName; this.selectedCheckpoint = cp.checkpointName;
}, },
showTimetableOnlyView() {
this.$router.push(`${this.$route.fullPath}&timetableOnly=1`);
},
}, },
mounted() { mounted() {
@@ -266,11 +273,21 @@ export default defineComponent({
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/variables.scss'; @import '../../styles/variables.scss';
// .scenery-timetable { .timetables-anim-move,
// height: 85vh; .timetables-anim-enter-active,
// max-height: 900px; .timetables-anim-leave-active {
// min-height: 450px; 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%;
@@ -293,7 +310,7 @@ export default defineComponent({
h3 { h3 {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 1.4em; font-size: 1.3em;
} }
} }
@@ -304,12 +321,14 @@ export default defineComponent({
&-item { &-item {
margin: 0.5em auto; margin: 0.5em auto;
padding: 0 0.5em; padding: 0.5em;
max-width: 1100px; max-width: 1100px;
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 0 0.5em; gap: 2em 0.5em;
overflow: hidden;
background: #353535; background: #353535;
@@ -324,9 +343,6 @@ export default defineComponent({
} }
&-general { &-general {
padding: 0.5rem 0;
border-radius: 10px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -337,6 +353,10 @@ export default defineComponent({
&-schedule { &-schedule {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(30px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(30px, 1fr));
width: 100%;
max-width: 400px;
margin: 0 auto;
} }
} }
@@ -351,17 +371,15 @@ export default defineComponent({
flex-wrap: wrap; flex-wrap: wrap;
font-size: 1.1em; font-size: 1.1em;
padding: 0.75em 0; padding: 0.75em 0;
.checkpoint_item {
&.current {
font-weight: bold;
color: $accentCol;
}
&:not(:last-child)::after { button.checkpoint_item {
margin: 0 0.5em; color: #aaa;
content: '•'; display: inline;
color: white; }
}
.checkpoint_item.current {
font-weight: bold;
color: $accentCol;
} }
} }
@@ -401,7 +419,6 @@ export default defineComponent({
} }
.info-route { .info-route {
margin-top: 0.5em;
width: 100%; width: 100%;
} }
@@ -417,38 +434,6 @@ export default defineComponent({
} }
} }
.general-status {
text-align: right;
span.arriving {
color: #ccc;
}
span.departed {
color: lime;
font-weight: bold;
&-away {
font-weight: bold;
color: #5ecc5e;
}
}
span.stopped {
color: #ffa600;
font-weight: bold;
}
span.online {
color: gold;
}
span.terminated {
color: salmon;
font-weight: bold;
}
}
.schedule { .schedule {
&-arrival, &-arrival,
&-stop, &-stop,
@@ -458,23 +443,40 @@ export default defineComponent({
align-items: center; align-items: center;
margin: 0 0.3rem; margin: 0 0.3rem;
font-size: 1.1em; font-size: 1.15em;
} }
&-stop { &-stop {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
font-size: 0.85em; font-size: 0.9em;
padding: 0.3em 0; padding: 0.3em 0;
.stop-line { .stop-line {
margin-top: 0.25em; 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 {
transform: translateY(-0.25em); position: absolute;
transform: translateY(-15px);
color: $accentCol;
} }
} }
} }
@@ -499,23 +501,9 @@ export default defineComponent({
} }
} }
@include smallScreen() { @include smallScreen {
.timetable { .timetable-item {
&-item { grid-template-columns: 1fr;
display: flex;
flex-direction: column;
align-items: center;
font-size: 1.05em;
}
&-general {
width: 100%;
}
&-schedule {
width: 100%;
}
} }
} }
</style> </style>
@@ -1,118 +1,112 @@
<template> <template>
<section class="scenery-timetables-history scenery-section"> <section class="scenery-timetables-history scenery-section">
<Loading v-if="dataStatus != 2" /> <Loading v-if="dataStatus != 2" />
<div class="list-warning" v-else-if="sceneryHistoryList.length == 0">{{ $t('scenery.history-list-empty') }}</div> <div class="list-warning" v-else-if="sceneryHistoryList.length == 0">{{ $t('scenery.history-list-empty') }}</div>
<ul class="history-list" v-else> <ul class="history-list" v-else>
<li class="list-item" v-for="historyItem in sceneryHistoryList"> <li class="list-item" v-for="historyItem in sceneryHistoryList">
<div> <div>
<b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b> <b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b>
{{ localeTime(historyItem.beginDate, $i18n.locale) }} {{ localeTime(historyItem.beginDate, $i18n.locale) }}
</div> </div>
<div> <div>
<router-link :to="`/journal/timetables?timetableId=${historyItem.timetableId}`"> <router-link :to="`/journal/timetables?timetableId=${historyItem.id}`">
<span class="text--grayed"> #{{ historyItem.timetableId }} </span> <span class="text--grayed"> #{{ historyItem.id }} </span>
<b class="text--primary">&nbsp;{{ historyItem.trainCategoryCode }} {{ historyItem.trainNo }}</b> <b class="text--primary">&nbsp;{{ historyItem.trainCategoryCode }} {{ historyItem.trainNo }}</b>
<div>{{ historyItem.driverName }}</div> <div>{{ historyItem.driverName }}</div>
</router-link> </router-link>
</div> </div>
<div>{{ historyItem.route.replace('|', ' -> ') }}</div> <div>{{ historyItem.route.replace('|', ' -> ') }}</div>
<!-- <div>{{ historyItem.routeDistance }} km</div> --> <!-- <div>{{ historyItem.routeDistance }} km</div> -->
<div> <div>
{{ $t('scenery.timetable-author-title') }}: {{ $t('scenery.timetable-author-title') }}:
<b v-if="historyItem.authorName">{{ historyItem.authorName }}</b> <b v-if="historyItem.authorName">{{ historyItem.authorName }}</b>
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i> <i v-else>{{ $t('scenery.timetable-author-unknown') }}</i>
</div> </div>
<!-- <div v-if="historyItem.authorId">{{ historyItem.authorName }}</div> --> <!-- <div v-if="historyItem.authorId">{{ historyItem.authorName }}</div> -->
</li> </li>
</ul> </ul>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import axios from 'axios';
import axios from 'axios'; import { defineComponent, PropType } from 'vue';
import { defineComponent, PropType } from 'vue'; import dateMixin from '../../mixins/dateMixin';
import dateMixin from '../../mixins/dateMixin'; import { DataStatus } from '../../scripts/enums/DataStatus';
import { DataStatus } from '../../scripts/enums/DataStatus'; import { TimetableHistory, SceneryTimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
import { TimetableHistory, SceneryTimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData'; 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';
export default defineComponent({
export default defineComponent({ name: 'SceneryTimetablesHistory',
name: 'SceneryTimetablesHistory', mixins: [dateMixin],
mixins: [dateMixin], 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[],
sceneryHistoryList: [] as TimetableHistory[], dataStatus: DataStatus.Loading,
dataStatus: DataStatus.Loading, };
}; },
}, mounted() {
mounted() { this.fetchAPIData();
this.fetchAPIData(); },
}, methods: {
methods: { async fetchAPIData(countFrom = 0, countLimit = 15) {
async fetchAPIData(countFrom = 0, countLimit = 15) { try {
try { const requestString = `${URLs.stacjownikAPI}/api/getSceneryTimetables?name=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`;
const requestString = `${URLs.stacjownikAPI}/api/getSceneryTimetables?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.sceneryHistoryList = historyAPIData.sceneryTimetables; this.dataStatus = DataStatus.Loaded;
this.dataStatus = DataStatus.Loaded; } catch (error) {
} catch (error) { console.error(error);
console.error(error); }
} },
}, },
}, components: { Loading },
components: { Loading }, });
}); </script>
</script>
<style lang="scss" scoped>
<style lang="scss" scoped> @import '../../styles/responsive.scss';
@import '../../styles/responsive.scss'; @import '../../styles/SceneryView/styles.scss';
@import '../../styles/SceneryView/styles.scss';
.list-warning {
.list-warning { padding: 1em 0.5em;
padding: 1em 0.5em; background-color: #444;
background-color: #444; font-size: 1.2em;
font-size: 1.2em; }
}
.history-list {
.history-list { padding: 0 0.5em;
padding: 0 0.5em; }
}
.list-item {
.list-item { display: grid;
display: grid; grid-template-columns: 1fr 2fr 2fr 1fr;
grid-template-columns: 1fr 2fr 2fr 1fr; gap: 1em;
gap: 1em; align-items: center;
align-items: center;
background-color: #353535;
background-color: #353535; padding: 0.5em;
padding: 0.5em; margin: 0.5em 0;
margin: 0.5em 0;
line-height: 1.5em;
line-height: 1.5em; }
}
@include smallScreen {
@include smallScreen { .list-item {
.history-list { grid-template-columns: 1fr 1fr;
font-size: 1.1em; }
} }
.list-item { </style>
grid-template-columns: 1fr 1fr;
font-size: 1.05em;
}
}
</style>
@@ -0,0 +1,88 @@
<template>
<div class="general-status">
<span :class="scheduledTrain.stopStatus">
<span v-if="scheduledTrain.stopStatus == 'arriving'">
<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>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import ScheduledTrain from '../../scripts/interfaces/ScheduledTrain';
export default defineComponent({
props: {
scheduledTrain: {
type: Object as PropType<ScheduledTrain>,
required: true,
},
},
});
</script>
<style lang="scss" scoped>
.general-status {
margin-top: 0.5em;
span.arriving {
color: #ccc;
}
span.departed {
color: lime;
font-weight: bold;
&-away {
font-weight: bold;
color: #5ecc5e;
}
}
span.stopped {
color: #ffa600;
font-weight: bold;
}
span.online {
color: gold;
}
span.terminated {
color: salmon;
font-weight: bold;
}
}
</style>
+30 -70
View File
@@ -1,23 +1,12 @@
<template> <template>
<div class="filter-option option"> <button class="btn--action" :class="option.section" :data-selected="option.value" @click="handleChange">
<label> {{ $t(`filters.${option.id}`) }}
<input </button>
type="checkbox"
:name="option.name"
:defaultValue="option.defaultValue"
:id="option.id"
v-model="option.value"
@change="handleChange"
/>
<span v-if="option.id != 'troll'" :class="option.section + (option.value ? ' checked' : '')"
>{{ option.id != 'troll' ? $t(`filters.${option.id}`) : 'ARKADIA ZDRÓJ' }}
</span>
</label>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
interface FilterOption { interface FilterOption {
id: string; id: string;
@@ -34,29 +23,26 @@ export default defineComponent({
required: true, required: true,
}, },
}, },
emits: ['optionChange'],
setup() {
return {
filterStore: useStationFiltersStore(),
};
},
methods: { methods: {
handleChange() { handleChange() {
if (this.option.name == 'troll') { this.option.value = !this.option.value;
location.href = 'https://www.youtube.com/watch?v=HIcSWuKMwOw';
return;
}
this.$emit('optionChange', { this.filterStore.changeFilterValue({
name: this.option.name, name: this.option.name,
value: this.option.value, value: !this.option.value,
}); });
}, },
}, },
setup() {
return {};
},
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/option.scss';
$accessCol: #e03b07; $accessCol: #e03b07;
$controlCol: #0085ff; $controlCol: #0085ff;
$signalCol: #bf7c00; $signalCol: #bf7c00;
@@ -64,63 +50,49 @@ $statusCol: #349b32;
$saveCol: #28a826; $saveCol: #28a826;
$routesCol: #9049c0; $routesCol: #9049c0;
.option span { button {
font-size: 0.9em; width: 100%;
&.checked { padding: 0.4em;
border-radius: 0.4em;
&:focus-visible {
outline: 1px solid white;
}
&[data-selected='true'] {
&.access { &.access {
background-color: $accessCol; background-color: $accessCol;
box-shadow: 0 0 6px 1px $accessCol;
&::before {
box-shadow: 0 0 6px 1px $accessCol;
}
} }
&.control { &.control {
background-color: $controlCol; background-color: $controlCol;
box-shadow: 0 0 6px 1px $controlCol;
&::before {
box-shadow: 0 0 6px 1px $controlCol;
}
} }
&.signals { &.signals {
background-color: $signalCol; background-color: $signalCol;
box-shadow: 0 0 6px 1px $signalCol;
&::before {
box-shadow: 0 0 6px 1px $signalCol;
}
} }
&.routes { &.routes {
background-color: $routesCol; background-color: $routesCol;
box-shadow: 0 0 6px 1px $routesCol;
&::before {
box-shadow: 0 0 6px 1px $routesCol;
}
} }
&.status { &.status {
background-color: $statusCol; background-color: $statusCol;
box-shadow: 0 0 6px 1px $statusCol;
&::before {
box-shadow: 0 0 6px 1px $statusCol;
}
} }
&.save { &.save {
background-color: $saveCol; background-color: $saveCol;
box-shadow: 0 0 6px 1px $saveCol;
&::before {
box-shadow: 0 0 6px 1px $saveCol;
}
} }
&.troll { &.troll {
background-color: firebrick; background-color: firebrick;
box-shadow: 0 0 6px 1px firebrick;
&::before {
box-shadow: 0 0 6px 1px firebrick;
}
} }
&.mode { &.mode {
@@ -129,18 +101,6 @@ $routesCol: #9049c0;
font-weight: 500; font-weight: 500;
} }
&::before {
position: absolute;
content: '';
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 0.5em;
}
} }
} }
</style> </style>
+127 -114
View File
@@ -1,20 +1,35 @@
<template> <template>
<section class="filter-card" v-click-outside="closeCard"> <section class="filter-card" v-click-outside="closeCard" @keydown.esc="closeCard">
<div class="card_btn"> <div class="card_controls">
<button class="btn btn--option" @click="toggleCard"> <button class="btn--filled btn--image" @click="toggleCard">
<img class="button_icon" :src="getIcon('filter2')" alt="icon-filter" /> <img class="button_icon" :src="getIcon('filter2')" alt="filter icon" />
{{ $t('options.filters') }} {{ $t('options.filters') }} [F]
</button> </button>
<label for="scenery-search">
<input
id="scenery-search"
list="sceneries"
:placeholder="$t('sceneries.scenery-search')"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
v-model="chosenSearchScenery"
/>
<datalist id="sceneries">
<option v-for="scenery in sortedStationList" :value="scenery.name"></option>
</datalist>
</label>
</div> </div>
<transition name="card-anim"> <transition name="card-anim">
<div class="card" v-if="isVisible"> <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>
<section class="card_options"> <section class="card_options">
<filter-option <filter-option
v-for="(option, i) in inputs.options" v-for="(option, i) in filterStore.inputs.options"
:option="option" :option="option"
:key="i" :key="i"
@optionChange="handleChange" @optionChange="handleChange"
@@ -23,7 +38,7 @@
<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">
<button @click="subHour">-</button> <button class="btn--action" @click="subHour">-</button>
<span>{{ <span>{{
minimumHours == 0 minimumHours == 0
? $t('filters.now') ? $t('filters.now')
@@ -31,7 +46,7 @@
? minimumHours + $t('filters.hour') ? minimumHours + $t('filters.hour')
: $t('filters.no-limit') : $t('filters.no-limit')
}}</span> }}</span>
<button @click="addHour">+</button> <button class="btn--action" @click="addHour">+</button>
</span> </span>
</section> </section>
@@ -42,11 +57,13 @@
name="authors" name="authors"
v-model="authorsInputValue" v-model="authorsInputValue"
@input="handleAuthorsInput" @input="handleAuthorsInput"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
/> />
</section> </section>
<section class="card_sliders"> <section class="card_sliders">
<div class="slider" v-for="(slider, i) in inputs.sliders" :key="i"> <div class="slider" v-for="(slider, i) in filterStore.inputs.sliders" :key="i">
<input <input
class="slider-input" class="slider-input"
type="range" type="range"
@@ -65,23 +82,13 @@
</section> </section>
<section class="card_actions"> <section class="card_actions">
<div> <div class="action-buttons">
<filter-option <button class="btn--action" style="width: 100%" @click="saveFilters" :data-selected="saveOptions">
@optionChange="saveFilters" {{ $t('filters.save') }}
:option="{ </button>
id: 'save',
name: 'save', <button class="btn--action" @click="resetFilters">{{ $t('filters.reset') }}</button>
section: 'mode', <button class="btn--action" @click="closeCard">{{ $t('filters.close') }}</button>
value: saveOptions,
defaultValue: true,
}"
/>
</div>
<div>
<action-button class="outlined" @click="resetFilters">
{{ $t('filters.reset') }}
</action-button>
<action-button class="outlined" @click="closeCard">{{ $t('filters.close') }}</action-button>
</div> </div>
</section> </section>
</div> </div>
@@ -91,11 +98,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, inject } from 'vue'; import { defineComponent, inject } from 'vue';
import inputData from '../../data/options.json';
import imageMixin from '../../mixins/imageMixin'; import imageMixin from '../../mixins/imageMixin';
import keyMixin from '../../mixins/keyMixin';
import routerMixin from '../../mixins/routerMixin';
import StorageManager from '../../scripts/managers/storageManager'; import StorageManager from '../../scripts/managers/storageManager';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import ActionButton from '../Global/ActionButton.vue'; import ActionButton from '../Global/ActionButton.vue';
@@ -103,12 +111,9 @@ import FilterOption from './FilterOption.vue';
export default defineComponent({ export default defineComponent({
components: { ActionButton, FilterOption }, components: { ActionButton, FilterOption },
emits: ['changeFilterValue', 'invertFilters', 'resetFilters'], mixins: [imageMixin, keyMixin, routerMixin],
mixins: [imageMixin],
data: () => ({ data: () => ({
inputs: { ...inputData },
saveOptions: false, saveOptions: false,
STORAGE_KEY: 'options_saved', STORAGE_KEY: 'options_saved',
@@ -118,15 +123,18 @@ export default defineComponent({
currentRegion: { id: '', value: '' }, currentRegion: { id: '', value: '' },
delayInputTimer: -1, delayInputTimer: -1,
chosenSearchScenery: '',
}), }),
setup() { setup() {
const isVisible = inject('isFilterCardVisible'); const isVisible = inject('isFilterCardVisible');
const store = useStore(); const store = useStore();
const filterStore = useStationFiltersStore();
return { return {
isVisible, isVisible,
store, store,
filterStore,
}; };
}, },
@@ -142,9 +150,39 @@ export default defineComponent({
this.currentRegion = this.store.region; this.currentRegion = this.store.region;
}, },
computed: {
sortedStationList() {
return this.store.stationList
.filter((s) => s.name.toLocaleLowerCase().includes(this.chosenSearchScenery.toLocaleLowerCase()))
.sort((s1, s2) => (s1.name > s2.name ? 1 : -1));
},
},
watch: {
chosenSearchScenery(value: string) {
const chosenStation = this.store.stationList.find(({ name }) => name == value);
if (chosenStation) {
this.$router.push(`/scenery?station=${chosenStation.name.replace(/ /g, '_')}`);
this.chosenSearchScenery = '';
}
},
isVisible(value: boolean) {
this.$nextTick(() => {
if (value) (this.$refs['cardEl'] as HTMLDivElement).focus();
});
},
},
methods: { methods: {
// Override keyMixin function
onKeyDownFunction() {
this.isVisible = !this.isVisible;
},
handleChange(change: { name: string; value: boolean }) { handleChange(change: { name: string; value: boolean }) {
this.$emit('changeFilterValue', { this.filterStore.changeFilterValue({
name: change.name, name: change.name,
value: !change.value, value: !change.value,
}); });
@@ -155,7 +193,7 @@ export default defineComponent({
handleInput(e: Event) { handleInput(e: Event) {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
this.$emit('changeFilterValue', { this.filterStore.changeFilterValue({
name: target.name, name: target.name,
value: target.value, value: target.value,
}); });
@@ -172,7 +210,7 @@ export default defineComponent({
}, },
changeNumericFilterValue(name: string, value: number, saveToStorage = false) { changeNumericFilterValue(name: string, value: number, saveToStorage = false) {
this.$emit('changeFilterValue', { this.filterStore.changeFilterValue({
name, name,
value, value,
}); });
@@ -192,17 +230,8 @@ export default defineComponent({
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true); this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
}, },
invertFilters() { saveFilters() {
this.inputs.options.forEach((option) => { this.saveOptions = !this.saveOptions;
option.value = !option.value;
StorageManager.setBooleanValue(option.name, option.value);
});
this.$emit('invertFilters');
},
saveFilters(change: { value: any }) {
this.saveOptions = change.value;
if (!this.saveOptions) { if (!this.saveOptions) {
StorageManager.unregisterStorage(this.STORAGE_KEY); StorageManager.unregisterStorage(this.STORAGE_KEY);
@@ -211,28 +240,16 @@ export default defineComponent({
StorageManager.registerStorage(this.STORAGE_KEY); StorageManager.registerStorage(this.STORAGE_KEY);
this.inputs.options.forEach((option) => StorageManager.setBooleanValue(option.name, option.value)); this.filterStore.inputs.options.forEach((option) => StorageManager.setBooleanValue(option.name, !option.value));
this.filterStore.inputs.sliders.forEach((slider) => StorageManager.setNumericValue(slider.name, slider.value));
this.inputs.sliders.forEach((slider) => StorageManager.setNumericValue(slider.name, slider.value));
}, },
resetFilters() { resetFilters() {
this.inputs.options.forEach((option) => {
option.value = option.defaultValue;
StorageManager.setBooleanValue(option.name, option.value);
});
this.inputs.sliders.forEach((slider) => {
slider.value = slider.defaultValue;
StorageManager.setNumericValue(slider.name, slider.value);
});
this.authorsInputValue = ''; this.authorsInputValue = '';
this.minimumHours = 0; this.minimumHours = 0;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true); this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
this.filterStore.resetFilters();
this.$emit('resetFilters');
}, },
closeCard() { closeCard() {
@@ -264,28 +281,24 @@ export default defineComponent({
} }
.card { .card {
&_btn { &_controls {
button { display: flex;
display: flex; gap: 0.5em;
align-items: center;
padding: 0.5em 1em; input {
border-radius: 0.75em 0.75em 0 0; border-radius: 0.5em 0.5em 0 0;
height: 100%;
font-weight: bold;
}
img {
width: 1.3em;
margin-right: 0.25em;
} }
} }
&_content { &_content {
display: grid; display: flex;
grid-template-rows: 70px 1fr 100px 50px auto; flex-direction: column;
min-height: 0; gap: 1em;
max-height: 100vh;
max-height: 90vh;
padding: 1em;
} }
&_title { &_title {
@@ -293,8 +306,6 @@ export default defineComponent({
font-weight: 700; font-weight: 700;
color: $accentCol; color: $accentCol;
margin: 0.5em 0;
text-align: center; text-align: center;
} }
@@ -342,32 +353,18 @@ export default defineComponent({
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 1.15em; font-size: 1.2em;
margin-top: 0.5em;
color: $accentCol; span {
font-weight: bold; min-width: 120px;
} font-weight: bold;
span {
min-width: 100px;
}
button {
border: none;
outline: none;
background: none;
padding: 0 0.45em;
cursor: pointer;
color: white;
font-size: 1.35em;
&:focus,
&:hover {
color: $accentCol; color: $accentCol;
} }
button {
padding: 0.2em 0.6em;
}
} }
} }
@@ -389,22 +386,33 @@ export default defineComponent({
input { input {
width: 100%; width: 100%;
padding: 0.5em; padding: 0.5em;
border: 1px solid white;
} }
} }
&_actions { &_actions {
margin-top: 1em; .filter-option {
max-width: 50%;
display: flex; margin: 0 auto;
flex-direction: column;
align-items: center;
button {
margin: 1em 0.25em;
} }
.option { .action-buttons {
font-size: 1.1em; display: flex;
gap: 0.5em;
width: 100%;
margin-top: 0.5em;
button {
width: 50%;
margin: 0 auto;
padding: 0.5em;
&[data-selected='true'] {
background-color: lightgreen;
color: black;
}
}
} }
} }
} }
@@ -435,8 +443,13 @@ export default defineComponent({
min-width: 25%; min-width: 25%;
max-width: 120px; max-width: 120px;
&:focus-visible ~ * {
color: gold;
}
&::-webkit-slider-thumb { &::-webkit-slider-thumb {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none;
height: 20px; height: 20px;
width: 20px; width: 20px;
+29 -15
View File
@@ -100,7 +100,10 @@
</td> </td>
<td class="station_dispatcher-exp"> <td class="station_dispatcher-exp">
<span v-if="station.onlineInfo" :style="calculateExpStyle(station.onlineInfo.dispatcherExp)"> <span
v-if="station.onlineInfo"
:style="calculateExpStyle(station.onlineInfo.dispatcherExp, station.onlineInfo.dispatcherIsSupporter)"
>
{{ 2 > station.onlineInfo.dispatcherExp ? 'L' : station.onlineInfo.dispatcherExp }} {{ 2 > station.onlineInfo.dispatcherExp ? 'L' : station.onlineInfo.dispatcherExp }}
</span> </span>
</td> </td>
@@ -182,7 +185,7 @@
</td> </td>
<td class="station_info" v-else> <td class="station_info" v-else>
<img class="icon-info" :src="getImage('unknown.png')" alt="icon-unknown" :title="$t('desc.unknown')" /> <img class="icon-info" :src="getIcon('unknown')" alt="icon-unknown" :title="$t('desc.unknown')" />
</td> </td>
<td class="station_users" :class="{ inactive: !station.onlineInfo }"> <td class="station_users" :class="{ inactive: !station.onlineInfo }">
@@ -230,6 +233,7 @@ import stationInfoMixin from '../../mixins/stationInfoMixin';
import styleMixin from '../../mixins/styleMixin'; import styleMixin from '../../mixins/styleMixin';
import { DataStatus } from '../../scripts/enums/DataStatus'; import { DataStatus } from '../../scripts/enums/DataStatus';
import Station from '../../scripts/interfaces/Station'; import Station from '../../scripts/interfaces/Station';
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';
@@ -239,48 +243,58 @@ export default defineComponent({
type: Array as () => Station[], type: Array as () => Station[],
required: true, required: true,
}, },
sorterActive: {
type: Object as () => {
index: number;
dir: number;
},
required: true,
},
setFocusedStation: { type: Function, required: true },
changeSorter: { type: Function, required: true },
}, },
components: { Loading },
mixins: [styleMixin, dateMixin, stationInfoMixin, returnBtnMixin, imageMixin], mixins: [styleMixin, dateMixin, stationInfoMixin, returnBtnMixin, imageMixin],
data: () => ({ data: () => ({
headIds: ['station', 'min-lvl', 'status', 'dispatcher', 'dispatcher-lvl', 'routes', 'general'], headIds: ['station', 'min-lvl', 'status', 'dispatcher', 'dispatcher-lvl', 'routes', 'general'],
headIconsIds: ['user', 'spawn', 'timetable'], headIconsIds: ['user', 'spawn', 'timetable'],
lastSelectedStationName: '', lastSelectedStationName: '',
}), }),
computed: {
sorterActive() {
return this.stationFiltersStore.sorterActive;
},
},
setup() { setup() {
const store = useStore(); const store = useStore();
const stationFiltersStore = useStationFiltersStore();
const isDataLoaded = computed(() => { const isDataLoaded = computed(() => {
return store.dataStatuses.sceneries != DataStatus.Loading; return store.dataStatuses.sceneries != DataStatus.Loading;
}); });
return { return {
isDataLoaded, isDataLoaded,
stationFiltersStore,
}; };
}, },
methods: { methods: {
setScenery(name: string) { setScenery(name: string) {
const station = this.stations.find((station) => station.name === name); const station = this.stations.find((station) => station.name === name);
if (!station) return; if (!station) return;
this.lastSelectedStationName = station.name; this.lastSelectedStationName = station.name;
this.$router.push({ this.$router.push({
name: 'SceneryView', name: 'SceneryView',
query: { station: station.name.replaceAll(' ', '_') }, query: { station: station.name.replaceAll(' ', '_') },
}); });
}, },
openForumSite(e: Event, url: string | undefined) { openForumSite(e: Event, url: string | undefined) {
if (!url) return; if (!url) return;
e.preventDefault(); e.preventDefault();
window.open(url, '_blank'); window.open(url, '_blank');
}, },
changeSorter(i: number) {
this.stationFiltersStore.changeSorter(i);
},
}, },
components: { Loading },
}); });
</script> </script>
@@ -289,7 +303,7 @@ export default defineComponent({
@import '../../styles/variables.scss'; @import '../../styles/variables.scss';
@import '../../styles/icons.scss'; @import '../../styles/icons.scss';
$rowCol: #4b4b4b; $rowCol: #424242;
.change-anim { .change-anim {
&-enter-active, &-enter-active,
@@ -328,7 +342,7 @@ table {
} }
thead tr { thead tr {
background-color: $primaryCol; background-color: $bgCol;
} }
thead th { thead th {
@@ -338,7 +352,7 @@ table {
min-width: 75px; min-width: 75px;
padding: 0.5em; padding: 0.5em;
background-color: $primaryCol; background-color: $bgCol;
white-space: pre-wrap; white-space: pre-wrap;
cursor: pointer; cursor: pointer;
+308 -287
View File
@@ -1,287 +1,308 @@
<template> <template>
<div class="train-info" tabindex="0"> <div class="train-info" tabindex="0">
<section class="train-route"> <section class="train-route">
<div class="train_general"> <div class="train_general">
<span> <span>
<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">
<span class="train-badge twr" v-if="train.timetableData?.TWR">TWR</span> <span class="train-badge twr" v-if="train.timetableData?.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">SKR</span>
</span> </span>
<strong v-if="train.timetableData">{{ train.timetableData.category }}&nbsp;</strong> <strong class="timetable-category" v-if="train.timetableData">
<strong>{{ train.trainNo }}</strong> {{ train.timetableData.category }}
<span>&nbsp;| {{ train.driverName }}&nbsp;</span> </strong>
</span> <strong class="train-number">&nbsp;{{ train.trainNo }}</strong>
</div> |
<span class="train-driver" :class="{ supporter: train.isSupporter }">{{ train.driverName }}</span>
<div class="timetable_route" v-if="train.timetableData">
<strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong> <b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
<img </span>
v-if="getSceneriesWithComments(train.timetableData).length > 0" </div>
class="image-warning"
:src="getIcon('warning')" <div class="timetable_route" v-if="train.timetableData">
:title="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(train.timetableData)})`" <strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong>
/> <img
</div> v-if="getSceneriesWithComments(train.timetableData).length > 0"
class="image-warning"
<hr style="margin: 0.25em 0" /> :src="getIcon('warning')"
:title="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(train.timetableData)})`"
<div class="timetable_stops" v-if="train.timetableData"> />
<span v-if="train.timetableData.followingStops.length > 2"> </div>
{{ $t('trains.via-title') }}
<span v-html="displayStopList(train.timetableData.followingStops)"></span> <hr style="margin: 0.25em 0" />
</span>
</div> <div class="timetable_stops" v-if="train.timetableData">
<span v-if="train.timetableData.followingStops.length > 2">
<div class="timetable_progress" style="margin-top: 0.5em" v-if="train.timetableData"> {{ $t('trains.via-title') }}
<!-- <span> </span> --> <span v-html="displayStopList(train.timetableData.followingStops)"></span>
<span class="timetable_progress-bar"> </span>
<!-- {{ confirmedPercentage(train.timetableData.followingStops) }}%&nbsp; --> </div>
<span class="bar-bg"></span>
<span <div class="timetable_progress" style="margin-top: 0.5em" v-if="train.timetableData">
class="bar-fg" <!-- <span> </span> -->
:style="{ width: `${Math.floor(confirmedPercentage(train.timetableData.followingStops))}%` }" <span class="timetable_progress-bar">
></span> <!-- {{ confirmedPercentage(train.timetableData.followingStops) }}%&nbsp; -->
</span> <span class="bar-bg"></span>
<span
<span class="timetable_progress-distance"> class="bar-fg"
&nbsp; {{ currentDistance(train.timetableData.followingStops) }} km / :style="{ width: `${Math.floor(confirmedPercentage(train.timetableData.followingStops))}%` }"
<span class="text--primary"> {{ train.timetableData.routeDistance }} km </span> ></span>
| </span>
<span v-html="currentDelay(train.timetableData.followingStops)"></span>
</span> <span class="timetable_progress-distance">
&nbsp; {{ currentDistance(train.timetableData.followingStops) }} km /
<div class="train-status-badges"> <span class="text--primary"> {{ train.timetableData.routeDistance }} km </span>
<div v-if="!train.currentStationHash" class="train-badge offline">{{ $t('trains.scenery-offline') }}</div> |
<div v-if="!train.online" class="train-badge offline">Offline {{ lastSeenMessage(train.lastSeen) }}</div> <span v-html="currentDelay(train.timetableData.followingStops)"></span>
</div> </span>
</div>
<div class="train-status-badges">
<div class="driver_position text--grayed" style="margin-top: 0.25em"> <div v-if="!train.currentStationHash" class="train-badge offline">{{ $t('trains.scenery-offline') }}</div>
{{ displayTrainPosition(train) }} <div v-if="!train.online" class="train-badge offline">Offline {{ lastSeenMessage(train.lastSeen) }}</div>
</div> </div>
</section> </div>
<section class="train-stats"> <div class="driver_position text--grayed" style="margin-top: 0.25em">
<div> {{ displayTrainPosition(train) }}
<img :src="train.locoURL" loading="lazy" alt="Loco image not found" @error="onImageError" /> </div>
</div> </section>
<div class="text--grayed"> <section class="train-stats">
{{ train.locoType }} <div>
<span v-if="train.cars.length > 0"> <img :src="train.locoURL" loading="lazy" alt="Loco image not found" @error="onImageError" />
&nbsp;&bull; {{ $t('trains.cars') }}: </div>
<span class="count">{{ train.cars.length }}</span>
</span> <div class="text--grayed">
</div> {{ train.locoType }}
<span v-if="train.cars.length > 0">
<div> &nbsp;&bull; {{ $t('trains.cars') }}:
<span v-for="(stat, i) in STATS.main" :key="stat.name"> <span class="count">{{ train.cars.length }}</span>
<span v-if="i > 0"> &bull; </span> </span>
<span>{{ `${~~((train as any)[stat.name] * (stat.multiplier || 1))}${stat.unit}` }} </span> </div>
</span>
</div> <div>
</section> <span v-for="(stat, i) in STATS.main" :key="stat.name">
</div> <span v-if="i > 0"> &bull; </span>
</template> <span>{{ `${~~((train as any)[stat.name] * (stat.multiplier || 1))}${stat.unit}` }} </span>
</span>
<script lang="ts"> </div>
import { defineComponent } from 'vue'; </section>
import imageMixin from '../../mixins/imageMixin'; </div>
import trainInfoMixin from '../../mixins/trainInfoMixin'; </template>
import Train from '../../scripts/interfaces/Train';
<script lang="ts">
export default defineComponent({ import { defineComponent } from 'vue';
props: { import imageMixin from '../../mixins/imageMixin';
train: { import trainInfoMixin from '../../mixins/trainInfoMixin';
type: Object as () => Train, import Train from '../../scripts/interfaces/Train';
required: true,
}, export default defineComponent({
props: {
extended: { train: {
type: Boolean, type: Object as () => Train,
default: true, required: true,
}, },
},
extended: {
mixins: [trainInfoMixin, imageMixin], type: Boolean,
}); default: true,
</script> },
},
<style lang="scss" scoped>
@import '../../styles/responsive.scss'; mixins: [trainInfoMixin, imageMixin],
});
.image-warning { </script>
height: 1em;
<style lang="scss" scoped>
margin-left: 0.5em; @import '../../styles/responsive.scss';
}
.image-warning {
.train-stats { height: 1em;
display: flex;
justify-content: center; margin-left: 0.5em;
align-content: center; }
flex-direction: column; .train-stats {
text-align: center; display: flex;
justify-content: center;
img { align-content: center;
margin: 0.5em 0;
width: 12em; flex-direction: column;
} text-align: center;
}
img {
.train-info { margin: 0.5em 0;
display: grid; width: 12em;
grid-template-columns: 2fr 1fr; }
grid-template-rows: 1fr; }
padding: 1em; .train-info {
display: grid;
background-color: #1a1a1a; grid-template-columns: 2fr 1fr;
gap: 0.5em; grid-template-rows: 1fr;
}
padding: 1em;
.timetable-id {
margin-right: 0.3em; background-color: #1a1a1a;
color: #d2d2d2; gap: 0.5em;
} }
.timetable_stops { .timetable-id {
font-size: 0.75em; margin-right: 0.3em;
} color: #d2d2d2;
}
.train_general {
display: flex; .warning-timeout {
align-items: center; background-color: #be3728;
flex-wrap: wrap;
} display: inline-block;
.train-status-badges { text-align: center;
display: flex;
flex-wrap: wrap; width: 1.25em;
} height: 1.25em;
border-radius: 50%;
.train-badge {
padding: 0.15em 0.35em; margin-left: 0.25em;
margin-right: 0.3em; }
font-weight: bold; .timetable_stops {
font-size: 0.75em;
font-size: 0.9em; }
&.twr { .train_general {
background-color: var(--clr-twr); display: flex;
} align-items: center;
flex-wrap: wrap;
&.skr { }
background-color: var(--clr-skr); .train-status-badges {
} display: flex;
flex-wrap: wrap;
&.offline { }
background-color: #b83b2d;
} .train-badge {
} padding: 0.15em 0.35em;
margin-right: 0.3em;
.timetable_route {
display: flex; font-weight: bold;
align-items: center;
font-size: 0.9em;
margin-top: 0.5em;
} &.twr {
background-color: var(--clr-twr);
.timetable_warnings { }
color: black;
} &.skr {
background-color: var(--clr-skr);
.timetable_progress { }
display: flex;
align-items: center; &.offline {
flex-wrap: wrap; background-color: #b83b2d;
} }
}
.timetable_progress-bar {
position: relative; .train-driver {
&.supporter {
width: 6em; color: orange;
height: 1em; text-shadow: orange 0 0 5px;
margin: 0.5em 0; }
}
.bar-fg,
.bar-bg { .timetable_route {
position: absolute; display: flex;
height: 1em; align-items: center;
width: 100%;
margin-top: 0.5em;
left: 0; }
}
.timetable_warnings {
.bar-fg { color: black;
background-color: springgreen; }
}
.timetable_progress {
.bar-bg { display: flex;
background-color: #5b5b5b; align-items: center;
} flex-wrap: wrap;
} }
.timetable_progress-distance { .timetable_progress-bar {
margin-right: 0.25em; position: relative;
}
width: 6em;
.comments { height: 1em;
display: flex; margin: 0.5em 0;
align-items: center;
.bar-fg,
font-size: 0.9em; .bar-bg {
position: absolute;
margin-top: 1em; height: 1em;
width: 100%;
img {
margin-right: 0.5em; left: 0;
} }
}
.bar-fg {
@include smallScreen() { background-color: springgreen;
.train-info { }
grid-template-columns: 1fr;
gap: 1em 0; .bar-bg {
text-align: center; background-color: #5b5b5b;
}
font-size: 1.15em; }
}
.timetable_progress-distance {
.train-stats { margin-right: 0.25em;
font-size: 1.1em; }
img { .comments {
display: none; display: flex;
} align-items: center;
}
font-size: 0.9em;
.train_general {
justify-content: center; margin-top: 1em;
}
img {
.train-status-badges { margin-right: 0.5em;
justify-content: center; }
} }
.timetable_route { @include smallScreen() {
justify-content: center; .train-info {
} grid-template-columns: 1fr;
gap: 1em 0;
.timetable_progress { text-align: center;
justify-content: center;
} font-size: 1.15em;
}
.comments {
flex-direction: column; .train-stats {
justify-content: center; font-size: 1.1em;
}
img {
margin: 0 0 0.5em 0; .train_general {
} justify-content: center;
} }
}
</style> .train-status-badges {
justify-content: center;
}
.timetable_route {
justify-content: center;
}
.timetable_progress {
justify-content: center;
}
.comments {
flex-direction: column;
justify-content: center;
img {
margin: 0 0 0.5em 0;
}
}
}
</style>
+153 -212
View File
@@ -1,267 +1,208 @@
<template> <template>
<div class="train-options"> <div class="filters-options" @keydown.esc="showOptions = false">
<div class="options_wrapper"> <div class="bg" v-if="showOptions" @click="showOptions = false"></div>
<div class="options_content">
<div class="content_select">
<select-box
:itemList="translatedSorterOptions"
:defaultItemIndex="0"
@selected="changeSorter"
:prefix="$t('trains.sorter-prefix')"
/>
</div>
<div class="content_search"> <button class="filter-button btn--filled btn--image" @click="toggleShowOptions" ref="button">
<div class="search-box"> <img :src="getIcon('filter2')" alt="Open filters" />
<input class="search-input" v-model="searchedTrain" :placeholder="$t('trains.search-train')" /> {{ $t('options.filters') }} [F]
<span class="active-indicator" v-if="currentOptionsActive"></span>
</button>
<img class="search-exit" :src="getIcon('exit')" alt="exit-icon" @click="() => (searchedTrain = '')" /> <transition name="options-anim">
<div class="options_wrapper" v-if="showOptions">
<div class="options_content">
<h1 class="option-title">{{ $t('options.search-title') }}</h1>
<div class="search_content">
<div class="search-box">
<input
class="search-input"
ref="initFocusedElement"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
:placeholder="$t(`options.search-train`)"
v-model="searchedTrain"
/>
<button class="search-exit">
<img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear('train')" />
</button>
</div>
<div class="search-box">
<input
class="search-input"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
:placeholder="$t(`options.search-driver`)"
v-model="searchedDriver"
/>
<button class="search-exit">
<img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear('driver')" />
</button>
</div>
</div> </div>
<div class="search-box"> <h1 class="option-title">{{ $t('options.sort-title') }}</h1>
<input class="search-input" v-model="searchedDriver" :placeholder="$t('trains.search-driver')" /> <div class="options_sorters">
<div v-for="opt in translatedSorterOptions">
<button
class="sort-option btn--option"
:data-selected="opt.id == sorterActive.id"
@click="onSorterChange(opt)"
>
{{ opt.value.toUpperCase() }}
</button>
</div>
</div>
<img class="search-exit" :src="getIcon('exit')" alt="exit-icon" @click="() => (searchedDriver = '')" /> <h1 class="option-title" v-if="trainFilterList.length != 0">{{ $t('options.filter-title') }}</h1>
<div class="options_filters">
<div class="filter-option" v-for="filter in trainFilterList">
<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>
</div> </transition>
<div class="filters">
<span
:class="{ active: filter.isActive }"
class="filter"
v-for="filter in filterList"
:key="filter.id"
tabindex="0"
@contextmenu="
(e) => {
e.preventDefault();
return false;
}
"
@click.left="toggleFilter(filter)"
@keydown.enter="toggleFilter(filter)"
@click.right="setFilterOnly(filter)"
@keydown.space="setFilterOnly(filter)"
>
{{ $t(`trains.filter-${filter.id}`) }}
</span>
<span class="filter reset-btn" @click="resetFilters" tabindex="0">
{{ $t('trains.filter-reset') }}
</span>
</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, inject, TrainFilter } from 'vue'; import { defineComponent, inject, PropType } from 'vue';
import { useI18n } from 'vue-i18n';
import imageMixin from '../../mixins/imageMixin'; import imageMixin from '../../mixins/imageMixin';
import keyMixin from '../../mixins/keyMixin';
import { TrainFilter } from '../../types/Trains/TrainOptionsTypes';
import ActionButton from '../Global/ActionButton.vue';
import SelectBox from '../Global/SelectBox.vue'; import SelectBox from '../Global/SelectBox.vue';
export default defineComponent({ export default defineComponent({
components: { SelectBox }, components: { SelectBox, ActionButton },
emits: ['changeSearchedTrain', 'changeSearchedDriver', 'changeSorter'], mixins: [imageMixin, keyMixin],
mixins: [imageMixin],
setup() { props: {
const { t } = useI18n(); sorterOptionIds: {
type: Array as PropType<Array<string>>,
required: true,
},
const sorterOptions = [ currentOptionsActive: {
{ type: Boolean,
id: 'distance', default: false,
value: 'kilometraż', },
}, },
{
id: 'progress',
value: 'przebyta trasa',
},
{
id: 'delay',
value: 'opóźnienie',
},
{
id: 'mass',
value: 'masa',
},
{
id: 'speed',
value: 'prędkość',
},
{
id: 'length',
value: 'długość',
},
];
let filterList = inject('filterList') as TrainFilter[];
const translatedSorterOptions = computed(() =>
sorterOptions.map(({ id }) => ({
id,
value: t(`trains.option-${id}`),
}))
);
data() {
return { return {
translatedSorterOptions, showOptions: false,
searchedTrain: inject('searchedTrain') as string, lastSelectedFilter: null as TrainFilter | null,
searchedDriver: inject('searchedDriver') as string,
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
filterList,
}; };
}, },
setup() {
return {
searchedTrain: inject('searchedTrain') as string,
searchedDriver: inject('searchedDriver') as string,
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
trainFilterList: inject('filterList') as TrainFilter[],
};
},
computed: {
translatedSorterOptions() {
return this.$props.sorterOptionIds.map((id) => ({
id,
value: this.$t(`options.sort-${id}`),
}));
},
},
methods: { methods: {
changeSorter(item: { id: string | number; value: string }) { // Override keyMixin function
onKeyDownFunction() {
this.toggleShowOptions();
},
toggleShowOptions() {
this.showOptions = !this.showOptions;
this.$nextTick(() => {
if (this.showOptions) (this.$refs['button'] as HTMLButtonElement)?.focus();
});
},
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;
}, },
toggleFilter(filter: TrainFilter) { onFilterChange(filter: TrainFilter) {
// if (this.lastSelectedFilter?.id === filter.id)
// this.trainFilterList.forEach((tf) => (tf.isActive = filter.id === tf.id));
filter.isActive = !filter.isActive; filter.isActive = !filter.isActive;
this.lastSelectedFilter = filter;
}, },
setFilterOnly(filter: TrainFilter) { clearAllFilters() {
this.filterList.forEach((f) => (f.isActive = f.id == filter.id)); this.trainFilterList.forEach((filter) => {
filter.isActive = false;
});
}, },
resetFilters() { resetAllFilters() {
this.filterList.forEach((f) => (f.isActive = true)); this.trainFilterList.forEach((filter) => {
this.searchedDriver = ""; filter.isActive = true;
this.searchedTrain = ""; });
},
onInputClear(id: 'driver' | 'train') {
if (id == 'driver') this.searchedDriver = '';
if (id == 'train') this.searchedTrain = '';
}, },
}, },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive'; @import '../../styles/filters_options.scss';
.train-options { .search_content > div {
@include smallScreen() { margin: 0.5em auto;
width: 100%;
}
} }
.options { .search_content > button {
&_wrapper {
display: flex;
}
&_content {
display: flex;
flex-wrap: wrap;
.content_search,
.content_select {
display: flex;
align-items: center;
flex-wrap: wrap;
padding: 0.25em 0.25em 0 0;
}
}
}
.search {
&-box {
position: relative;
background: #333;
border-radius: 0.5em;
min-width: 200px;
margin-right: 0.25em;
}
&-input {
border: none;
min-width: 100%;
padding: 0.35em 0.5em;
}
&-exit {
position: absolute;
cursor: pointer;
top: 50%;
right: 10px;
transform: translateY(-50%);
width: 1em;
}
}
.filters {
display: flex; display: flex;
flex-wrap: wrap; justify-content: center;
margin: 0 auto;
}
margin-top: 0.5em; .filter-option {
button {
color: white;
font-weight: bold;
@include smallScreen() { &[data-disabled='true'] {
justify-content: center; color: #888;
}
} }
} }
.filter { .filter-actions {
background: #333; display: flex;
padding: 0.2em 0.25em; gap: 0.5em;
margin: 0.25em 0.25em 0 0; width: 100%;
font-weight: bold;
cursor: pointer; margin-top: 1em;
color: gray;
&.active { button {
color: var(--clr-primary);
}
&.reset-btn {
color: salmon;
}
}
@include smallScreen() {
.journal-options {
width: 100%; width: 100%;
} }
.options {
&_wrapper {
justify-content: center;
}
&_content {
padding: 0 1em;
flex-direction: column;
.content_select {
margin: 0 auto;
padding: 0;
}
.content_search {
justify-content: center;
}
}
}
.search {
&-box,
&-button {
margin: 0.5em 0 0 0;
}
&-box {
width: 100%;
}
&-button {
width: 80%;
max-width: 300px;
}
}
} }
</style> </style>
@@ -1,144 +0,0 @@
<template>
<section class="filter-card" v-click-outside="closeCard">
<div class="card_btn">
<action-button @click="toggleCard">
<img class="button_icon" :src="getIcon('filter2')" alt="icon-filter" />
<p>{{ $t('options.filters') }}</p>
</action-button>
</div>
<transition name="card-anim">
<div class="card_content card" v-if="isVisible">
<div class="card_exit" @click="closeCard"></div>
<div class="options_wrapper">
<div class="options_content">
<div class="content_select">
<select-box
:itemList="translatedSorterOptions"
:defaultItemIndex="0"
@selected="changeSorter"
:prefix="$t('trains.sorter-prefix')"
/>
</div>
<div class="content_search">
<div class="search-box">
<input class="search-input" v-model="searchedTrain" :placeholder="$t('trains.search-train')" />
<img class="search-exit" :src="getIcon('exit')" alt="exit-icon" @click="() => (searchedTrain = '')" />
</div>
<div class="search-box">
<input class="search-input" v-model="searchedDriver" :placeholder="$t('trains.search-driver')" />
<img class="search-exit" :src="getIcon('exit')" alt="exit-icon" @click="() => (searchedDriver = '')" />
</div>
</div>
</div>
</div>
<section class="card_actions flex">
<action-button class="outlined">
{{ $t('filters.reset') }}
</action-button>
<action-button class="outlined" @click="closeCard">{{ $t('filters.close') }}</action-button>
</section>
</div>
</transition>
</section>
</template>
<script lang="ts">
import inputData from "../../data/options.json";
import { TrainFilter, computed, defineComponent, inject } from 'vue';
import { useI18n } from 'vue-i18n';
import SelectBox from '../Global/SelectBox.vue';
import ActionButton from '../Global/ActionButton.vue';
import { sorterOptions } from '../../data/trainOptions';
import imageMixin from "../../mixins/imageMixin";
export default defineComponent({
components: { ActionButton, SelectBox },
emits: ['changeFilterValue', 'invertFilters', 'resetFilters'],
mixins: [imageMixin],
data: () => ({
inputs: { ...inputData },
}),
setup() {
const isVisible = inject('isTrainOptionsCardVisible');
const { t } = useI18n();
let filterList = inject('filterList') as TrainFilter[];
const translatedSorterOptions = computed(() =>
sorterOptions.map(({ id }) => ({
id,
value: t(`trains.option-${id}`),
}))
);
return {
translatedSorterOptions,
searchedTrain: inject('searchedTrain') as string,
searchedDriver: inject('searchedDriver') as string,
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
filterList,
isVisible,
};
},
methods: {
closeCard() {
this.isVisible = false;
},
toggleCard() {
this.isVisible = !this.isVisible;
},
changeSorter(item: { id: string | number; value: string }) {
this.sorterActive.id = item.id;
this.sorterActive.dir = -1;
},
toggleFilter(filter: TrainFilter) {
filter.isActive = !filter.isActive;
},
setFilterOnly(filter: TrainFilter) {
this.filterList.forEach((f) => (f.isActive = f.id == filter.id));
},
resetFilters() {
this.filterList.forEach((f) => (f.isActive = true));
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive';
@import '../../styles/card';
.card {
section {
margin: 0.5em 0;
}
&_title {
font-size: 2em;
font-weight: 700;
color: $accentCol;
margin: 0.5em 0;
text-align: center;
}
}
</style>
+8 -5
View File
@@ -60,7 +60,9 @@
<b>{{ stop.stopNameRAW }} </b>: <span v-html="stop.comments"></span> <b>{{ stop.stopNameRAW }} </b>: <span v-html="stop.comments"></span>
</div> </div>
<span v-if="stop.departureLine == train.timetableData!.followingStops[i + 1].arrivalLine && !/sbl/gi.test(stop.departureLine!)"> <span
v-if="stop.departureLine == train.timetableData!.followingStops[i + 1].arrivalLine && !/sbl/gi.test(stop.departureLine!)"
>
{{ stop.departureLine }} {{ stop.departureLine }}
</span> </span>
@@ -175,10 +177,6 @@ $stopNameClr: #22a8d1;
.train-schedule { .train-schedule {
padding: 0 0.25em; padding: 0 0.25em;
@include smallScreen() {
font-size: 1.1em;
}
} }
.train-stock { .train-stock {
@@ -198,6 +196,11 @@ ul.stock-list {
color: #aaa; color: #aaa;
font-size: 0.9em; font-size: 0.9em;
} }
img {
max-height: 60px;
max-width: 320px;
}
} }
.schedule-wrapper { .schedule-wrapper {
+51 -53
View File
@@ -2,13 +2,22 @@
<div class="train-table"> <div class="train-table">
<transition name="anim" mode="out-in"> <transition name="anim" mode="out-in">
<div :key="store.dataStatuses.trains"> <div :key="store.dataStatuses.trains">
<Loading v-if="trains.length == 0 && store.dataStatuses.trains == 0" /> <div class="table-info" v-if="store.isOffline">
{{ $t('app.offline') }}
</div>
<div class="table-info no-trains" v-if="trains.length == 0 && store.dataStatuses.trains != 0"> <Loading v-else-if="trains.length == 0 && store.dataStatuses.trains == 0" />
<div class="table-info no-trains" v-else-if="trains.length == 0 && store.dataStatuses.trains != 0">
{{ $t('trains.no-trains') }} {{ $t('trains.no-trains') }}
</div> </div>
<ul class="train-list"> <!-- <div class="timeouts-warning" v-if="trainNumbersWithTimeouts.length == 0">
<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"
@@ -25,39 +34,30 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, inject, Ref, computed } from 'vue'; import { computed, defineComponent, inject, PropType, Ref } from 'vue';
import modalTrainMixin from '../../mixins/modalTrainMixin'; import modalTrainMixin from '../../mixins/modalTrainMixin';
import returnBtnMixin from '../../mixins/returnBtnMixin'; import returnBtnMixin from '../../mixins/returnBtnMixin';
import Train from '../../scripts/interfaces/Train'; import Train from '../../scripts/interfaces/Train';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import TrainModal from '../Global/TrainModal.vue';
import TrainInfo from './TrainInfo.vue'; import TrainInfo from './TrainInfo.vue';
import TrainSchedule from './TrainSchedule.vue';
export default defineComponent({ export default defineComponent({
components: { components: { Loading, TrainInfo },
TrainSchedule,
TrainInfo,
Loading,
TrainModal,
},
mixins: [returnBtnMixin, modalTrainMixin],
props: { props: {
trains: { trains: {
type: Array as () => Train[], type: Array as PropType<Train[]>,
required: true, required: true,
}, },
}, },
mixins: [returnBtnMixin, modalTrainMixin],
setup(props) { setup(props) {
const store = useStore(); const store = useStore();
const searchedTrain = inject('searchedTrain') as Ref<string>; const searchedTrain = inject('searchedTrain') as Ref<string>;
const searchedDriver = inject('searchedDriver') as Ref<string>; const searchedDriver = inject('searchedDriver') as Ref<string>;
const currentTrains = computed(() => { const currentTrains = computed(() => {
return props.trains; return props.trains;
}); });
@@ -67,53 +67,32 @@ export default defineComponent({
searchedDriver, searchedDriver,
currentTrains, currentTrains,
store, store,
sorterActive: inject('sorterActive') as {
sorterActive: inject('sorterActive') as { id: string | number; dir: number }, id: string | number;
dir: number;
},
distanceLimitExceeded: computed( distanceLimitExceeded: computed(
() => props.trains.findIndex(({ timetableData }) => timetableData && timetableData.routeDistance > 200) != -1 () => 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) {
this.searchedDriver = query.driverName.toString(); this.searchedDriver = query.driverName.toString();
this.searchedTrain = query.trainNo.toString(); this.searchedTrain = query.trainNo.toString();
setTimeout(() => { setTimeout(() => {
this.selectModalTrain(query.driverName + <string>query.trainNo); this.selectModalTrain(query.driverName! + query.trainNo!.toString());
}, 20); }, 20);
} }
}, },
methods: {
enter(el: HTMLElement) {
const maxHeight = getComputedStyle(el).height;
el.style.height = '0px';
getComputedStyle(el);
setTimeout(() => {
el.style.height = maxHeight;
}, 10);
},
afterEnter(el: HTMLElement) {
el.style.height = 'auto';
},
leave(el: HTMLElement) {
el.style.height = getComputedStyle(el).height;
setTimeout(() => {
el.style.height = '0px';
}, 10);
},
},
}); });
</script> </script>
@@ -139,11 +118,10 @@ export default defineComponent({
text-align: center; text-align: center;
padding: 1em 0; padding: 1em 0;
margin: 1em 0;
font-size: 1.5em; font-size: 1.5em;
background: #333; background: #1a1a1a;
} }
img.train-image { img.train-image {
@@ -156,12 +134,32 @@ img.train-image {
background: var(--clr-warning); background: var(--clr-warning);
} }
.timeouts-warning {
background-color: #333;
font-weight: bold;
font-size: 1.05em;
margin-bottom: 0.5em;
padding: 0.5em;
}
.warning-timeout {
background-color: #be3728;
color: white;
display: inline-block;
text-align: center;
width: 1.25em;
height: 1.25em;
border-radius: 50%;
}
.train { .train {
&-list { &-list {
overflow: auto; overflow: auto;
margin-top: 1em;
@include smallScreen() { @include smallScreen() {
width: 100%; width: 100%;
} }
@@ -0,0 +1,28 @@
import { JournalFilterType } from "../../scripts/enums/JournalFilterType";
import { JournalTimetableFilter } from "../../types/Journal/JournalTimetablesTypes";
export const journalTimetableFilters: JournalTimetableFilter[] = [
{
id: JournalFilterType.all,
filterSection: 'timetable-status',
isActive: true,
},
{
id: JournalFilterType.active,
filterSection: 'timetable-status',
isActive: false,
},
{
id: JournalFilterType.fulfilled,
filterSection: 'timetable-status',
isActive: false,
},
{
id: JournalFilterType.abandoned,
filterSection: 'timetable-status',
isActive: false,
},
];
@@ -1,60 +1,60 @@
import { TrainFilter } from "vue"; import { TrainFilterType } from '../../scripts/enums/TrainFilterType';
import { TrainFilterType } from "../scripts/enums/TrainFilterType"; import { TrainFilter } from '../../types/Trains/TrainOptionsTypes';
export const trainFilters: TrainFilter[] = [ export const trainFilters: TrainFilter[] = [
{ {
id: TrainFilterType.twr, id: TrainFilterType.twr,
isActive: true, isActive: true,
}, },
{ {
id: TrainFilterType.skr, id: TrainFilterType.skr,
isActive: true, isActive: true,
}, },
{ {
id: TrainFilterType.passenger, id: TrainFilterType.passenger,
isActive: true, isActive: true,
}, },
{ {
id: TrainFilterType.freight, id: TrainFilterType.freight,
isActive: true, isActive: true,
}, },
{ {
id: TrainFilterType.other, id: TrainFilterType.other,
isActive: true, isActive: true,
}, },
{ {
id: TrainFilterType.comments, id: TrainFilterType.comments,
isActive: true, isActive: true,
}, },
{ {
id: TrainFilterType.noTimetable, id: TrainFilterType.noTimetable,
isActive: true, isActive: true,
}, },
]; ];
export const sorterOptions = [ export const sorterOptions = [
{ {
id: 'distance', id: 'distance',
value: 'kilometraż', value: 'kilometraż',
}, },
{ {
id: 'progress', id: 'progress',
value: 'przebyta trasa', value: 'przebyta trasa',
}, },
{ {
id: 'delay', id: 'delay',
value: 'opóźnienie', value: 'opóźnienie',
}, },
{ {
id: 'mass', id: 'mass',
value: 'masa', value: 'masa',
}, },
{ {
id: 'speed', id: 'speed',
value: 'prędkość', value: 'prędkość',
}, },
{ {
id: 'length', id: 'length',
value: 'długość', value: 'długość',
} },
]; ];
-30
View File
@@ -1,30 +0,0 @@
import { JournalFilter } from "vue";
import { JournalFilterType } from "../scripts/enums/JournalFilterType";
export const journalTimetableFilters: JournalFilter[] = [
{
id: JournalFilterType.all,
filterSection: "timetable-status",
isActive: true
},
{
id: JournalFilterType.active,
filterSection: "timetable-status",
isActive: false
},
{
id: JournalFilterType.fulfilled,
filterSection: "timetable-status",
isActive: false
},
{
id: JournalFilterType.abandoned,
filterSection: "timetable-status",
isActive: false
},
]
export const journalDispatcherFilters: JournalFilter[] = []
-9
View File
@@ -198,15 +198,6 @@
"section": "status", "section": "status",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
},
{
"id": "troll",
"name": "troll",
"iconName": "",
"section": "troll",
"value": true,
"defaultValue": true
} }
], ],
"sliders": [ "sliders": [
+99 -58
View File
@@ -1,4 +1,7 @@
{ {
"general": {
"and": " and "
},
"app": { "app": {
"sceneries": "SCENERIES", "sceneries": "SCENERIES",
"trains": "TRAINS", "trains": "TRAINS",
@@ -8,15 +11,18 @@
"error": "An error occured while loading data!", "error": "An error occured while loading data!",
"no-result": "No results for current search!", "no-result": "No results for current search!",
"migration-warning": "Stacjownik services will be unavailable 2/06/2022 between 1-3am (CEST time) due to the migration of API hostings!", "migration-warning": "Stacjownik services will be unavailable 2/06/2022 between 1-3am (CEST time) due to the migration of API hostings!",
"migration-confirm": "Roger that!" "migration-confirm": "Roger that!",
"offline": "App is in the offline mode!"
}, },
"update": { "update": {
"title": "New Stacjownik version 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!",
"release-link": "Click here to browse version changelog (GitHub)", "release-link": "Click here to browse version changelog (GitHub)",
"confirm-button": "Understood!" "confirm-button": "UPDATE NOW",
"later-button": "LATER"
}, },
"data-status": { "data-status": {
"S1-offline": "<b>S1 signal</b> <br> The app is working in offline mode!",
"S1a-connection": "<b>S1a signal</b> <br> Cannot connect with Stacjownik API service!", "S1a-connection": "<b>S1a signal</b> <br> Cannot connect with Stacjownik API service!",
"S1a-sceneries": "<b>S1a signal</b> <br> Cannot load online stations data!", "S1a-sceneries": "<b>S1a signal</b> <br> Cannot load online stations data!",
"S2": "<b>S2 signal</b> <br> All data loaded successfully!", "S2": "<b>S2 signal</b> <br> All data loaded successfully!",
@@ -72,7 +78,52 @@
}, },
"options": { "options": {
"filters": "FILTERS", "filters": "FILTERS",
"donate": "DONATE" "donate": "DONATE",
"search-button": "Search",
"reset-button": "Reset",
"sort-title": "SORT BY:",
"filter-title": "FILTER BY:",
"search-title": "SEARCH:",
"search-train-no": "Train no. / #",
"search-train": "Train no.",
"search-driver": "Driver name",
"search-dispatcher": "Dispatcher name",
"search-station": "Scenery name",
"search-author": "Timetable author name",
"search-date": "Timetable date (CEST / GMT+2)",
"sort-mass": "mass",
"sort-speed": "speed",
"sort-length": "length",
"sort-distance": "distance",
"sort-timetable": "train no.",
"sort-progress": "route progress",
"sort-delay": "current delay",
"sort-total-stops": "total stops",
"sort-beginDate": "date",
"sort-timetableId": "timetable ID",
"sort-timestampFrom": "date",
"sort-duration": "duration",
"filter-comments": "COMMENTS",
"filter-twr": "TWR",
"filter-skr": "SKR",
"filter-passenger": "PASSENGER",
"filter-freight": "FREIGHT",
"filter-other": "OTHER",
"filter-noTimetable": "NO TIMETABLE",
"filter-reset": "RESET FILTERS",
"filter-clear": "CLEAR FILTERS",
"filter-all": "ALL ENTRIES",
"filter-abandoned": "ABANDONED",
"filter-fulfilled": "FULFILLED",
"filter-active": "ACTIVE"
}, },
"filters": { "filters": {
"endingStatus": "ENDS SOON", "endingStatus": "ENDS SOON",
@@ -116,7 +167,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": "SAVE FILTERS",
"reset": "RESET FILTERS", "reset": "RESET FILTERS",
"close": "CLOSE FILTERS" "close": "CLOSE FILTERS"
}, },
@@ -131,7 +182,8 @@
"users": "Drivers online", "users": "Drivers online",
"spawns": "Spawns online", "spawns": "Spawns online",
"timetables": "Active timetables", "timetables": "Active timetables",
"no-stations": "No stations to show here!" "no-stations": "No stations to show here!",
"scenery-search": "Search for scenery..."
}, },
"trains": { "trains": {
"no-trains": "No trains to show here!", "no-trains": "No trains to show here!",
@@ -150,28 +202,6 @@
"current-signal": "at signal", "current-signal": "at signal",
"current-track": "on track", "current-track": "on track",
"option-mass": "mass",
"option-speed": "speed",
"option-length": "length",
"option-distance": "distance",
"option-timetable": "train no.",
"option-progress": "route progress",
"option-delay": "current delay",
"option-comments": "comments",
"filter-comments": "comments",
"filter-twr": "TWR",
"filter-skr": "SKR",
"filter-passenger": "passenger",
"filter-freight": "freight",
"filter-other": "other",
"filter-noTimetable": "no timetable",
"filter-reset": "X RESET",
"sorter-prefix": "Sort: ",
"search-train": "Train no.",
"search-driver": "Driver name",
"delayed": "Delayed: ", "delayed": "Delayed: ",
"preponed": "Ahead of schedule: ", "preponed": "Ahead of schedule: ",
"on-time": "On time", "on-time": "On time",
@@ -195,7 +225,8 @@
"last-seen-min": "since one minute", "last-seen-min": "since one minute",
"last-seen-ago": "since {minutes} minutes", "last-seen-ago": "since {minutes} minutes",
"scenery-offline": "Offline ride" "scenery-offline": "Offline ride",
"timeout": "An error occured while trying to refresh SWDR timetable data!"
}, },
"journal": { "journal": {
"title": "DISPATCHER HISTORY", "title": "DISPATCHER HISTORY",
@@ -205,26 +236,6 @@
"section-timetables": "TIMETABLES", "section-timetables": "TIMETABLES",
"section-dispatchers": "DISPATCHERS", "section-dispatchers": "DISPATCHERS",
"search": "Search",
"search-train": "Train no. / #",
"search-driver": "Driver name",
"search-dispatcher": "Dispatcher name",
"search-station": "Scenery name",
"sort-prefix": "Sort: ",
"option-distance": "distance",
"option-total-stops": "total stops",
"option-beginDate": "date",
"option-timetableId": "timetable ID",
"option-timestampFrom": "date",
"option-duration": "duration",
"filter-all": "ALL ENTRIES",
"filter-abandoned": "ABANDONED",
"filter-fulfilled": "FULFILLED",
"filter-active": "ACTIVE",
"no-further-data": "No further data for current parameters", "no-further-data": "No further data for current parameters",
"loading-further-data": "Loading...", "loading-further-data": "Loading...",
@@ -239,7 +250,41 @@
"online-since": "ONLINE SINCE", "online-since": "ONLINE SINCE",
"duty-lasted": "The duty lasted", "duty-lasted": "The duty lasted",
"minutes": "{minutes} mins", "minutes": "{minutes} mins",
"hours": "{hours}h {minutes} mins" "hours": "{hours}h {minutes} mins",
"stock-info": "STOCK INFO",
"stock-length": "Length",
"stock-mass": "Mass",
"stock-max-speed": "Maximum registered speed",
"load-data": "Load further data...",
"last-seen-at": "Last seen at",
"currently-at": "Currently at",
"stats-title": "DRIVING STATISTICS OF",
"stats-timetables": "TIMETABLES",
"stats-longest-timetable": "LONGEST TIMETABLE",
"stats-avg-timetable": "AVERAGE TIMETABLE LENGTH",
"stats-distance": "DISTANCE",
"stats-stations": "STATIONS",
"timetable-stats-total": "Today, dispatchers made so far {count} with total distance of {distance}",
"timetable-stats-longest": "The longest timetable today is #{id} made by {author} for {driver} - {distance}",
"timetable-stats-most-active": "The most active dispatcher today is {dispatcher} who created {count}",
"timetable-stats-most-active-many": "The most active dispatchers today are {dispatchers} who created {count} each",
"timetable-count": "timetable | timetables",
"daily-stats-title": "DAILY STATS",
"daily-stats-info": "Today's statistics are unavailable yet!",
"driver-stats-title": "DRIVER STATS",
"driver-stats-info": "Enter a proper nickname into filters [F] to see user's driving statistics!",
"stats-loading": "Fetching statistics...",
"stats-error": "Oops! An unexpected error occurred while trying to fetch statistics! :/"
}, },
"scenery": { "scenery": {
"users": "PLAYERS ONLINE", "users": "PLAYERS ONLINE",
@@ -281,12 +326,8 @@
}, },
"timetables": { "timetables": {
"timetable-only": "Switch to timetable-only view", "timetable-only": "Switch to timetable-only view",
"online": "At station", "end": "Timetable terminates here",
"departed": "Dispatched to:", "terminated": "Timetable terminated",
"departed-away": "Departed to:",
"arriving": "Arriving from:",
"stopped": "Stopped",
"terminated": "Terminated",
"begins": "BEGINS HERE", "begins": "BEGINS HERE",
"terminates": "TERMINATES\nHERE" "terminates": "TERMINATES\nHERE"
}, },
+98 -55
View File
@@ -1,4 +1,7 @@
{ {
"general": {
"and": " oraz "
},
"app": { "app": {
"sceneries": "SCENERIE", "sceneries": "SCENERIE",
"trains": "POCIĄGI", "trains": "POCIĄGI",
@@ -8,17 +11,20 @@
"error": "Wystąpił problem z załadowaniem danych!", "error": "Wystąpił problem z załadowaniem danych!",
"no-result": "Brak wyników o podanych kryteriach!", "no-result": "Brak wyników o podanych kryteriach!",
"migration-warning": "Usługi Stacjownika będą niedostępne w godzinach 1:00-3:00 2 czerwca 2022r. z powodu migracji hostingów API!", "migration-warning": "Usługi Stacjownika będą niedostępne w godzinach 1:00-3:00 2 czerwca 2022r. z powodu migracji hostingów API!",
"migration-confirm": "Przyjąłem!" "migration-confirm": "Przyjąłem!",
"offline": "Aplikacja w trybie offline!"
}, },
"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!",
"release-link": "Kliknij, aby przejrzeć listę zmian (GitHub)", "release-link": "Kliknij, aby przejrzeć listę zmian (GitHub)",
"confirm-button": "Przyjąłem!" "confirm-button": "ZAKTUALIZUJ",
"later-button": "PÓŹNIEJ"
}, },
"data-status": { "data-status": {
"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!",
"S1a-sceneries": "<b>Sygnał S1a</b> <br> Błąd podczas pobierania danych o sceneriach online!", "S1a-sceneries": "<b>Sygnał S1a</b> <br> Błąd podczas pobierania danych o sceneriach online!",
"S2": "<b>Sygnał S2</b> <br> Pomyślnie załadowano dane!", "S2": "<b>Sygnał S2</b> <br> Pomyślnie załadowano dane!",
@@ -74,7 +80,53 @@
}, },
"options": { "options": {
"filters": "FILTRY", "filters": "FILTRY",
"donate": "WESPRZYJ" "donate": "WESPRZYJ",
"search-button": "Szukaj",
"reset-button": "Zresetuj",
"sort-title": "SORTUJ WG:",
"filter-title": "FILTRUJ WG:",
"search-title": "SZUKAJ:",
"search-train-no": "Nr pociągu",
"search-train": "Nr pociągu / #",
"search-driver": "Nick maszynisty",
"search-dispatcher": "Nick dyżurnego",
"search-station": "Nazwa scenerii",
"search-author": "Nick autora rozkładu jazdy",
"search-date": "Data rozkładu jazdy (czas polski)",
"sort-distance": "kilometraż",
"sort-total-stops": "stacje",
"sort-beginDate": "data",
"sort-timetableId": "ID rozkładu",
"sort-timestampFrom": "data",
"sort-duration": "czas dyżuru",
"sort-mass": "masa",
"sort-speed": "prędkość",
"sort-length": "długość",
"sort-timetable": "nr pociągu",
"sort-progress": "przebyta trasa",
"sort-delay": "opóźnienie",
"sort-comments": "uwagi ekspl.",
"filter-comments": "UWAGI EKSPLOATACYJNE",
"filter-twr": "TWR",
"filter-skr": "PRZEKR. SKRAJNIA",
"filter-passenger": "PASAŻERSKIE",
"filter-freight": "TOWAROWE",
"filter-other": "INNE",
"filter-noTimetable": "BEZ RJ",
"filter-reset": "ZRESETUJ FILTRY",
"filter-clear": "WYŁĄCZ FILTRY",
"filter-all": "WSZYSTKIE",
"filter-abandoned": "PORZUCONE",
"filter-fulfilled": "WYPEŁNIONE",
"filter-active": "AKTYWNE"
}, },
"filters": { "filters": {
"endingStatus": "KOŃCZY", "endingStatus": "KOŃCZY",
@@ -118,7 +170,7 @@
"hour": " godz.", "hour": " godz.",
"no-limit": "BEZ LIMITU", "no-limit": "BEZ LIMITU",
"include-selected": "POKAŻ ZAZNACZONE", "include-selected": "POKAŻ ZAZNACZONE",
"save": "ZAPISZ FILTRY", "save": "ZAPISZ FILTRY",
"reset": "RESETUJ FILTRY", "reset": "RESETUJ FILTRY",
"close": "ZAMKNIJ FILTRY" "close": "ZAMKNIJ FILTRY"
}, },
@@ -133,7 +185,8 @@
"users": "Maszyniści online", "users": "Maszyniści online",
"spawns": "Otwarte spawny", "spawns": "Otwarte spawny",
"timetables": "Aktywne rozkłady jazdy", "timetables": "Aktywne rozkłady jazdy",
"no-stations": "Brak stacji do wyświetlenia!" "no-stations": "Brak stacji do wyświetlenia!",
"scenery-search": "Wyszukaj scenerię..."
}, },
"trains": { "trains": {
"no-trains": "Brak pociągów do wyświetlenia!", "no-trains": "Brak pociągów do wyświetlenia!",
@@ -152,28 +205,6 @@
"current-signal": "przy semaforze", "current-signal": "przy semaforze",
"current-track": "na szlaku", "current-track": "na szlaku",
"option-mass": "masa",
"option-speed": "prędkość",
"option-length": "długość",
"option-distance": "kilometraż",
"option-timetable": "nr pociągu",
"option-progress": "przebyta trasa",
"option-delay": "opóźnienie",
"option-comments": "uwagi ekspl.",
"filter-comments": "uwagi ekspl.",
"filter-twr": "TWR",
"filter-skr": "SKR",
"filter-passenger": "pasażerskie",
"filter-freight": "towarowe",
"filter-other": "inne",
"filter-noTimetable": "bez RJ",
"filter-reset": "X RESETUJ",
"sorter-prefix": "Sortuj: ",
"search-train": "Numer pociągu",
"search-driver": "Nick maszynisty",
"delayed": "Opóźniony: ", "delayed": "Opóźniony: ",
"preponed": "Przed czasem: ", "preponed": "Przed czasem: ",
"on-time": "Planowo", "on-time": "Planowo",
@@ -197,7 +228,9 @@
"last-seen-min": "od minuty", "last-seen-min": "od minuty",
"last-seen-ago": "od {minutes} minut", "last-seen-ago": "od {minutes} minut",
"scenery-offline": "Przejazd offline" "scenery-offline": "Przejazd offline",
"timeout": "Wystąpił problem z aktualizacją rozkładów jazdy z SWDR"
}, },
"journal": { "journal": {
"title": "HISTORIA DYŻURÓW", "title": "HISTORIA DYŻURÓW",
@@ -207,26 +240,6 @@
"section-timetables": "ROZKŁADY JAZDY", "section-timetables": "ROZKŁADY JAZDY",
"section-dispatchers": "DYŻURNI", "section-dispatchers": "DYŻURNI",
"search": "Szukaj",
"search-train": "Nr pociągu / #",
"search-driver": "Nick maszynisty",
"search-dispatcher": "Nick dyżurnego",
"search-station": "Nazwa scenerii",
"sort-prefix": "Sortuj: ",
"option-distance": "kilometraż",
"option-total-stops": "stacje",
"option-beginDate": "data",
"option-timetableId": "ID rozkładu",
"option-timestampFrom": "data",
"option-duration": "czas dyżuru",
"filter-all": "WSZYSTKIE",
"filter-abandoned": "PORZUCONE",
"filter-fulfilled": "WYPEŁNIONE",
"filter-active": "AKTYWNE",
"no-further-data": "Brak dalszych wyników dla podanych parametrów", "no-further-data": "Brak dalszych wyników dla podanych parametrów",
"loading-further-data": "Ładowanie...", "loading-further-data": "Ładowanie...",
@@ -241,7 +254,41 @@
"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-length": "Długość",
"stock-mass": "Masa",
"stock-max-speed": "Maks. zarejestrowana prędkość",
"load-data": "Pobierz dalszą historię...",
"stats-title": "STATYSTYKI MASZYNISTY",
"last-seen-at": "Ostatnio widziany na: ",
"currently-at": "Obecnie na scenerii: ",
"stats-timetables": "ROZKŁADY JAZDY",
"stats-longest-timetable": "NAJDŁUŻSZY RJ",
"stats-avg-timetable": "ŚREDNIA DŁUGOŚĆ RJ",
"stats-distance": "DYSTANS",
"stats-stations": "STACJE",
"timetable-stats-total": "Dyżurni stworzyli dziś {count} o łącznym dystansie {distance}",
"timetable-stats-longest": "Najdłuższym rozkładem jazdy jest dzisiaj #{id} stworzony przez dyżurnego {author} dla maszynisty {driver} - {distance}",
"timetable-stats-most-active": "Dzisiejszym najaktywniejszym dyżurnym jest {dispatcher}, który stworzył {count}",
"timetable-stats-most-active-many": "Dzisiejszymi najaktywniejszymi dyżurnymi są {dispatchers}, którzy stworzyli po {count}",
"timetable-count": "rozkład jazdy | rozkładów jazdy",
"daily-stats-title": "STATYSTYKI DNIA",
"daily-stats-info": "Dzisiejsze statystyki nie są jeszcze dostępne!",
"driver-stats-title": "STATYSTYKI GRACZA",
"driver-stats-info": "Wpisz nazwę użytkownika w filtrach [F], aby zobaczyć jego statystyki maszynisty!",
"stats-loading": "Pobieranie statystyk...",
"stats-error": "Ups! Wystąpił błąd podczas próby pobrania statystyk! :/"
}, },
"scenery": { "scenery": {
"users": "GRACZE ONLINE", "users": "GRACZE ONLINE",
@@ -283,12 +330,8 @@
}, },
"timetables": { "timetables": {
"timetable-only": "Wyodrębnij rozkłady jazdy", "timetable-only": "Wyodrębnij rozkłady jazdy",
"online": "Na stacji", "end": "Koniec rozkładu jazdy",
"departed": "Odprawiony do:", "terminated": "Rozkład jazdy zakończony",
"departed-away": "Odjechał do:",
"arriving": "W drodze z:",
"stopped": "Postój",
"terminated": "Skończył bieg",
"begins": "ROZPOCZYNA\nBIEG", "begins": "ROZPOCZYNA\nBIEG",
"terminates": "KOŃCZY BIEG" "terminates": "KOŃCZY BIEG"
}, },
+1
View File
@@ -10,6 +10,7 @@ import { createPinia } from 'pinia';
const i18n = createI18n({ const i18n = createI18n({
locale: 'pl', locale: 'pl',
legacy: false,
fallbackLocale: 'pl', fallbackLocale: 'pl',
messages: { messages: {
en: enLang, en: enLang,
+26
View File
@@ -0,0 +1,26 @@
import { defineComponent } from 'vue';
export default defineComponent({
data() {
return {
preventKeyDown: false,
};
},
activated() {
window.addEventListener('keydown', this.handleKeyDown);
},
deactivated() {
window.removeEventListener('keydown', this.handleKeyDown);
},
methods: {
onKeyDownFunction() {},
handleKeyDown(e: KeyboardEvent) {
if (!e.key) return;
if (e.key.toLowerCase() == 'f' && !this.preventKeyDown && !e.ctrlKey && !e.altKey) this.onKeyDownFunction();
},
},
});
+31 -30
View File
@@ -1,30 +1,31 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useStore } from '../store/store'; import { useStore } from '../store/store';
export default defineComponent({ export default defineComponent({
setup() { data() {
return { return {
store: useStore(), store: useStore(),
}; };
}, },
mounted() { computed: {
console.log('Mixin mounted'); chosenTrain() {
}, return this.store.trainList.find((train) => train.trainId == this.store.chosenModalTrainId);
},
computed: { },
chosenTrain() {
return this.store.trainList.find((train) => train.trainId == this.store.chosenModalTrainId); methods: {
}, selectModalTrain(trainId: string) {
}, this.store.chosenModalTrainId = trainId;
document.body.classList.add('no-scroll');
methods: { },
selectModalTrain(trainId: string) {
this.store.chosenModalTrainId = trainId; closeModal() {
}, this.store.chosenModalTrainId = undefined;
closeModal() { setTimeout(() => {
this.store.chosenModalTrainId = undefined; document.body.classList.remove('no-scroll');
}, }, 150);
}, },
}); },
});
+34 -34
View File
@@ -1,34 +1,34 @@
import { defineComponent, h } from 'vue'; import { defineComponent, h } from 'vue';
import imageMixin from './imageMixin'; import imageMixin from './imageMixin';
export default defineComponent({ export default defineComponent({
mixins: [imageMixin], mixins: [imageMixin],
data() { data() {
return { return {
icons: { icons: {
arrow: this.getIcon('arrow-asc'), arrow: this.getIcon('arrow-asc'),
}, },
showReturnButton: false, showReturnButton: false,
}; };
}, },
methods: { methods: {
scrollToTop() { scrollToTop() {
window.scrollTo({ top: 0 }); window.scrollTo({ top: 0 });
}, },
handleScroll() { handleScroll() {
this.showReturnButton = window.scrollY > window.innerHeight * 0.35; this.showReturnButton = window.scrollY > window.innerHeight * 0.35;
}, },
}, },
activated() { activated() {
window.addEventListener('scroll', this.handleScroll); window.addEventListener('wheel', this.handleScroll);
}, },
deactivated() { deactivated() {
window.removeEventListener('scroll', this.handleScroll); window.removeEventListener('wheel', this.handleScroll);
}, },
}); });
+1 -1
View File
@@ -5,7 +5,7 @@ export default defineComponent({
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 > 15 || 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 10px 2px ${bgColor};` : '';
return `background-color: ${bgColor}; color: ${fontColor}; ${boxShadow}`; return `background-color: ${bgColor}; color: ${fontColor}; ${boxShadow}`;
+13
View File
@@ -0,0 +1,13 @@
import { useRegisterSW } from 'virtual:pwa-register/vue';
export default () => {
const { needRefresh, updateServiceWorker, offlineReady } = useRegisterSW({
immediate: true,
});
return {
needRefresh,
updateServiceWorker,
offlineReady,
};
};
+21 -31
View File
@@ -1,6 +1,6 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import JournalDispatchersVue from '../components/JournalView/JournalDispatchers.vue'; import JournalDispatchersVue from '../views/JournalDispatchers.vue';
import JournalTimetablesVue from '../components/JournalView/JournalTimetables.vue'; import JournalTimetablesVue from '../views/JournalTimetables.vue';
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ {
@@ -12,42 +12,32 @@ const routes: Array<RouteRecordRaw> = [
path: '/trains', path: '/trains',
name: 'TrainsView', name: 'TrainsView',
component: () => import('../views/TrainsView.vue'), component: () => import('../views/TrainsView.vue'),
props: (route) => ({ train: route.query.train, driver: route.query.driver }), props: (route) => ({ train: route.query.train, driver: route.query.driver, trainId: route.query.trainId }),
}, },
{ {
path: '/scenery', path: '/scenery',
name: 'SceneryView', name: 'SceneryView',
component: () => import('../views/SceneryView.vue'), component: () => import('../views/SceneryView.vue'),
props: true,
}, },
{ {
path: '/journal', path: '/journal',
name: 'JournalView', redirect: '/journal/timetables'
component: () => import('../views/JournalView.vue'), },
children: [ {
{ path: '/journal/timetables',
path: '', name: 'JournalTimetables',
name: 'JournalTimetables', component: JournalTimetablesVue,
component: JournalTimetablesVue, props: (route) => ({
alias: '/timetables', trainNo: route.query.trainNo,
}, driverName: route.query.driverName,
{ timetableId: route.query.timetableId,
path: 'dispatchers', }),
name: 'JournalDispatchers', },
component: JournalDispatchersVue, {
props: (route) => ({ sceneryName: route.query.sceneryName, dispatcherName: route.query.dispatcherName }), path: '/journal/dispatchers',
}, name: 'JournalDispatchers',
{ component: JournalDispatchersVue,
path: 'timetables', props: (route) => ({ sceneryName: route.query.sceneryName, dispatcherName: route.query.dispatcherName }),
name: 'JournalTimetables',
component: JournalTimetablesVue,
props: (route) => ({
trainNo: route.query.trainNo,
driverName: route.query.driverName,
timetableId: route.query.timetableId,
}),
},
],
}, },
{ {
path: '/:catchAll(.*)', path: '/:catchAll(.*)',
@@ -59,7 +49,7 @@ const router = createRouter({
scrollBehavior(to, from) { scrollBehavior(to, from) {
if (to.name == 'SceneryView' && from.name) return { el: `.app_main` }; if (to.name == 'SceneryView' && from.name) return { el: `.app_main` };
if (from.name == 'SceneryView' && to.name == 'StationsView') return { el: `.last-selected`, top: 20 }; // if (from.name == 'SceneryView' && to.name == 'StationsView') return { el: `.last-selected`, top: 20 };
}, },
history: createWebHistory(), history: createWebHistory(),
routes, routes,
+1 -1
View File
@@ -1,4 +1,4 @@
export const enum DataStatus { export enum DataStatus {
Initialized = -1, Initialized = -1,
Loading = 0, Loading = 0,
Error = 1, Error = 1,
+6
View File
@@ -20,6 +20,12 @@ export default interface ScheduledTrain {
arrivingLine: string | null; arrivingLine: string | null;
departureLine: string | null; departureLine: string | null;
prevDepartureLine: string | null;
nextArrivalLine: string | null;
signal: string;
connectedTrack: string;
stopLabel: string; stopLabel: string;
stopStatus: string; stopStatus: string;
stopStatusID: number; stopStatusID: number;
+3 -1
View File
@@ -19,9 +19,11 @@ export default interface Train {
online: boolean; online: boolean;
lastSeen: number; lastSeen: number;
region: string; region: string;
cars: string[]; cars: string[];
isTimeout: boolean;
isSupporter: boolean;
timetableData?: { timetableData?: {
timetableId: number; timetableId: number;
category: string; category: string;
@@ -1,12 +1,16 @@
export interface DispatcherHistory { export interface DispatcherHistory {
currentDuration: number; id: string;
dispatcherId: number;
dispatcherName: string; currentDuration: number;
isOnline: boolean; dispatcherId: number;
lastOnlineTimestamp: number; dispatcherName: string;
region: string; dispatcherLevel: number | null;
stationHash: string; dispatcherIsSupporter: boolean;
stationName: string; isOnline: boolean;
timestampFrom: number; lastOnlineTimestamp: number;
timestampTo?: number; region: string;
stationHash: string;
stationName: string;
timestampFrom: number;
timestampTo?: number;
} }
@@ -0,0 +1,30 @@
import { TimetableHistory } from './TimetablesAPIData';
export interface ITimetablesDailyStats {
totalTimetables: number;
distanceSum: number;
distanceAvg: number;
timetableId: number;
timetableAuthor: string;
timetableDriver: string;
timetableRouteDistance: number;
mostActiveDispatchers: {
name: string;
count: number;
}[];
}
export interface ITimetablesDailyStatsResponse {
totalTimetables: number;
distanceSum: number;
distanceAvg: number;
maxTimetable: TimetableHistory | null;
mostActiveDispatchers: {
name: string;
count: number;
}[];
}
+46 -35
View File
@@ -1,35 +1,46 @@
export interface TimetableHistory { export interface TimetableHistory {
timetableId: number; id: number;
trainNo: number;
trainCategoryCode: string; timetableId: number;
driverId: number; trainNo: number;
driverName: string; trainCategoryCode: string;
route: string; driverId: number;
twr: number; driverName: string;
skr: number; route: string;
sceneriesString: string; twr: number;
skr: number;
routeDistance: number; sceneriesString: string;
currentDistance: number;
routeDistance: number;
confirmedStopsCount: number; currentDistance: number;
allStopsCount: number;
confirmedStopsCount: number;
beginDate: string; allStopsCount: number;
endDate: string;
beginDate: string;
scheduledBeginDate: string; endDate: string;
scheduledEndDate: string;
scheduledBeginDate: string;
terminated: boolean; scheduledEndDate: string;
fulfilled: boolean;
terminated: boolean;
authorName?: string; fulfilled: boolean;
authorId?: number;
} authorName?: string;
authorId?: number;
export interface SceneryTimetableHistory {
sceneryTimetables: TimetableHistory[]; stockString?: string;
totalCount: number; stockMass?: number;
sceneryName: string; stockLength?: number;
} maxSpeed?: number;
hashesString?: string;
currentSceneryName?: string;
currentSceneryHash?: string;
}
export interface SceneryTimetableHistory {
sceneryTimetables: TimetableHistory[];
totalCount: number;
sceneryName: string;
}
@@ -21,6 +21,7 @@ export default interface TrainAPIData {
lastSeen: number; lastSeen: number;
region: string; region: string;
isTimeout: boolean;
timetable?: { timetable?: {
timetableId: number; timetableId: number;
+7
View File
@@ -23,6 +23,13 @@ export default class StorageManager {
window.localStorage.setItem(key, val); window.localStorage.setItem(key, val);
} }
static setValue(key: string, val: any) {
if (typeof val == 'boolean') this.setBooleanValue(key, val);
else if (typeof val == 'number') this.setNumericValue(key, val);
else if (typeof val == 'string') this.setStringValue(key, val);
else this.setStringValue(key, val);
}
static removeValue(key: string) { static removeValue(key: string) {
window.localStorage.removeItem(key); window.localStorage.removeItem(key);
} }
+114 -114
View File
@@ -1,115 +1,115 @@
import { TrainFilter } from "vue"; import { TrainFilter } from "../../types/Trains/TrainOptionsTypes";
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; if (!train.timetableData) return filters.find(filter => filter.id == TrainFilterType.noTimetable)!.isActive;
switch (f.id) { switch (f.id) {
case TrainFilterType.comments: case TrainFilterType.comments:
return !train.timetableData.followingStops.some(stop => stop.comments); return !train.timetableData.followingStops.some(stop => stop.comments);
case TrainFilterType.twr: case TrainFilterType.twr:
return !train.timetableData.TWR; return !train.timetableData.TWR;
case TrainFilterType.skr: case TrainFilterType.skr:
return !train.timetableData.SKR; return !train.timetableData.SKR;
case TrainFilterType.passenger: case TrainFilterType.passenger:
return !/^[AMRE]\D{2}$/.test(train.timetableData.category); return !/^[AMRE]\D{2}$/.test(train.timetableData.category);
case TrainFilterType.freight: case TrainFilterType.freight:
return !train.timetableData.category.startsWith('T'); return !train.timetableData.category.startsWith('T');
case TrainFilterType.other: case TrainFilterType.other:
return !/^[PXZL]\D{2}$/.test(train.timetableData.category); return !/^[PXZL]\D{2}$/.test(train.timetableData.category);
default: default:
return true; return true;
} }
}) })
return (searchedTrain.length > 0 ? train.trainNo.toString().startsWith(searchedTrain) : true) && return (searchedTrain.length > 0 ? train.trainNo.toString().startsWith(searchedTrain) : true) &&
(searchedDriver.length > 0 ? train.driverName.toLowerCase().startsWith(searchedDriver.toLowerCase()) : true) && isFiltered (searchedDriver.length > 0 ? train.driverName.toLowerCase().startsWith(searchedDriver.toLowerCase()) : true) && 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 'mass':
if (a.mass > b.mass) return sorterActive.dir; if (a.mass > b.mass) return sorterActive.dir;
return -sorterActive.dir; return -sorterActive.dir;
case 'distance': case 'distance':
if ((a.timetableData?.routeDistance || -1) > (b.timetableData?.routeDistance || -1)) return sorterActive.dir; if ((a.timetableData?.routeDistance || -1) > (b.timetableData?.routeDistance || -1)) return sorterActive.dir;
return -sorterActive.dir; return -sorterActive.dir;
case 'progress': case 'progress':
if (confirmedPercentage(a.timetableData?.followingStops) > confirmedPercentage(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 'delay': case 'delay':
if (currentDelay(a.timetableData?.followingStops) > currentDelay(b.timetableData?.followingStops)) if (currentDelay(a.timetableData?.followingStops) > currentDelay(b.timetableData?.followingStops))
return sorterActive.dir; return sorterActive.dir;
return -sorterActive.dir; return -sorterActive.dir;
case 'speed': case 'speed':
if (a.speed > b.speed) return sorterActive.dir; if (a.speed > b.speed) return sorterActive.dir;
return -sorterActive.dir; return -sorterActive.dir;
case 'timetable': case 'timetable':
if (a.trainNo > b.trainNo) return sorterActive.dir; if (a.trainNo > b.trainNo) return sorterActive.dir;
return -sorterActive.dir; return -sorterActive.dir;
case 'length': case 'length':
if (a.length > b.length) return sorterActive.dir; if (a.length > b.length) return sorterActive.dir;
return -sorterActive.dir; return -sorterActive.dir;
default: default:
break; break;
} }
return 0; 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)];
}; };
+1 -5
View File
@@ -1,9 +1,5 @@
export const URLs = { export const URLs = {
stacjownikAPI: stacjownikAPI:
import.meta.env.VITE_APP_API_DEV == 1 && !import.meta.env.PROD import.meta.env.VITE_APP_API_DEV == 1 && !import.meta.env.PROD ? 'http://localhost:3000' : 'https://spythere.pl',
? 'http://localhost:3000'
: 'https://stacjownik.eu-4.evennode.com',
stacjownikAPIDev: 'localhost:3000', stacjownikAPIDev: 'localhost:3000',
// trains: "https://api.td2.info.pl:9640/?method=getTrainsOnline",
// getTimetableURL: (trainNo: string | number, region = "eu") => `https://api.td2.info.pl:9640/?method=readFromSWDR&value=getTimetable%3B${trainNo}%3B${region}`
}; };
+22 -7
View File
@@ -117,31 +117,37 @@ export function getScheduledTrain(train: Train, trainStopIndex: number, stationN
let prevStationName = '', let prevStationName = '',
nextStationName = ''; nextStationName = '';
let prevDepartureLine: string | null = null,
nextArrivalLine: string | null = null;
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; prevStationName = followingStops[i].stopNameRAW.replace(/,.*/g,"");
break; break;
} }
} }
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; nextStationName = followingStops[i].stopNameRAW.replace(/,.*/g,"");
break; break;
} }
} }
let departureLine: string | null = trainStop.departureLine; let departureLine: string | null = null;
let arrivingLine: string | null = trainStop.arrivalLine; let arrivingLine: string | null = null;
for (let i = trainStopIndex; i < followingStops.length; i++) { for (let i = trainStopIndex; i < followingStops.length; i++) {
const currentStop = followingStops[i]; const currentStop = followingStops[i];
if (currentStop.departureLine == null) break; if (currentStop.departureLine == null) continue;
if (!/-|_|it|sbl/gi.test(currentStop.departureLine)) { if (!/-|_|it|sbl/gi.test(currentStop.departureLine)) {
departureLine = currentStop.departureLine; departureLine = currentStop.departureLine;
nextArrivalLine = followingStops[i + 1]?.arrivalLine || null;
break; break;
} }
} }
@@ -149,10 +155,12 @@ export function getScheduledTrain(train: Train, trainStopIndex: number, stationN
for (let i = trainStopIndex; i >= 0; i--) { for (let i = trainStopIndex; i >= 0; i--) {
const currentStop = followingStops[i]; const currentStop = followingStops[i];
if (currentStop.arrivalLine == null) break; if (currentStop.arrivalLine == null) continue;
if (!/-|_|it|sbl/gi.test(currentStop.arrivalLine)) { if (!/-|_|it|sbl/gi.test(currentStop.arrivalLine)) {
arrivingLine = currentStop.arrivalLine; arrivingLine = currentStop.arrivalLine;
prevDepartureLine = followingStops[i - 1]?.departureLine || null;
break; break;
} }
} }
@@ -160,7 +168,11 @@ export function getScheduledTrain(train: Train, trainStopIndex: number, stationN
return { return {
trainNo: train.trainNo, trainNo: train.trainNo,
trainId: train.trainId, trainId: train.trainId,
signal: train.signal,
connectedTrack: train.connectedTrack,
driverName: train.driverName, driverName: train.driverName,
driverId: train.driverId, driverId: train.driverId,
currentStationName: train.currentStationName, currentStationName: train.currentStationName,
@@ -179,5 +191,8 @@ export function getScheduledTrain(train: Train, trainStopIndex: number, stationN
arrivingLine, arrivingLine,
departureLine, departureLine,
nextArrivalLine,
prevDepartureLine,
}; };
} }
@@ -1,292 +1,308 @@
import Filter from '../interfaces/Filter'; import { defineStore } from 'pinia';
import Station from '../interfaces/Station'; import inputData from '../data/options.json';
import StorageManager from './storageManager'; import Filter from '../scripts/interfaces/Filter';
import Station from '../scripts/interfaces/Station';
const sortStations = (a: Station, b: Station, sorter: { index: number; dir: number }) => { import StorageManager from '../scripts/managers/storageManager';
switch (sorter.index) { import { useStore } from './store';
case 1:
if ((a.generalInfo?.reqLevel || 0) > (b.generalInfo?.reqLevel || 0)) return sorter.dir; const sortStations = (a: Station, b: Station, sorter: { index: number; dir: number }) => {
if ((a.generalInfo?.reqLevel || 0) < (b.generalInfo?.reqLevel || 0)) return -sorter.dir; switch (sorter.index) {
break; case 0:
return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
case 2:
if ((a.onlineInfo?.statusTimestamp || 0) > (b.onlineInfo?.statusTimestamp || 0)) return sorter.dir; case 1:
if ((a.onlineInfo?.statusTimestamp || 0) < (b.onlineInfo?.statusTimestamp || 0)) return -sorter.dir; if ((a.generalInfo?.reqLevel || 0) > (b.generalInfo?.reqLevel || 0)) return sorter.dir;
break; if ((a.generalInfo?.reqLevel || 0) < (b.generalInfo?.reqLevel || 0)) return -sorter.dir;
break;
case 3:
if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') > (b.onlineInfo?.dispatcherName.toLowerCase() || '')) case 2:
return sorter.dir; if ((a.onlineInfo?.statusTimestamp || 0) > (b.onlineInfo?.statusTimestamp || 0)) return sorter.dir;
if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') < (b.onlineInfo?.dispatcherName.toLowerCase() || '')) if ((a.onlineInfo?.statusTimestamp || 0) < (b.onlineInfo?.statusTimestamp || 0)) return -sorter.dir;
return -sorter.dir; break;
break;
case 3:
case 4: if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') > (b.onlineInfo?.dispatcherName.toLowerCase() || ''))
if ((a.onlineInfo?.dispatcherExp || 0) > (b.onlineInfo?.dispatcherExp || 0)) return sorter.dir; return sorter.dir;
if ((a.onlineInfo?.dispatcherExp || 0) < (b.onlineInfo?.dispatcherExp || 0)) return -sorter.dir; if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') < (b.onlineInfo?.dispatcherName.toLowerCase() || ''))
break; return -sorter.dir;
break;
case 7:
if ((a.onlineInfo?.currentUsers || 0) > (b.onlineInfo?.currentUsers || 0)) return sorter.dir; case 4:
if ((a.onlineInfo?.currentUsers || 0) < (b.onlineInfo?.currentUsers || 0)) return -sorter.dir; if ((a.onlineInfo?.dispatcherExp || 0) > (b.onlineInfo?.dispatcherExp || 0)) return sorter.dir;
if ((a.onlineInfo?.dispatcherExp || 0) < (b.onlineInfo?.dispatcherExp || 0)) return -sorter.dir;
if ((a.onlineInfo?.maxUsers || 0) > (b.onlineInfo?.maxUsers || 0)) return sorter.dir; break;
if ((a.onlineInfo?.maxUsers || 0) < (b.onlineInfo?.maxUsers || 0)) return -sorter.dir;
break; case 7:
if ((a.onlineInfo?.currentUsers || 0) > (b.onlineInfo?.currentUsers || 0)) return sorter.dir;
case 8: if ((a.onlineInfo?.currentUsers || 0) < (b.onlineInfo?.currentUsers || 0)) return -sorter.dir;
if ((a.onlineInfo?.spawns.length || 0) > (b.onlineInfo?.spawns.length || 0)) return sorter.dir;
if ((a.onlineInfo?.spawns.length || 0) < (b.onlineInfo?.spawns.length || 0)) return -sorter.dir; 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; break;
case 9: case 8:
if ((a.onlineInfo?.scheduledTrains?.length || 0) > (b.onlineInfo?.scheduledTrains?.length || 0)) if ((a.onlineInfo?.spawns.length || 0) > (b.onlineInfo?.spawns.length || 0)) return sorter.dir;
return sorter.dir; if ((a.onlineInfo?.spawns.length || 0) < (b.onlineInfo?.spawns.length || 0)) return -sorter.dir;
if ((a.onlineInfo?.scheduledTrains?.length || 0) < (b.onlineInfo?.scheduledTrains?.length || 0))
return -sorter.dir; break;
default: case 9:
break; 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))
return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name); return -sorter.dir;
};
default:
const filterStations = (station: Station, filters: Filter) => { break;
const returnMode = false; }
if ((station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && filters['nonPublic']) return a.name.localeCompare(b.name);
return returnMode; };
if (station.onlineInfo?.statusID == 'ending' && filters['ending']) return returnMode; const filterStations = (station: Station, filters: Filter, isOffline = false) => {
const returnMode = false;
if (
station.onlineInfo && if ((station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && filters['nonPublic'])
station.onlineInfo.statusTimestamp > 0 && return returnMode;
filters['onlineFromHours'] < 8 &&
station.onlineInfo.statusTimestamp <= Date.now() + filters['onlineFromHours'] * 3600000 if (station.onlineInfo?.statusID == 'ending' && filters['ending']) return returnMode;
)
return returnMode; if (
station.onlineInfo &&
if (filters['onlineFromHours'] > 0 && station.onlineInfo && station.onlineInfo.statusTimestamp <= 0) station.onlineInfo.statusTimestamp > 0 &&
return returnMode; filters['onlineFromHours'] < 8 &&
if (filters['onlineFromHours'] == 8 && station.onlineInfo?.statusID != 'no-limit') return returnMode; station.onlineInfo.statusTimestamp <= Date.now() + filters['onlineFromHours'] * 3600000
)
if (station.onlineInfo?.statusID == 'ending' && filters['endingStatus']) return returnMode; return returnMode;
if (
(station.onlineInfo?.statusID == 'not-signed' || station.onlineInfo?.statusID == 'unavailable') && if (filters['onlineFromHours'] > 0 && station.onlineInfo && station.onlineInfo.statusTimestamp <= 0)
filters['unavailableStatus'] return returnMode;
) if (filters['onlineFromHours'] == 8 && station.onlineInfo?.statusID != 'no-limit') return returnMode;
return returnMode;
if (station.onlineInfo?.statusID == 'brb' && filters['afkStatus']) return returnMode; if (station.onlineInfo?.statusID == 'ending' && filters['endingStatus']) return returnMode;
if (station.onlineInfo?.statusID == 'no-space' && filters['noSpaceStatus']) return returnMode; if (
(station.onlineInfo?.statusID == 'not-signed' || station.onlineInfo?.statusID == 'unavailable') &&
if (station.onlineInfo && filters['occupied']) return returnMode; filters['unavailableStatus']
if (!station.onlineInfo && filters['free']) return returnMode; )
if (station.generalInfo?.availability == 'unavailable' && filters['unavailable'] && !station.onlineInfo) return returnMode;
return returnMode; if (station.onlineInfo?.statusID == 'brb' && filters['afkStatus']) return returnMode;
if (station.onlineInfo?.statusID == 'no-space' && filters['noSpaceStatus']) return returnMode;
if (station.generalInfo) {
const routes = station.generalInfo.routes; if (station.onlineInfo && filters['occupied']) return returnMode;
const availability = station.generalInfo.availability; if (!station.onlineInfo && filters['free']) return returnMode;
if (station.generalInfo?.availability == 'unavailable' && filters['unavailable'] && !station.onlineInfo)
if (filters['abandoned'] && availability == 'abandoned') return returnMode; return returnMode;
if (availability == 'default' && filters['default']) return returnMode; if (station.generalInfo) {
if ( const routes = station.generalInfo.routes;
availability != 'default' && const availability = station.generalInfo.availability;
filters['notDefault'] &&
!(availability == 'abandoned' || availability == 'unavailable') if (filters['abandoned'] && availability == 'abandoned' && !station.onlineInfo) return returnMode;
)
return returnMode; if (availability == 'default' && filters['default']) return returnMode;
if (
if (filters['real'] && station.generalInfo.lines != '') return returnMode; availability != 'default' &&
if ( filters['notDefault'] &&
filters['fictional'] && !(availability == 'abandoned' || availability == 'unavailable')
station.generalInfo.lines == '' && )
availability != 'abandoned' && return returnMode;
availability != 'unavailable'
) if (filters['real'] && station.generalInfo.lines != '') return returnMode;
return returnMode; if (
filters['fictional'] &&
if ( station.generalInfo.lines == '' &&
station.generalInfo.reqLevel + availability != 'abandoned' &&
(availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned' ? 1 : 0) < availability != 'unavailable'
filters['minLevel'] )
) return returnMode;
return returnMode;
if ( if (
station.generalInfo.reqLevel + station.generalInfo.reqLevel +
(availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned' ? 1 : 0) > (availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned' ? 1 : 0) <
filters['maxLevel'] filters['minLevel']
) )
return returnMode; return returnMode;
if (
if ( station.generalInfo.reqLevel +
filters['no-1track'] && (availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned' ? 1 : 0) >
(routes.oneWayCatenaryRouteNames.length != 0 || routes.oneWayNoCatenaryRouteNames.length != 0) filters['maxLevel']
) )
return returnMode; return returnMode;
if (
filters['no-2track'] && if (
(routes.twoWayCatenaryRouteNames.length != 0 || routes.twoWayNoCatenaryRouteNames.length != 0) filters['no-1track'] &&
) (routes.oneWayCatenaryRouteNames.length != 0 || routes.oneWayNoCatenaryRouteNames.length != 0)
return returnMode; )
return returnMode;
if (routes.oneWayCatenaryRouteNames.length < filters['minOneWayCatenary']) return returnMode; if (
if (routes.oneWayNoCatenaryRouteNames.length < filters['minOneWay']) return returnMode; filters['no-2track'] &&
(routes.twoWayCatenaryRouteNames.length != 0 || routes.twoWayNoCatenaryRouteNames.length != 0)
if (routes.twoWayCatenaryRouteNames.length < filters['minTwoWayCatenary']) return returnMode; )
if (routes.twoWayNoCatenaryRouteNames.length < filters['minTwoWay']) return returnMode; return returnMode;
if (filters[station.generalInfo.controlType]) return returnMode; if (routes.oneWayCatenaryRouteNames.length < filters['minOneWayCatenary']) return returnMode;
if (filters[station.generalInfo.signalType]) return returnMode; if (routes.oneWayNoCatenaryRouteNames.length < filters['minOneWay']) return returnMode;
if ( if (routes.twoWayCatenaryRouteNames.length < filters['minTwoWayCatenary']) return returnMode;
filters['SPK'] && if (routes.twoWayNoCatenaryRouteNames.length < filters['minTwoWay']) return returnMode;
(station.generalInfo.controlType === 'SPK' || station.generalInfo.controlType.includes('+SPK'))
) if (filters[station.generalInfo.controlType]) return returnMode;
return returnMode; if (filters[station.generalInfo.signalType]) return returnMode;
if (
filters['SCS'] && if (
(station.generalInfo.controlType === 'SCS' || station.generalInfo.controlType.includes('+SCS')) filters['SPK'] &&
) (station.generalInfo.controlType === 'SPK' || station.generalInfo.controlType.includes('+SPK'))
return returnMode; )
if ( return returnMode;
filters['SPE'] && if (
(station.generalInfo.controlType === 'SPE' || station.generalInfo.controlType.includes('+SPE')) filters['SCS'] &&
) (station.generalInfo.controlType === 'SCS' || station.generalInfo.controlType.includes('+SCS'))
return returnMode; )
if (filters['SUP'] && station.generalInfo.SUP) return returnMode; return returnMode;
if (
if ( filters['SPE'] &&
filters['SCS'] && (station.generalInfo.controlType === 'SPE' || station.generalInfo.controlType.includes('+SPE'))
filters['SPK'] && )
(station.generalInfo.controlType.includes('SPK') || station.generalInfo.controlType.includes('SCS')) return returnMode;
) if (filters['SUP'] && station.generalInfo.SUP) return returnMode;
return returnMode;
if (
if (filters['mechaniczne'] && station.generalInfo.controlType.includes('mechaniczne')) return returnMode; filters['SCS'] &&
filters['SPK'] &&
if (filters['ręczne'] && station.generalInfo.controlType.includes('ręczne')) return returnMode; (station.generalInfo.controlType.includes('SPK') || station.generalInfo.controlType.includes('SCS'))
)
if (filters['SBL'] && routes.sblRouteNames.length > 0) return returnMode; return returnMode;
if ( if (filters['mechaniczne'] && station.generalInfo.controlType.includes('mechaniczne')) return returnMode;
filters['authors'].length > 3 &&
!station.generalInfo.authors?.map((a) => a.toLocaleLowerCase()).includes(filters['authors'].toLocaleLowerCase()) if (filters['ręczne'] && station.generalInfo.controlType.includes('ręczne')) return returnMode;
)
return returnMode; if (filters['SBL'] && routes.sblRouteNames.length > 0) return returnMode;
}
if (
return true; filters['authors'].length > 3 &&
}; !station.generalInfo.authors?.map((a) => a.toLocaleLowerCase()).includes(filters['authors'].toLocaleLowerCase())
)
export default class StationFilterManager { return returnMode;
private filterInitStates: Filter = { }
default: false,
notDefault: false, return true;
real: false, };
fictional: false,
SPK: false, const filterInitStates: Filter = {
SCS: false, default: false,
SPE: false, notDefault: false,
SUP: false, real: false,
ręczne: false, fictional: false,
mechaniczne: false, SPK: false,
współczesna: false, SCS: false,
kształtowa: false, SPE: false,
historyczna: false, SUP: false,
mieszana: false, ręczne: false,
SBL: false, mechaniczne: false,
minLevel: 0, współczesna: false,
maxLevel: 20, kształtowa: false,
minOneWayCatenary: 0, historyczna: false,
minOneWay: 0, mieszana: false,
minTwoWayCatenary: 0, SBL: false,
minTwoWay: 0, minLevel: 0,
'include-selected': false, maxLevel: 20,
'no-1track': false, minOneWayCatenary: 0,
'no-2track': false, minOneWay: 0,
free: true, minTwoWayCatenary: 0,
occupied: false, minTwoWay: 0,
ending: false, 'include-selected': false,
nonPublic: false, 'no-1track': false,
unavailable: true, 'no-2track': false,
abandoned: true, free: true,
afkStatus: false, occupied: false,
endingStatus: false, ending: false,
noSpaceStatus: false, nonPublic: false,
unavailableStatus: false, unavailable: true,
unsignedStatus: false, abandoned: true,
afkStatus: false,
authors: '', endingStatus: false,
noSpaceStatus: false,
onlineFromHours: 0, unavailableStatus: false,
}; unsignedStatus: false,
private filters: Filter = { ...this.filterInitStates }; authors: '',
private sorter: { index: number; dir: number } = { index: 0, dir: 1 }; onlineFromHours: 0,
};
checkFilters() {
if (!StorageManager.isRegistered('options_saved')) return; export const useStationFiltersStore = defineStore('stationFiltersStore', {
state() {
Object.keys(this.filterInitStates).forEach((filterKey) => { return {
if (StorageManager.isRegistered(filterKey)) return; inputs: inputData,
filters: { ...filterInitStates },
const filterType = typeof this.filterInitStates[filterKey]; sorterActive: { index: 0, dir: 1 },
store: useStore(),
if (filterType === 'boolean') };
StorageManager.setBooleanValue(filterKey, !this.filterInitStates[filterKey] as boolean); },
if (filterType === 'number') actions: {
StorageManager.setNumericValue(filterKey, this.filterInitStates[filterKey] as number); getFilteredStationList(stationList: Station[], region: string): Station[] {
}); return stationList
} .map((station) => {
if (station.onlineInfo && station.onlineInfo.region != region) {
getFilteredStationList(stationList: Station[], region: string): Station[] { delete station.onlineInfo;
return stationList }
.map((station) => {
if (station.onlineInfo && station.onlineInfo.region != region) { return station;
delete station.onlineInfo; })
} .filter((station) => filterStations(station, this.filters, this.store.isOffline))
.sort((a, b) => sortStations(a, b, this.sorterActive));
return station; },
})
.filter((station) => filterStations(station, this.filters)) setupFilters() {
.sort((a, b) => sortStations(a, b, this.sorter)); if (!StorageManager.isRegistered('options_saved')) return;
}
this.inputs.options.forEach((option) => {
changeFilterValue(filter: { name: string; value: number }) { if (!StorageManager.isRegistered(option.id)) return;
this.filters[filter.name] = filter.value; const savedValue = StorageManager.getBooleanValue(option.id);
// if(filter.name == 'authors') this.filters[option.id] = savedValue;
} option.value = !savedValue;
});
resetFilters() {
this.filters = { ...this.filterInitStates }; this.inputs.sliders.forEach((slider) => {
} if (!StorageManager.isRegistered(slider.name)) return;
const savedValue = StorageManager.getNumericValue(slider.name);
invertFilters() {
Object.keys(this.filters).forEach((prop) => { this.filters[slider.name] = savedValue;
if (typeof this.filters[prop] !== 'boolean') return; slider.value = savedValue;
});
this.filters[prop] = !this.filters[prop]; },
});
} changeFilterValue(filter: { name: string; value: any }) {
this.filters[filter.name] = filter.value;
changeSorter(index: number) {
if (index > 4 && index < 7) return; if (StorageManager.isRegistered('options_saved')) StorageManager.setValue(filter.name, filter.value);
},
if (index == this.sorter.index) this.sorter.dir = -1 * this.sorter.dir;
else this.sorter.dir = 1; resetFilters() {
this.filters = { ...filterInitStates };
this.sorter.index = index;
} this.inputs.options.forEach((option) => {
option.value = option.defaultValue;
getSorter() { StorageManager.setBooleanValue(option.name, !option.defaultValue);
return this.sorter; });
}
} 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;
},
},
});
+413 -398
View File
@@ -1,398 +1,413 @@
import axios from 'axios'; import axios from 'axios';
import { defineStore } from 'pinia'; 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';
import { URLs } from '../scripts/utils/apiURLs'; import { URLs } from '../scripts/utils/apiURLs';
import { import {
getLocoURL, getLocoURL,
getStatusTimestamp, getStatusTimestamp,
getStatusID, getStatusID,
getScheduledTrain, getScheduledTrain,
parseSpawns, parseSpawns,
} from '../scripts/utils/storeUtils'; } from '../scripts/utils/storeUtils';
import { APIData, StationJSONData, StoreState } from './storeTypes'; import { APIData, StationJSONData, StoreState } from './storeTypes';
export const useStore = defineStore('store', { export const useStore = defineStore('store', {
state: () => state: () =>
({ ({
apiData: {} as unknown, apiData: {} as unknown,
stationList: [], stationList: [],
trainList: [], trainList: [],
sceneryData: [], sceneryData: [],
lastDispatcherStatuses: [], lastDispatcherStatuses: [],
region: { id: 'eu', value: 'PL1' }, region: { id: 'eu', value: 'PL1' },
trainCount: 0, trainCount: 0,
stationCount: 0, stationCount: 0,
webSocket: undefined, webSocket: undefined,
isOffline: false,
dispatcherStatsName: '',
dispatcherStatsData: undefined, dispatcherStatsName: '',
dispatcherStatsData: undefined,
driverStatsName: '',
driverStatsData: undefined, driverStatsName: '',
driverStatsData: undefined,
chosenModalTrainId: undefined, driverStatsStatus: DataStatus.Initialized,
dataStatuses: { chosenModalTrainId: undefined,
connection: DataStatus.Loading,
sceneries: DataStatus.Loading, dataStatuses: {
timetables: DataStatus.Loading, connection: DataStatus.Loading,
dispatchers: DataStatus.Loading, sceneries: DataStatus.Loading,
trains: DataStatus.Loading, timetables: DataStatus.Loading,
}, dispatchers: DataStatus.Loading,
trains: DataStatus.Loading,
listenerLaunched: false, },
} as StoreState),
currentStatsTab: 'daily',
actions: {
setTrainsOnlineData() { blockScroll: false,
const { trains } = this.apiData; listenerLaunched: false,
} as StoreState),
if (!trains) return [];
actions: {
this.trainList = trains setTrainsOnlineData() {
.filter( const { trains } = this.apiData;
(train) =>
train.region === this.region.id && (train.online || train.timetable || train.lastSeen > Date.now() - 180000) if (!trains) return [];
)
.map((train) => { this.trainList = trains
const stock = train.stockString.split(';'); .filter(
const locoType = stock ? stock[0] : train.stockString; (train) =>
train.region === this.region.id && (train.online || train.timetable || train.lastSeen > Date.now() - 180000)
const timetable = train.timetable; )
.map((train) => {
return { const stock = train.stockString.split(';');
trainId: train.driverName + train.trainNo.toString(), const locoType = stock ? stock[0] : train.stockString;
trainNo: train.trainNo, const timetable = train.timetable;
mass: train.mass,
length: train.length, return {
speed: train.speed, trainId: train.driverName + train.trainNo.toString(),
region: train.region,
trainNo: train.trainNo,
distance: train.distance, mass: train.mass,
signal: train.signal, length: train.length,
online: train.online, speed: train.speed,
driverId: train.driverId, region: train.region,
driverName: train.driverName,
currentStationName: train.currentStationName, distance: train.distance,
currentStationHash: train.currentStationHash, signal: train.signal,
connectedTrack: train.connectedTrack, online: train.online,
locoType, driverId: train.driverId,
locoURL: getLocoURL(locoType), driverName: train.driverName,
cars: stock.slice(1), currentStationName: train.currentStationName,
currentStationHash: train.currentStationHash,
lastSeen: train.lastSeen, connectedTrack: train.connectedTrack,
locoType,
timetableData: timetable locoURL: getLocoURL(locoType),
? { cars: stock.slice(1),
timetableId: timetable.timetableId,
SKR: timetable.SKR, lastSeen: train.lastSeen,
TWR: timetable.TWR, isTimeout: train.isTimeout,
route: timetable.route,
category: timetable.category, isSupporter: train.driverIsSupporter,
followingStops: timetable.stopList,
routeDistance: timetable.stopList[timetable.stopList.length - 1].stopDistance, timetableData: timetable
sceneries: timetable.sceneries, ? {
} timetableId: timetable.timetableId,
: undefined, SKR: timetable.SKR,
}; TWR: timetable.TWR,
}) as Train[]; route: timetable.route,
}, category: timetable.category,
followingStops: timetable.stopList,
getDispatcherStatus(onlineStationData: StationAPIData) { routeDistance: timetable.stopList[timetable.stopList.length - 1].stopDistance,
const { dispatchers } = this.apiData; sceneries: timetable.sceneries,
}
const prevDispatcherStatus = this.lastDispatcherStatuses.find( : undefined,
(dispatcher) => dispatcher.hash === onlineStationData.stationHash };
); }) as Train[];
},
const stationStatus = !dispatchers
? undefined getDispatcherStatus(onlineStationData: StationAPIData) {
: dispatchers.find( const { dispatchers } = this.apiData;
(status: string[]) => status[0] == onlineStationData.stationHash && status[1] == this.region.id
) || -1; const prevDispatcherStatus = this.lastDispatcherStatuses.find(
(dispatcher) => dispatcher.hash === onlineStationData.stationHash
const statusTimestamp = );
prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusTimestamp : getStatusTimestamp(stationStatus);
const statusID = const stationStatus = !dispatchers
prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusID : getStatusID(stationStatus); ? undefined
: dispatchers.find(
return { (status: string[]) => status[0] == onlineStationData.stationHash && status[1] == this.region.id
hash: onlineStationData.stationHash, ) || -1;
statusID,
statusTimestamp, const statusTimestamp =
}; prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusTimestamp : getStatusTimestamp(stationStatus);
}, const statusID =
prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusID : getStatusID(stationStatus);
getScheduledTrains(stationGeneralInfo: Station['generalInfo'], stationAPIData: StationAPIData) {
const stationName = stationAPIData.stationName.toLowerCase(); return {
hash: onlineStationData.stationHash,
stationGeneralInfo?.checkpoints.forEach((cp) => (cp.scheduledTrains.length = 0)); statusID,
statusTimestamp,
return this.trainList.reduce((acc: ScheduledTrain[], train) => { };
if (!train.timetableData) return acc; },
const timetable = train.timetableData; getScheduledTrains(stationGeneralInfo: Station['generalInfo'], stationAPIData: StationAPIData) {
if (!timetable.sceneries.includes(stationAPIData.stationHash)) return acc; const stationName = stationAPIData.stationName.toLowerCase();
const stopInfoIndex = timetable.followingStops.findIndex((stop) => { stationGeneralInfo?.checkpoints.forEach((cp) => (cp.scheduledTrains.length = 0));
const stopName = stop.stopNameRAW.toLowerCase();
return this.trainList.reduce((acc: ScheduledTrain[], train) => {
if (stationName === stopName) return true; if (!train.timetableData) return acc;
if (stopName.includes(stationName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.'))
return true; const timetable = train.timetableData;
if (!timetable.sceneries.includes(stationAPIData.stationHash)) return acc;
if (stationName.includes(stopName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.'))
return true; const stopInfoIndex = timetable.followingStops.findIndex((stop) => {
const stopName = stop.stopNameRAW.toLowerCase();
if (
stopName.includes('podg.') && if (stationName === stopName) return true;
stopName.split(', podg.')[0] && if (stopName.includes(stationName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.'))
stationName.includes(stopName.split(', podg.')[0]) return true;
)
return true; if (stationName.includes(stopName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.'))
return true;
if (
stationGeneralInfo && if (
stationGeneralInfo.checkpoints && stopName.includes('podg.') &&
stationGeneralInfo.checkpoints.length > 0 && stopName.split(', podg.')[0] &&
stationGeneralInfo.checkpoints.some((cp) => stationName.includes(stopName.split(', podg.')[0])
cp.checkpointName.toLowerCase().includes(stop.stopNameRAW.toLowerCase()) )
) return true;
)
return true; if (
stationGeneralInfo &&
return false; stationGeneralInfo.checkpoints &&
}); stationGeneralInfo.checkpoints.length > 0 &&
stationGeneralInfo.checkpoints.some((cp) =>
if (stopInfoIndex == -1) return acc; cp.checkpointName.toLowerCase().includes(stop.stopNameRAW.toLowerCase())
)
const scheduledStopTrain = getScheduledTrain(train, stopInfoIndex, stationAPIData.stationName); )
return true;
if (stationGeneralInfo?.checkpoints) {
for (const checkpoint of stationGeneralInfo.checkpoints) { return false;
const index = timetable.followingStops.findIndex( });
(stop) => stop.stopNameRAW.toLowerCase() == checkpoint.checkpointName.toLowerCase()
); if (stopInfoIndex == -1) return acc;
if (index == -1) continue; const scheduledStopTrain = getScheduledTrain(train, stopInfoIndex, stationAPIData.stationName);
const scheduledCheckpointTrain = getScheduledTrain(train, index, stationAPIData.stationName); if (stationGeneralInfo?.checkpoints) {
checkpoint.scheduledTrains.push(scheduledCheckpointTrain); for (const checkpoint of stationGeneralInfo.checkpoints) {
} const index = timetable.followingStops.findIndex(
} (stop) => stop.stopNameRAW.toLowerCase() == checkpoint.checkpointName.toLowerCase()
);
acc.push(scheduledStopTrain);
return acc; if (index == -1) continue;
}, []) as ScheduledTrain[];
}, const scheduledCheckpointTrain = getScheduledTrain(train, index, stationAPIData.stationName);
checkpoint.scheduledTrains.push(scheduledCheckpointTrain);
getStationTrains(stationAPIData: StationAPIData) { }
return this.trainList }
.filter(
(train) => acc.push(scheduledStopTrain);
train?.region === this.region.id && train.online && train.currentStationName === stationAPIData.stationName return acc;
) }, []) as ScheduledTrain[];
.map((train) => ({ },
driverName: train.driverName,
driverId: train.driverId, getStationTrains(stationAPIData: StationAPIData) {
trainNo: train.trainNo, return this.trainList
trainId: train.trainId, .filter(
})); (train) =>
}, train?.region === this.region.id && train.online && train.currentStationName === stationAPIData.stationName
)
setStationsOnlineInfo() { .map((train) => ({
const onlineStationNames: string[] = []; driverName: train.driverName,
const prevDispatcherStatuses: StoreState['lastDispatcherStatuses'] = []; driverId: train.driverId,
trainNo: train.trainNo,
this.apiData.stations?.forEach((stationAPIData) => { trainId: train.trainId,
if (stationAPIData.region !== this.region.id || !stationAPIData.isOnline) return; }));
const station = this.stationList.find((s) => s.name === stationAPIData.stationName); },
onlineStationNames.push(stationAPIData.stationName); setStationsOnlineInfo() {
const onlineStationNames: string[] = [];
const dispatcherStatus = this.getDispatcherStatus(stationAPIData); const prevDispatcherStatuses: StoreState['lastDispatcherStatuses'] = [];
prevDispatcherStatuses.push(dispatcherStatus);
if (this.isOffline) {
const stationTrains = this.getStationTrains(stationAPIData); this.stationList.forEach((station) => {
const scheduledTrains = this.getScheduledTrains(station?.generalInfo, stationAPIData); station.onlineInfo = undefined;
});
const onlineInfo = {
name: stationAPIData.stationName, return;
hash: stationAPIData.stationHash, }
region: stationAPIData.region,
maxUsers: stationAPIData.maxUsers, this.apiData.stations?.forEach((stationAPIData) => {
currentUsers: stationAPIData.currentUsers, if (stationAPIData.region !== this.region.id || !stationAPIData.isOnline) return;
spawns: parseSpawns(stationAPIData.spawnString), const station = this.stationList.find((s) => s.name === stationAPIData.stationName);
dispatcherName: stationAPIData.dispatcherName,
dispatcherRate: stationAPIData.dispatcherRate, onlineStationNames.push(stationAPIData.stationName);
dispatcherId: stationAPIData.dispatcherId,
dispatcherExp: stationAPIData.dispatcherExp, const dispatcherStatus = this.getDispatcherStatus(stationAPIData);
dispatcherIsSupporter: stationAPIData.dispatcherIsSupporter, prevDispatcherStatuses.push(dispatcherStatus);
stationTrains,
statusTimestamp: dispatcherStatus.statusTimestamp, const stationTrains = this.getStationTrains(stationAPIData);
statusID: dispatcherStatus.statusID, const scheduledTrains = this.getScheduledTrains(station?.generalInfo, stationAPIData);
scheduledTrains,
}; const onlineInfo = {
name: stationAPIData.stationName,
if (!station) { hash: stationAPIData.stationHash,
this.stationList.push({ region: stationAPIData.region,
name: stationAPIData.stationName, maxUsers: stationAPIData.maxUsers,
onlineInfo, currentUsers: stationAPIData.currentUsers,
}); spawns: parseSpawns(stationAPIData.spawnString),
dispatcherName: stationAPIData.dispatcherName,
return; dispatcherRate: stationAPIData.dispatcherRate,
} dispatcherId: stationAPIData.dispatcherId,
dispatcherExp: stationAPIData.dispatcherExp,
station.onlineInfo = { ...onlineInfo }; dispatcherIsSupporter: stationAPIData.dispatcherIsSupporter,
stationTrains,
this.stationList statusTimestamp: dispatcherStatus.statusTimestamp,
.filter((station) => !onlineStationNames.includes(station.name) && station.onlineInfo) statusID: dispatcherStatus.statusID,
.forEach((offlineStation) => { scheduledTrains,
offlineStation.onlineInfo = undefined; };
});
}); if (!station) {
this.stationList.push({
if (this.apiData.dispatchers != null) this.lastDispatcherStatuses = prevDispatcherStatuses; name: stationAPIData.stationName,
}, onlineInfo,
});
async fetchStationsGeneralInfo() {
const sceneryData: StationJSONData[] = await ( return;
await axios.get(`${URLs.stacjownikAPI}/api/getSceneries?timestamp=${Math.floor(Date.now() / 1800000)}`) }
).data;
station.onlineInfo = { ...onlineInfo };
if (!sceneryData) {
this.dataStatuses.sceneries = DataStatus.Error; this.stationList
return; .filter((station) => !onlineStationNames.includes(station.name) && station.onlineInfo)
} .forEach((offlineStation) => {
offlineStation.onlineInfo = undefined;
this.stationList = sceneryData.map((scenery) => ({ });
name: scenery.name, });
generalInfo: { if (this.apiData.dispatchers != null) this.lastDispatcherStatuses = prevDispatcherStatuses;
...scenery, },
authors: scenery.authors?.split(',').map((a) => a.trim()),
routes: async fetchStationsGeneralInfo() {
scenery.routes const sceneryData: StationJSONData[] = await (
?.split(';') await axios.get(`${URLs.stacjownikAPI}/api/getSceneries?timestamp=${Math.floor(Date.now() / 1800000)}`)
.filter((routeString) => routeString) ).data;
.reduce(
(acc, routeString) => { if (!sceneryData) {
const specs1 = routeString.split('_')[0]; this.dataStatuses.sceneries = DataStatus.Error;
const isInternal = specs1.startsWith('!'); return;
const name = isInternal ? specs1.replace('!', '') : specs1; }
const specs2 = routeString.split('_')[1].split(''); this.stationList = sceneryData.map((scenery) => ({
const twoWay = specs2[0] == '2'; name: scenery.name,
const catenary = specs2[1] == 'E';
const SBL = specs2[2] == 'S'; generalInfo: {
const TWB = specs2[3] ? true : false; ...scenery,
authors: scenery.authors?.split(',').map((a) => a.trim()),
const propName = twoWay routes:
? catenary scenery.routes
? 'twoWayCatenaryRouteNames' ?.split(';')
: 'twoWayNoCatenaryRouteNames' .filter((routeString) => routeString)
: catenary .reduce(
? 'oneWayCatenaryRouteNames' (acc, routeString) => {
: 'oneWayNoCatenaryRouteNames'; const specs1 = routeString.split('_')[0];
const isInternal = specs1.startsWith('!');
acc[twoWay ? 'twoWay' : 'oneWay'].push({ const name = isInternal ? specs1.replace('!', '') : specs1;
name,
SBL, const specs2 = routeString.split('_')[1].split('');
TWB, const twoWay = specs2[0] == '2';
catenary, const catenary = specs2[1] == 'E';
isInternal, const SBL = specs2[2] == 'S';
tracks: twoWay ? 2 : 1, const TWB = specs2[3] ? true : false;
});
if (!isInternal) acc[propName].push(name); const propName = twoWay
? catenary
if (SBL) acc['sblRouteNames'].push(name); ? 'twoWayCatenaryRouteNames'
: 'twoWayNoCatenaryRouteNames'
return acc; : catenary
}, ? 'oneWayCatenaryRouteNames'
{ : 'oneWayNoCatenaryRouteNames';
oneWay: [],
twoWay: [], acc[twoWay ? 'twoWay' : 'oneWay'].push({
sblRouteNames: [], name,
oneWayCatenaryRouteNames: [], SBL,
oneWayNoCatenaryRouteNames: [], TWB,
twoWayCatenaryRouteNames: [], catenary,
twoWayNoCatenaryRouteNames: [], isInternal,
} as StationRoutes tracks: twoWay ? 2 : 1,
) || {}, });
checkpoints: scenery.checkpoints if (!isInternal) acc[propName].push(name);
? scenery.checkpoints.split(';').map((sub) => ({ checkpointName: sub, scheduledTrains: [] }))
: [], if (SBL) acc['sblRouteNames'].push(name);
},
})); return acc;
}, },
{
connectToWebsocket() { oneWay: [],
const socket = io(URLs.stacjownikAPI, { twoWay: [],
transports: ['websocket', 'polling'], sblRouteNames: [],
rememberUpgrade: true, oneWayCatenaryRouteNames: [],
reconnection: true, oneWayNoCatenaryRouteNames: [],
timeout: 10000, twoWayCatenaryRouteNames: [],
}); twoWayNoCatenaryRouteNames: [],
} as StationRoutes
socket.on('connect_error', (err) => { ) || {},
this.dataStatuses.connection = DataStatus.Error; checkpoints: scenery.checkpoints
this.webSocket = undefined; ? scenery.checkpoints.split(';').map((sub) => ({ checkpointName: sub, scheduledTrains: [] }))
}); : [],
},
socket.on('UPDATE', (data: APIData) => { }));
this.apiData = data; },
this.dataStatuses.connection = DataStatus.Loaded;
this.setOnlineData(); connectToWebsocket() {
}); const socket = io(URLs.stacjownikAPI, {
transports: ['websocket', 'polling'],
socket.emit('FETCH_DATA', {}, (data: APIData) => { rememberUpgrade: true,
this.apiData = data; reconnection: true,
this.setOnlineData(); timeout: 2000,
}); });
this.webSocket = socket; socket.on('connect_error', (err) => {
}, this.dataStatuses.connection = DataStatus.Error;
});
async connectToAPI() {
await this.fetchStationsGeneralInfo(); socket.on('UPDATE', (data: APIData) => {
this.apiData = data;
this.connectToWebsocket(); this.dataStatuses.connection = DataStatus.Loaded;
}, this.setOnlineData();
});
async changeRegion(region: StoreState['region']) {
this.region = region; socket.emit('FETCH_DATA', {}, (data: APIData) => {
this.apiData = data;
await this.setOnlineData(); this.dataStatuses.connection = DataStatus.Loaded;
}, this.setOnlineData();
});
async setOnlineData() {
if (!this.apiData.stations) { this.webSocket = socket;
this.dataStatuses.sceneries = DataStatus.Error; },
this.dataStatuses.trains = DataStatus.Error;
this.dataStatuses.dispatchers = DataStatus.Error; async connectToAPI() {
await this.fetchStationsGeneralInfo();
return;
} this.connectToWebsocket();
},
this.dataStatuses.sceneries = DataStatus.Loaded;
this.dataStatuses.trains = !this.apiData.trains ? DataStatus.Warning : DataStatus.Loaded; async changeRegion(region: StoreState['region']) {
this.dataStatuses.dispatchers = !this.apiData.dispatchers ? DataStatus.Warning : DataStatus.Loaded; this.region = region;
this.setTrainsOnlineData(); await this.setOnlineData();
this.setStationsOnlineInfo(); },
},
}, async setOnlineData() {
}); if (!this.apiData.stations) {
this.dataStatuses.sceneries = DataStatus.Error;
this.dataStatuses.trains = DataStatus.Error;
this.dataStatuses.dispatchers = DataStatus.Error;
return;
}
this.dataStatuses.sceneries = DataStatus.Loaded;
this.dataStatuses.trains = !this.apiData.trains ? DataStatus.Warning : DataStatus.Loaded;
this.dataStatuses.dispatchers = !this.apiData.dispatchers ? DataStatus.Warning : DataStatus.Loaded;
this.setTrainsOnlineData();
this.setStationsOnlineInfo();
},
},
});
+76 -71
View File
@@ -1,71 +1,76 @@
import { Socket } from 'socket.io-client';
import { Socket } from 'socket.io-client'; import { DataStatus } from '../scripts/enums/DataStatus';
import { DataStatus } from '../scripts/enums/DataStatus'; import StationAPIData from '../scripts/interfaces/api/StationAPIData';
import { DispatcherStatsAPIData } from '../scripts/interfaces/api/DispatcherStatsAPIData'; import TrainAPIData from '../scripts/interfaces/api/TrainAPIData';
import { DriverStatsAPIData } from '../scripts/interfaces/api/DriverStatsAPIData'; import Station from '../scripts/interfaces/Station';
import StationAPIData from '../scripts/interfaces/api/StationAPIData'; import Train from '../scripts/interfaces/Train';
import TrainAPIData from '../scripts/interfaces/api/TrainAPIData'; import { DispatcherStatsAPIData } from '../scripts/interfaces/api/DispatcherStatsAPIData';
import Station from '../scripts/interfaces/Station'; import { DriverStatsAPIData } from '../scripts/interfaces/api/DriverStatsAPIData';
import Train from '../scripts/interfaces/Train';
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
export interface StoreState {
export interface StoreState { stationList: Station[];
stationList: Station[]; trainList: Train[];
trainList: Train[]; apiData: APIData;
apiData: APIData;
lastDispatcherStatuses: { hash: string; statusTimestamp: number; statusID: string }[];
lastDispatcherStatuses: { hash: string; statusTimestamp: number; statusID: string }[];
sceneryData: any[][];
sceneryData: any[][];
region: { id: string; value: string };
region: { id: string; value: string }; trainCount: number;
trainCount: number; stationCount: number;
stationCount: number;
webSocket?: Socket;
webSocket?: Socket; isOffline: boolean;
dispatcherStatsName: string; dispatcherStatsName: string;
dispatcherStatsData?: DispatcherStatsAPIData; dispatcherStatsData?: DispatcherStatsAPIData;
driverStatsName: string; driverStatsName: string;
driverStatsData?: DriverStatsAPIData; driverStatsData?: DriverStatsAPIData;
driverStatsStatus: DataStatus;
chosenModalTrainId?: string;
chosenModalTrainId?: string;
dataStatuses: {
connection: DataStatus; currentStatsTab: 'daily' | 'driver';
sceneries: DataStatus;
timetables: DataStatus; dataStatuses: {
dispatchers: DataStatus; connection: DataStatus;
trains: DataStatus; sceneries: DataStatus;
}; timetables: DataStatus;
dispatchers: DataStatus;
listenerLaunched: boolean; trains: DataStatus;
} };
export interface APIData { listenerLaunched: boolean;
stations?: StationAPIData[]; blockScroll: boolean;
dispatchers?: string[][]; }
trains?: TrainAPIData[];
} export interface APIData {
stations?: StationAPIData[];
export interface StationJSONData { dispatchers?: string[][];
name: string; trains?: TrainAPIData[];
url: string; connectedSocketCount: number;
lines: string; }
project: string;
export interface StationJSONData {
reqLevel: number; name: string;
url: string;
signalType: string; lines: string;
controlType: string; project: string;
SUP: boolean; reqLevel: number;
routes: string; signalType: string;
checkpoints: string | null; controlType: string;
authors?: string;
SUP: boolean;
availability: Availability;
} routes: string;
checkpoints: string | null;
authors?: string;
availability: Availability;
}
+92 -65
View File
@@ -1,65 +1,92 @@
@import 'responsive.scss'; @import 'responsive.scss';
// Animations // Animations
.warning { .warning {
&-enter-from, &-enter-from,
&-leave-to { &-leave-to {
opacity: 0; opacity: 0;
} }
&-enter-active { &-enter-active {
transition: all 150ms ease-out; transition: all 150ms 100ms ease-out;
} }
&-leave-active { &-leave-active {
transition: all 150ms ease-out; transition: all 150ms 100ms ease-out;
} }
} }
//Styles //Styles
.journal-wrapper { .list_wrapper {
width: 1350px; overflow-y: auto;
padding: 1em 0; height: 90vh;
} min-height: 550px;
.journal_warning { padding-right: 0.2em;
text-align: center; }
font-size: 1.3em;
.journal_wrapper {
&.error { max-width: 1350px;
background-color: var(--clr-error); width: 100%;
}
} margin: 0 auto;
.schedule-dates > * { padding: 1em 0;
margin-right: 0.25em; }
}
.journal_warning {
.journal_item, text-align: center;
.journal_warning { font-size: 1.3em;
background: #202020;
padding: 1em; &.error {
margin: 1em 0; background-color: var(--clr-error);
} }
}
.journal_top-bar {
display: flex; .schedule-dates > * {
justify-content: space-between; margin-right: 0.25em;
align-items: center; }
}
.journal_item,
button.btn { .journal_warning {
padding: 0.5em 0.7em; background-color: #1a1a1a;
} padding: 1em;
margin-bottom: 1em;
@include smallScreen() { }
.journal-wrapper {
font-size: 1.25em; .journal_top-bar {
} display: flex;
justify-content: space-between;
.journal_top-bar { align-items: center;
justify-content: center; gap: 0.5em;
flex-wrap: wrap;
} position: relative;
}
margin-bottom: 0.5em;
}
.btn--load-data {
padding: 0.5em 1em;
display: flex;
margin: 0 auto;
font-size: 1.2em;
}
@include smallScreen() {
.list_wrapper {
font-size: 1.1em;
}
.journal_top-bar {
justify-content: center;
flex-wrap: wrap;
}
}
@media (orientation: landscape) {
.list_wrapper {
font-size: 1em;
}
}
+50
View File
@@ -0,0 +1,50 @@
@import 'variables.scss';
@import 'responsive.scss';
.stats-tab {
background-color: #1a1a1a;
box-shadow: 0 0 5px 1px $accentCol;
padding: 1em;
display: flex;
align-items: flex-end;
margin-bottom: 0.5em;
width: 100%;
}
.info-stats {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 1em;
}
.stat-badge {
display: flex;
span {
background-color: $accentCol;
color: black;
font-weight: bold;
padding: 0.2em 0.5em;
}
span:first-child {
background-color: #333;
color: white;
}
}
@include smallScreen {
.journal-stats {
text-align: center;
}
.info-stats {
justify-content: center;
}
}
-2
View File
@@ -28,8 +28,6 @@
width: 600px; width: 600px;
padding: 0.5em 1em;
@include smallScreen { @include smallScreen {
width: 100%; width: 100%;
height: 80vh; height: 80vh;
+160
View File
@@ -0,0 +1,160 @@
@import 'responsive.scss';
@import 'variables.scss';
@import 'search_box.scss';
.filters-options {
margin-bottom: 0.5em;
}
.filter-button .active-indicator {
width: 7px;
height: 7px;
background-color: lightgreen;
border-radius: 50%;
margin-left: 10px;
}
h1.option-title {
position: relative;
font-size: 1.1em;
margin: 0.7em 0 0.25em 0;
&::before {
content: '';
position: absolute;
top: -4px;
width: 50%;
height: 2px;
background-color: white;
border-radius: 2px;
}
}
.options-anim {
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateY(10px);
}
&-enter-active,
&-leave-active {
transition: all 150ms ease;
}
}
.bg {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 10;
}
.options_wrapper {
position: absolute;
background-color: $bgCol;
box-shadow: 0 5px 10px 2px #0f0f0f;
width: 97%;
max-width: 500px;
padding: 1em;
z-index: 100;
}
.options_sorters {
display: flex;
align-items: center;
flex-wrap: wrap;
padding: 0.25em 0.25em 0 0;
}
.options_filters {
display: flex;
flex-wrap: wrap;
margin: 0.5em 0 0 0;
}
.sort-option,
.filter-option {
margin: 0.25em 0.25em 0.25em 0;
}
.sort-option[data-selected='true'] {
color: $accentCol;
font-weight: bold;
}
.filter-option {
&#abandoned {
color: salmon;
}
&#fulfilled {
color: lightgreen;
}
&#active {
color: lightblue;
}
}
.search_content {
.search {
margin: 0.5em auto;
}
.search_actions {
display: flex;
gap: 0.5em;
margin: 1em 0;
width: 100%;
button {
width: 100%;
}
}
.search-box {
.search-exit {
position: absolute;
transform: translateY(-50%);
top: 50%;
right: 0;
}
}
}
@include smallScreen() {
h1 {
text-align: center;
&::before {
width: 75%;
left: 50%;
transform: translateX(-50%);
}
}
.options_wrapper {
font-size: 1.1em;
max-width: 100%;
}
.filter-option,
.sort-option {
margin: 0.25em 0.25em;
}
.options_filters,
.options_sorters {
justify-content: center;
}
}
+101 -59
View File
@@ -3,6 +3,7 @@
--clr-secondary: #2f2f2f; --clr-secondary: #2f2f2f;
--clr-bg: #4d4d4d; --clr-bg: #4d4d4d;
--clr-bg2: #1b1b1b;
--clr-accent: #1085b3; --clr-accent: #1085b3;
--clr-accent2: #ff3d5d; --clr-accent2: #ff3d5d;
@@ -12,6 +13,24 @@
--clr-error: #df3e3e; --clr-error: #df3e3e;
--clr-warning: #c59429; --clr-warning: #c59429;
font-size: 16px;
}
::-webkit-scrollbar {
width: 1rem;
height: 1rem;
background-color: transparent;
&-track {
border-radius: 0.5em;
background-color: #333;
}
&-thumb {
border-radius: 0.5em;
background-color: #666;
}
} }
html { html {
@@ -25,28 +44,14 @@ body {
padding: 0; padding: 0;
font-family: 'Quicksand', sans-serif; font-family: 'Quicksand', sans-serif;
overflow-y: scroll; overflow-y: scroll;
}
*:focus-visible { &.no-scroll {
outline: 1px solid white; overflow-y: hidden;
outline-offset: 1px; padding-right: 1rem;
}
:root { @include smallScreen() {
font-size: 16px; padding: 0;
} }
::-webkit-scrollbar {
width: 0.5rem;
height: 0.5rem;
&-track {
background: #222;
}
&-thumb {
border-radius: 1rem;
background: #777;
} }
} }
@@ -105,12 +110,12 @@ select {
} }
input { input {
border: 1px solid white;
background: none; background: none;
color: white; color: white;
font-size: 1em; font-size: 1em;
padding: 0.15em; background-color: #333;
padding: 0.15em 0.5em;
outline: none; outline: none;
@@ -129,6 +134,14 @@ input {
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
*:focus {
outline: none;
}
*:focus-visible {
outline: 1px solid $accentCol;
}
.title { .title {
color: $accentCol; color: $accentCol;
font-weight: 600; font-weight: 600;
@@ -182,54 +195,68 @@ ul {
} }
} }
.btn { button {
background: none;
cursor: pointer; cursor: pointer;
font-size: 1em; color: white;
background: none;
&--text { display: flex;
color: white; align-items: center;
transition: color 0.3s; justify-content: center;
&:hover:not(:disabled), padding: 0.25em 0.5em;
&:focus:not(:disabled) {
color: $accentCol;
}
&.checked { transition: all 100ms ease;
color: var(--clr-primary);
font-weight: bold; &[data-disabled='true'] {
} user-select: none;
pointer-events: none;
opacity: 0.85;
} }
&--image { &[data-inactive='true'] {
color: white; opacity: 0.55;
transition: color 0.3s;
} }
}
&--option { button.btn--filled {
cursor: pointer; background-color: #1a1a1a;
border-radius: 0.25em;
color: white; &:hover {
background-color: #333; background-color: #2a2a2a;
border-radius: 0.25em;
padding: 0.25em 0.5em;
&:hover:not(:disabled) {
background-color: #3c3c3c;
}
&.checked {
color: var(--clr-primary);
font-weight: bold;
background-color: #3c3c3c;
}
} }
}
&:disabled { button.btn--action {
opacity: 0.65; background-color: #424242;
border-radius: 0.25em;
&:hover {
background-color: #555;
}
}
button.btn--option {
color: white;
background-color: #333;
&.checked {
color: var(--clr-primary);
font-weight: bold;
background-color: #3c3c3c;
}
}
button.btn--image {
font-weight: bold;
padding: 0.35em 0.75em;
img {
width: 1.5em;
margin-right: 0.5em;
vertical-align: middle;
} }
} }
@@ -274,3 +301,18 @@ ul {
transform: translateX(-50%); transform: translateX(-50%);
} }
} }
@include smallScreen {
::-webkit-scrollbar {
width: 0.5em;
height: 0.5em;
&-track {
background-color: #222;
}
&-thumb {
background-color: #777;
}
}
}
+52
View File
@@ -0,0 +1,52 @@
@import 'responsive.scss';
.search {
label {
display: block;
color: #ccc;
margin-bottom: 0.25em;
}
&-box {
position: relative;
display: flex;
border-radius: 0.5em;
min-width: 200px;
margin-right: 0.25em;
}
&-input {
border: none;
background-color: #424242;
padding: 0.35em 0.5em;
width: 100%;
}
&-exit {
background-color: #424242;
img {
vertical-align: middle;
height: 1.3em;
}
}
&-button {
width: 80%;
max-width: 300px;
}
@include smallScreen {
&-box,
&-button {
margin: 0.5em 0 0 0;
}
&-box {
width: 100%;
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
$primaryCol: #2c2c2c; $primaryCol: #2c2c2c;
$secondaryCol: #01e733; $secondaryCol: #01e733;
$bgCol: #4d4d4d; $bgCol: #1d1d1d;
$bgLigtherCol: #5b5b5b; $bgLigtherCol: #5b5b5b;
$errorCol: #ff1919; $errorCol: #ff1919;
@@ -0,0 +1,8 @@
export type JournalDispatcherSearcher = {
[key in 'search-dispatcher' | 'search-station' | 'search-date']: string;
};
export interface JournalDispatcherSorter {
id: 'timestampFrom' | 'duration';
dir: -1 | 1;
}
@@ -0,0 +1,18 @@
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: 'beginDate' | 'distance' | 'total-stops';
dir: -1 | 1;
}
+6
View File
@@ -0,0 +1,6 @@
import { TrainFilterType } from "../../scripts/enums/TrainFilterType";
export interface TrainFilter {
id: TrainFilterType;
isActive: boolean;
}
+288
View File
@@ -0,0 +1,288 @@
<template>
<section class="journal-timetables">
<JournalHeader />
<div class="journal_wrapper">
<JournalOptions
@on-search-confirm="fetchHistoryData"
@on-options-reset="resetOptions"
:sorter-option-ids="['timestampFrom', 'duration']"
:data-status="dataStatus"
:current-options-active="currentOptionsActive"
/>
<div class="list_wrapper" @scroll="handleScroll">
<!-- <transition name="warning" mode="out-in"> -->
<!-- <div :key="dataStatus"> -->
<div class="journal_warning" v-if="store.isOffline">
{{ $t('app.offline') }}
</div>
<Loading v-else-if="dataStatus == DataStatus.Initialized || 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="historyList.length == 0">
{{ $t('app.no-result') }}
</div>
<div v-else>
<JournalDispatchersList :dispatcherHistory="computedHistoryList" />
<button
class="btn btn--option btn--load-data"
v-if="!scrollNoMoreData && scrollDataLoaded && computedHistoryList.length > 15"
@click="addHistoryData"
>
{{ $t('journal.load-data') }}
</button>
</div>
<!-- </div>
</transition> -->
<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>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent, provide, reactive, Ref, ref } from 'vue';
import axios from 'axios';
import ActionButton from '../components/Global/ActionButton.vue';
import JournalOptions from '../components/JournalView/JournalOptions.vue';
import DispatcherStats from '../components/JournalView/DispatcherStats.vue';
import SearchBox from '../components/Global/SearchBox.vue';
import Loading from '../components/Global/Loading.vue';
import { URLs } from '../scripts/utils/apiURLs';
import { DataStatus } from '../scripts/enums/DataStatus';
import { useStore } from '../store/store';
import JournalDispatchersList from '../components/JournalView/JournalDispatchersList.vue';
import { JournalDispatcherSearcher, JournalDispatcherSorter } from '../types/Journal/JournalDispatcherTypes';
import { DispatcherHistory } from '../scripts/interfaces/api/DispatchersAPIData';
import JournalHeader from '../components/JournalView/JournalHeader.vue';
import { LocationQuery } from 'vue-router';
const DISPATCHERS_API_URL = `${URLs.stacjownikAPI}/api/getDispatchers`;
export default defineComponent({
components: {
SearchBox,
ActionButton,
JournalOptions,
DispatcherStats,
Loading,
JournalDispatchersList,
JournalHeader,
},
name: 'JournalDispatchers',
props: {
sceneryName: {
type: String,
required: false,
},
dispatcherName: {
type: String,
required: false,
},
},
data: () => ({
currentQuery: '',
currentQueryArray: [] as string[],
scrollDataLoaded: true,
scrollNoMoreData: false,
showReturnButton: false,
statsCardOpen: false,
currentOptionsActive: false,
dataStatus: DataStatus.Initialized,
DataStatus,
historyList: [] as DispatcherHistory[],
}),
setup() {
const sorterActive: JournalDispatcherSorter = reactive({ id: 'timestampFrom', dir: -1 });
const journalFilterActive = ref({});
const searchersValues = reactive({
'search-dispatcher': '',
'search-station': '',
'search-date': '',
} as JournalDispatcherSearcher);
const countFromIndex = ref(0);
const countLimit = 15;
provide('sorterActive', sorterActive);
provide('journalFilterActive', journalFilterActive);
provide('searchersValues', searchersValues);
const scrollElement: Ref<HTMLElement | null> = ref(null);
return {
store: useStore(),
sorterActive,
searchersValues,
countFromIndex,
countLimit,
scrollElement,
maxCount: ref(15),
};
},
watch: {
currentQueryArray(q: string[]) {
this.currentOptionsActive =
q.length > 2 || q.some((qv) => qv.startsWith('sortBy=') && qv.split('=')[1] != 'timestampFrom');
},
},
computed: {
computedHistoryList() {
return this.historyList.filter(
(doc) => doc.isOnline || (doc.currentDuration && doc.currentDuration > 10 * 60000)
);
},
},
beforeRouteUpdate(to, _) {
this.handleQueries(to.query);
this.fetchHistoryData();
},
activated() {
this.handleQueries(this.$route.query);
this.fetchHistoryData();
},
methods: {
handleScroll(e: Event) {
const listElement = e.target as HTMLElement;
const scrollTop = listElement.scrollTop;
const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
if (scrollTop > elementHeight * 0.85) this.addHistoryData();
},
handleQueries(query: LocationQuery) {
if ('sceneryName' in query) this.searchersValues['search-station'] = `${query.sceneryName}`;
if ('dispatcherName' in query) this.searchersValues['search-dispatcher'] = `${query.dispatcherName}`;
},
setSearchers(date: string, station: string, dispatcher: string) {
this.searchersValues['search-date'] = date;
this.searchersValues['search-station'] = station;
this.searchersValues['search-dispatcher'] = dispatcher;
},
resetOptions() {
this.setSearchers('', '', '');
this.sorterActive.id = 'timestampFrom';
this.fetchHistoryData();
},
async addHistoryData() {
this.scrollDataLoaded = false;
const countFrom = this.historyList.length;
const responseData: DispatcherHistory[] = await (
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
).data;
if (!responseData) return;
if (responseData.length == 0) {
this.scrollNoMoreData = true;
return;
}
this.historyList.push(...responseData);
this.scrollDataLoaded = true;
},
async fetchHistoryData() {
const queries: string[] = [];
const dispatcher = this.searchersValues['search-dispatcher'].trim();
const station = this.searchersValues['search-station'].trim();
const dateString = this.searchersValues['search-date'].trim();
const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
if (dispatcher) queries.push(`dispatcherName=${dispatcher}`);
if (station) queries.push(`stationName=${station}`);
if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
if (this.sorterActive.id == 'timestampFrom') queries.push('sortBy=timestampFrom');
else if (this.sorterActive.id == 'duration') queries.push('sortBy=currentDuration');
else queries.push('sortBy=timestampFrom');
queries.push('countLimit=30');
if (this.currentQuery != queries.join('&')) this.dataStatus = DataStatus.Loading;
this.currentQuery = queries.join('&');
this.currentQueryArray = queries;
try {
const responseData: DispatcherHistory[] = await (
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}`)
).data;
if (!responseData) {
this.dataStatus = DataStatus.Error;
return;
}
if (!responseData) return;
// Response data exists
this.historyList = responseData;
// Stats display
this.store.dispatcherStatsName =
this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim()
? this.historyList[0].dispatcherName
: '';
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>
+301
View File
@@ -0,0 +1,301 @@
<template>
<section class="journal-timetables">
<JournalHeader />
<div class="journal_wrapper">
<JournalOptions
@on-search-confirm="fetchHistoryData"
@on-options-reset="resetOptions"
:sorter-option-ids="[ 'beginDate', 'distance', 'total-stops']"
:filters="journalTimetableFilters"
:currentOptionsActive="currentOptionsActive"
:data-status="dataStatus"
/>
<JournalStats />
<div class="list_wrapper" @scroll="handleScroll">
<!-- <transition name="warning" mode="out-in"> -->
<!-- <div :key="dataStatus"> -->
<div class="journal_warning" v-if="store.isOffline">
{{ $t('app.offline') }}
</div>
<Loading v-else-if="dataStatus == DataStatus.Initialized || dataStatus == DataStatus.Loading" />
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
{{ $t('app.error') }}
</div>
<div v-else-if="timetableHistory.length == 0" class="journal_warning">
{{ $t('app.no-result') }}
</div>
<div v-else>
<JournalTimetablesList :timetableHistory="timetableHistory" />
<button
class="btn btn--option btn--load-data"
v-if="!scrollNoMoreData && scrollDataLoaded && timetableHistory.length >= 15"
@click="addHistoryData"
>
{{ $t('journal.load-data') }}
</button>
</div>
<!-- </div> -->
<!-- </transition> -->
<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>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent, provide, reactive, Ref, ref, watch } from 'vue';
import axios from 'axios';
import DriverStats from '../components/JournalView/JournalDriverStats.vue';
import Loading from '../components/Global/Loading.vue';
import { JournalTimetableFilter, JournalTimetableSorter } from '../types/Journal/JournalTimetablesTypes';
import dateMixin from '../mixins/dateMixin';
import routerMixin from '../mixins/routerMixin';
import { DataStatus } from '../scripts/enums/DataStatus';
import { JournalFilterType } from '../scripts/enums/JournalFilterType';
import { TimetableHistory } from '../scripts/interfaces/api/TimetablesAPIData';
import { URLs } from '../scripts/utils/apiURLs';
import { useStore } from '../store/store';
import JournalOptions from '../components/JournalView/JournalOptions.vue';
import { JournalTimetableSearchType } from '../types/Journal/JournalTimetablesTypes';
import modalTrainMixin from '../mixins/modalTrainMixin';
import imageMixin from '../mixins/imageMixin';
import JournalTimetablesList from '../components/JournalView/JournalTimetablesList.vue';
import { journalTimetableFilters } from '../constants/Journal/JournalTimetablesConsts';
import JournalStats from '../components/JournalView/JournalStats.vue';
import JournalHeader from '../components/JournalView/JournalHeader.vue';
import { LocationQuery } from 'vue-router';
const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`;
export default defineComponent({
components: { DriverStats, Loading, JournalOptions, JournalTimetablesList, JournalStats, JournalHeader },
mixins: [dateMixin, routerMixin, modalTrainMixin, imageMixin],
name: 'JournalTimetables',
props: {
timetableId: {
type: String,
},
},
data: () => ({
currentQuery: '',
currentQueryArray: [] as string[],
scrollDataLoaded: true,
scrollNoMoreData: false,
showReturnButton: false,
statsCardOpen: false,
currentOptionsActive: false,
timetableHistory: [] as TimetableHistory[],
journalTimetableFilters,
dataStatus: DataStatus.Initialized,
dataErrorMessage: '',
DataStatus,
}),
setup() {
const sorterActive: JournalTimetableSorter = reactive({ id: 'beginDate', dir: 1 });
const journalFilterActive = ref(journalTimetableFilters[0]);
const searchersValues = reactive({
'search-train': '',
'search-driver': '',
'search-dispatcher': '',
'search-date': '',
} as JournalTimetableSearchType);
const countFromIndex = ref(0);
const countLimit = 15;
provide('searchersValues', searchersValues);
provide('sorterActive', sorterActive);
provide('journalFilterActive', journalFilterActive);
const scrollElement: Ref<HTMLElement | null> = ref(null);
return {
sorterActive,
journalFilterActive,
searchersValues,
countFromIndex,
countLimit,
scrollElement,
store: useStore(),
};
},
watch: {
currentQueryArray(q: string[]) {
this.currentOptionsActive =
q.length > 2 || q.some((qv) => qv.startsWith('sortBy=') && qv.split('=')[1] != 'beginDate');
},
},
// Handle route updates for route-links
beforeRouteUpdate(to, _) {
this.handleQueries(to.query);
this.fetchHistoryData();
},
activated() {
this.handleQueries(this.$route.query);
this.fetchHistoryData();
},
methods: {
handleScroll(e: Event) {
const listElement = e.target as HTMLElement;
const scrollTop = listElement.scrollTop;
const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
if (scrollTop > elementHeight * 0.85) this.addHistoryData();
},
handleQueries(query: LocationQuery) {
if ('timetableId' in query) this.searchersValues['search-train'] = `#${query.timetableId}`;
},
setSearchers(date: string, driver: string, train: string, dispatcher: string) {
this.searchersValues['search-date'] = date;
this.searchersValues['search-driver'] = driver;
this.searchersValues['search-train'] = train;
this.searchersValues['search-dispatcher'] = dispatcher;
},
resetOptions() {
this.setSearchers('', '', '', '');
this.journalFilterActive = this.journalTimetableFilters[0];
this.sorterActive.id = 'beginDate';
this.fetchHistoryData();
},
async addHistoryData() {
this.scrollDataLoaded = false;
const countFrom = this.timetableHistory.length;
const responseData: TimetableHistory[] = await (
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
).data;
if (!responseData) return;
if (responseData.length == 0) {
this.scrollNoMoreData = true;
return;
}
this.timetableHistory.push(...responseData);
this.scrollDataLoaded = true;
},
async fetchHistoryData() {
const queries: string[] = [];
const driverName = this.searchersValues['search-driver'].trim();
const trainNo = this.searchersValues['search-train'].trim();
const authorName = this.searchersValues['search-dispatcher'].trim();
const dateString = this.searchersValues['search-date'].trim();
const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
if (driverName) queries.push(`driverName=${driverName}`);
if (trainNo)
queries.push(trainNo.startsWith('#') ? `timetableId=${trainNo.replace('#', '')}` : `trainNo=${trainNo}`);
if (authorName) queries.push(`authorName=${authorName}`);
if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
if (this.sorterActive.id == 'distance') queries.push('sortBy=routeDistance');
else if (this.sorterActive.id == 'total-stops') queries.push('sortBy=allStopsCount');
else if (this.sorterActive.id == 'beginDate') queries.push('sortBy=beginDate');
// else queries.push('sortBy=timetableId');
queries.push('countLimit=15');
switch (this.journalFilterActive.id) {
case JournalFilterType.abandoned:
queries.push('fulfilled=0', 'terminated=1');
break;
case JournalFilterType.active:
queries.push('terminated=0');
break;
case JournalFilterType.fulfilled:
queries.push('fulfilled=1');
break;
default:
break;
}
if (this.currentQuery != queries.join('&')) this.dataStatus = DataStatus.Loading;
this.currentQuery = queries.join('&');
this.currentQueryArray = queries;
try {
const responseData: TimetableHistory[] = await (
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}`)
).data;
if (!responseData) {
this.dataStatus = DataStatus.Error;
this.dataErrorMessage = 'Brak danych!';
return;
}
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;
} 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>
-77
View File
@@ -1,77 +0,0 @@
<template>
<section class="journal-view">
<div class="journal-type-options">
<router-link class="router-link" active-class="route-active" to="/journal/timetables" exact>
{{ $t('journal.section-timetables') }}
</router-link>
&nbsp;&bull;&nbsp;
<router-link class="router-link" active-class="route-active" to="/journal/dispatchers">
{{ $t('journal.section-dispatchers') }}
</router-link>
</div>
<div class="journal-section">
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" :key="$route.path" />
</keep-alive>
</router-view>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import JournalDispatchers from '../components/JournalView/JournalDispatchers.vue';
import JournalTimetables from '../components/JournalView/JournalTimetables.vue';
export default defineComponent({
components: { JournalDispatchers, JournalTimetables },
setup() {
const journalTypeChosen = ref('journalTimetables');
return {
activeJournalComponent: journalTypeChosen,
};
},
methods: {
changeJournalType(type: string) {
this.activeJournalComponent = type;
},
},
activated() {
const query = this.$route.query;
if (query.view == 'dispatchers') this.activeJournalComponent = 'journalDispatchers';
},
});
</script>
<style lang="scss" scoped>
.journal-type-options {
display: flex;
justify-content: center;
background-color: #2c2c2c;
max-width: 18em;
font-size: 1.2em;
margin: 0 auto;
border-radius: 0 0 0.5em 0.5em;
padding: 0.1em 0;
}
.journal-section > section {
height: 100%;
display: flex;
justify-content: center;
}
.router-link.active {
color: gold;
}
</style>
+28 -10
View File
@@ -8,16 +8,16 @@
</action-button> </action-button>
</div> </div>
<div class="scenery-wrapper" v-if="stationInfo" ref="card-wrapper"> <div class="scenery-wrapper" v-if="stationInfo" ref="card-wrapper" :data-timetable-only="timetableOnly">
<div class="scenery-left"> <div class="scenery-left" v-if="!timetableOnly">
<div class="scenery-actions"> <div class="scenery-actions">
<button v-if="!timetableOnly" class="back-btn btn" :title="$t('scenery.return-btn')" @click="navigateTo('/')"> <button class="back-btn btn" :title="$t('scenery.return-btn')" @click="navigateTo('/')">
<img :src="getIcon('back')" alt="Back to scenery" /> <img :src="getIcon('back')" alt="Back to scenery" />
</button> </button>
</div> </div>
<SceneryHeader :station="stationInfo" /> <SceneryHeader :station="stationInfo" />
<SceneryInfo :station="stationInfo" :timetableOnly="timetableOnly" /> <SceneryInfo :station="stationInfo" />
</div> </div>
<div class="scenery-right"> <div class="scenery-right">
@@ -33,7 +33,12 @@
</div> </div>
<keep-alive> <keep-alive>
<component :is="currentViewCompontent" :station="stationInfo" :key="currentViewCompontent"></component> <component
:is="currentViewCompontent"
:station="stationInfo"
:timetableOnly="timetableOnly"
:key="currentViewCompontent"
></component>
</keep-alive> </keep-alive>
</div> </div>
</div> </div>
@@ -41,7 +46,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent } from 'vue'; import { computed, defineComponent, PropType } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import routerMixin from '../mixins/routerMixin'; import routerMixin from '../mixins/routerMixin';
import { useStore } from '../store/store'; import { useStore } from '../store/store';
@@ -68,7 +73,9 @@ export default defineComponent({
SceneryTimetablesHistory, SceneryTimetablesHistory,
SceneryDispatchersHistory, SceneryDispatchersHistory,
}, },
mixins: [routerMixin, imageMixin], mixins: [routerMixin, imageMixin],
data: () => ({ data: () => ({
viewModes: [ viewModes: [
{ {
@@ -89,17 +96,22 @@ export default defineComponent({
currentViewCompontent: 'SceneryTimetable', currentViewCompontent: 'SceneryTimetable',
onlineFrom: -1, onlineFrom: -1,
}), }),
activated() { activated() {
this.loadSelectedCheckpoint(); this.loadSelectedCheckpoint();
}, },
setup() { setup() {
const route = useRoute(); const route = useRoute();
const store = useStore(); const store = useStore();
const timetableOnly = computed(() => (route.query['timetable_only'] == '1' ? true : false));
const timetableOnly = computed(() => (route.query['timetableOnly'] == '1' ? true : false));
const isComponentVisible = computed(() => route.path === '/scenery'); const isComponentVisible = computed(() => route.path === '/scenery');
const stationInfo = computed(() => { const stationInfo = computed(() => {
return store.stationList.find((station) => station.name === route.query.station?.toString().replace(/_/g, ' ')); return store.stationList.find((station) => station.name === route.query.station?.toString().replace(/_/g, ' '));
}); });
return { return {
timetableOnly, timetableOnly,
isComponentVisible, isComponentVisible,
@@ -111,11 +123,13 @@ export default defineComponent({
setViewMode(componentName: string) { setViewMode(componentName: string) {
this.currentViewCompontent = componentName; this.currentViewCompontent = componentName;
}, },
loadSelectedCheckpoint() { loadSelectedCheckpoint() {
if (!this.stationInfo?.generalInfo?.checkpoints) return; if (!this.stationInfo?.generalInfo?.checkpoints) return;
if (this.stationInfo.generalInfo.checkpoints.length == 0) return; if (this.stationInfo.generalInfo.checkpoints.length == 0) return;
this.selectedCheckpoint = this.stationInfo.generalInfo.checkpoints[0].checkpointName; this.selectedCheckpoint = this.stationInfo.generalInfo.checkpoints[0].checkpointName;
}, },
selectCheckpoint(cp: { checkpointName: string }) { selectCheckpoint(cp: { checkpointName: string }) {
this.selectedCheckpoint = cp.checkpointName; this.selectedCheckpoint = cp.checkpointName;
}, },
@@ -169,8 +183,12 @@ button.back-btn {
max-width: 1700px; max-width: 1700px;
margin: 1rem 0; margin: 1rem 0;
text-align: center; text-align: center;
&[data-timetable-only='true'] {
grid-template-columns: 1fr;
max-width: 1000px;
}
} }
.scenery-left { .scenery-left {
@@ -209,15 +227,15 @@ button.back-btn {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-wrap: wrap;
gap: 0.75em;
.btn { .btn {
margin: 0.5em;
padding: 0.5em; padding: 0.5em;
box-shadow: 0 0 10px 4px #242424; box-shadow: 0 0 10px 4px #242424;
&[data-checked='true'] { &[data-checked='true'] {
color: var(--clr-primary); color: var(--clr-primary);
font-weight: bold;
} }
} }
} }
+28 -72
View File
@@ -3,39 +3,23 @@
<div class="wrapper"> <div class="wrapper">
<div class="body"> <div class="body">
<div class="options-bar"> <div class="options-bar">
<StationFilterCard <StationFilterCard :showCard="filterCardOpen" :exit="(filterCardOpen = false)" ref="filterCardRef" />
:showCard="filterCardOpen"
:exit="closeCard"
@changeFilterValue="changeFilterValue"
@invertFilters="invertFilters"
@resetFilters="resetFilters"
ref="filterCardRef"
/>
</div> </div>
<StationTable <StationTable :stations="computedStationList" />
:stations="computedStations"
:sorterActive="filterManager.getSorter()"
:setFocusedStation="setFocusedStation"
:changeSorter="changeSorter"
/>
</div> </div>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue';
import inputData from '../data/options.json';
import { computed, ComputedRef, defineComponent, reactive } from 'vue';
import { useStore } from '../store/store';
import StationFilterManager from '../scripts/managers/stationFilterManager';
import Station from '../scripts/interfaces/Station';
import StorageManager from '../scripts/managers/storageManager'; 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';
import { useStationFiltersStore } from '../store/stationFiltersStore';
import { useStore } from '../store/store';
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -43,70 +27,42 @@ export default defineComponent({
StationFilterCard, StationFilterCard,
SelectBox, SelectBox,
}, },
data: () => ({ data: () => ({
filterCardOpen: false, filterCardOpen: false,
modalHidden: true, modalHidden: true,
STORAGE_KEY: 'options_saved', STORAGE_KEY: 'options_saved',
inputs: inputData, focusedStationName: '',
}), }),
setup() { setup() {
const store = useStore();
const filterManager = reactive(new StationFilterManager());
const focusedStationName = '';
const computedStations: ComputedRef<Station[]> = computed(
() => filterManager.getFilteredStationList(store.stationList, store.region.id)
// .filter((station) => !station.onlineInfo || station.onlineInfo.region == store.region.id)
);
return { return {
computedStations, filterStore: useStationFiltersStore(),
filterManager, store: useStore(),
focusedStationName,
}; };
}, },
mounted() {
if (!StorageManager.isRegistered(this.STORAGE_KEY)) return;
this.filterManager.checkFilters(); computed: {
computedStationList() {
const list = this.filterStore.getFilteredStationList(this.store.stationList, this.store.region.id);
this.inputs.options.forEach((option) => { return list;
const value = StorageManager.getBooleanValue(option.name); },
this.changeFilterValue({ name: option.name, value: value ? 0 : 1 });
option.value = value;
});
this.inputs.sliders.forEach((slider) => {
const value = StorageManager.getNumericValue(slider.name);
this.changeFilterValue({ name: slider.name, value });
slider.value = value;
});
}, },
methods: {
toggleCardsState(name: string): void { mounted() {
if (name == 'filter') { this.filterStore.setupFilters();
this.filterCardOpen = !this.filterCardOpen; // this.filterStore.inputs.options.forEach((option) => {
} // const value = StorageManager.getBooleanValue(option.name);
}, // option.value = value;
changeSorter(index: number) { // this.filterStore.changeFilterValue({ name: option.name, value: value });
this.filterManager.changeSorter(index); // });
},
changeFilterValue(filter: { name: string; value: number }) { // this.filterStore.inputs.sliders.forEach((slider) => {
this.filterManager.changeFilterValue(filter); // const value = StorageManager.getNumericValue(slider.name);
}, // slider.value = value;
resetFilters() { // this.filterStore.changeFilterValue({ name: slider.name, value: value });
this.filterManager.resetFilters(); // });
},
invertFilters() {
this.filterManager.invertFilters();
},
closeCard() {
this.filterCardOpen = false;
},
setFocusedStation(name: string) {
this.focusedStationName = this.focusedStationName == name ? '' : name;
},
}, },
}); });
</script> </script>
+36 -27
View File
@@ -1,9 +1,10 @@
<template> <template>
<section class="trains-view"> <section class="trains-view">
<div class="wrapper"> <div class="trains_wrapper">
<div class="options-bar"> <TrainOptions
<train-options /> :sorter-option-ids="['distance', 'progress', 'delay', 'mass', 'speed', 'length']"
</div> :current-options-active="currentOptionsActive"
/>
<TrainTable :trains="computedTrains" /> <TrainTable :trains="computedTrains" />
</div> </div>
@@ -11,14 +12,16 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, ComputedRef, defineComponent, provide, reactive, ref, TrainFilter } from 'vue'; import { computed, ComputedRef, defineComponent, provide, reactive, ref, watch } from 'vue';
import TrainOptions from '../components/TrainsView/TrainOptions.vue'; import TrainOptions from '../components/TrainsView/TrainOptions.vue';
import TrainStats from '../components/TrainsView/TrainStats.vue'; import TrainStats from '../components/TrainsView/TrainStats.vue';
import TrainTable from '../components/TrainsView/TrainTable.vue'; import TrainTable from '../components/TrainsView/TrainTable.vue';
import { trainFilters } from '../data/trainOptions'; import { trainFilters } from '../constants/Trains/TrainOptionsConsts';
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';
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -27,6 +30,8 @@ export default defineComponent({
TrainOptions, TrainOptions,
}, },
mixins: [modalTrainMixin],
props: { props: {
train: { train: {
type: String, type: String,
@@ -37,6 +42,11 @@ export default defineComponent({
type: String, type: String,
required: false, required: false,
}, },
trainId: {
type: String,
required: false,
},
}, },
data: () => ({ data: () => ({
@@ -45,10 +55,12 @@ export default defineComponent({
setup() { setup() {
const store = useStore(); const store = useStore();
const initTrainFilters = [...trainFilters.map((f) => ({ ...f }))];
const sorterActive = ref({ id: 'distance', dir: -1 }); const sorterActive = reactive({ id: 'distance', dir: -1 });
const filterList = reactive([...trainFilters]) as TrainFilter[]; const filterList = reactive([...trainFilters]) as TrainFilter[];
const isTrainOptionsCardVisible = ref(false);
const currentOptionsActive = ref(false);
const searchedDriver = ref(''); const searchedDriver = ref('');
const searchedTrain = ref(''); const searchedTrain = ref('');
@@ -57,16 +69,15 @@ export default defineComponent({
provide('searchedDriver', searchedDriver); provide('searchedDriver', searchedDriver);
provide('sorterActive', sorterActive); provide('sorterActive', sorterActive);
provide('filterList', filterList); provide('filterList', filterList);
provide('isTrainOptionsCardVisible', isTrainOptionsCardVisible);
const computedTrains: ComputedRef<Train[]> = computed(() => { const computedTrains: ComputedRef<Train[]> = computed(() => {
return filteredTrainList( return filteredTrainList(store.trainList, searchedTrain.value, searchedDriver.value, sorterActive, filterList);
store.trainList, });
searchedTrain.value,
searchedDriver.value, watch([searchedTrain, searchedDriver, sorterActive, filterList], ([sT, sD, sA, fL]) => {
sorterActive.value, const areFiltersActive = fL.some((f, i) => f.isActive !== initTrainFilters[i].isActive);
filterList
); currentOptionsActive.value = sT.length > 0 || sD.length > 0 || sA.id != 'distance' || areFiltersActive;
}); });
return { return {
@@ -74,6 +85,8 @@ export default defineComponent({
searchedTrain, searchedTrain,
searchedDriver, searchedDriver,
sorterActive, sorterActive,
store,
currentOptionsActive,
}; };
}, },
@@ -82,10 +95,12 @@ export default defineComponent({
this.searchedTrain = this.train; this.searchedTrain = this.train;
this.searchedDriver = this.driver || ''; this.searchedDriver = this.driver || '';
} }
// if (this.train) {
// this.searchedTrain = this.train; this.$nextTick(() => {
// if(this.x) this.searchedDriver = this.x; if (this.trainId) {
// } this.selectModalTrain(this.trainId);
}
});
}, },
}); });
</script> </script>
@@ -98,14 +113,8 @@ export default defineComponent({
position: relative; position: relative;
} }
.wrapper { .trains_wrapper {
margin: 1rem auto; margin: 1rem auto;
max-width: 1350px; max-width: 1350px;
} }
@include smallScreen {
.options-bar {
font-size: 1.25em;
}
}
</style> </style>
-30
View File
@@ -1,30 +0,0 @@
import { ComponentCustomProperties } from 'vue'
import { Store } from 'vuex'
import { JournalFilterType } from './scripts/enums/JournalFilterType';
import { TrainFilterType } from './scripts/enums/TrainFilterType';
declare module '@vue/runtime-core' {
// declare your own store states
interface State {
count: number
}
// provide typings for `this.$store`
interface ComponentCustomProperties {
$store: Store<State>
}
// Train filter for TrainView
interface TrainFilter {
id: TrainFilterType;
isActive: boolean;
}
interface JournalFilter {
id: JournalFilterType;
filterSection: string;
isActive: boolean;
}
}
+1
View File
@@ -14,6 +14,7 @@
"ESNext", "ESNext",
"DOM" "DOM"
], ],
"types": ["vite/client", "vite-plugin-pwa/client"],
"skipLibCheck": true "skipLibCheck": true
}, },
"include": [ "include": [
+47 -27
View File
@@ -1,34 +1,54 @@
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';
export default defineConfig({ export default defineConfig({
plugins: [vue()], server: {
port: 5001,
},
plugins: [
vue(),
VitePWA({
registerType: 'prompt',
workbox: {
globPatterns: ['**/*.{js,css,html,png,svg,jpg}'],
runtimeCaching: [
{
urlPattern: new RegExp('^https://spythere.pl/api/getSceneries', 'i'),
handler: 'NetworkFirst',
options: {
cacheName: 'sceneries-cache',
expiration: {
maxEntries: 1,
maxAgeSeconds: 60 * 60 * 24 * 7, // <== 7 days
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
urlPattern: /^https:\/\/rj.td2.info.pl\/dist\/img\/thumbnails\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'images-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24 * 60,
},
cacheableResponse: {
statuses: [0, 200, 404],
},
},
},
],
},
devOptions: {
enabled: true,
},
}),
],
}); });
// PWA
// VitePWA({
// registerType: 'autoUpdate',
// workbox: {
// globPatterns: ['**/*.{js,css,html,png,svg,img}'],
// runtimeCaching: [
// {
// urlPattern: new RegExp('^https://stacjownik.eu-4.evennode.com/api/getSceneries'),
// handler: 'NetworkFirst',
// options: {
// cacheName: 'sceneries-cache',
// expiration: {
// maxEntries: 200,
// maxAgeSeconds: 60 * 60 * 24 * 60, // <== 60 days
// },
// cacheableResponse: {
// statuses: [0, 200],
// },
// },
// },
// ],
// },
// devOptions: {
// enabled: true,
// },
// }),
+3607 -1157
View File
File diff suppressed because it is too large Load Diff