Compare commits

...

68 Commits

Author SHA1 Message Date
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
69 changed files with 5394 additions and 5200 deletions
@@ -1,20 +0,0 @@
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools
name: Deploy to Firebase Hosting on merge
'on':
push:
branches:
- master
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm ci && npm run build
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STACJOWNIK_TD2 }}'
channelId: live
projectId: stacjownik-td2
@@ -1,14 +0,0 @@
name: Deploy to Firebase Hosting on PR
'on': pull_request
jobs:
build_and_preview:
if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm ci && npm run build
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STACJOWNIK_TD2 }}'
projectId: stacjownik-td2
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.10.2-alpha", "version": "1.10.4",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "stacjownik", "name": "stacjownik",
"version": "1.10.2-alpha", "version": "1.10.4",
"dependencies": { "dependencies": {
"core-js": "^3.12.1", "core-js": "^3.12.1",
"dotenv": "^8.6.0", "dotenv": "^8.6.0",
+36 -35
View File
@@ -1,35 +1,36 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.10.0", "version": "1.10.6",
"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": "vite preview"
"dependencies": { },
"core-js": "^3.12.1", "dependencies": {
"dotenv": "^8.6.0", "core-js": "^3.12.1",
"firebase": "^9.8.1", "dotenv": "^8.6.0",
"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": "^17.0.35",
"axios": "^0.21.1", "@vitejs/plugin-vue": "^3.0.0",
"typescript": "^4.6.4", "axios": "^0.21.1",
"vite": "^3.0.0", "typescript": "^4.6.4",
"vue-tsc": "^0.38.4" "vite": "^3.0.0",
}, "vue-tsc": "^0.38.4"
"browserslist": [ },
"> 1%", "browserslist": [
"last 2 versions", "> 1%",
"not dead" "last 2 versions",
] "not dead"
} ]
}
+1 -157
View File
@@ -45,7 +45,7 @@
font-size: 1rem; font-size: 1rem;
@include smallScreen() { @include smallScreen() {
font-size: calc(0.4rem + 1.4vw); font-size: calc(0.55rem + 1vw);
} }
} }
@@ -81,162 +81,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%;
+28 -98
View File
@@ -1,72 +1,17 @@
<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"> <AppHeader :current-lang="currentLang" @change-lang="changeLang" />
<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">
<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>
<component :is="Component" :key="$route.path" /> <component :is="Component" :key="$route.name" />
</keep-alive> </keep-alive>
</router-view> </router-view>
</main> </main>
@@ -82,28 +27,28 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, provide, ref } from 'vue'; import { computed, defineComponent, 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';
export default defineComponent({ export default defineComponent({
components: { components: {
Clock, Clock,
StatusIndicator, StatusIndicator,
SelectBox, SelectBox,
UpdateModal,
TrainModal, TrainModal,
AppHeader,
}, },
mixins: [imageMixin], mixins: [imageMixin],
@@ -127,30 +72,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: '',
@@ -161,15 +84,22 @@ export default defineComponent({
}, },
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 +107,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

+266
View File
@@ -0,0 +1,266 @@
<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">
<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">{{ 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" to="/journal/timetables">
{{ $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;
@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>
+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;
+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;
+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>
+94 -141
View File
@@ -1,141 +1,94 @@
<template> <template>
<div class="card-dimmer" @click="closeCard"></div> <div class="journal-stats" v-if="store.driverStatsData?._sum.routeDistance != null">
<h1>
<div class="stats-card card"> STATYSTYKI MASZYNISTY <span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
<div> </h1>
<h2 class="card-title">
STATYSTYKI MASZYNISTY <span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span> <div class="info-stats">
</h2> <span class="stat-badge">
<span>{{ $t('journal.stats-timetables') }}</span>
<div class="loading" v-if="!store.driverStatsData">Ładowanie...</div> <span>{{ store.driverStatsData._count.fulfilled }} / {{ store.driverStatsData._count._all }}</span>
</span>
<div v-else>
<div class="info-stats" v-if="store.driverStatsData._sum.routeDistance != null"> <span class="stat-badge">
<span class="stat-badge"> <span>{{ $t('journal.stats-longest-timetable') }}</span>
<span>PRZEBYTO</span> <span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span>
<span>{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km</span> </span>
</span>
<span class="stat-badge"> <span class="stat-badge">
<span>PORZUCONO</span> <span>{{ $t('journal.stats-avg-timetable') }}</span>
<span> <span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
{{ (store.driverStatsData._sum.routeDistance - store.driverStatsData._sum.currentDistance).toFixed(2) }}km </span>
</span>
</span> <span class="stat-badge">
<span>{{ $t('journal.stats-distance') }}</span>
<span class="stat-badge"> <span>
<span>WYPEŁNIONO</span> {{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
<span>{{ store.driverStatsData._count.fulfilled }} RJ</span> {{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km
</span> </span>
</span>
<span class="stat-badge">
<span>PORZUCONO</span> <span class="stat-badge">
<span>{{ store.driverStatsData._count._all - store.driverStatsData._count.fulfilled }} RJ</span> <span>{{ $t('journal.stats-stations') }}</span>
</span> <span>
{{ store.driverStatsData._sum.confirmedStopsCount }} /
<span class="stat-badge"> {{ store.driverStatsData._sum.allStopsCount }}
<span>ZATWIERDZONO</span> </span>
<span>{{ store.driverStatsData._sum.confirmedStopsCount }} stacji</span> </span>
</span> </div>
</div>
<span class="stat-badge"> </template>
<span>PORZUCONO</span>
<span> <script lang="ts">
{{ store.driverStatsData._sum.allStopsCount - store.driverStatsData._sum.confirmedStopsCount }} import axios from 'axios';
stacji import { computed, defineComponent, ref } from 'vue';
</span> import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData';
</span> import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
</div> import { URLs } from '../../scripts/utils/apiURLs';
</div> import { useStore } from '../../store/store';
</div>
</div> export default defineComponent({
</template> emits: ['closeCard'],
<script lang="ts"> setup() {
const store = useStore();
import axios from 'axios'; return {
import { defineComponent } from 'vue'; store,
import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData'; driverStatsName: computed(() => store.driverStatsName),
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData'; };
import { URLs } from '../../scripts/utils/apiURLs'; },
import { useStore } from '../../store/store';
data() {
export default defineComponent({ return {
emits: ['closeCard'], test: Math.random(),
lastDispatcherName: '',
setup() {
const store = useStore(); lastTimetables: [] as TimetableHistory[],
return { };
store, },
};
}, watch: {
driverStatsName(value: string) {
data() { this.fetchDispatcherStats();
return { },
test: Math.random(), },
lastDispatcherName: '',
methods: {
lastTimetables: [] as TimetableHistory[], async fetchDispatcherStats() {
}; this.store.driverStatsData = undefined;
},
if (!this.store.driverStatsName) return;
activated() {
this.fetchDispatcherStats(); const statsData: DriverStatsAPIData = await (
}, await axios.get(`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`)
).data;
methods: {
async fetchDispatcherStats() { this.store.driverStatsData = statsData;
this.store.driverStatsData = undefined; },
},
const statsData: DriverStatsAPIData = await ( });
await axios.get(`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`) </script>
).data;
<style lang="scss" scoped>
const recentTimetablesData: TimetableHistory[] = await ( @import '../../styles/JournalStats.scss';
await axios.get(`${URLs.stacjownikAPI}/api/getTimetables?driverName=${this.store.driverStatsName}`) </style>
).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>
+264 -425
View File
@@ -1,425 +1,264 @@
<template> <template>
<section class="journal-timetables"> <section class="journal-timetables">
<div class="journal-wrapper"> <div class="journal_wrapper">
<div class="journal_top-bar"> <JournalOptions
<JournalOptions @on-search-confirm="searchHistory"
@on-filter-change="search" @on-options-reset="resetOptions"
@on-input-change="search" :sorter-option-ids="['timestampFrom', 'duration']"
@on-sorter-change="search" :data-status="dataStatus"
:sorter-option-ids="['timestampFrom', 'duration']" />
/>
<div class="list_wrapper" @scroll="handleScroll">
<!-- <DispatcherStats /> --> <!-- <transition name="warning" mode="out-in"> -->
</div> <!-- <div :key="dataStatus"> -->
<Loading v-if="dataStatus == DataStatus.Initialized || dataStatus == DataStatus.Loading" />
<div class="journal-list">
<div class="list-wrapper" ref="scrollElement"> <div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
<transition name="warning" mode="out-in"> {{ $t('app.error') }}
<div :key="historyDataStatus.status"> </div>
<Loading v-if="isDataLoading || isDataInit" />
<div class="journal_warning" v-else-if="historyList.length == 0">
<div v-else-if="isDataError" class="journal_warning error"> {{ $t('app.no-result') }}
{{ $t('app.error') }} </div>
</div>
<div v-else>
<div class="journal_warning" v-else-if="historyList.length == 0"> <JournalDispatchersList :dispatcherHistory="computedHistoryList" />
{{ $t('app.no-result') }}
</div> <button
class="btn btn--option btn--load-data"
<ul v-else> v-if="!scrollNoMoreData && scrollDataLoaded && computedHistoryList.length > 15"
<transition-group name="journal-list-anim"> @click="addHistoryData"
<li v-for="(doc, i) in computedHistoryList" :key="doc.id"> >
<div class="journal_day" v-if="isAnotherDay(i - 1, i)"> {{ $t('journal.load-data') }}
<span>{{ new Date(doc.timestampFrom).toLocaleDateString('pl-PL') }}</span> </button>
</div> </div>
<!-- </div>
<div </transition> -->
class="journal_item"
:class="{ online: doc.isOnline }" <div class="journal_warning" v-if="scrollNoMoreData">
@click="navigateToScenery(doc.stationName, doc.isOnline)" {{ $t('journal.no-further-data') }}
@keydown.enter="navigateToScenery(doc.stationName, doc.isOnline)" </div>
tabindex="0"
> <div class="journal_warning" v-else-if="!scrollDataLoaded">
<span> {{ $t('journal.loading-further-data') }}
<b class="text--primary">{{ doc.dispatcherName }}</b> &bull; <b>{{ doc.stationName }}</b> </div>
<span class="text--grayed">&nbsp;#{{ doc.stationHash }}&nbsp;</span> </div>
<span class="region-badge" :class="doc.region">PL1</span> </div>
</span> </section>
<span> </template>
<span :data-status="doc.isOnline">
{{ doc.isOnline ? $t('journal.online-since') : 'OFFLINE' }}&nbsp; <script lang="ts">
</span> import { defineComponent, provide, reactive, Ref, ref } from 'vue';
<span> import axios from 'axios';
{{ new Date(doc.timestampFrom).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
</span> import ActionButton from '../../components/Global/ActionButton.vue';
import JournalOptions from '../../components/JournalView/JournalOptions.vue';
<span v-if="doc.currentDuration && doc.isOnline"> import DispatcherStats from '../../components/JournalView/DispatcherStats.vue';
({{ calculateDuration(doc.currentDuration) }}) import SearchBox from '../Global/SearchBox.vue';
</span>
import Loading from '../Global/Loading.vue';
<span v-if="doc.timestampTo"> import { URLs } from '../../scripts/utils/apiURLs';
&gt; import { DataStatus } from '../../scripts/enums/DataStatus';
{{ new Date(doc.timestampTo).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }} import { useStore } from '../../store/store';
({{ $t('journal.duty-lasted') }} {{ calculateDuration(doc.currentDuration!) }}) import JournalDispatchersList from './JournalDispatchersList.vue';
</span> import { JournalDispatcherSearcher, JournalDispatcherSorter } from '../../types/Journal/JournalDispatcherTypes';
</span> import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
</div> import { JournalTimetableFilter } from '../../types/Journal/JournalTimetablesTypes';
</li>
</transition-group> const DISPATCHERS_API_URL = `${URLs.stacjownikAPI}/api/getDispatchers`;
</ul>
</div> export default defineComponent({
</transition> components: { SearchBox, ActionButton, JournalOptions, DispatcherStats, Loading, JournalDispatchersList },
</div> name: 'JournalDispatchers',
</div>
props: {
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div> sceneryName: {
<div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div> type: String,
</div> required: false,
</section> },
</template>
dispatcherName: {
<script lang="ts"> type: String,
import { computed, defineComponent, JournalFilter, provide, reactive, Ref, ref } from 'vue'; required: false,
import axios from 'axios'; },
},
import ActionButton from '../../components/Global/ActionButton.vue';
import JournalOptions from '../../components/JournalView/JournalOptions.vue'; data: () => ({
import DispatcherStats from '../../components/JournalView/DispatcherStats.vue'; currentQuery: '',
import SearchBox from '../Global/SearchBox.vue'; scrollDataLoaded: true,
scrollNoMoreData: false,
import Loading from '../Global/Loading.vue';
import { URLs } from '../../scripts/utils/apiURLs'; showReturnButton: false,
import dateMixin from '../../mixins/dateMixin'; statsCardOpen: false,
import { DataStatus } from '../../scripts/enums/DataStatus';
import { useStore } from '../../store/store'; dataStatus: DataStatus.Initialized,
DataStatus,
const DISPATCHERS_API_URL = `${URLs.stacjownikAPI}/api/getDispatchers`;
historyList: [] as DispatcherHistory[],
interface DispatcherHistoryItem { }),
id: string;
setup() {
stationName: string; const sorterActive: JournalDispatcherSorter = reactive({ id: 'timestampFrom', dir: -1 });
stationHash: string; const journalFilterActive = ref({});
region: string;
const searchersValues = reactive({
dispatcherName: string; 'search-dispatcher': '',
dispatcherId: number; 'search-station': '',
'search-date': '',
timestampFrom: number; } as JournalDispatcherSearcher);
timestampTo?: number;
currentDuration?: number; const countFromIndex = ref(0);
const countLimit = 15;
lastOnlineTimestamp: number;
provide('sorterActive', sorterActive);
isOnline: boolean; provide('journalFilterActive', journalFilterActive);
} provide('searchersValues', searchersValues);
type JournalDispatcherSearcher = { const scrollElement: Ref<HTMLElement | null> = ref(null);
[key in 'search-dispatcher' | 'search-station']: string;
}; return {
store: useStore(),
export default defineComponent({
components: { SearchBox, ActionButton, JournalOptions, DispatcherStats, Loading }, sorterActive,
mixins: [dateMixin], searchersValues,
name: 'JournalDispatchers',
countFromIndex,
props: { countLimit,
sceneryName: {
type: String, scrollElement,
required: false, maxCount: ref(15),
}, };
},
dispatcherName: {
type: String, computed: {
required: false, computedHistoryList() {
}, return this.historyList.filter(
}, (doc) => doc.isOnline || (doc.currentDuration && doc.currentDuration > 10 * 60000)
);
data: () => ({ },
currentQuery: '', },
scrollDataLoaded: true,
scrollNoMoreData: false, activated() {
if (this.sceneryName || this.dispatcherName) {
showReturnButton: false, this.searchersValues['search-station'] = this.sceneryName?.toString() || '';
statsCardOpen: false, this.searchersValues['search-dispatcher'] = this.dispatcherName?.toString() || '';
}), this.searchHistory();
}
setup() { },
const historyDataStatus: Ref<{ status: DataStatus; error: string | null }> = ref({
status: DataStatus.Loading, mounted() {
error: null, if (!this.sceneryName && !this.dispatcherName) {
}); this.searchHistory();
}
const sorterActive = ref({ id: 'timestampFrom', dir: -1 }); },
const journalFilterActive = ref({});
const searchersValues = reactive({ methods: {
'search-dispatcher': '', handleScroll(e: Event) {
'search-station': '', const listElement = e.target as HTMLElement;
} as JournalDispatcherSearcher); const scrollTop = listElement.scrollTop;
const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
const countFromIndex = ref(0);
const countLimit = 15; if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
provide('sorterActive', sorterActive); if (scrollTop > elementHeight * 0.85) this.addHistoryData();
provide('journalFilterActive', journalFilterActive); },
provide('searchersValues', searchersValues);
resetOptions() {
const scrollElement: Ref<HTMLElement | null> = ref(null); this.searchersValues['search-station'] = '';
this.searchersValues['search-dispatcher'] = '';
return { this.sorterActive.id = 'timestampFrom';
store: useStore(),
this.searchHistory();
historyList: ref([]) as Ref<DispatcherHistoryItem[]>, },
historyDataStatus,
searchHistory() {
isDataLoading: computed(() => historyDataStatus.value.status === DataStatus.Loading), this.fetchHistoryData({
isDataError: computed(() => historyDataStatus.value.status === DataStatus.Error), searchers: this.searchersValues,
isDataInit: computed(() => historyDataStatus.value.status === DataStatus.Initialized), });
sorterActive, this.scrollNoMoreData = false;
searchersValues, this.scrollDataLoaded = true;
},
countFromIndex,
countLimit, async addHistoryData() {
this.scrollDataLoaded = false;
scrollElement,
maxCount: ref(15), const countFrom = this.historyList.length;
};
}, const responseData: DispatcherHistory[] = await (
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
computed: { ).data;
computedHistoryList() {
return this.historyList.filter( if (!responseData) return;
(doc) => doc.isOnline || (doc.currentDuration && doc.currentDuration > 10 * 60000)
); if (responseData.length == 0) {
}, this.scrollNoMoreData = true;
}, return;
}
activated() {
if (this.sceneryName || this.dispatcherName) { this.historyList.push(...responseData);
this.searchersValues['search-station'] = this.sceneryName?.toString() || ''; this.scrollDataLoaded = true;
this.searchersValues['search-dispatcher'] = this.dispatcherName?.toString() || ''; },
this.search();
} async fetchHistoryData(
props: {
window.addEventListener('scroll', this.handleScroll); searchers?: JournalDispatcherSearcher;
}, filter?: JournalTimetableFilter;
} = {}
mounted() { ) {
if (!this.sceneryName && !this.dispatcherName) { this.dataStatus = DataStatus.Loading;
this.search();
} const queries: string[] = [];
},
const dispatcher = props.searchers?.['search-dispatcher'].trim();
deactivated() { const station = props.searchers?.['search-station'].trim();
window.removeEventListener('scroll', this.handleScroll); const dateString = props.searchers?.['search-date'].trim();
}, const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
methods: {
closeDispatcherStatsCard() { if (dispatcher) queries.push(`dispatcherName=${dispatcher}`);
this.statsCardOpen = false; if (station) queries.push(`stationName=${station}`);
}, if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
navigateToScenery(name: string, isOnline: boolean) { // Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
if (!isOnline) return; if (this.sorterActive.id == 'timestampFrom') queries.push('sortBy=timestampFrom');
else if (this.sorterActive.id == 'duration') queries.push('sortBy=currentDuration');
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`); else queries.push('sortBy=timestampFrom');
},
queries.push('countLimit=30');
isAnotherDay(prevIndex: number, currIndex: number) {
if (currIndex == 0) return true; this.currentQuery = queries.join('&');
return ( try {
new Date(this.computedHistoryList[prevIndex].timestampFrom).getDate() != const responseData: DispatcherHistory[] = await (
new Date(this.computedHistoryList[currIndex].timestampFrom).getDate() await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}`)
); ).data;
},
if (!responseData) {
handleScroll() { this.dataStatus = DataStatus.Error;
this.showReturnButton = window.scrollY > window.innerHeight; return;
}
const element = this.$refs.scrollElement as HTMLElement;
if (!responseData) return;
if (
element.getBoundingClientRect().bottom * 0.85 < window.innerHeight && // Response data exists
this.scrollDataLoaded && this.historyList = responseData;
!this.scrollNoMoreData &&
this.historyDataStatus.status == DataStatus.Loaded // Stats display
) this.store.dispatcherStatsName =
this.addHistoryData(); this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim()
}, ? this.historyList[0].dispatcherName
: '';
scrollToTop() {
window.scrollTo({ top: 0 }); this.dataStatus = DataStatus.Loaded;
}, } catch (error) {
this.dataStatus = DataStatus.Error;
search() { }
this.fetchHistoryData({ },
searchers: this.searchersValues, },
}); });
</script>
this.scrollNoMoreData = false;
this.scrollDataLoaded = true; <style lang="scss" scoped>
}, @import '../../styles/JournalSection.scss';
</style>
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,156 @@
<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 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';
export default defineComponent({
props: {
dispatcherHistory: {
type: Array as PropType<DispatcherHistory[]>,
required: true,
},
},
mixins: [dateMixin],
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;
}
}
@include smallScreen() {
.journal_item {
flex-direction: column;
span {
margin-top: 0.25em;
text-align: center;
}
}
}
</style>
+189 -260
View File
@@ -1,260 +1,189 @@
<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="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" </button>
@selected="onSorterChange"
:prefix="$t('journal.sort-prefix')" <transition name="options-anim">
/> <div class="options_wrapper" v-if="showOptions">
</div> <div class="options_content">
<h1 class="option-title">{{ $t('options.search-title') }}</h1>
<div class="content_search"> <div class="search_content">
<div class="search-box" v-for="(value, propName) in searchersValues" :key="propName"> <div class="search" v-for="(_, propName) in searchersValues" :key="propName">
<input <label v-if="propName == 'search-date'" for="date">{{ $t('options.search-date') }}</label>
class="search-input"
:placeholder="$t(`journal.${propName}`)" <div class="search-box">
v-model="searchersValues[propName]" <input
@keydown.enter="onInputSearch" v-if="propName == 'search-date'"
/> class="search-input"
id="date"
<img class="search-exit" :src="getIcon('exit')" alt="exit-icon" @click="onInputClear(propName)" /> type="date"
</div> min="2022-02-01"
<!-- <div class="search-box"> @keydown.enter="onSearchConfirm"
<input v-model="searchersValues[propName]"
class="search-input" />
v-model="searchedTrain"
:placeholder="$t('journal.search-train')" <input
@keydown.enter="search" v-else
/> class="search-input"
@keydown.enter="onSearchConfirm"
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="clearTrain" /> @focus="preventKeyDown = true"
</div> @blur="preventKeyDown = false"
:placeholder="$t(`options.${propName}`)"
<div class="search-box"> v-model="searchersValues[propName]"
<input />
class="search-input"
v-model="searchedDriver" <button class="search-exit">
:placeholder="$t('journal.search-driver')" <img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear(propName)" />
@keydown.enter="search" </button>
/> </div>
</div>
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="clearDriver" />
</div> --> <div class="search_actions">
<button class="btn--action" @click="onResetButtonClick">
<action-button class="search-button" @click="onInputSearch"> {{ $t('options.reset-button') }}
{{ $t('journal.search') }} </button>
</action-button> <button class="btn--action" @click="onSearchButtonConfirm">
</div> {{ $t('options.search-button') }}
</div> </button>
</div>
<div class="options_filters"> </div>
<button
v-for="filter in filters" <h1 class="option-title">{{ $t('options.sort-title') }}</h1>
class="journal-filter-option btn--option" <div class="options_sorters">
:class="{ checked: journalFilterActive.id === filter.id }" <div v-for="opt in translatedSorterOptions">
:id="filter.id" <button
@click="onFilterChange(filter)" class="sort-option btn--option"
> :data-selected="opt.id == sorterActive.id"
{{ $t(`journal.filter-${filter.id}`) }} @click="onSorterChange(opt)"
</button> >
</div> {{ opt.value.toUpperCase() }}
</div> </button>
</div> </div>
</template> </div>
<script lang="ts"> <h1 class="option-title" v-if="filters.length != 0">{{ $t('options.filter-title') }}</h1>
import { defineComponent, inject, JournalFilter, PropType } from 'vue'; <div class="options_filters">
import imageMixin from '../../mixins/imageMixin'; <button
import ActionButton from '../Global/ActionButton.vue'; v-for="filter in filters"
import SelectBox from '../Global/SelectBox.vue'; class="filter-option btn--option"
:class="{ checked: journalFilterActive.id === filter.id }"
export default defineComponent({ :id="filter.id"
components: { SelectBox, ActionButton }, @click="onFilterChange(filter)"
emits: ['onSorterChange', 'onInputChange', 'onFilterChange'], >
mixins: [imageMixin], {{ $t(`options.filter-${filter.id}`) }}
</button>
props: { </div>
sorterOptionIds: { </div>
type: Array as PropType<Array<string>>, </div>
required: true, </transition>
}, </div>
</template>
filters: {
type: Array as PropType<JournalFilter[]>, <script lang="ts">
default: [], import { defineComponent, inject, Prop, PropType } from 'vue';
}, import imageMixin from '../../mixins/imageMixin';
}, import keyMixin from '../../mixins/keyMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { JournalTimetableFilter } from '../../types/Journal/JournalTimetablesTypes';
setup() { import ActionButton from '../Global/ActionButton.vue';
return { import SelectBox from '../Global/SelectBox.vue';
searchersValues: inject('searchersValues') as {[key: string]: string},
sorterActive: inject('sorterActive') as { id: string | number; dir: number }, export default defineComponent({
journalFilterActive: inject('journalFilterActive') as JournalFilter, components: { SelectBox, ActionButton },
}; emits: ['onSearchConfirm', 'onOptionsReset'],
}, mixins: [imageMixin, keyMixin],
computed: { props: {
translatedSorterOptions() { sorterOptionIds: {
return this.$props.sorterOptionIds.map((id) => ({ type: Array as PropType<Array<string>>,
id, required: true,
value: this.$t(`journal.option-${id}`), },
}));
}, filters: {
}, type: Array as PropType<JournalTimetableFilter[]>,
default: [],
methods: { },
onSorterChange(item: { id: string | number; value: string }) {
this.sorterActive.id = item.id; dataStatus: {
this.sorterActive.dir = -1; type: Number as PropType<DataStatus>,
default: DataStatus.Initialized,
this.$emit('onSorterChange'); },
}, },
onFilterChange(filter: JournalFilter) { data() {
this.journalFilterActive = filter; return {
this.$emit('onFilterChange'); showOptions: false,
}, DataStatus,
};
onInputSearch() { },
this.$emit('onInputChange');
}, setup() {
return {
onInputClear(id: any) { searchersValues: inject('searchersValues') as { [key: string]: string },
this.searchersValues[id] = ''; sorterActive: inject('sorterActive') as { id: string | number; dir: number },
this.onInputSearch(); journalFilterActive: inject('journalFilterActive') as JournalTimetableFilter,
}, };
}, },
});
</script> computed: {
translatedSorterOptions() {
<style lang="scss" scoped> return this.$props.sorterOptionIds.map((id) => ({
@import '../../styles/responsive'; id,
@import '../../styles/option.scss'; value: this.$t(`options.sort-${id}`),
}));
.options { },
&_wrapper { },
display: flex;
flex-direction: column; methods: {
} // Override keyMixin function
onKeyDownFunction() {
&_content { this.showOptions = !this.showOptions;
display: flex;
flex-wrap: wrap; this.$nextTick(() => {
if (this.showOptions) (this.$refs['button'] as HTMLButtonElement)?.focus();
.content_search, });
.content_select { },
display: flex;
align-items: center; focusEnd() {
flex-wrap: wrap; console.log('focus end');
},
padding: 0.25em 0.25em 0 0;
} onSorterChange(item: { id: string | number; value: string }) {
} this.sorterActive.id = item.id;
this.sorterActive.dir = -1;
&_filters { this.$emit('onSearchConfirm');
display: flex; },
flex-wrap: wrap;
margin: 0.5em 0 0 0; onFilterChange(filter: JournalTimetableFilter) {
this.journalFilterActive = filter;
.journal-filter-option { this.$emit('onSearchConfirm');
margin: 0 0.25em 0 0; },
&#abandoned { onInputClear(id: any) {
color: salmon; this.searchersValues[id] = '';
} this.$emit('onSearchConfirm');
},
&#fulfilled {
color: lightgreen; onSearchConfirm() {
} this.$emit('onSearchConfirm');
},
&#active {
color: lightblue; onSearchButtonConfirm() {
} this.showOptions = false;
} this.$emit('onSearchConfirm');
} },
}
onResetButtonClick() {
.search { this.$emit('onOptionsReset');
&-box { },
position: relative; },
});
background: #333; </script>
border-radius: 0.5em;
min-width: 200px; <style lang="scss" scoped>
margin-right: 0.25em; @import '../../styles/filters_options.scss';
} </style>
&-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;
}
}
@include smallScreen() {
.journal-options {
width: 100%;
}
.options {
&_wrapper {
justify-content: center;
align-items: center;
}
&_content {
padding: 0 1em;
flex-direction: column;
.content_select {
margin: 0 auto;
padding: 0;
}
.content_search {
justify-content: center;
}
}
&_filters {
justify-content: center;
.journal-filter-option {
margin: 0.25em 0.25em;
}
}
}
.search {
&-box,
&-button {
margin: 0.5em 0 0 0;
}
&-box {
width: 100%;
}
&-button {
width: 80%;
max-width: 300px;
}
}
}
</style>
+284 -439
View File
@@ -1,439 +1,284 @@
<template> <template>
<section class="journal-timetables"> <section class="journal-timetables">
<keep-alive>
<DriverStats v-if="statsCardOpen" @close-card="closeCard" /> <div class="journal_wrapper">
</keep-alive> <JournalOptions
@on-search-confirm="searchHistory"
<div class="journal-wrapper"> @on-options-reset="resetOptions"
<div class="journal_top-bar"> :sorter-option-ids="['timetableId', 'beginDate', 'distance', 'total-stops']"
<JournalOptions :filters="journalTimetableFilters"
@on-input-change="search" :data-status="dataStatus"
@on-filter-change="search" />
@on-sorter-change="search"
:sorter-option-ids="['timetableId', 'beginDate', 'distance', 'total-stops']" <DriverStats />
:filters="journalTimetableFilters" <!-- <button @click="statsCardOpen = true">Stats</button> -->
/>
</div> <div class="list_wrapper" @scroll="handleScroll">
<!-- <transition name="warning" mode="out-in"> -->
<div class="journal-list"> <!-- <div :key="dataStatus"> -->
<div class="list-wrapper" ref="scrollElement"> <Loading v-if="dataStatus == DataStatus.Initialized || dataStatus == DataStatus.Loading" />
<transition name="warning" mode="out-in">
<div :key="historyDataStatus.status"> <div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
<Loading v-if="isDataLoading || isDataInit" /> {{ $t('app.error') }}
</div>
<div v-else-if="isDataError" class="journal_warning error">
{{ $t('app.error') }} <div v-else-if="timetableHistory.length == 0" class="journal_warning">
</div> {{ $t('app.no-result') }}
</div>
<div class="journal_warning" v-else-if="historyList.length == 0">
{{ $t('app.no-result') }} <div v-else>
</div> <JournalTimetablesList :timetableHistory="timetableHistory" />
<ul v-else> <button
<transition-group name="journal-list-anim"> class="btn btn--option btn--load-data"
<li v-for="(item, i) in historyList" class="journal_item" :key="item.timetableId"> v-if="!scrollNoMoreData && scrollDataLoaded && timetableHistory.length >= 15"
<div class="journal_item-top"> @click="addHistoryData"
<span> >
<span {{ $t('journal.load-data') }}
tabindex="0" </button>
@click="navigateToTimetable(item)" </div>
@keydown.enter="navigateToTimetable(item)" <!-- </div> -->
style="cursor: pointer" <!-- </transition> -->
>
<b class="text--primary">{{ item.trainCategoryCode }}&nbsp;</b> <div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
<b>{{ item.trainNo }}</b> <div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div>
| <span>{{ item.driverName }}</span> | </div>
<span class="text--grayed">#{{ item.timetableId }}</span> </div>
</span> </section>
</template>
<div>
<b>{{ item.route.replace('|', ' - ') }}</b> <script lang="ts">
</div> import { defineComponent, provide, reactive, Ref, ref } from 'vue';
import axios from 'axios';
<hr style="margin: 0.25em 0" />
import DriverStats from './DriverStats.vue';
<div class="scenery-list"> import Loading from '../Global/Loading.vue';
<span import { JournalTimetableFilter, JournalTimetableSorter } from '../../types/Journal/JournalTimetablesTypes';
v-for="(scenery, i) in getSceneryList(item)" import dateMixin from '../../mixins/dateMixin';
:key="scenery.name" import routerMixin from '../../mixins/routerMixin';
:class="{ confirmed: scenery.confirmed }" import { DataStatus } from '../../scripts/enums/DataStatus';
> import { JournalFilterType } from '../../scripts/enums/JournalFilterType';
{{ i > 0 ? ' > ' : '' }} {{ scenery.name }} import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
</span> import { URLs } from '../../scripts/utils/apiURLs';
</div> import { useStore } from '../../store/store';
import JournalOptions from './JournalOptions.vue';
<div class="schedule-dates"> import { JorunalTimetableSearchType } from '../../types/Journal/JournalTimetablesTypes';
<!-- Data odjazdu ze stacji początkowej --> import modalTrainMixin from '../../mixins/modalTrainMixin';
<b>{{ item.route.split('|')[0] }}:</b> import imageMixin from '../../mixins/imageMixin';
<s v-if="item.beginDate != item.scheduledBeginDate" class="text--grayed"> import JournalTimetablesList from './JournalTimetablesList.vue';
{{ localeTime(item.beginDate, $i18n.locale) }} import { journalTimetableFilters } from '../../constants/Journal/JournalTimetablesConsts';
</s>
<span>{{ localeTime(item.scheduledBeginDate, $i18n.locale) }} </span>&bull; const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`;
<!-- Data przyjazdu na stację końcową / porzucenia --> export default defineComponent({
<b v-if="(item.fulfilled && item.terminated) || !item.terminated"> components: { DriverStats, Loading, JournalOptions, JournalTimetablesList },
{{ item.route.split('|').slice(-1)[0] }}: mixins: [dateMixin, routerMixin, modalTrainMixin, imageMixin],
</b>
<i v-else>{{ $t('journal.timetable-abandoned') }} </i> name: 'JournalTimetables',
<s v-if="item.endDate != item.scheduledEndDate && item.terminated" class="text--grayed"> props: {
{{ localeTime(item.fulfilled ? item.endDate : item.scheduledEndDate, $i18n.locale) }} timetableId: {
</s> type: String,
<span },
>{{ localeTime(item.fulfilled ? item.scheduledEndDate : item.endDate, $i18n.locale) }} },
</span>
</div> data: () => ({
</span> currentQuery: '',
scrollDataLoaded: true,
<b scrollNoMoreData: false,
class="journal_item-status"
:class="{ showReturnButton: false,
fulfilled: item.fulfilled || item.currentDistance >= item.routeDistance * 0.9, statsCardOpen: false,
terminated: item.terminated && !item.fulfilled,
active: !item.terminated, timetableHistory: [] as TimetableHistory[],
}" journalTimetableFilters,
>
{{ dataStatus: DataStatus.Initialized,
!item.terminated dataErrorMessage: '',
? $t('journal.timetable-active')
: item.fulfilled || item.currentDistance >= item.routeDistance * 0.9 DataStatus,
? $t('journal.timetable-fulfilled') }),
: $t('journal.timetable-abandoned')
}} setup() {
</b> const sorterActive: JournalTimetableSorter = reactive({ id: 'timetableId', dir: 1 });
</div> const journalFilterActive = ref(journalTimetableFilters[0]);
<div style="margin-top: 1em"> const searchersValues = reactive({
<div> 'search-train': '',
{{ $t('journal.timetable-day') }} <b>{{ localeDay(item.beginDate, $i18n.locale) }}</b> 'search-driver': '',
</div> 'search-author': '',
'search-date': '',
<!-- Nick dyżurnego --> } as JorunalTimetableSearchType);
<div v-if="item.authorName">
<b class="text--grayed">{{ $t('journal.dispatcher-name') }}&nbsp;</b> const countFromIndex = ref(0);
<router-link const countLimit = 15;
class="dispatcher-link"
:to="`/journal/dispatchers?dispatcherName=${item.authorName}`" provide('searchersValues', searchersValues);
>{{ item.authorName }}</router-link provide('sorterActive', sorterActive);
> provide('journalFilterActive', journalFilterActive);
</div>
</div> const scrollElement: Ref<HTMLElement | null> = ref(null);
<div style="margin-top: 1em"> return {
<div> sorterActive,
<b>{{ $t('journal.route-length') }}</b> journalFilterActive,
{{ !item.fulfilled ? item.currentDistance + ' /' : '' }} searchersValues,
{{ item.routeDistance }} km
</div> countFromIndex,
countLimit,
<div>
<b>{{ $t('journal.station-count') }}</b> scrollElement,
{{ item.confirmedStopsCount }} / store: useStore(),
{{ item.allStopsCount }} };
</div> },
</div>
</li> activated() {
</transition-group> if (this.timetableId) {
</ul> this.searchersValues['search-train'] = `#${this.timetableId}`;
</div> this.searchHistory();
</transition> }
</div> },
</div>
mounted() {
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div> if (!this.timetableId) this.searchHistory();
<div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div> },
</div>
</section> methods: {
</template> handleScroll(e: Event) {
const listElement = e.target as HTMLElement;
<script lang="ts"> const scrollTop = listElement.scrollTop;
import { computed, defineComponent, JournalFilter, provide, reactive, Ref, ref } from 'vue'; const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
import axios from 'axios';
if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
import DriverStats from './DriverStats.vue';
import Loading from '../Global/Loading.vue'; if (scrollTop > elementHeight * 0.85) this.addHistoryData();
import { journalTimetableFilters } from '../../data/journalFilters'; },
import dateMixin from '../../mixins/dateMixin';
import routerMixin from '../../mixins/routerMixin'; resetOptions() {
import { DataStatus } from '../../scripts/enums/DataStatus'; this.searchersValues['search-date'] = '';
import { JournalFilterType } from '../../scripts/enums/JournalFilterType'; this.searchersValues['search-driver'] = '';
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData'; this.searchersValues['search-train'] = '';
import { URLs } from '../../scripts/utils/apiURLs'; this.searchersValues['search-author'] = '';
import { useStore } from '../../store/store';
import JournalOptions from './JournalOptions.vue'; this.journalFilterActive = this.journalTimetableFilters[0];
this.sorterActive.id = 'timetableId';
const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`;
this.searchHistory();
type JournalTimetableSearcher = { },
[key in 'search-driver' | 'search-train']: string;
}; searchHistory() {
this.fetchHistoryData({
export default defineComponent({ searchers: this.searchersValues,
components: { DriverStats, Loading, JournalOptions }, filter: this.journalFilterActive,
mixins: [dateMixin, routerMixin], });
name: 'JournalTimetables', this.scrollNoMoreData = false;
this.scrollDataLoaded = true;
props: { },
timetableId: {
type: String, async addHistoryData() {
}, this.scrollDataLoaded = false;
},
const countFrom = this.timetableHistory.length;
data: () => ({
currentQuery: '', const responseData: TimetableHistory[] = await (
scrollDataLoaded: true, await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
scrollNoMoreData: false, ).data;
showReturnButton: false, if (!responseData) return;
statsCardOpen: false,
if (responseData.length == 0) {
journalTimetableFilters, this.scrollNoMoreData = true;
}), return;
}
setup() {
const historyDataStatus: Ref<{ status: DataStatus; error: string | null }> = ref({ this.timetableHistory.push(...responseData);
status: DataStatus.Loading, this.scrollDataLoaded = true;
error: null, },
});
async fetchHistoryData(
const sorterActive = ref({ id: 'timetableId', dir: -1 }); props: {
const journalFilterActive = ref(journalTimetableFilters[0]); searchers?: JorunalTimetableSearchType;
filter?: JournalTimetableFilter;
const searchersValues = reactive({ } = {}
'search-train': '', ) {
'search-driver': '', this.dataStatus = DataStatus.Loading;
} as JournalTimetableSearcher);
const queries: string[] = [];
const countFromIndex = ref(0);
const countLimit = 15; const driverName = props.searchers?.['search-driver'].trim();
const trainNo = props.searchers?.['search-train'].trim();
provide('searchersValues', searchersValues); const authorName = props.searchers?.['search-author'].trim();
provide('sorterActive', sorterActive);
provide('journalFilterActive', journalFilterActive); const dateString = props.searchers?.['search-date'].trim();
const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
const scrollElement: Ref<HTMLElement | null> = ref(null); const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
return { if (driverName) queries.push(`driverName=${driverName}`);
historyList: ref([]) as Ref<TimetableHistory[]>, if (trainNo)
historyDataStatus, queries.push(trainNo.startsWith('#') ? `timetableId=${trainNo.replace('#', '')}` : `trainNo=${trainNo}`);
if (authorName) queries.push(`authorName=${authorName}`);
isDataLoading: computed(() => historyDataStatus.value.status === DataStatus.Loading), if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
isDataError: computed(() => historyDataStatus.value.status === DataStatus.Error),
isDataInit: computed(() => historyDataStatus.value.status === DataStatus.Initialized), // Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
if (this.sorterActive.id == 'distance') queries.push('sortBy=routeDistance');
sorterActive, else if (this.sorterActive.id == 'total-stops') queries.push('sortBy=allStopsCount');
journalFilterActive, else if (this.sorterActive.id == 'beginDate') queries.push('sortBy=beginDate');
searchersValues, else queries.push('sortBy=timetableId');
countFromIndex, queries.push('countLimit=15');
countLimit,
switch (props.filter?.id) {
scrollElement, case JournalFilterType.abandoned:
maxCount: ref(15), queries.push('fulfilled=0', 'terminated=1');
store: useStore(), break;
};
}, case JournalFilterType.active:
queries.push('terminated=0');
activated() { break;
window.addEventListener('scroll', this.handleScroll);
case JournalFilterType.fulfilled:
if (this.timetableId) { queries.push('fulfilled=1');
this.searchersValues['search-train'] = `#${this.timetableId}`; break;
this.search();
} default:
}, break;
}
mounted() {
if (!this.timetableId) this.search(); this.currentQuery = queries.join('&');
},
try {
deactivated() { const responseData: TimetableHistory[] = await (
window.removeEventListener('scroll', this.handleScroll); await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}`)
}, ).data;
methods: { if (!responseData) {
navigateToTimetable(historyItem: TimetableHistory) { this.dataStatus = DataStatus.Error;
if (historyItem.terminated) return; this.dataErrorMessage = 'Brak danych!';
return;
this.navigateTo('/trains', { }
trainNo: historyItem.trainNo,
driverName: historyItem.driverName, if (!responseData) return;
});
}, // Response data exists
this.timetableHistory = responseData;
closeCard() {
this.statsCardOpen = false; // Stats display
}, this.store.driverStatsName =
this.timetableHistory.length > 0 && this.searchersValues['search-driver'].trim()
getSceneryList(historyItem: TimetableHistory) { ? this.timetableHistory[0].driverName
return historyItem.sceneriesString : '';
.split('%')
.map((name, i) => ({ name, confirmed: i < historyItem.confirmedStopsCount })); this.dataStatus = DataStatus.Loaded;
}, } catch (error) {
this.dataStatus = DataStatus.Error;
handleScroll() { this.dataErrorMessage = 'Ups! Coś poszło nie tak!';
this.showReturnButton = window.scrollY > window.innerHeight; }
},
const element = this.$refs.scrollElement as HTMLElement; },
});
if ( </script>
element.getBoundingClientRect().bottom * 0.85 < window.innerHeight &&
this.scrollDataLoaded && <style lang="scss" scoped>
!this.scrollNoMoreData && @import '../../styles/JournalSection.scss';
this.historyDataStatus.status == DataStatus.Loaded </style>
)
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,314 @@
<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.timetableId }}</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>
</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>
+1 -2
View File
@@ -109,7 +109,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 +127,6 @@ h3.section-header {
.info-general { .info-general {
margin-top: 1em; margin-top: 1em;
font-size: 1.1em;
} }
.general-list { .general-list {
@@ -64,6 +64,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 +83,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')"
/> />
+36 -24
View File
@@ -10,18 +10,27 @@
<span class="text--grayed"> <span class="text--grayed">
{{ station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed).length || '0' }} {{ station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed).length || '0' }}
</span> </span>
<!--
<button class="btn--image" v-if="!timetableOnly">
<a :href="`${$route.path}?station=${$route.query.station}&timetableOnly=1`">
<img :src="getIcon('view')" alt="View image" />
</a>
</button> -->
</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>
@@ -36,7 +45,7 @@
{{ $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>
@@ -182,11 +191,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 +262,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() {
@@ -293,7 +309,7 @@ export default defineComponent({
h3 { h3 {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 1.4em; font-size: 1.3em;
} }
} }
@@ -351,17 +367,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;
} }
} }
@@ -505,8 +519,6 @@ export default defineComponent({
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
font-size: 1.05em;
} }
&-general { &-general {
@@ -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.timetableId}`">
<span class="text--grayed"> #{{ historyItem.timetableId }} </span> <span class="text--grayed"> #{{ historyItem.timetableId }} </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>
+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>
+119 -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 store.stationList" :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,31 @@ export default defineComponent({
this.currentRegion = this.store.region; this.currentRegion = this.store.region;
}, },
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 +185,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 +202,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 +222,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 +232,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 +273,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 +298,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 +345,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 +378,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 +435,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;
+25 -14
View File
@@ -182,7 +182,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 +230,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 +240,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 +300,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 +339,7 @@ table {
} }
thead tr { thead tr {
background-color: $primaryCol; background-color: $bgCol;
} }
thead th { thead th {
@@ -338,7 +349,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;
+295 -287
View File
@@ -1,287 +1,295 @@
<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 v-if="train.timetableData">{{ train.timetableData.category }}&nbsp;</strong>
<strong>{{ train.trainNo }}</strong> <strong>{{ train.trainNo }}</strong>
<span>&nbsp;| {{ train.driverName }}&nbsp;</span> <span>&nbsp;| {{ train.driverName }}&nbsp;</span>
</span> <b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
</div> </span>
</div>
<div class="timetable_route" v-if="train.timetableData">
<strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong> <div class="timetable_route" v-if="train.timetableData">
<img <strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong>
v-if="getSceneriesWithComments(train.timetableData).length > 0" <img
class="image-warning" v-if="getSceneriesWithComments(train.timetableData).length > 0"
:src="getIcon('warning')" class="image-warning"
:title="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(train.timetableData)})`" :src="getIcon('warning')"
/> :title="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(train.timetableData)})`"
</div> />
</div>
<hr style="margin: 0.25em 0" />
<hr style="margin: 0.25em 0" />
<div class="timetable_stops" v-if="train.timetableData">
<span v-if="train.timetableData.followingStops.length > 2"> <div class="timetable_stops" v-if="train.timetableData">
{{ $t('trains.via-title') }} <span v-if="train.timetableData.followingStops.length > 2">
<span v-html="displayStopList(train.timetableData.followingStops)"></span> {{ $t('trains.via-title') }}
</span> <span v-html="displayStopList(train.timetableData.followingStops)"></span>
</div> </span>
</div>
<div class="timetable_progress" style="margin-top: 0.5em" v-if="train.timetableData">
<!-- <span> </span> --> <div class="timetable_progress" style="margin-top: 0.5em" v-if="train.timetableData">
<span class="timetable_progress-bar"> <!-- <span> </span> -->
<!-- {{ confirmedPercentage(train.timetableData.followingStops) }}%&nbsp; --> <span class="timetable_progress-bar">
<span class="bar-bg"></span> <!-- {{ confirmedPercentage(train.timetableData.followingStops) }}%&nbsp; -->
<span <span class="bar-bg"></span>
class="bar-fg" <span
:style="{ width: `${Math.floor(confirmedPercentage(train.timetableData.followingStops))}%` }" class="bar-fg"
></span> :style="{ width: `${Math.floor(confirmedPercentage(train.timetableData.followingStops))}%` }"
</span> ></span>
</span>
<span class="timetable_progress-distance">
&nbsp; {{ currentDistance(train.timetableData.followingStops) }} km / <span class="timetable_progress-distance">
<span class="text--primary"> {{ train.timetableData.routeDistance }} km </span> &nbsp; {{ currentDistance(train.timetableData.followingStops) }} km /
| <span class="text--primary"> {{ train.timetableData.routeDistance }} km </span>
<span v-html="currentDelay(train.timetableData.followingStops)"></span> |
</span> <span v-html="currentDelay(train.timetableData.followingStops)"></span>
</span>
<div class="train-status-badges">
<div v-if="!train.currentStationHash" class="train-badge offline">{{ $t('trains.scenery-offline') }}</div> <div class="train-status-badges">
<div v-if="!train.online" class="train-badge offline">Offline {{ lastSeenMessage(train.lastSeen) }}</div> <div v-if="!train.currentStationHash" class="train-badge offline">{{ $t('trains.scenery-offline') }}</div>
</div> <div v-if="!train.online" class="train-badge offline">Offline {{ lastSeenMessage(train.lastSeen) }}</div>
</div> </div>
</div>
<div class="driver_position text--grayed" style="margin-top: 0.25em">
{{ displayTrainPosition(train) }} <div class="driver_position text--grayed" style="margin-top: 0.25em">
</div> {{ displayTrainPosition(train) }}
</section> </div>
</section>
<section class="train-stats">
<div> <section class="train-stats">
<img :src="train.locoURL" loading="lazy" alt="Loco image not found" @error="onImageError" /> <div>
</div> <img :src="train.locoURL" loading="lazy" alt="Loco image not found" @error="onImageError" />
</div>
<div class="text--grayed">
{{ train.locoType }} <div class="text--grayed">
<span v-if="train.cars.length > 0"> {{ train.locoType }}
&nbsp;&bull; {{ $t('trains.cars') }}: <span v-if="train.cars.length > 0">
<span class="count">{{ train.cars.length }}</span> &nbsp;&bull; {{ $t('trains.cars') }}:
</span> <span class="count">{{ train.cars.length }}</span>
</div> </span>
</div>
<div>
<span v-for="(stat, i) in STATS.main" :key="stat.name"> <div>
<span v-if="i > 0"> &bull; </span> <span v-for="(stat, i) in STATS.main" :key="stat.name">
<span>{{ `${~~((train as any)[stat.name] * (stat.multiplier || 1))}${stat.unit}` }} </span> <span v-if="i > 0"> &bull; </span>
</span> <span>{{ `${~~((train as any)[stat.name] * (stat.multiplier || 1))}${stat.unit}` }} </span>
</div> </span>
</section> </div>
</div> </section>
</template> </div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'; <script lang="ts">
import imageMixin from '../../mixins/imageMixin'; import { defineComponent } from 'vue';
import trainInfoMixin from '../../mixins/trainInfoMixin'; import imageMixin from '../../mixins/imageMixin';
import Train from '../../scripts/interfaces/Train'; import trainInfoMixin from '../../mixins/trainInfoMixin';
import Train from '../../scripts/interfaces/Train';
export default defineComponent({
props: { export default defineComponent({
train: { props: {
type: Object as () => Train, train: {
required: true, type: Object as () => Train,
}, required: true,
},
extended: {
type: Boolean, extended: {
default: true, type: Boolean,
}, default: true,
}, },
},
mixins: [trainInfoMixin, imageMixin],
}); mixins: [trainInfoMixin, imageMixin],
</script> });
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss'; <style lang="scss" scoped>
@import '../../styles/responsive.scss';
.image-warning {
height: 1em; .image-warning {
height: 1em;
margin-left: 0.5em;
} margin-left: 0.5em;
}
.train-stats {
display: flex; .train-stats {
justify-content: center; display: flex;
align-content: center; justify-content: center;
align-content: center;
flex-direction: column;
text-align: center; flex-direction: column;
text-align: center;
img {
margin: 0.5em 0; img {
width: 12em; margin: 0.5em 0;
} width: 12em;
} }
}
.train-info {
display: grid; .train-info {
grid-template-columns: 2fr 1fr; display: grid;
grid-template-rows: 1fr; grid-template-columns: 2fr 1fr;
grid-template-rows: 1fr;
padding: 1em;
padding: 1em;
background-color: #1a1a1a;
gap: 0.5em; background-color: #1a1a1a;
} gap: 0.5em;
}
.timetable-id {
margin-right: 0.3em; .timetable-id {
color: #d2d2d2; margin-right: 0.3em;
} color: #d2d2d2;
}
.timetable_stops {
font-size: 0.75em; .warning-timeout {
} background-color: #be3728;
.train_general { display: inline-block;
display: flex; text-align: center;
align-items: center;
flex-wrap: wrap; width: 1.25em;
} height: 1.25em;
.train-status-badges { border-radius: 50%;
display: flex; }
flex-wrap: wrap;
} .timetable_stops {
font-size: 0.75em;
.train-badge { }
padding: 0.15em 0.35em;
margin-right: 0.3em; .train_general {
display: flex;
font-weight: bold; align-items: center;
flex-wrap: wrap;
font-size: 0.9em; }
.train-status-badges {
&.twr { display: flex;
background-color: var(--clr-twr); flex-wrap: wrap;
} }
&.skr { .train-badge {
background-color: var(--clr-skr); padding: 0.15em 0.35em;
} margin-right: 0.3em;
&.offline { font-weight: bold;
background-color: #b83b2d;
} font-size: 0.9em;
}
&.twr {
.timetable_route { background-color: var(--clr-twr);
display: flex; }
align-items: center;
&.skr {
margin-top: 0.5em; background-color: var(--clr-skr);
} }
.timetable_warnings { &.offline {
color: black; background-color: #b83b2d;
} }
}
.timetable_progress {
display: flex; .timetable_route {
align-items: center; display: flex;
flex-wrap: wrap; align-items: center;
}
margin-top: 0.5em;
.timetable_progress-bar { }
position: relative;
.timetable_warnings {
width: 6em; color: black;
height: 1em; }
margin: 0.5em 0;
.timetable_progress {
.bar-fg, display: flex;
.bar-bg { align-items: center;
position: absolute; flex-wrap: wrap;
height: 1em; }
width: 100%;
.timetable_progress-bar {
left: 0; position: relative;
}
width: 6em;
.bar-fg { height: 1em;
background-color: springgreen; margin: 0.5em 0;
}
.bar-fg,
.bar-bg { .bar-bg {
background-color: #5b5b5b; position: absolute;
} height: 1em;
} width: 100%;
.timetable_progress-distance { left: 0;
margin-right: 0.25em; }
}
.bar-fg {
.comments { background-color: springgreen;
display: flex; }
align-items: center;
.bar-bg {
font-size: 0.9em; background-color: #5b5b5b;
}
margin-top: 1em; }
img { .timetable_progress-distance {
margin-right: 0.5em; margin-right: 0.25em;
} }
}
.comments {
@include smallScreen() { display: flex;
.train-info { align-items: center;
grid-template-columns: 1fr;
gap: 1em 0; font-size: 0.9em;
text-align: center;
margin-top: 1em;
font-size: 1.15em;
} img {
margin-right: 0.5em;
.train-stats { }
font-size: 1.1em; }
img { @include smallScreen() {
display: none; .train-info {
} grid-template-columns: 1fr;
} gap: 1em 0;
text-align: center;
.train_general {
justify-content: center; font-size: 1.15em;
} }
.train-status-badges { .train-stats {
justify-content: center; font-size: 1.1em;
} }
.timetable_route { .train_general {
justify-content: center; justify-content: center;
} }
.timetable_progress { .train-status-badges {
justify-content: center; justify-content: center;
} }
.comments { .timetable_route {
flex-direction: column; justify-content: center;
justify-content: center; }
img { .timetable_progress {
margin: 0 0 0.5em 0; justify-content: center;
} }
}
} .comments {
</style> flex-direction: column;
justify-content: center;
img {
margin: 0 0 0.5em 0;
}
}
}
</style>
+143 -213
View File
@@ -1,267 +1,197 @@
<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="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]
</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-disabled="!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>>,
const sorterOptions = [ required: true,
{ },
id: 'distance', },
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,
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) {
filter.isActive = !filter.isActive; filter.isActive = !filter.isActive;
}, },
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 {
+44 -50
View File
@@ -8,6 +8,11 @@
{{ $t('trains.no-trains') }} {{ $t('trains.no-trains') }}
</div> </div>
<div class="timeouts-warning" v-if="trainNumbersWithTimeouts.length != 0">
<b class="warning-timeout">?</b>
{{ $t('trains.timeout') }}
</div>
<ul class="train-list"> <ul class="train-list">
<li <li
class="train-row" class="train-row"
@@ -25,39 +30,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 +63,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 +114,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 +130,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": [
+69 -51
View File
@@ -11,10 +11,10 @@
"migration-confirm": "Roger that!" "migration-confirm": "Roger that!"
}, },
"update": { "update": {
"title": "New Stacjownik version is available!", "title": "New Stacjownik version 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": "Understood!"
}, },
"data-status": { "data-status": {
"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!",
@@ -72,7 +72,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 +161,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 +176,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 +196,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 +219,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 +230,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 +244,20 @@
"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...",
"stats-timetables": "TIMETABLES",
"stats-longest-timetable": "LONGEST TIMETABLE",
"stats-avg-timetable": "AVERAGE TIMETABLE LENGTH",
"stats-distance": "DISTANCE",
"stats-stations": "STATIONS"
}, },
"scenery": { "scenery": {
"users": "PLAYERS ONLINE", "users": "PLAYERS ONLINE",
+67 -47
View File
@@ -74,7 +74,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 +164,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 +179,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 +199,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 +222,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 +234,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 +248,20 @@
"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-timetables": "ROZKŁADY JAZDY",
"stats-longest-timetable": "NAJDŁUŻSZY RJ",
"stats-avg-timetable": "ŚREDNIA DŁUGOŚĆ RJ",
"stats-distance": "DYSTANS",
"stats-stations": "STACJE"
}, },
"scenery": { "scenery": {
"users": "GRACZE ONLINE", "users": "GRACZE ONLINE",
+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() { setup() {
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);
}, },
}); });
+2 -3
View File
@@ -12,13 +12,12 @@ 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',
@@ -59,7 +58,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,
+2 -1
View File
@@ -19,9 +19,10 @@ export default interface Train {
online: boolean; online: boolean;
lastSeen: number; lastSeen: number;
region: string; region: string;
cars: string[]; cars: string[];
isTimeout: boolean;
timetableData?: { timetableData?: {
timetableId: number; timetableId: number;
category: string; category: string;
@@ -1,12 +1,14 @@
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; isOnline: boolean;
stationHash: string; lastOnlineTimestamp: number;
stationName: string; region: string;
timestampFrom: number; stationHash: string;
timestampTo?: number; stationName: string;
timestampFrom: number;
timestampTo?: number;
} }
+44 -35
View File
@@ -1,35 +1,44 @@
export interface TimetableHistory { export interface TimetableHistory {
timetableId: number; timetableId: number;
trainNo: number; trainNo: number;
trainCategoryCode: string; trainCategoryCode: string;
driverId: number; driverId: number;
driverName: string; driverName: string;
route: string; route: string;
twr: number; twr: number;
skr: number; skr: number;
sceneriesString: string; sceneriesString: string;
routeDistance: number; routeDistance: number;
currentDistance: number; currentDistance: number;
confirmedStopsCount: number; confirmedStopsCount: number;
allStopsCount: number; allStopsCount: number;
beginDate: string; beginDate: string;
endDate: string; endDate: string;
scheduledBeginDate: string; scheduledBeginDate: string;
scheduledEndDate: string; scheduledEndDate: string;
terminated: boolean; terminated: boolean;
fulfilled: boolean; fulfilled: boolean;
authorName?: string; authorName?: string;
authorId?: number; authorId?: number;
}
stockString?: string;
export interface SceneryTimetableHistory { stockMass?: number;
sceneryTimetables: TimetableHistory[]; stockLength?: number;
totalCount: number; maxSpeed?: number;
sceneryName: string;
} 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,292 +1,305 @@
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) {
case 1: 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) {
if ((a.generalInfo?.reqLevel || 0) < (b.generalInfo?.reqLevel || 0)) return -sorter.dir; case 0:
break; return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
case 2: 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;
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; break;
case 3: case 2:
if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') > (b.onlineInfo?.dispatcherName.toLowerCase() || '')) if ((a.onlineInfo?.statusTimestamp || 0) > (b.onlineInfo?.statusTimestamp || 0)) return sorter.dir;
return sorter.dir; if ((a.onlineInfo?.statusTimestamp || 0) < (b.onlineInfo?.statusTimestamp || 0)) return -sorter.dir;
if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') < (b.onlineInfo?.dispatcherName.toLowerCase() || '')) break;
return -sorter.dir;
break; case 3:
if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') > (b.onlineInfo?.dispatcherName.toLowerCase() || ''))
case 4: return sorter.dir;
if ((a.onlineInfo?.dispatcherExp || 0) > (b.onlineInfo?.dispatcherExp || 0)) return sorter.dir; if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') < (b.onlineInfo?.dispatcherName.toLowerCase() || ''))
if ((a.onlineInfo?.dispatcherExp || 0) < (b.onlineInfo?.dispatcherExp || 0)) return -sorter.dir; return -sorter.dir;
break; break;
case 7: 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?.currentUsers || 0) < (b.onlineInfo?.currentUsers || 0)) return -sorter.dir; if ((a.onlineInfo?.dispatcherExp || 0) < (b.onlineInfo?.dispatcherExp || 0)) return -sorter.dir;
break;
if ((a.onlineInfo?.maxUsers || 0) > (b.onlineInfo?.maxUsers || 0)) return sorter.dir;
if ((a.onlineInfo?.maxUsers || 0) < (b.onlineInfo?.maxUsers || 0)) return -sorter.dir; case 7:
break; if ((a.onlineInfo?.currentUsers || 0) > (b.onlineInfo?.currentUsers || 0)) return sorter.dir;
if ((a.onlineInfo?.currentUsers || 0) < (b.onlineInfo?.currentUsers || 0)) return -sorter.dir;
case 8:
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?.spawns.length || 0) < (b.onlineInfo?.spawns.length || 0)) return -sorter.dir; if ((a.onlineInfo?.maxUsers || 0) < (b.onlineInfo?.maxUsers || 0)) return -sorter.dir;
break;
break;
case 8:
case 9: 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)) if ((a.onlineInfo?.spawns.length || 0) < (b.onlineInfo?.spawns.length || 0)) return -sorter.dir;
return sorter.dir;
if ((a.onlineInfo?.scheduledTrains?.length || 0) < (b.onlineInfo?.scheduledTrains?.length || 0)) break;
return -sorter.dir;
case 9:
default: if ((a.onlineInfo?.scheduledTrains?.length || 0) > (b.onlineInfo?.scheduledTrains?.length || 0))
break; return sorter.dir;
} if ((a.onlineInfo?.scheduledTrains?.length || 0) < (b.onlineInfo?.scheduledTrains?.length || 0))
return -sorter.dir;
return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
}; default:
break;
const filterStations = (station: Station, filters: Filter) => { }
const returnMode = false;
return a.name.localeCompare(b.name);
if ((station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && filters['nonPublic']) };
return returnMode;
const filterStations = (station: Station, filters: Filter) => {
if (station.onlineInfo?.statusID == 'ending' && filters['ending']) return returnMode; const returnMode = false;
if ( if ((station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && filters['nonPublic'])
station.onlineInfo && return returnMode;
station.onlineInfo.statusTimestamp > 0 &&
filters['onlineFromHours'] < 8 && if (station.onlineInfo?.statusID == 'ending' && filters['ending']) return returnMode;
station.onlineInfo.statusTimestamp <= Date.now() + filters['onlineFromHours'] * 3600000
) if (
return returnMode; station.onlineInfo &&
station.onlineInfo.statusTimestamp > 0 &&
if (filters['onlineFromHours'] > 0 && station.onlineInfo && station.onlineInfo.statusTimestamp <= 0) filters['onlineFromHours'] < 8 &&
return returnMode; station.onlineInfo.statusTimestamp <= Date.now() + filters['onlineFromHours'] * 3600000
if (filters['onlineFromHours'] == 8 && station.onlineInfo?.statusID != 'no-limit') return returnMode; )
return returnMode;
if (station.onlineInfo?.statusID == 'ending' && filters['endingStatus']) return returnMode;
if ( if (filters['onlineFromHours'] > 0 && station.onlineInfo && station.onlineInfo.statusTimestamp <= 0)
(station.onlineInfo?.statusID == 'not-signed' || station.onlineInfo?.statusID == 'unavailable') && return returnMode;
filters['unavailableStatus'] if (filters['onlineFromHours'] == 8 && station.onlineInfo?.statusID != 'no-limit') return returnMode;
)
return returnMode; if (station.onlineInfo?.statusID == 'ending' && filters['endingStatus']) return returnMode;
if (station.onlineInfo?.statusID == 'brb' && filters['afkStatus']) return returnMode; if (
if (station.onlineInfo?.statusID == 'no-space' && filters['noSpaceStatus']) return returnMode; (station.onlineInfo?.statusID == 'not-signed' || station.onlineInfo?.statusID == 'unavailable') &&
filters['unavailableStatus']
if (station.onlineInfo && filters['occupied']) return returnMode; )
if (!station.onlineInfo && filters['free']) return returnMode; return returnMode;
if (station.generalInfo?.availability == 'unavailable' && filters['unavailable'] && !station.onlineInfo) if (station.onlineInfo?.statusID == 'brb' && filters['afkStatus']) return returnMode;
return returnMode; if (station.onlineInfo?.statusID == 'no-space' && filters['noSpaceStatus']) return returnMode;
if (station.generalInfo) { if (station.onlineInfo && filters['occupied']) return returnMode;
const routes = station.generalInfo.routes; if (!station.onlineInfo && filters['free']) return returnMode;
const availability = station.generalInfo.availability; if (station.generalInfo?.availability == 'unavailable' && filters['unavailable'] && !station.onlineInfo)
return returnMode;
if (filters['abandoned'] && availability == 'abandoned') return returnMode;
if (station.generalInfo) {
if (availability == 'default' && filters['default']) return returnMode; const routes = station.generalInfo.routes;
if ( const availability = station.generalInfo.availability;
availability != 'default' &&
filters['notDefault'] && if (filters['abandoned'] && availability == 'abandoned' && !station.onlineInfo) return returnMode;
!(availability == 'abandoned' || availability == 'unavailable')
) if (availability == 'default' && filters['default']) return returnMode;
return returnMode; if (
availability != 'default' &&
if (filters['real'] && station.generalInfo.lines != '') return returnMode; filters['notDefault'] &&
if ( !(availability == 'abandoned' || availability == 'unavailable')
filters['fictional'] && )
station.generalInfo.lines == '' && return returnMode;
availability != 'abandoned' &&
availability != 'unavailable' if (filters['real'] && station.generalInfo.lines != '') return returnMode;
) if (
return returnMode; filters['fictional'] &&
station.generalInfo.lines == '' &&
if ( availability != 'abandoned' &&
station.generalInfo.reqLevel + availability != 'unavailable'
(availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned' ? 1 : 0) < )
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['minLevel']
filters['maxLevel'] )
) return returnMode;
return returnMode; if (
station.generalInfo.reqLevel +
if ( (availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned' ? 1 : 0) >
filters['no-1track'] && filters['maxLevel']
(routes.oneWayCatenaryRouteNames.length != 0 || routes.oneWayNoCatenaryRouteNames.length != 0) )
) return returnMode;
return returnMode;
if ( if (
filters['no-2track'] && filters['no-1track'] &&
(routes.twoWayCatenaryRouteNames.length != 0 || routes.twoWayNoCatenaryRouteNames.length != 0) (routes.oneWayCatenaryRouteNames.length != 0 || routes.oneWayNoCatenaryRouteNames.length != 0)
) )
return returnMode; return returnMode;
if (
if (routes.oneWayCatenaryRouteNames.length < filters['minOneWayCatenary']) return returnMode; filters['no-2track'] &&
if (routes.oneWayNoCatenaryRouteNames.length < filters['minOneWay']) return returnMode; (routes.twoWayCatenaryRouteNames.length != 0 || routes.twoWayNoCatenaryRouteNames.length != 0)
)
if (routes.twoWayCatenaryRouteNames.length < filters['minTwoWayCatenary']) return returnMode; return returnMode;
if (routes.twoWayNoCatenaryRouteNames.length < filters['minTwoWay']) return returnMode;
if (routes.oneWayCatenaryRouteNames.length < filters['minOneWayCatenary']) return returnMode;
if (filters[station.generalInfo.controlType]) return returnMode; if (routes.oneWayNoCatenaryRouteNames.length < filters['minOneWay']) return returnMode;
if (filters[station.generalInfo.signalType]) return returnMode;
if (routes.twoWayCatenaryRouteNames.length < filters['minTwoWayCatenary']) return returnMode;
if ( if (routes.twoWayNoCatenaryRouteNames.length < filters['minTwoWay']) return returnMode;
filters['SPK'] &&
(station.generalInfo.controlType === 'SPK' || station.generalInfo.controlType.includes('+SPK')) if (filters[station.generalInfo.controlType]) return returnMode;
) if (filters[station.generalInfo.signalType]) return returnMode;
return returnMode;
if ( if (
filters['SCS'] && filters['SPK'] &&
(station.generalInfo.controlType === 'SCS' || station.generalInfo.controlType.includes('+SCS')) (station.generalInfo.controlType === 'SPK' || station.generalInfo.controlType.includes('+SPK'))
) )
return returnMode; return returnMode;
if ( if (
filters['SPE'] && filters['SCS'] &&
(station.generalInfo.controlType === 'SPE' || station.generalInfo.controlType.includes('+SPE')) (station.generalInfo.controlType === 'SCS' || station.generalInfo.controlType.includes('+SCS'))
) )
return returnMode; return returnMode;
if (filters['SUP'] && station.generalInfo.SUP) return returnMode; if (
filters['SPE'] &&
if ( (station.generalInfo.controlType === 'SPE' || station.generalInfo.controlType.includes('+SPE'))
filters['SCS'] && )
filters['SPK'] && return returnMode;
(station.generalInfo.controlType.includes('SPK') || station.generalInfo.controlType.includes('SCS')) if (filters['SUP'] && station.generalInfo.SUP) return returnMode;
)
return returnMode; if (
filters['SCS'] &&
if (filters['mechaniczne'] && station.generalInfo.controlType.includes('mechaniczne')) return returnMode; filters['SPK'] &&
(station.generalInfo.controlType.includes('SPK') || station.generalInfo.controlType.includes('SCS'))
if (filters['ręczne'] && station.generalInfo.controlType.includes('ręczne')) return returnMode; )
return returnMode;
if (filters['SBL'] && routes.sblRouteNames.length > 0) return returnMode;
if (filters['mechaniczne'] && station.generalInfo.controlType.includes('mechaniczne')) return returnMode;
if (
filters['authors'].length > 3 && if (filters['ręczne'] && station.generalInfo.controlType.includes('ręczne')) return returnMode;
!station.generalInfo.authors?.map((a) => a.toLocaleLowerCase()).includes(filters['authors'].toLocaleLowerCase())
) if (filters['SBL'] && routes.sblRouteNames.length > 0) return returnMode;
return returnMode;
} if (
filters['authors'].length > 3 &&
return true; !station.generalInfo.authors?.map((a) => a.toLocaleLowerCase()).includes(filters['authors'].toLocaleLowerCase())
}; )
return returnMode;
export default class StationFilterManager { }
private filterInitStates: Filter = {
default: false, return true;
notDefault: false, };
real: false,
fictional: false, const filterInitStates: Filter = {
SPK: false, default: false,
SCS: false, notDefault: false,
SPE: false, real: false,
SUP: false, fictional: false,
ręczne: false, SPK: false,
mechaniczne: false, SCS: false,
współczesna: false, SPE: false,
kształtowa: false, SUP: false,
historyczna: false, ręczne: false,
mieszana: false, mechaniczne: false,
SBL: false, współczesna: false,
minLevel: 0, kształtowa: false,
maxLevel: 20, historyczna: false,
minOneWayCatenary: 0, mieszana: false,
minOneWay: 0, SBL: false,
minTwoWayCatenary: 0, minLevel: 0,
minTwoWay: 0, maxLevel: 20,
'include-selected': false, minOneWayCatenary: 0,
'no-1track': false, minOneWay: 0,
'no-2track': false, minTwoWayCatenary: 0,
free: true, minTwoWay: 0,
occupied: false, 'include-selected': false,
ending: false, 'no-1track': false,
nonPublic: false, 'no-2track': false,
unavailable: true, free: true,
abandoned: true, occupied: false,
afkStatus: false, ending: false,
endingStatus: false, nonPublic: false,
noSpaceStatus: false, unavailable: true,
unavailableStatus: false, abandoned: true,
unsignedStatus: false, afkStatus: false,
endingStatus: false,
authors: '', noSpaceStatus: false,
unavailableStatus: false,
onlineFromHours: 0, unsignedStatus: false,
};
authors: '',
private filters: Filter = { ...this.filterInitStates };
onlineFromHours: 0,
private sorter: { index: number; dir: number } = { index: 0, dir: 1 }; };
checkFilters() { export const useStationFiltersStore = defineStore('stationFiltersStore', {
if (!StorageManager.isRegistered('options_saved')) return; state() {
return {
Object.keys(this.filterInitStates).forEach((filterKey) => { inputs: inputData,
if (StorageManager.isRegistered(filterKey)) return; filters: { ...filterInitStates },
sorterActive: { index: 0, dir: 1 },
const filterType = typeof this.filterInitStates[filterKey]; };
},
if (filterType === 'boolean')
StorageManager.setBooleanValue(filterKey, !this.filterInitStates[filterKey] as boolean); actions: {
getFilteredStationList(stationList: Station[], region: string): Station[] {
if (filterType === 'number') return stationList
StorageManager.setNumericValue(filterKey, this.filterInitStates[filterKey] as number); .map((station) => {
}); if (station.onlineInfo && station.onlineInfo.region != region) {
} delete station.onlineInfo;
}
getFilteredStationList(stationList: Station[], region: string): Station[] {
return stationList return station;
.map((station) => { })
if (station.onlineInfo && station.onlineInfo.region != region) { .filter((station) => filterStations(station, this.filters))
delete station.onlineInfo; .sort((a, b) => sortStations(a, b, this.sorterActive));
} },
return station; setupFilters() {
}) if (!StorageManager.isRegistered('options_saved')) return;
.filter((station) => filterStations(station, this.filters))
.sort((a, b) => sortStations(a, b, this.sorter)); this.inputs.options.forEach((option) => {
} if (!StorageManager.isRegistered(option.id)) return;
const savedValue = StorageManager.getBooleanValue(option.id);
changeFilterValue(filter: { name: string; value: number }) {
this.filters[filter.name] = filter.value; this.filters[option.id] = savedValue;
option.value = !savedValue;
// if(filter.name == 'authors') });
}
this.inputs.sliders.forEach((slider) => {
resetFilters() { if (!StorageManager.isRegistered(slider.name)) return;
this.filters = { ...this.filterInitStates }; const savedValue = StorageManager.getNumericValue(slider.name);
}
this.filters[slider.name] = savedValue;
invertFilters() { slider.value = savedValue;
Object.keys(this.filters).forEach((prop) => { });
if (typeof this.filters[prop] !== 'boolean') return; },
this.filters[prop] = !this.filters[prop]; changeFilterValue(filter: { name: string; value: any }) {
}); this.filters[filter.name] = filter.value;
}
if (StorageManager.isRegistered('options_saved')) StorageManager.setValue(filter.name, filter.value);
changeSorter(index: number) { },
if (index > 4 && index < 7) return;
resetFilters() {
if (index == this.sorter.index) this.sorter.dir = -1 * this.sorter.dir; this.filters = { ...filterInitStates };
else this.sorter.dir = 1;
this.inputs.options.forEach((option) => {
this.sorter.index = index; option.value = option.defaultValue;
} StorageManager.setBooleanValue(option.name, !option.defaultValue);
});
getSorter() {
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;
},
},
});
+401 -398
View File
@@ -1,398 +1,401 @@
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', {
state: () => export const useStore = defineStore('store', {
({ state: () =>
apiData: {} as unknown, ({
apiData: {} as unknown,
stationList: [],
trainList: [], stationList: [],
trainList: [],
sceneryData: [],
lastDispatcherStatuses: [], sceneryData: [],
lastDispatcherStatuses: [],
region: { id: 'eu', value: 'PL1' },
region: { id: 'eu', value: 'PL1' },
trainCount: 0,
stationCount: 0, trainCount: 0,
stationCount: 0,
webSocket: undefined,
webSocket: undefined,
dispatcherStatsName: '',
dispatcherStatsData: undefined, dispatcherStatsName: '',
dispatcherStatsData: undefined,
driverStatsName: '',
driverStatsData: undefined, driverStatsName: '',
driverStatsData: undefined,
chosenModalTrainId: undefined,
chosenModalTrainId: undefined,
dataStatuses: {
connection: DataStatus.Loading, dataStatuses: {
sceneries: DataStatus.Loading, connection: DataStatus.Loading,
timetables: DataStatus.Loading, sceneries: DataStatus.Loading,
dispatchers: DataStatus.Loading, timetables: DataStatus.Loading,
trains: DataStatus.Loading, dispatchers: DataStatus.Loading,
}, trains: DataStatus.Loading,
},
listenerLaunched: false,
} as StoreState), blockScroll: false,
listenerLaunched: false,
actions: {
setTrainsOnlineData() { } as StoreState),
const { trains } = this.apiData;
actions: {
if (!trains) return []; setTrainsOnlineData() {
const { trains } = this.apiData;
this.trainList = trains
.filter( if (!trains) return [];
(train) =>
train.region === this.region.id && (train.online || train.timetable || train.lastSeen > Date.now() - 180000) this.trainList = trains
) .filter(
.map((train) => { (train) =>
const stock = train.stockString.split(';'); train.region === this.region.id && (train.online || train.timetable || train.lastSeen > Date.now() - 180000)
const locoType = stock ? stock[0] : train.stockString; )
.map((train) => {
const timetable = train.timetable; const stock = train.stockString.split(';');
const locoType = stock ? stock[0] : train.stockString;
return {
trainId: train.driverName + train.trainNo.toString(), const timetable = train.timetable;
trainNo: train.trainNo, return {
mass: train.mass, trainId: train.driverName + train.trainNo.toString(),
length: train.length,
speed: train.speed, trainNo: train.trainNo,
region: train.region, mass: train.mass,
length: train.length,
distance: train.distance, speed: train.speed,
signal: train.signal, region: train.region,
online: train.online,
driverId: train.driverId, distance: train.distance,
driverName: train.driverName, signal: train.signal,
currentStationName: train.currentStationName, online: train.online,
currentStationHash: train.currentStationHash, driverId: train.driverId,
connectedTrack: train.connectedTrack, driverName: train.driverName,
locoType, currentStationName: train.currentStationName,
locoURL: getLocoURL(locoType), currentStationHash: train.currentStationHash,
cars: stock.slice(1), connectedTrack: train.connectedTrack,
locoType,
lastSeen: train.lastSeen, locoURL: getLocoURL(locoType),
cars: stock.slice(1),
timetableData: timetable
? { lastSeen: train.lastSeen,
timetableId: timetable.timetableId, isTimeout: train.isTimeout,
SKR: timetable.SKR,
TWR: timetable.TWR, timetableData: timetable
route: timetable.route, ? {
category: timetable.category, timetableId: timetable.timetableId,
followingStops: timetable.stopList, SKR: timetable.SKR,
routeDistance: timetable.stopList[timetable.stopList.length - 1].stopDistance, TWR: timetable.TWR,
sceneries: timetable.sceneries, route: timetable.route,
} category: timetable.category,
: undefined, followingStops: timetable.stopList,
}; routeDistance: timetable.stopList[timetable.stopList.length - 1].stopDistance,
}) as Train[]; sceneries: timetable.sceneries,
}, }
: undefined,
getDispatcherStatus(onlineStationData: StationAPIData) { };
const { dispatchers } = this.apiData; }) as Train[];
},
const prevDispatcherStatus = this.lastDispatcherStatuses.find(
(dispatcher) => dispatcher.hash === onlineStationData.stationHash getDispatcherStatus(onlineStationData: StationAPIData) {
); const { dispatchers } = this.apiData;
const stationStatus = !dispatchers const prevDispatcherStatus = this.lastDispatcherStatuses.find(
? undefined (dispatcher) => dispatcher.hash === onlineStationData.stationHash
: dispatchers.find( );
(status: string[]) => status[0] == onlineStationData.stationHash && status[1] == this.region.id
) || -1; const stationStatus = !dispatchers
? undefined
const statusTimestamp = : dispatchers.find(
prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusTimestamp : getStatusTimestamp(stationStatus); (status: string[]) => status[0] == onlineStationData.stationHash && status[1] == this.region.id
const statusID = ) || -1;
prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusID : getStatusID(stationStatus);
const statusTimestamp =
return { prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusTimestamp : getStatusTimestamp(stationStatus);
hash: onlineStationData.stationHash, const statusID =
statusID, prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusID : getStatusID(stationStatus);
statusTimestamp,
}; return {
}, hash: onlineStationData.stationHash,
statusID,
getScheduledTrains(stationGeneralInfo: Station['generalInfo'], stationAPIData: StationAPIData) { statusTimestamp,
const stationName = stationAPIData.stationName.toLowerCase(); };
},
stationGeneralInfo?.checkpoints.forEach((cp) => (cp.scheduledTrains.length = 0));
getScheduledTrains(stationGeneralInfo: Station['generalInfo'], stationAPIData: StationAPIData) {
return this.trainList.reduce((acc: ScheduledTrain[], train) => { const stationName = stationAPIData.stationName.toLowerCase();
if (!train.timetableData) return acc;
stationGeneralInfo?.checkpoints.forEach((cp) => (cp.scheduledTrains.length = 0));
const timetable = train.timetableData;
if (!timetable.sceneries.includes(stationAPIData.stationHash)) return acc; return this.trainList.reduce((acc: ScheduledTrain[], train) => {
if (!train.timetableData) return acc;
const stopInfoIndex = timetable.followingStops.findIndex((stop) => {
const stopName = stop.stopNameRAW.toLowerCase(); const timetable = train.timetableData;
if (!timetable.sceneries.includes(stationAPIData.stationHash)) return acc;
if (stationName === stopName) return true;
if (stopName.includes(stationName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.')) const stopInfoIndex = timetable.followingStops.findIndex((stop) => {
return true; const stopName = stop.stopNameRAW.toLowerCase();
if (stationName.includes(stopName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.')) if (stationName === stopName) return true;
return true; if (stopName.includes(stationName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.'))
return true;
if (
stopName.includes('podg.') && if (stationName.includes(stopName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.'))
stopName.split(', podg.')[0] && return true;
stationName.includes(stopName.split(', podg.')[0])
) if (
return true; stopName.includes('podg.') &&
stopName.split(', podg.')[0] &&
if ( stationName.includes(stopName.split(', podg.')[0])
stationGeneralInfo && )
stationGeneralInfo.checkpoints && return true;
stationGeneralInfo.checkpoints.length > 0 &&
stationGeneralInfo.checkpoints.some((cp) => if (
cp.checkpointName.toLowerCase().includes(stop.stopNameRAW.toLowerCase()) stationGeneralInfo &&
) stationGeneralInfo.checkpoints &&
) stationGeneralInfo.checkpoints.length > 0 &&
return true; stationGeneralInfo.checkpoints.some((cp) =>
cp.checkpointName.toLowerCase().includes(stop.stopNameRAW.toLowerCase())
return false; )
}); )
return true;
if (stopInfoIndex == -1) return acc;
return false;
const scheduledStopTrain = getScheduledTrain(train, stopInfoIndex, stationAPIData.stationName); });
if (stationGeneralInfo?.checkpoints) { if (stopInfoIndex == -1) return acc;
for (const checkpoint of stationGeneralInfo.checkpoints) {
const index = timetable.followingStops.findIndex( const scheduledStopTrain = getScheduledTrain(train, stopInfoIndex, stationAPIData.stationName);
(stop) => stop.stopNameRAW.toLowerCase() == checkpoint.checkpointName.toLowerCase()
); if (stationGeneralInfo?.checkpoints) {
for (const checkpoint of stationGeneralInfo.checkpoints) {
if (index == -1) continue; const index = timetable.followingStops.findIndex(
(stop) => stop.stopNameRAW.toLowerCase() == checkpoint.checkpointName.toLowerCase()
const scheduledCheckpointTrain = getScheduledTrain(train, index, stationAPIData.stationName); );
checkpoint.scheduledTrains.push(scheduledCheckpointTrain);
} if (index == -1) continue;
}
const scheduledCheckpointTrain = getScheduledTrain(train, index, stationAPIData.stationName);
acc.push(scheduledStopTrain); checkpoint.scheduledTrains.push(scheduledCheckpointTrain);
return acc; }
}, []) as ScheduledTrain[]; }
},
acc.push(scheduledStopTrain);
getStationTrains(stationAPIData: StationAPIData) { return acc;
return this.trainList }, []) as ScheduledTrain[];
.filter( },
(train) =>
train?.region === this.region.id && train.online && train.currentStationName === stationAPIData.stationName getStationTrains(stationAPIData: StationAPIData) {
) return this.trainList
.map((train) => ({ .filter(
driverName: train.driverName, (train) =>
driverId: train.driverId, train?.region === this.region.id && train.online && train.currentStationName === stationAPIData.stationName
trainNo: train.trainNo, )
trainId: train.trainId, .map((train) => ({
})); driverName: train.driverName,
}, driverId: train.driverId,
trainNo: train.trainNo,
setStationsOnlineInfo() { trainId: train.trainId,
const onlineStationNames: string[] = []; }));
const prevDispatcherStatuses: StoreState['lastDispatcherStatuses'] = []; },
this.apiData.stations?.forEach((stationAPIData) => { setStationsOnlineInfo() {
if (stationAPIData.region !== this.region.id || !stationAPIData.isOnline) return; const onlineStationNames: string[] = [];
const station = this.stationList.find((s) => s.name === stationAPIData.stationName); const prevDispatcherStatuses: StoreState['lastDispatcherStatuses'] = [];
onlineStationNames.push(stationAPIData.stationName); this.apiData.stations?.forEach((stationAPIData) => {
if (stationAPIData.region !== this.region.id || !stationAPIData.isOnline) return;
const dispatcherStatus = this.getDispatcherStatus(stationAPIData); const station = this.stationList.find((s) => s.name === stationAPIData.stationName);
prevDispatcherStatuses.push(dispatcherStatus);
onlineStationNames.push(stationAPIData.stationName);
const stationTrains = this.getStationTrains(stationAPIData);
const scheduledTrains = this.getScheduledTrains(station?.generalInfo, stationAPIData); const dispatcherStatus = this.getDispatcherStatus(stationAPIData);
prevDispatcherStatuses.push(dispatcherStatus);
const onlineInfo = {
name: stationAPIData.stationName, const stationTrains = this.getStationTrains(stationAPIData);
hash: stationAPIData.stationHash, const scheduledTrains = this.getScheduledTrains(station?.generalInfo, stationAPIData);
region: stationAPIData.region,
maxUsers: stationAPIData.maxUsers, const onlineInfo = {
currentUsers: stationAPIData.currentUsers, name: stationAPIData.stationName,
spawns: parseSpawns(stationAPIData.spawnString), hash: stationAPIData.stationHash,
dispatcherName: stationAPIData.dispatcherName, region: stationAPIData.region,
dispatcherRate: stationAPIData.dispatcherRate, maxUsers: stationAPIData.maxUsers,
dispatcherId: stationAPIData.dispatcherId, currentUsers: stationAPIData.currentUsers,
dispatcherExp: stationAPIData.dispatcherExp, spawns: parseSpawns(stationAPIData.spawnString),
dispatcherIsSupporter: stationAPIData.dispatcherIsSupporter, dispatcherName: stationAPIData.dispatcherName,
stationTrains, dispatcherRate: stationAPIData.dispatcherRate,
statusTimestamp: dispatcherStatus.statusTimestamp, dispatcherId: stationAPIData.dispatcherId,
statusID: dispatcherStatus.statusID, dispatcherExp: stationAPIData.dispatcherExp,
scheduledTrains, dispatcherIsSupporter: stationAPIData.dispatcherIsSupporter,
}; stationTrains,
statusTimestamp: dispatcherStatus.statusTimestamp,
if (!station) { statusID: dispatcherStatus.statusID,
this.stationList.push({ scheduledTrains,
name: stationAPIData.stationName, };
onlineInfo,
}); if (!station) {
this.stationList.push({
return; name: stationAPIData.stationName,
} onlineInfo,
});
station.onlineInfo = { ...onlineInfo };
return;
this.stationList }
.filter((station) => !onlineStationNames.includes(station.name) && station.onlineInfo)
.forEach((offlineStation) => { station.onlineInfo = { ...onlineInfo };
offlineStation.onlineInfo = undefined;
}); this.stationList
}); .filter((station) => !onlineStationNames.includes(station.name) && station.onlineInfo)
.forEach((offlineStation) => {
if (this.apiData.dispatchers != null) this.lastDispatcherStatuses = prevDispatcherStatuses; offlineStation.onlineInfo = undefined;
}, });
});
async fetchStationsGeneralInfo() {
const sceneryData: StationJSONData[] = await ( if (this.apiData.dispatchers != null) this.lastDispatcherStatuses = prevDispatcherStatuses;
await axios.get(`${URLs.stacjownikAPI}/api/getSceneries?timestamp=${Math.floor(Date.now() / 1800000)}`) },
).data;
async fetchStationsGeneralInfo() {
if (!sceneryData) { const sceneryData: StationJSONData[] = await (
this.dataStatuses.sceneries = DataStatus.Error; await axios.get(`${URLs.stacjownikAPI}/api/getSceneries?timestamp=${Math.floor(Date.now() / 1800000)}`)
return; ).data;
}
if (!sceneryData) {
this.stationList = sceneryData.map((scenery) => ({ this.dataStatuses.sceneries = DataStatus.Error;
name: scenery.name, return;
}
generalInfo: {
...scenery, this.stationList = sceneryData.map((scenery) => ({
authors: scenery.authors?.split(',').map((a) => a.trim()), name: scenery.name,
routes:
scenery.routes generalInfo: {
?.split(';') ...scenery,
.filter((routeString) => routeString) authors: scenery.authors?.split(',').map((a) => a.trim()),
.reduce( routes:
(acc, routeString) => { scenery.routes
const specs1 = routeString.split('_')[0]; ?.split(';')
const isInternal = specs1.startsWith('!'); .filter((routeString) => routeString)
const name = isInternal ? specs1.replace('!', '') : specs1; .reduce(
(acc, routeString) => {
const specs2 = routeString.split('_')[1].split(''); const specs1 = routeString.split('_')[0];
const twoWay = specs2[0] == '2'; const isInternal = specs1.startsWith('!');
const catenary = specs2[1] == 'E'; const name = isInternal ? specs1.replace('!', '') : specs1;
const SBL = specs2[2] == 'S';
const TWB = specs2[3] ? true : false; const specs2 = routeString.split('_')[1].split('');
const twoWay = specs2[0] == '2';
const propName = twoWay const catenary = specs2[1] == 'E';
? catenary const SBL = specs2[2] == 'S';
? 'twoWayCatenaryRouteNames' const TWB = specs2[3] ? true : false;
: 'twoWayNoCatenaryRouteNames'
: catenary const propName = twoWay
? 'oneWayCatenaryRouteNames' ? catenary
: 'oneWayNoCatenaryRouteNames'; ? 'twoWayCatenaryRouteNames'
: 'twoWayNoCatenaryRouteNames'
acc[twoWay ? 'twoWay' : 'oneWay'].push({ : catenary
name, ? 'oneWayCatenaryRouteNames'
SBL, : 'oneWayNoCatenaryRouteNames';
TWB,
catenary, acc[twoWay ? 'twoWay' : 'oneWay'].push({
isInternal, name,
tracks: twoWay ? 2 : 1, SBL,
}); TWB,
if (!isInternal) acc[propName].push(name); catenary,
isInternal,
if (SBL) acc['sblRouteNames'].push(name); tracks: twoWay ? 2 : 1,
});
return acc; if (!isInternal) acc[propName].push(name);
},
{ if (SBL) acc['sblRouteNames'].push(name);
oneWay: [],
twoWay: [], return acc;
sblRouteNames: [], },
oneWayCatenaryRouteNames: [], {
oneWayNoCatenaryRouteNames: [], oneWay: [],
twoWayCatenaryRouteNames: [], twoWay: [],
twoWayNoCatenaryRouteNames: [], sblRouteNames: [],
} as StationRoutes oneWayCatenaryRouteNames: [],
) || {}, oneWayNoCatenaryRouteNames: [],
checkpoints: scenery.checkpoints twoWayCatenaryRouteNames: [],
? scenery.checkpoints.split(';').map((sub) => ({ checkpointName: sub, scheduledTrains: [] })) twoWayNoCatenaryRouteNames: [],
: [], } as StationRoutes
}, ) || {},
})); checkpoints: scenery.checkpoints
}, ? scenery.checkpoints.split(';').map((sub) => ({ checkpointName: sub, scheduledTrains: [] }))
: [],
connectToWebsocket() { },
const socket = io(URLs.stacjownikAPI, { }));
transports: ['websocket', 'polling'], },
rememberUpgrade: true,
reconnection: true, connectToWebsocket() {
timeout: 10000, const socket = io(URLs.stacjownikAPI, {
}); transports: ['websocket', 'polling'],
rememberUpgrade: true,
socket.on('connect_error', (err) => { reconnection: true,
this.dataStatuses.connection = DataStatus.Error; timeout: 10000,
this.webSocket = undefined; });
});
socket.on('connect_error', (err) => {
socket.on('UPDATE', (data: APIData) => { this.dataStatuses.connection = DataStatus.Error;
this.apiData = data; this.webSocket = undefined;
this.dataStatuses.connection = DataStatus.Loaded; });
this.setOnlineData();
}); socket.on('UPDATE', (data: APIData) => {
this.apiData = data;
socket.emit('FETCH_DATA', {}, (data: APIData) => { this.dataStatuses.connection = DataStatus.Loaded;
this.apiData = data; this.setOnlineData();
this.setOnlineData(); });
});
socket.emit('FETCH_DATA', {}, (data: APIData) => {
this.webSocket = socket; this.apiData = data;
}, this.setOnlineData();
});
async connectToAPI() {
await this.fetchStationsGeneralInfo(); this.webSocket = socket;
},
this.connectToWebsocket();
}, async connectToAPI() {
await this.fetchStationsGeneralInfo();
async changeRegion(region: StoreState['region']) {
this.region = region; this.connectToWebsocket();
},
await this.setOnlineData();
}, async changeRegion(region: StoreState['region']) {
this.region = region;
async setOnlineData() {
if (!this.apiData.stations) { await this.setOnlineData();
this.dataStatuses.sceneries = DataStatus.Error; },
this.dataStatuses.trains = DataStatus.Error;
this.dataStatuses.dispatchers = DataStatus.Error; async setOnlineData() {
if (!this.apiData.stations) {
return; this.dataStatuses.sceneries = DataStatus.Error;
} this.dataStatuses.trains = DataStatus.Error;
this.dataStatuses.dispatchers = DataStatus.Error;
this.dataStatuses.sceneries = DataStatus.Loaded;
this.dataStatuses.trains = !this.apiData.trains ? DataStatus.Warning : DataStatus.Loaded; return;
this.dataStatuses.dispatchers = !this.apiData.dispatchers ? DataStatus.Warning : DataStatus.Loaded; }
this.setTrainsOnlineData(); this.dataStatuses.sceneries = DataStatus.Loaded;
this.setStationsOnlineInfo(); this.dataStatuses.trains = !this.apiData.trains ? DataStatus.Warning : DataStatus.Loaded;
}, this.dataStatuses.dispatchers = !this.apiData.dispatchers ? DataStatus.Warning : DataStatus.Loaded;
},
}); this.setTrainsOnlineData();
this.setStationsOnlineInfo();
},
},
});
+71 -71
View File
@@ -1,71 +1,71 @@
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;
dispatcherStatsName: string;
dispatcherStatsName: string; dispatcherStatsData?: DispatcherStatsAPIData;
dispatcherStatsData?: DispatcherStatsAPIData;
driverStatsName: string;
driverStatsName: string; driverStatsData?: DriverStatsAPIData;
driverStatsData?: DriverStatsAPIData;
chosenModalTrainId?: string;
chosenModalTrainId?: string;
dataStatuses: {
dataStatuses: { connection: DataStatus;
connection: DataStatus; sceneries: DataStatus;
sceneries: DataStatus; timetables: DataStatus;
timetables: DataStatus; dispatchers: DataStatus;
dispatchers: DataStatus; trains: DataStatus;
trains: DataStatus; };
};
listenerLaunched: boolean;
listenerLaunched: boolean; blockScroll: boolean;
} }
export interface APIData { export interface APIData {
stations?: StationAPIData[]; stations?: StationAPIData[];
dispatchers?: string[][]; dispatchers?: string[][];
trains?: TrainAPIData[]; trains?: TrainAPIData[];
} }
export interface StationJSONData { export interface StationJSONData {
name: string; name: string;
url: string; url: string;
lines: string; lines: string;
project: string; project: string;
reqLevel: number; reqLevel: number;
signalType: string; signalType: string;
controlType: string; controlType: string;
SUP: boolean; SUP: boolean;
routes: string; routes: string;
checkpoints: string | null; checkpoints: string | null;
authors?: string; authors?: string;
availability: Availability; availability: Availability;
} }
+85 -65
View File
@@ -1,65 +1,85 @@
@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%;
}
} padding: 1em 0;
}
.schedule-dates > * {
margin-right: 0.25em; .journal_warning {
} text-align: center;
font-size: 1.3em;
.journal_item,
.journal_warning { &.error {
background: #202020; background-color: var(--clr-error);
padding: 1em; }
margin: 1em 0; }
}
.schedule-dates > * {
.journal_top-bar { margin-right: 0.25em;
display: flex; }
justify-content: space-between;
align-items: center; .journal_item,
} .journal_warning {
background-color: #1a1a1a;
button.btn { padding: 1em;
padding: 0.5em 0.7em; margin-bottom: 1em;
} }
@include smallScreen() { .journal_top-bar {
.journal-wrapper { display: flex;
font-size: 1.25em; justify-content: space-between;
} align-items: center;
}
.journal_top-bar {
justify-content: center; .btn--load-data {
flex-wrap: wrap; 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;
}
}
+42
View File
@@ -0,0 +1,42 @@
@import 'variables.scss';
@import 'responsive.scss';
.journal-stats {
background-color: #1a1a1a;
padding: 1em;
margin-bottom: 1em;
}
.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;
+153
View File
@@ -0,0 +1,153 @@
@import 'responsive.scss';
@import 'variables.scss';
@import 'search_box.scss';
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;
}
.filters-options {
position: relative;
margin-bottom: 0.5em;
}
.options_wrapper {
position: absolute;
background-color: $bgCol;
box-shadow: 0 5px 10px 2px #0f0f0f;
width: 100%;
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;
}
}
+90 -58
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,58 @@ 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;
} button.btn--filled {
background-color: #333;
border-radius: 0.25em;
&:hover {
background-color: #2a2a2a;
} }
}
&--image { button.btn--action {
color: white; background-color: #424242;
transition: color 0.3s; border-radius: 0.25em;
&:hover {
background-color: #555;
} }
}
&--option { button.btn--option {
cursor: pointer; color: white;
background-color: #333;
color: white; &.checked {
background-color: #333; color: var(--clr-primary);
font-weight: bold;
border-radius: 0.25em; background-color: #3c3c3c;
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--image {
opacity: 0.65; font-weight: bold;
padding: 0.35em 0.75em;
img {
width: 1.5em;
margin-right: 0.5em;
vertical-align: middle;
} }
} }
@@ -274,3 +291,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,16 @@
import { JournalFilterType } from '../../scripts/enums/JournalFilterType';
export type JorunalTimetableSearchType = {
[key in 'search-driver' | 'search-train' | 'search-date' | 'search-author']: string;
};
export interface JournalTimetableFilter {
id: JournalFilterType;
filterSection: string;
isActive: boolean;
}
export interface JournalTimetableSorter {
id: 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
dir: -1 | 1;
}
+6
View File
@@ -0,0 +1,6 @@
import { TrainFilterType } from "../../scripts/enums/TrainFilterType";
export interface TrainFilter {
id: TrainFilterType;
isActive: boolean;
}
+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>
+21 -19
View File
@@ -1,9 +1,7 @@
<template> <template>
<section class="trains-view"> <section class="trains-view">
<div class="wrapper"> <div class="trains_wrapper">
<div class="options-bar"> <TrainOptions :sorter-option-ids="['distance', 'progress', 'delay', 'mass', 'speed', 'length']" />
<train-options />
</div>
<TrainTable :trains="computedTrains" /> <TrainTable :trains="computedTrains" />
</div> </div>
@@ -11,14 +9,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 } 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 +27,8 @@ export default defineComponent({
TrainOptions, TrainOptions,
}, },
mixins: [modalTrainMixin],
props: { props: {
train: { train: {
type: String, type: String,
@@ -37,6 +39,11 @@ export default defineComponent({
type: String, type: String,
required: false, required: false,
}, },
trainId: {
type: String,
required: false,
},
}, },
data: () => ({ data: () => ({
@@ -48,7 +55,6 @@ export default defineComponent({
const sorterActive = ref({ id: 'distance', dir: -1 }); const sorterActive = ref({ id: 'distance', dir: -1 });
const filterList = reactive([...trainFilters]) as TrainFilter[]; const filterList = reactive([...trainFilters]) as TrainFilter[];
const isTrainOptionsCardVisible = ref(false);
const searchedDriver = ref(''); const searchedDriver = ref('');
const searchedTrain = ref(''); const searchedTrain = ref('');
@@ -57,7 +63,6 @@ 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(
@@ -74,6 +79,7 @@ export default defineComponent({
searchedTrain, searchedTrain,
searchedDriver, searchedDriver,
sorterActive, sorterActive,
store,
}; };
}, },
@@ -82,10 +88,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 +106,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"],
"skipLibCheck": true "skipLibCheck": true
}, },
"include": [ "include": [
+1001 -906
View File
File diff suppressed because it is too large Load Diff