mirror of
https://github.com/Spythere/stacjownik.git
synced 2026-05-03 05:18:11 +00:00
Compare commits
160 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| febb22e1bc | |||
| 500f3c1223 | |||
| 221e0c7e82 | |||
| ca19f7e397 | |||
| a71ccd3e1a | |||
| d496c70fa8 | |||
| b9868ba52e | |||
| 59bd3fa2ef | |||
| e14d328ed9 | |||
| 36d71292bc | |||
| 2f6e2e7402 | |||
| e959eac6c5 | |||
| 8bedc4dfc6 | |||
| 73563d5db7 | |||
| 3f818069cd | |||
| cdf0b2a426 | |||
| c29ddeb78c | |||
| b81d98cab7 | |||
| 0e45bca5da | |||
| 715e66879f | |||
| 1747e15dc8 | |||
| 6a923a8e1d | |||
| 25a248e95e | |||
| aa7a6b220e | |||
| deb7b68985 | |||
| 633f05f690 | |||
| 73828867da | |||
| 75685c1e0e | |||
| 496ff95236 | |||
| 7e25327832 | |||
| 272c9f50f8 | |||
| 255e07372e | |||
| 279bbfa4db | |||
| a5c829faf5 | |||
| 5fdfaeac5e | |||
| 9beb30e3d5 | |||
| 48582e2eea | |||
| 2e721fb8bf | |||
| f93c1fbfec | |||
| c06e7b6468 | |||
| 22a6d266cb | |||
| 5f8a16401b | |||
| c9be01aa29 | |||
| 4ec058b33c | |||
| 27a5d2a406 | |||
| 58169e26f6 | |||
| fee1f4bbd5 | |||
| 240817acc3 | |||
| db3be87dd8 | |||
| 1665134d6f | |||
| df289ab734 | |||
| f74440ba6f | |||
| a25dbe9fd5 | |||
| 4fff136d6b | |||
| d06f2d5d2e | |||
| 9f68d628d0 | |||
| d64b906dac | |||
| f3e193e68a | |||
| 5640ce9f2b | |||
| 50100eb2f9 | |||
| e478c510b2 | |||
| 7ea558642f | |||
| 493145f7f2 | |||
| 4f72535365 | |||
| 8e3bf80715 | |||
| 6da586d08a | |||
| be53b9c7fb | |||
| 94ed1160a1 | |||
| 859d8d2631 | |||
| 5f3abd73c5 | |||
| d71c8bb6f9 | |||
| a3db13d79c | |||
| 8cb3da66f2 | |||
| 6e07897ac0 | |||
| 726b859f5c | |||
| 651c60707a | |||
| d4fee84603 | |||
| 86539cdf23 | |||
| 69772460b8 | |||
| 6988a83355 | |||
| b6425564c8 | |||
| caf0a9b4c5 | |||
| bd5f433d6e | |||
| 8d9cc721d6 | |||
| cceeffe49d | |||
| fcb8357489 | |||
| ceffd8e675 | |||
| 5aa53521f7 | |||
| d8b559694b | |||
| c82ac04a91 | |||
| 284bdcbf2a | |||
| 7f4df98349 | |||
| aecbcf62df | |||
| 2a817365a6 | |||
| ecf3a00cab | |||
| beb2f3c0d4 | |||
| a65b09981b | |||
| 4ec544e8a9 | |||
| 7e108c5183 | |||
| 72361b157e | |||
| 1cc4d76e4d | |||
| 846d4d0547 | |||
| 751cadd218 | |||
| 3b44adff44 | |||
| 29a02dd98f | |||
| c5e68c4d03 | |||
| 95f7c2a4d9 | |||
| 84412822ff | |||
| 42bb056e66 | |||
| 053e9d2b6a | |||
| c729d75541 | |||
| a9b72d0b7a | |||
| 95a027f284 | |||
| dbba83b28b | |||
| 65abe550f5 | |||
| 531108c25a | |||
| bcf750d451 | |||
| 0a8bfe4c52 | |||
| 0f19bc767a | |||
| 8eb0266874 | |||
| ae5b5ff965 | |||
| 3a0c4bc151 | |||
| 4f5fcb3189 | |||
| 3a2978bbe3 | |||
| a81cc4559b | |||
| 065143c359 | |||
| 1661881127 | |||
| 93aa889414 | |||
| 2a131ab1fb | |||
| 387f42985a | |||
| 6c83ce90bf | |||
| 3d519e874f | |||
| 99cdb3442a | |||
| a6c0fe86c8 | |||
| 828421efe0 | |||
| 21bacb1c95 | |||
| 0d9a3f4b4f | |||
| 76b8534d63 | |||
| 0821fd708e | |||
| b0a9939446 | |||
| 2a64b8f10d | |||
| dc1c457ea4 | |||
| 1f95bc5230 | |||
| 5a06920e5b | |||
| ee0d9e7ed4 | |||
| 30ad3ad4f2 | |||
| c2bd5a8a1b | |||
| 7101d0972d | |||
| 82bbfcdf70 | |||
| b90ac6c09e | |||
| 76d0ff88f1 | |||
| 951afcedeb | |||
| 96de3f0dcc | |||
| 03950eef66 | |||
| 6dd8cb2dad | |||
| aae51d4139 | |||
| 9994a541b1 | |||
| bc3a603ba2 | |||
| 7857377cab | |||
| 0034f43be4 |
@@ -1,3 +1,6 @@
|
|||||||
|
# This file was auto-generated by the Firebase CLI
|
||||||
|
# https://github.com/firebase/firebase-tools
|
||||||
|
|
||||||
name: Deploy to Firebase Hosting on PR
|
name: Deploy to Firebase Hosting on PR
|
||||||
'on': pull_request
|
'on': pull_request
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
/dist
|
|
||||||
/dev-dist
|
/dev-dist
|
||||||
|
/dist
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
|
|||||||
+5
-2
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"hosting": {
|
"hosting": {
|
||||||
"public": "dist",
|
"public": "dist",
|
||||||
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
"ignore": [
|
||||||
|
"firebase.json",
|
||||||
|
"**/.*",
|
||||||
|
"**/node_modules/**"
|
||||||
|
],
|
||||||
"rewrites": [
|
"rewrites": [
|
||||||
{
|
{
|
||||||
"source": "**",
|
"source": "**",
|
||||||
@@ -10,4 +14,3 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-14
@@ -25,20 +25,6 @@
|
|||||||
<link rel="icon" href="favicon.ico" />
|
<link rel="icon" href="favicon.ico" />
|
||||||
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;700&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;700&display=swap" rel="stylesheet" />
|
||||||
|
|
||||||
<script src="https://www.gstatic.com/firebasejs/8.1.1/firebase-app.js"></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const firebaseConfig = {
|
|
||||||
apiKey: 'AIzaSyBI36X2-p7vU1flxoJdCEc0noByyTe1mpw',
|
|
||||||
authDomain: 'stacjownik-td2.firebaseapp.com',
|
|
||||||
databaseURL: 'https://stacjownik-td2.firebaseio.com',
|
|
||||||
projectId: 'stacjownik-td2',
|
|
||||||
storageBucket: 'stacjownik-td2.appspot.com',
|
|
||||||
};
|
|
||||||
|
|
||||||
firebase.initializeApp(firebaseConfig);
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Generated
+8615
-1025
File diff suppressed because it is too large
Load Diff
+11
-9
@@ -1,15 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "stacjownik",
|
"name": "stacjownik",
|
||||||
"version": "1.10.0",
|
"version": "1.12.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
"preview": "vite preview"
|
"deploy": "yarn build && firebase deploy --only hosting",
|
||||||
|
"preview": "yarn build && vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-js": "^3.12.1",
|
"core-js": "^3.12.1",
|
||||||
"dotenv": "^8.6.0",
|
"dotenv": "^16.0.3",
|
||||||
"firebase": "^9.8.1",
|
"firebase": "^9.8.1",
|
||||||
"howler": "^2.2.1",
|
"howler": "^2.2.1",
|
||||||
"pinia": "^2.0.14",
|
"pinia": "^2.0.14",
|
||||||
@@ -20,12 +21,13 @@
|
|||||||
"vue-router": "^4.0.0-0"
|
"vue-router": "^4.0.0-0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^17.0.35",
|
"@types/node": "^18.11.17",
|
||||||
"@vitejs/plugin-vue": "^3.0.0",
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^1.2.1",
|
||||||
"typescript": "^4.6.4",
|
"typescript": "^4.9.4",
|
||||||
"vite": "^3.0.0",
|
"vite": "^4.0.3",
|
||||||
"vue-tsc": "^0.38.4"
|
"vite-plugin-pwa": "^0.14.0",
|
||||||
|
"vue-tsc": "^1.0.18"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
|
|||||||
+7
-158
@@ -33,7 +33,8 @@
|
|||||||
.route {
|
.route {
|
||||||
margin: 0 0.2em;
|
margin: 0 0.2em;
|
||||||
|
|
||||||
&-active {
|
&-active,
|
||||||
|
&[data-active='true'] {
|
||||||
color: $accentCol;
|
color: $accentCol;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
@@ -45,7 +46,11 @@
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|
||||||
@include smallScreen() {
|
@include smallScreen() {
|
||||||
font-size: calc(0.4rem + 1.4vw);
|
font-size: calc(0.55rem + 1.1vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include screenLandscape() {
|
||||||
|
font-size: calc(0.45rem + 0.8vw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,162 +86,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%;
|
||||||
|
|||||||
+59
-99
@@ -1,72 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app_container">
|
<div class="app_container">
|
||||||
<UpdateModal />
|
|
||||||
|
|
||||||
<transition name="modal-anim">
|
<transition name="modal-anim">
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<TrainModal v-if="store.chosenModalTrainId" />
|
<TrainModal v-if="store.chosenModalTrainId" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<header class="app_header">
|
<UpdatePrompt />
|
||||||
<div class="header_container">
|
|
||||||
<div class="header_icons">
|
|
||||||
<span class="icons-top">
|
|
||||||
<img :src="getIcon('pl')" alt="icon-pl" @click="changeLang('en')" v-if="currentLang == 'pl'" />
|
|
||||||
<img :src="getIcon('en', 'jpg')" alt="icon-en" @click="changeLang('pl')" v-else />
|
|
||||||
</span>
|
|
||||||
<span class="icons-bottom">
|
|
||||||
<a href="https://www.paypal.com/paypalme/spythere" target="_blank">
|
|
||||||
<img :src="getIcon('dollar')" alt="icon paypal" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="https://discord.gg/x2mpNN3svk" target="_blank">
|
<AppHeader :current-lang="currentLang" @change-lang="changeLang" />
|
||||||
<img :src="getIcon('discord', 'png')" alt="icon discord" />
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="header_body">
|
|
||||||
<status-indicator />
|
|
||||||
<span class="header_brand">
|
|
||||||
<img :src="getImage('stacjownik-header-logo.svg')" alt="Stacjownik" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="header_info">
|
|
||||||
<Clock />
|
|
||||||
|
|
||||||
<div class="info_counter">
|
|
||||||
<img :src="getIcon('dispatcher')" alt="icon dispatcher" />
|
|
||||||
<span class="text--primary">{{ onlineDispatchers.length }}</span>
|
|
||||||
<span class="text--grayed"> / </span>
|
|
||||||
<span class="text--primary">{{ trainList.length }}</span>
|
|
||||||
<img :src="getIcon('train')" alt="icon train" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="info_region">
|
|
||||||
<SelectBox :itemList="computedRegions" :defaultItemIndex="0" @selected="changeRegion" />
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="header_links">
|
|
||||||
<router-link class="route" active-class="route-active" to="/" exact>
|
|
||||||
{{ $t('app.sceneries') }}
|
|
||||||
</router-link>
|
|
||||||
/
|
|
||||||
<router-link class="route" active-class="route-active" to="/trains">{{ $t('app.trains') }}</router-link>
|
|
||||||
/
|
|
||||||
<router-link class="route" active-class="route-active" to="/journal/timetables">
|
|
||||||
{{ $t('app.journal') }}
|
|
||||||
</router-link>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="app_main">
|
<main class="app_main">
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<keep-alive>
|
<keep-alive exclude="JournalView">
|
||||||
<component :is="Component" :key="$route.path" />
|
<component :is="Component" :key="$route.name" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</router-view>
|
</router-view>
|
||||||
</main>
|
</main>
|
||||||
@@ -74,7 +21,8 @@
|
|||||||
<footer class="app_footer">
|
<footer class="app_footer">
|
||||||
©
|
©
|
||||||
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
|
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
|
||||||
{{ new Date().getUTCFullYear() }} | <a :href="releaseURL" target="_blank">v{{ VERSION }}</a>
|
{{ new Date().getUTCFullYear() }} |
|
||||||
|
<a :href="releaseURL" target="_blank">v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}</a>
|
||||||
|
|
||||||
<div style="display: none">∫ ukryta taktyczna całka do programowania w HTMLu</div>
|
<div style="display: none">∫ ukryta taktyczna całka do programowania w HTMLu</div>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -82,28 +30,33 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, provide, ref } from 'vue';
|
import { computed, defineComponent, KeepAlive, provide, ref, watch } from 'vue';
|
||||||
|
|
||||||
import Clock from './components/App/Clock.vue';
|
import Clock from './components/App/Clock.vue';
|
||||||
|
|
||||||
import packageInfo from '.././package.json';
|
import packageInfo from '.././package.json';
|
||||||
import options from './data/options.json';
|
|
||||||
|
|
||||||
import StatusIndicator from './components/App/StatusIndicator.vue';
|
import StatusIndicator from './components/App/StatusIndicator.vue';
|
||||||
import SelectBox from './components/Global/SelectBox.vue';
|
import SelectBox from './components/Global/SelectBox.vue';
|
||||||
import { useStore } from './store/store';
|
import { useStore } from './store/store';
|
||||||
import UpdateModal from './components/App/UpdateModal.vue';
|
|
||||||
import TrainModal from './components/Global/TrainModal.vue';
|
import TrainModal from './components/Global/TrainModal.vue';
|
||||||
import StorageManager from './scripts/managers/storageManager';
|
import StorageManager from './scripts/managers/storageManager';
|
||||||
import imageMixin from './mixins/imageMixin';
|
import imageMixin from './mixins/imageMixin';
|
||||||
|
import AppHeader from './components/App/AppHeader.vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
import UpdatePrompt from './components/App/UpdatePrompt.vue';
|
||||||
|
import { VERSION } from 'vue-i18n';
|
||||||
|
import { RouterView } from 'vue-router';
|
||||||
|
import useCustomSW from './mixins/useCustomSW';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
Clock,
|
Clock,
|
||||||
StatusIndicator,
|
StatusIndicator,
|
||||||
SelectBox,
|
SelectBox,
|
||||||
UpdateModal,
|
|
||||||
TrainModal,
|
TrainModal,
|
||||||
|
AppHeader,
|
||||||
|
UpdatePrompt,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [imageMixin],
|
mixins: [imageMixin],
|
||||||
@@ -112,6 +65,8 @@ export default defineComponent({
|
|||||||
const store = useStore();
|
const store = useStore();
|
||||||
store.connectToAPI();
|
store.connectToAPI();
|
||||||
|
|
||||||
|
const { offlineReady } = useCustomSW();
|
||||||
|
|
||||||
const isFilterCardVisible = ref(false);
|
const isFilterCardVisible = ref(false);
|
||||||
|
|
||||||
provide('isFilterCardVisible', isFilterCardVisible);
|
provide('isFilterCardVisible', isFilterCardVisible);
|
||||||
@@ -127,49 +82,54 @@ 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: '',
|
||||||
|
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.loadLang();
|
this.loadLang();
|
||||||
|
|
||||||
|
this.store.isOffline = !window.navigator.onLine;
|
||||||
|
|
||||||
|
window.addEventListener('offline', () => {
|
||||||
|
this.store.isOffline = true;
|
||||||
|
|
||||||
|
this.store.apiData = {
|
||||||
|
stations: [],
|
||||||
|
dispatchers: [],
|
||||||
|
trains: [],
|
||||||
|
connectedSocketCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.store.setOnlineData();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('online', () => {
|
||||||
|
this.store.isOffline = false;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.updateStorage();
|
|
||||||
this.setReleaseURL();
|
this.setReleaseURL();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => this.store.blockScroll,
|
||||||
|
(value) => {
|
||||||
|
if (value) {
|
||||||
|
document.body.classList.add('no-scroll');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.classList.remove('no-scroll');
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
changeRegion(region: { id: string; value: string }) {
|
|
||||||
this.store.changeRegion(region);
|
|
||||||
},
|
|
||||||
|
|
||||||
changeLang(lang: string) {
|
changeLang(lang: string) {
|
||||||
this.$i18n.locale = lang;
|
this.$i18n.locale = lang;
|
||||||
this.currentLang = lang;
|
this.currentLang = lang;
|
||||||
@@ -177,18 +137,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);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<svg width="144" height="147" viewBox="0 0 144 147" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g filter="url(#filter0_d_1343_19)">
|
||||||
|
<path d="M115.039 101.247C116.397 98.6665 115.405 95.4739 112.824 94.1167C110.243 92.7594 107.05 93.7514 105.693 96.3323L115.039 101.247ZM89.4447 44.0402L94.1179 46.4977L99.0329 37.1513L94.3597 34.6938L89.4447 44.0402ZM105.693 96.3323C95.7398 115.259 72.3278 122.534 53.4008 112.581L48.4858 121.927C72.5746 134.595 102.372 125.336 115.039 101.247L105.693 96.3323ZM53.4008 112.581C34.4739 102.627 27.1993 79.2155 37.1525 60.2885L27.8061 55.3735C15.1383 79.4623 24.397 109.259 48.4858 121.927L53.4008 112.581ZM37.1525 60.2885C47.1057 41.3616 70.5177 34.087 89.4447 44.0402L94.3597 34.6938C70.2709 22.026 40.4738 31.2846 27.8061 55.3735L37.1525 60.2885Z" fill="white"/>
|
||||||
|
<path d="M91.2258 38.7627L101.056 20.0698L116.15 51.8695L81.3956 57.4555L91.2258 38.7627Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d_1343_19" x="18.1328" y="20.0698" width="102.017" height="115.531" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dy="4"/>
|
||||||
|
<feGaussianBlur stdDeviation="2"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1343_19"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1343_19" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -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 |
@@ -0,0 +1,275 @@
|
|||||||
|
<template>
|
||||||
|
<header class="app_header">
|
||||||
|
<div class="header_container">
|
||||||
|
<div class="header_icons">
|
||||||
|
<span class="icons-top">
|
||||||
|
<img :src="getIcon('pl')" alt="icon-pl" @click="changeLang('en')" v-if="currentLang == 'pl'" />
|
||||||
|
<img :src="getIcon('en', 'jpg')" alt="icon-en" @click="changeLang('pl')" v-else />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="icons-bottom">
|
||||||
|
<a href="https://www.paypal.com/paypalme/spythere" target="_blank">
|
||||||
|
<img :src="getIcon('dollar')" alt="icon paypal" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://discord.gg/x2mpNN3svk" target="_blank">
|
||||||
|
<img :src="getIcon('discord', 'png')" alt="icon discord" />
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header_body">
|
||||||
|
<StatusIndicator />
|
||||||
|
|
||||||
|
<span class="header_brand">
|
||||||
|
<router-link to="/">
|
||||||
|
<img :src="getImage('stacjownik-header-logo.svg')" alt="Stacjownik" />
|
||||||
|
</router-link>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="header_info">
|
||||||
|
<Clock />
|
||||||
|
|
||||||
|
<div class="info_counter">
|
||||||
|
<img :src="getIcon('dispatcher')" alt="icon dispatcher" />
|
||||||
|
<span class="text--primary">{{ onlineDispatchersCount }}</span>
|
||||||
|
<span class="text--grayed"> / </span>
|
||||||
|
<span class="text--primary">{{ onlineTrainsCount }}</span>
|
||||||
|
<img :src="getIcon('train')" alt="icon train" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="info_region">
|
||||||
|
<SelectBox :itemList="computedRegions" :defaultItemIndex="0" @selected="changeRegion" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="header_links">
|
||||||
|
<router-link class="route" active-class="route-active" to="/" exact>
|
||||||
|
{{ $t('app.sceneries') }}
|
||||||
|
</router-link>
|
||||||
|
/
|
||||||
|
<router-link class="route" active-class="route-active" to="/trains">{{ $t('app.trains') }}</router-link>
|
||||||
|
/
|
||||||
|
<router-link
|
||||||
|
class="route"
|
||||||
|
active-class="route-active"
|
||||||
|
:data-active="$route.path.startsWith('/journal')"
|
||||||
|
to="/journal"
|
||||||
|
>
|
||||||
|
{{ $t('app.journal') }}
|
||||||
|
</router-link>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { useStore } from '../../store/store';
|
||||||
|
import options from '../../data/options.json';
|
||||||
|
import imageMixin from '../../mixins/imageMixin';
|
||||||
|
import SelectBox from '../Global/SelectBox.vue';
|
||||||
|
import StatusIndicator from './StatusIndicator.vue';
|
||||||
|
import Clock from './Clock.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
emits: ['changeLang'],
|
||||||
|
mixins: [imageMixin],
|
||||||
|
props: {
|
||||||
|
currentLang: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
store: useStore(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeRegion(region: { id: string; value: string }) {
|
||||||
|
this.store.changeRegion(region);
|
||||||
|
},
|
||||||
|
changeLang(lang: string) {
|
||||||
|
this.$emit('changeLang', lang);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
onlineTrainsCount() {
|
||||||
|
return this.store.trainList.filter((train) => train.online).length;
|
||||||
|
},
|
||||||
|
onlineDispatchersCount() {
|
||||||
|
return this.store.stationList.filter(
|
||||||
|
(station) => station.onlineInfo && station.onlineInfo.region == this.store.region.id
|
||||||
|
).length;
|
||||||
|
},
|
||||||
|
computedRegions() {
|
||||||
|
return options.regions.map((region) => {
|
||||||
|
const regionStationCount =
|
||||||
|
this.store.apiData.stations?.filter((station) => station.region == region.id && station.isOnline).length || 0;
|
||||||
|
const regionTrainCount =
|
||||||
|
this.store.apiData.trains?.filter((train) => train.region == region.id && train.online).length || 0;
|
||||||
|
return {
|
||||||
|
id: region.id,
|
||||||
|
value: `${region.value} <div class='text--grayed'>${regionStationCount} / ${regionTrainCount}</div>`,
|
||||||
|
selectedValue: region.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: { SelectBox, StatusIndicator, Clock },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../styles/variables.scss';
|
||||||
|
@import '../../styles/responsive.scss';
|
||||||
|
|
||||||
|
// HEADER
|
||||||
|
.app_header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
background-color: $primaryCol;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
&_body {
|
||||||
|
max-width: 21em;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
@include smallScreen {
|
||||||
|
max-width: 18em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&_container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
width: 1350px;
|
||||||
|
padding: 0.5em 0.3em 0 0.3em;
|
||||||
|
border-radius: 0 0 1em 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_brand {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&_info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_links {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
border-radius: 0.7em;
|
||||||
|
|
||||||
|
font-size: 1.25em;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_icons {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: flex-end;
|
||||||
|
padding: 0.5em 0.5em;
|
||||||
|
|
||||||
|
@include smallScreen() {
|
||||||
|
right: auto;
|
||||||
|
left: 0.75em;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ICONS
|
||||||
|
.icons {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&-top {
|
||||||
|
img {
|
||||||
|
width: 2.5em;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-bottom {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
a {
|
||||||
|
margin-left: 0.6em;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 1.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include smallScreen() {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
a {
|
||||||
|
margin: 0.25em 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// COUNTER
|
||||||
|
.info_counter {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin: 0 0.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 1.35em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// REGION SELECTION
|
||||||
|
.info_region {
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.select-box_content button {
|
||||||
|
background-color: transparent;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0.1em 0.5em;
|
||||||
|
color: paleturquoise;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="loading">{{message}}</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: ["message"],
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.loading {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
min-height: 100%;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
|
|
||||||
font-size: calc(0.75rem + 1vw);
|
|
||||||
|
|
||||||
color: #fdc62f;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -161,7 +161,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
import { DataStatus } from '../../scripts/enums/DataStatus';
|
||||||
import { useStore } from '../../store/store';
|
import { useStore } from '../../store/store';
|
||||||
@@ -172,6 +171,7 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
tooltipActive: false,
|
tooltipActive: false,
|
||||||
indicator: {
|
indicator: {
|
||||||
|
offline: false,
|
||||||
status: DataStatus.Loading,
|
status: DataStatus.Loading,
|
||||||
message: 'data-status.S3',
|
message: 'data-status.S3',
|
||||||
},
|
},
|
||||||
@@ -193,6 +193,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
dataStatus: store.dataStatuses,
|
dataStatus: store.dataStatuses,
|
||||||
|
store,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -206,6 +207,13 @@ export default defineComponent({
|
|||||||
const trainsDataStatus = statuses.trains;
|
const trainsDataStatus = statuses.trains;
|
||||||
const dispatcherDataStatus = statuses.dispatchers;
|
const dispatcherDataStatus = statuses.dispatchers;
|
||||||
|
|
||||||
|
if (this.store.isOffline) {
|
||||||
|
this.setSignalStatus(DataStatus.Initialized);
|
||||||
|
this.indicator.status = DataStatus.Initialized;
|
||||||
|
this.indicator.message = 'data-status.S1-offline';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (connectionStatus == DataStatus.Error) {
|
if (connectionStatus == DataStatus.Error) {
|
||||||
this.setSignalStatus(connectionStatus);
|
this.setSignalStatus(connectionStatus);
|
||||||
this.indicator.status = connectionStatus;
|
this.indicator.status = connectionStatus;
|
||||||
@@ -252,6 +260,10 @@ export default defineComponent({
|
|||||||
this.orangeLight = false;
|
this.orangeLight = false;
|
||||||
this.redBottomLight = false;
|
this.redBottomLight = false;
|
||||||
|
|
||||||
|
if (status == DataStatus.Initialized) {
|
||||||
|
this.redTopLight = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (status == DataStatus.Loaded) {
|
if (status == DataStatus.Loaded) {
|
||||||
this.greenLight = true;
|
this.greenLight = true;
|
||||||
}
|
}
|
||||||
@@ -291,9 +303,8 @@ export default defineComponent({
|
|||||||
|
|
||||||
.status-indicator {
|
.status-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 110%;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
transform: translateX(12em);
|
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<div class="update-prompt">
|
||||||
|
<transition name="prompt-anim">
|
||||||
|
<div class="prompt_content" v-if="!hidePrompt && needRefresh">
|
||||||
|
<div>{{ $t('update.title') }}</div>
|
||||||
|
|
||||||
|
<div class="prompt_actions">
|
||||||
|
<button class="btn btn--filled" @click="updateServiceWorker(true)">{{ $t('update.confirm-button') }}</button>
|
||||||
|
<button class="btn btn--filled" @click="hidePrompt = true">{{ $t('update.later-button') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import useCustomSW from '../../mixins/useCustomSW';
|
||||||
|
|
||||||
|
const hidePrompt = ref(false);
|
||||||
|
const { needRefresh, updateServiceWorker } = useCustomSW();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../styles/variables.scss';
|
||||||
|
|
||||||
|
.update-prompt {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt_content {
|
||||||
|
margin: 1em;
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: black;
|
||||||
|
|
||||||
|
box-shadow: 0 0 10px 1px $accentCol;
|
||||||
|
border-radius: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt_actions {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 1em;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animation
|
||||||
|
.prompt-anim {
|
||||||
|
&-enter-active,
|
||||||
|
&-leave-active {
|
||||||
|
transition: all 120ms ease-in;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-enter-from,
|
||||||
|
&-leave-to {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -1,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;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default defineComponent({
|
|||||||
.loading {
|
.loading {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
<template>
|
||||||
|
<section class="daily-stats">
|
||||||
|
<span :data-active="data.statsStatus">
|
||||||
|
<b v-if="data.statsStatus == DataStatus.Loading">
|
||||||
|
{{ $t('app.loading') }}
|
||||||
|
</b>
|
||||||
|
|
||||||
|
<b v-else-if="data.stats.distanceSum == null">
|
||||||
|
{{ $t('journal.daily-stats-info') }}
|
||||||
|
</b>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
<div v-if="data.stats.totalTimetables">
|
||||||
|
•
|
||||||
|
<i18n-t keypath="journal.timetable-stats-total">
|
||||||
|
<template #count>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ data.stats.totalTimetables }}
|
||||||
|
{{ $t('journal.timetable-count', data.stats.totalTimetables) }}
|
||||||
|
</b>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #distance>
|
||||||
|
<b class="text--primary"> {{ data.stats.distanceSum?.toFixed(2) }} km </b>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="data.stats.timetableId">
|
||||||
|
•
|
||||||
|
<i18n-t keypath="journal.timetable-stats-longest">
|
||||||
|
<template #id>
|
||||||
|
<router-link :to="`/journal/timetables?timetableId=${data.stats.timetableId}`">
|
||||||
|
<b>{{ data.stats.timetableId }}</b>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
<template #author>
|
||||||
|
<router-link :to="`/journal/dispatchers?dispatcherName=${data.stats.timetableAuthor}`">
|
||||||
|
<b>{{ data.stats.timetableAuthor }}</b>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
<template #driver>
|
||||||
|
<b>{{ data.stats.timetableDriver }}</b>
|
||||||
|
</template>
|
||||||
|
<template #distance>
|
||||||
|
<b class="text--primary">{{ data.stats.timetableRouteDistance }} km</b>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="firstPlaceDispatchers.length == 1">
|
||||||
|
•
|
||||||
|
<i18n-t keypath="journal.timetable-stats-most-active">
|
||||||
|
<template #dispatcher>
|
||||||
|
<router-link :to="`/journal/dispatchers?dispatcherName=${firstPlaceDispatchers[0].name}`">
|
||||||
|
<b>{{ firstPlaceDispatchers[0].name }}</b>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
<template #count>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ firstPlaceDispatchers[0].count }}
|
||||||
|
{{ $t('journal.timetable-count', firstPlaceDispatchers[0].count) }}
|
||||||
|
</b>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="firstPlaceDispatchers.length > 1">
|
||||||
|
•
|
||||||
|
<i18n-t keypath="journal.timetable-stats-most-active-many">
|
||||||
|
<template #dispatchers>
|
||||||
|
<span v-for="(disp, i) in firstPlaceDispatchers">
|
||||||
|
<span v-if="i == firstPlaceDispatchers.length - 1"> {{ $t('general.and') }} </span>
|
||||||
|
|
||||||
|
<router-link :to="`/journal/dispatchers?dispatcherName=${disp.name}`">
|
||||||
|
<b>{{ disp.name }}</b>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<span v-if="i < firstPlaceDispatchers.length - 2">, </span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #count>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ firstPlaceDispatchers[0].count }}
|
||||||
|
{{ $t('journal.timetable-count', firstPlaceDispatchers[0].count) }}
|
||||||
|
</b>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import axios from 'axios';
|
||||||
|
import { computed, reactive, ref } from 'vue';
|
||||||
|
import { DataStatus } from '../../scripts/enums/DataStatus';
|
||||||
|
import { ITimetablesDailyStats, ITimetablesDailyStatsResponse } from '../../scripts/interfaces/api/StatsAPIData';
|
||||||
|
import { URLs } from '../../scripts/utils/apiURLs';
|
||||||
|
|
||||||
|
const intervalId = ref(-1);
|
||||||
|
|
||||||
|
const data = reactive({
|
||||||
|
statsStatus: DataStatus.Loading,
|
||||||
|
|
||||||
|
stats: {
|
||||||
|
totalTimetables: 0,
|
||||||
|
distanceSum: 0,
|
||||||
|
distanceAvg: 0,
|
||||||
|
timetableAuthor: '',
|
||||||
|
timetableDriver: '',
|
||||||
|
timetableId: 0,
|
||||||
|
timetableRouteDistance: 0,
|
||||||
|
|
||||||
|
mostActiveDispatchers: [],
|
||||||
|
} as ITimetablesDailyStats,
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstPlaceDispatchers = computed(() => {
|
||||||
|
if (data.stats.mostActiveDispatchers.length == 0) return [];
|
||||||
|
const maxCount = data.stats.mostActiveDispatchers[0].count;
|
||||||
|
|
||||||
|
return data.stats.mostActiveDispatchers.filter((disp) => disp.count === maxCount);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchDailyTimetableStats() {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
distanceAvg,
|
||||||
|
distanceSum,
|
||||||
|
maxTimetable,
|
||||||
|
totalTimetables,
|
||||||
|
mostActiveDispatchers,
|
||||||
|
}: ITimetablesDailyStatsResponse = await (
|
||||||
|
await axios.get(`${URLs.stacjownikAPI}/api/getDailyTimetableStats`)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
data.stats = {
|
||||||
|
totalTimetables,
|
||||||
|
distanceSum,
|
||||||
|
distanceAvg,
|
||||||
|
timetableAuthor: maxTimetable?.authorName || '',
|
||||||
|
timetableDriver: maxTimetable?.driverName || '',
|
||||||
|
timetableId: maxTimetable?.id || 0,
|
||||||
|
timetableRouteDistance: maxTimetable?.routeDistance || 0,
|
||||||
|
|
||||||
|
mostActiveDispatchers,
|
||||||
|
};
|
||||||
|
|
||||||
|
data.statsStatus = DataStatus.Loaded;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...');
|
||||||
|
data.statsStatus = DataStatus.Error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startFetchingDailyStats() {
|
||||||
|
fetchDailyTimetableStats();
|
||||||
|
intervalId.value = setInterval(fetchDailyTimetableStats, 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopFetchingDailyStats() {
|
||||||
|
clearInterval(intervalId.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
startFetchingDailyStats,
|
||||||
|
stopFetchingDailyStats,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.daily-stats {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.daily-stats > span[data-active='0'] {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="card-dimmer" @click="closeCard"></div>
|
|
||||||
|
|
||||||
<div class="stats-card card">
|
|
||||||
<div>
|
|
||||||
<h2 class="card-title">
|
|
||||||
STATYSTYKI MASZYNISTY <span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="loading" v-if="!store.driverStatsData">Ładowanie...</div>
|
|
||||||
|
|
||||||
<div v-else>
|
|
||||||
<div class="info-stats" v-if="store.driverStatsData._sum.routeDistance != null">
|
|
||||||
<span class="stat-badge">
|
|
||||||
<span>PRZEBYTO</span>
|
|
||||||
<span>{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km</span>
|
|
||||||
</span>
|
|
||||||
<span class="stat-badge">
|
|
||||||
<span>PORZUCONO</span>
|
|
||||||
<span>
|
|
||||||
{{ (store.driverStatsData._sum.routeDistance - store.driverStatsData._sum.currentDistance).toFixed(2) }}km
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="stat-badge">
|
|
||||||
<span>WYPEŁNIONO</span>
|
|
||||||
<span>{{ store.driverStatsData._count.fulfilled }} RJ</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="stat-badge">
|
|
||||||
<span>PORZUCONO</span>
|
|
||||||
<span>{{ store.driverStatsData._count._all - store.driverStatsData._count.fulfilled }} RJ</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="stat-badge">
|
|
||||||
<span>ZATWIERDZONO</span>
|
|
||||||
<span>{{ store.driverStatsData._sum.confirmedStopsCount }} stacji</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="stat-badge">
|
|
||||||
<span>PORZUCONO</span>
|
|
||||||
<span>
|
|
||||||
{{ store.driverStatsData._sum.allStopsCount - store.driverStatsData._sum.confirmedStopsCount }}
|
|
||||||
stacji
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import axios from 'axios';
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData';
|
|
||||||
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
|
|
||||||
import { URLs } from '../../scripts/utils/apiURLs';
|
|
||||||
import { useStore } from '../../store/store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
emits: ['closeCard'],
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
const store = useStore();
|
|
||||||
return {
|
|
||||||
store,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
test: Math.random(),
|
|
||||||
lastDispatcherName: '',
|
|
||||||
|
|
||||||
lastTimetables: [] as TimetableHistory[],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
activated() {
|
|
||||||
this.fetchDispatcherStats();
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
async fetchDispatcherStats() {
|
|
||||||
this.store.driverStatsData = undefined;
|
|
||||||
|
|
||||||
const statsData: DriverStatsAPIData = await (
|
|
||||||
await axios.get(`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`)
|
|
||||||
).data;
|
|
||||||
|
|
||||||
const recentTimetablesData: TimetableHistory[] = await (
|
|
||||||
await axios.get(`${URLs.stacjownikAPI}/api/getTimetables?driverName=${this.store.driverStatsName}`)
|
|
||||||
).data;
|
|
||||||
|
|
||||||
this.store.driverStatsData = statsData;
|
|
||||||
this.lastTimetables = recentTimetablesData || [];
|
|
||||||
},
|
|
||||||
|
|
||||||
closeCard() {
|
|
||||||
this.$emit('closeCard');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '../../styles/responsive.scss';
|
|
||||||
|
|
||||||
.timetable-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 4fr 1fr 1fr 2fr 2fr;
|
|
||||||
gap: 0.2em;
|
|
||||||
margin: 0.5em 0;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
span {
|
|
||||||
min-width: 100px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
background-color: #4d4d4d;
|
|
||||||
padding: 0.5em 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include smallScreen() {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
span {
|
|
||||||
padding: 0.2em 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
background-color: #4d4d4d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,425 +0,0 @@
|
|||||||
<template>
|
|
||||||
<section class="journal-timetables">
|
|
||||||
<div class="journal-wrapper">
|
|
||||||
<div class="journal_top-bar">
|
|
||||||
<JournalOptions
|
|
||||||
@on-filter-change="search"
|
|
||||||
@on-input-change="search"
|
|
||||||
@on-sorter-change="search"
|
|
||||||
:sorter-option-ids="['timestampFrom', 'duration']"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- <DispatcherStats /> -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="journal-list">
|
|
||||||
<div class="list-wrapper" ref="scrollElement">
|
|
||||||
<transition name="warning" mode="out-in">
|
|
||||||
<div :key="historyDataStatus.status">
|
|
||||||
<Loading v-if="isDataLoading || isDataInit" />
|
|
||||||
|
|
||||||
<div v-else-if="isDataError" class="journal_warning error">
|
|
||||||
{{ $t('app.error') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="journal_warning" v-else-if="historyList.length == 0">
|
|
||||||
{{ $t('app.no-result') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul v-else>
|
|
||||||
<transition-group name="journal-list-anim">
|
|
||||||
<li v-for="(doc, i) in computedHistoryList" :key="doc.id">
|
|
||||||
<div class="journal_day" v-if="isAnotherDay(i - 1, i)">
|
|
||||||
<span>{{ new Date(doc.timestampFrom).toLocaleDateString('pl-PL') }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="journal_item"
|
|
||||||
:class="{ online: doc.isOnline }"
|
|
||||||
@click="navigateToScenery(doc.stationName, doc.isOnline)"
|
|
||||||
@keydown.enter="navigateToScenery(doc.stationName, doc.isOnline)"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<b class="text--primary">{{ doc.dispatcherName }}</b> • <b>{{ doc.stationName }}</b>
|
|
||||||
<span class="text--grayed"> #{{ doc.stationHash }} </span>
|
|
||||||
<span class="region-badge" :class="doc.region">PL1</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<span :data-status="doc.isOnline">
|
|
||||||
{{ doc.isOnline ? $t('journal.online-since') : 'OFFLINE' }}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{{ new Date(doc.timestampFrom).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span v-if="doc.currentDuration && doc.isOnline">
|
|
||||||
({{ calculateDuration(doc.currentDuration) }})
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span v-if="doc.timestampTo">
|
|
||||||
>
|
|
||||||
{{ new Date(doc.timestampTo).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
|
|
||||||
({{ $t('journal.duty-lasted') }} {{ calculateDuration(doc.currentDuration!) }})
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</transition-group>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
|
|
||||||
<div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, JournalFilter, provide, reactive, Ref, ref } from 'vue';
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
import ActionButton from '../../components/Global/ActionButton.vue';
|
|
||||||
import JournalOptions from '../../components/JournalView/JournalOptions.vue';
|
|
||||||
import DispatcherStats from '../../components/JournalView/DispatcherStats.vue';
|
|
||||||
import SearchBox from '../Global/SearchBox.vue';
|
|
||||||
|
|
||||||
import Loading from '../Global/Loading.vue';
|
|
||||||
import { URLs } from '../../scripts/utils/apiURLs';
|
|
||||||
import dateMixin from '../../mixins/dateMixin';
|
|
||||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
|
||||||
import { useStore } from '../../store/store';
|
|
||||||
|
|
||||||
const DISPATCHERS_API_URL = `${URLs.stacjownikAPI}/api/getDispatchers`;
|
|
||||||
|
|
||||||
interface DispatcherHistoryItem {
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
stationName: string;
|
|
||||||
stationHash: string;
|
|
||||||
region: string;
|
|
||||||
|
|
||||||
dispatcherName: string;
|
|
||||||
dispatcherId: number;
|
|
||||||
|
|
||||||
timestampFrom: number;
|
|
||||||
timestampTo?: number;
|
|
||||||
currentDuration?: number;
|
|
||||||
|
|
||||||
lastOnlineTimestamp: number;
|
|
||||||
|
|
||||||
isOnline: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
type JournalDispatcherSearcher = {
|
|
||||||
[key in 'search-dispatcher' | 'search-station']: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: { SearchBox, ActionButton, JournalOptions, DispatcherStats, Loading },
|
|
||||||
mixins: [dateMixin],
|
|
||||||
name: 'JournalDispatchers',
|
|
||||||
|
|
||||||
props: {
|
|
||||||
sceneryName: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
dispatcherName: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data: () => ({
|
|
||||||
currentQuery: '',
|
|
||||||
scrollDataLoaded: true,
|
|
||||||
scrollNoMoreData: false,
|
|
||||||
|
|
||||||
showReturnButton: false,
|
|
||||||
statsCardOpen: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
const historyDataStatus: Ref<{ status: DataStatus; error: string | null }> = ref({
|
|
||||||
status: DataStatus.Loading,
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const sorterActive = ref({ id: 'timestampFrom', dir: -1 });
|
|
||||||
const journalFilterActive = ref({});
|
|
||||||
const searchersValues = reactive({
|
|
||||||
'search-dispatcher': '',
|
|
||||||
'search-station': '',
|
|
||||||
} as JournalDispatcherSearcher);
|
|
||||||
|
|
||||||
const countFromIndex = ref(0);
|
|
||||||
const countLimit = 15;
|
|
||||||
|
|
||||||
provide('sorterActive', sorterActive);
|
|
||||||
provide('journalFilterActive', journalFilterActive);
|
|
||||||
provide('searchersValues', searchersValues);
|
|
||||||
|
|
||||||
const scrollElement: Ref<HTMLElement | null> = ref(null);
|
|
||||||
|
|
||||||
return {
|
|
||||||
store: useStore(),
|
|
||||||
|
|
||||||
historyList: ref([]) as Ref<DispatcherHistoryItem[]>,
|
|
||||||
historyDataStatus,
|
|
||||||
|
|
||||||
isDataLoading: computed(() => historyDataStatus.value.status === DataStatus.Loading),
|
|
||||||
isDataError: computed(() => historyDataStatus.value.status === DataStatus.Error),
|
|
||||||
isDataInit: computed(() => historyDataStatus.value.status === DataStatus.Initialized),
|
|
||||||
|
|
||||||
sorterActive,
|
|
||||||
searchersValues,
|
|
||||||
|
|
||||||
countFromIndex,
|
|
||||||
countLimit,
|
|
||||||
|
|
||||||
scrollElement,
|
|
||||||
maxCount: ref(15),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
computedHistoryList() {
|
|
||||||
return this.historyList.filter(
|
|
||||||
(doc) => doc.isOnline || (doc.currentDuration && doc.currentDuration > 10 * 60000)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
activated() {
|
|
||||||
if (this.sceneryName || this.dispatcherName) {
|
|
||||||
this.searchersValues['search-station'] = this.sceneryName?.toString() || '';
|
|
||||||
this.searchersValues['search-dispatcher'] = this.dispatcherName?.toString() || '';
|
|
||||||
this.search();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('scroll', this.handleScroll);
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
if (!this.sceneryName && !this.dispatcherName) {
|
|
||||||
this.search();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
deactivated() {
|
|
||||||
window.removeEventListener('scroll', this.handleScroll);
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
closeDispatcherStatsCard() {
|
|
||||||
this.statsCardOpen = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
navigateToScenery(name: string, isOnline: boolean) {
|
|
||||||
if (!isOnline) return;
|
|
||||||
|
|
||||||
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
isAnotherDay(prevIndex: number, currIndex: number) {
|
|
||||||
if (currIndex == 0) return true;
|
|
||||||
|
|
||||||
return (
|
|
||||||
new Date(this.computedHistoryList[prevIndex].timestampFrom).getDate() !=
|
|
||||||
new Date(this.computedHistoryList[currIndex].timestampFrom).getDate()
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleScroll() {
|
|
||||||
this.showReturnButton = window.scrollY > window.innerHeight;
|
|
||||||
|
|
||||||
const element = this.$refs.scrollElement as HTMLElement;
|
|
||||||
|
|
||||||
if (
|
|
||||||
element.getBoundingClientRect().bottom * 0.85 < window.innerHeight &&
|
|
||||||
this.scrollDataLoaded &&
|
|
||||||
!this.scrollNoMoreData &&
|
|
||||||
this.historyDataStatus.status == DataStatus.Loaded
|
|
||||||
)
|
|
||||||
this.addHistoryData();
|
|
||||||
},
|
|
||||||
|
|
||||||
scrollToTop() {
|
|
||||||
window.scrollTo({ top: 0 });
|
|
||||||
},
|
|
||||||
|
|
||||||
search() {
|
|
||||||
this.fetchHistoryData({
|
|
||||||
searchers: this.searchersValues,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.scrollNoMoreData = false;
|
|
||||||
this.scrollDataLoaded = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
async addHistoryData() {
|
|
||||||
this.scrollDataLoaded = false;
|
|
||||||
|
|
||||||
const countFrom = this.historyList.length;
|
|
||||||
|
|
||||||
const responseData: DispatcherHistoryItem[] = await (
|
|
||||||
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
|
|
||||||
).data;
|
|
||||||
|
|
||||||
if (!responseData) return;
|
|
||||||
|
|
||||||
if (responseData.length == 0) {
|
|
||||||
this.scrollNoMoreData = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.historyList.push(...responseData);
|
|
||||||
this.scrollDataLoaded = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchHistoryData(
|
|
||||||
props: {
|
|
||||||
searchers?: JournalDispatcherSearcher;
|
|
||||||
filter?: JournalFilter;
|
|
||||||
} = {}
|
|
||||||
) {
|
|
||||||
this.historyDataStatus.status = DataStatus.Loading;
|
|
||||||
|
|
||||||
const queries: string[] = [];
|
|
||||||
|
|
||||||
// const dispatcher = props.searchers?.find((s) => s.id == 'search-dispatcher')?.value.trim();
|
|
||||||
// const station = props.searchers?.find((s) => s.id == 'search-station')?.value.trim();
|
|
||||||
|
|
||||||
const dispatcher = props.searchers?.['search-dispatcher'].trim();
|
|
||||||
const station = props.searchers?.['search-station'].trim();
|
|
||||||
|
|
||||||
if (dispatcher) queries.push(`dispatcherName=${dispatcher}`);
|
|
||||||
if (station) queries.push(`stationName=${station}`);
|
|
||||||
|
|
||||||
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
|
|
||||||
if (this.sorterActive.id == 'timestampFrom') queries.push('sortBy=timestampFrom');
|
|
||||||
else if (this.sorterActive.id == 'duration') queries.push('sortBy=currentDuration');
|
|
||||||
else queries.push('sortBy=timestampFrom');
|
|
||||||
|
|
||||||
queries.push('countLimit=15');
|
|
||||||
|
|
||||||
this.currentQuery = queries.join('&');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const responseData: DispatcherHistoryItem[] = await (
|
|
||||||
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}`)
|
|
||||||
).data;
|
|
||||||
|
|
||||||
if (!responseData) {
|
|
||||||
this.historyDataStatus.status = DataStatus.Error;
|
|
||||||
this.historyDataStatus.error = 'Brak danych!';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!responseData) return;
|
|
||||||
|
|
||||||
// Response data exists
|
|
||||||
this.historyList = responseData;
|
|
||||||
|
|
||||||
// Stats display
|
|
||||||
this.store.dispatcherStatsName =
|
|
||||||
this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim()
|
|
||||||
? this.historyList[0].dispatcherName
|
|
||||||
: '';
|
|
||||||
|
|
||||||
this.historyDataStatus.status = DataStatus.Loaded;
|
|
||||||
} catch (error) {
|
|
||||||
this.historyDataStatus.status = DataStatus.Error;
|
|
||||||
this.historyDataStatus.error = 'Ups! Coś poszło nie tak!';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '../../styles/JournalSection.scss';
|
|
||||||
@import '../../styles/responsive.scss';
|
|
||||||
|
|
||||||
.region-badge {
|
|
||||||
padding: 0.1em 0.5em;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
&.eu {
|
|
||||||
background-color: forestgreen;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-wrapper {
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.journal_item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
&.online {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
span[data-status='true'] {
|
|
||||||
color: springgreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
span[data-status='false'] {
|
|
||||||
color: salmon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.journal_day {
|
|
||||||
position: relative;
|
|
||||||
text-align: center;
|
|
||||||
background-color: #4d4d4d;
|
|
||||||
|
|
||||||
span {
|
|
||||||
position: relative;
|
|
||||||
background-color: #4d4d4d;
|
|
||||||
z-index: 10;
|
|
||||||
|
|
||||||
padding: 0 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
position: absolute;
|
|
||||||
content: '';
|
|
||||||
|
|
||||||
z-index: 0;
|
|
||||||
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
|
|
||||||
height: 3px;
|
|
||||||
width: 60%;
|
|
||||||
min-width: 200px;
|
|
||||||
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include smallScreen() {
|
|
||||||
.journal_item {
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
span {
|
|
||||||
margin-top: 0.25em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<transition-group class="journal-list" tag="ul" name="list-anim">
|
||||||
|
<li
|
||||||
|
v-for="item in computedDispatcherHistory"
|
||||||
|
:key="typeof item === 'string' ? item : item.timestampFrom + item.dispatcherId"
|
||||||
|
: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 class="item-general">
|
||||||
|
<b
|
||||||
|
v-if="item.dispatcherLevel !== null"
|
||||||
|
class="level-badge dispatcher"
|
||||||
|
:style="calculateExpStyle(item.dispatcherLevel, item.dispatcherIsSupporter)"
|
||||||
|
>
|
||||||
|
{{ item.dispatcherLevel >= 2 ? item.dispatcherLevel : 'L' }}
|
||||||
|
</b>
|
||||||
|
|
||||||
|
<b class="text--primary">{{ item.dispatcherName }}</b> • <b>{{ item.stationName }}</b>
|
||||||
|
<span class="text--grayed"> #{{ item.stationHash }} </span>
|
||||||
|
<span class="region-badge" :class="item.region">PL1</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="item-time">
|
||||||
|
<span :data-status="item.isOnline"> {{ item.isOnline ? $t('journal.online-since') : 'OFFLINE' }} </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">
|
||||||
|
>
|
||||||
|
{{ new Date(item.timestampTo).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
|
||||||
|
({{ $t('journal.duty-lasted') }} {{ calculateDuration(item.currentDuration!) }})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</transition-group>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType } from 'vue';
|
||||||
|
import dateMixin from '../../mixins/dateMixin';
|
||||||
|
import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
|
||||||
|
import styleMixin from '../../mixins/styleMixin';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
dispatcherHistory: {
|
||||||
|
type: Array as PropType<DispatcherHistory[]>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [dateMixin, styleMixin],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
computedDispatcherHistory() {
|
||||||
|
return this.dispatcherHistory.reduce((acc, historyItem, i) => {
|
||||||
|
if (this.isAnotherDay(i - 1, i)) acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL'));
|
||||||
|
acc.push(historyItem);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, [] as (DispatcherHistory | string)[]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
navigateToScenery(name: string, isOnline: boolean) {
|
||||||
|
if (!isOnline) return;
|
||||||
|
|
||||||
|
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
isAnotherDay(prevIndex: number, currIndex: number) {
|
||||||
|
if (currIndex == 0) return true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
new Date(this.dispatcherHistory[prevIndex].timestampFrom).getDate() !=
|
||||||
|
new Date(this.dispatcherHistory[currIndex].timestampFrom).getDate()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../styles/animations.scss';
|
||||||
|
@import '../../styles/responsive.scss';
|
||||||
|
@import '../../styles/badge.scss';
|
||||||
|
@import '../../styles/JournalSection.scss';
|
||||||
|
|
||||||
|
li.sticky {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal_item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
gap: 0.25em;
|
||||||
|
|
||||||
|
line-height: 1.7em;
|
||||||
|
padding: 0.75em;
|
||||||
|
|
||||||
|
&.online {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
span[data-status='true'] {
|
||||||
|
color: springgreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
span[data-status='false'] {
|
||||||
|
color: salmon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-general {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
|
||||||
|
.level-badge {
|
||||||
|
margin-right: 0.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<div class="journal-stats">
|
||||||
|
<span v-if="store.driverStatsData">
|
||||||
|
<h3>
|
||||||
|
{{ $t('journal.stats-title') }} <span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="info-stats">
|
||||||
|
<span class="stat-badge">
|
||||||
|
<span>{{ $t('journal.stats-timetables') }}</span>
|
||||||
|
<span>{{ store.driverStatsData._count.fulfilled }} / {{ store.driverStatsData._count._all }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="stat-badge">
|
||||||
|
<span>{{ $t('journal.stats-longest-timetable') }}</span>
|
||||||
|
<span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="stat-badge">
|
||||||
|
<span>{{ $t('journal.stats-avg-timetable') }}</span>
|
||||||
|
<span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="stat-badge">
|
||||||
|
<span>{{ $t('journal.stats-distance') }}</span>
|
||||||
|
<span>
|
||||||
|
{{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
|
||||||
|
{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="stat-badge">
|
||||||
|
<span>{{ $t('journal.stats-stations') }}</span>
|
||||||
|
<span>
|
||||||
|
{{ store.driverStatsData._sum.confirmedStopsCount }} /
|
||||||
|
{{ store.driverStatsData._sum.allStopsCount }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<b v-else-if="store.driverStatsStatus == DataStatus.Loading">{{ $t('journal.stats-loading') }}</b>
|
||||||
|
<b v-else-if="store.driverStatsStatus == DataStatus.Error">
|
||||||
|
{{ $t('journal.stats-error ') }}
|
||||||
|
</b>
|
||||||
|
<b v-else>{{ $t('journal.driver-stats-info') }}</b>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { DataStatus } from '../../scripts/enums/DataStatus';
|
||||||
|
import { useStore } from '../../store/store';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
store: useStore(),
|
||||||
|
DataStatus,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../styles/JournalStats.scss';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<section class="journal-header">
|
||||||
|
<div class="journal-type-options">
|
||||||
|
<router-link class="router-link" active-class="route-active" to="/journal/timetables" exact>
|
||||||
|
{{ $t('journal.section-timetables') }}
|
||||||
|
</router-link>
|
||||||
|
•
|
||||||
|
<router-link class="router-link" active-class="route-active" to="/journal/dispatchers">
|
||||||
|
{{ $t('journal.section-dispatchers') }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.journal-type-options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
background-color: #2c2c2c;
|
||||||
|
max-width: 18em;
|
||||||
|
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
border-radius: 0 0 0.5em 0.5em;
|
||||||
|
padding: 0.1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-section > section {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.router-link.active {
|
||||||
|
color: gold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,80 +1,113 @@
|
|||||||
<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="actions-bar">
|
||||||
|
<button class="filter-button btn--filled btn--image" @click="showOptions = !showOptions" ref="button">
|
||||||
|
<img :src="getIcon('filter2')" alt="Open filters" />
|
||||||
|
{{ $t('options.filters') }} [F]
|
||||||
|
<span class="active-indicator" v-if="currentOptionsActive"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="filter-button btn--filled btn--image" @click="refreshData">
|
||||||
|
<img :src="getIcon('refresh')" alt="Refresh data" />
|
||||||
|
{{ $t('general.refresh') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<datalist id="search-driver">
|
||||||
|
<option v-for="sugg in driverSuggestions" :value="sugg"></option>
|
||||||
|
</datalist>
|
||||||
|
|
||||||
|
<datalist id="search-dispatcher">
|
||||||
|
<option v-for="sugg in dispatcherSuggestions" :value="sugg"></option>
|
||||||
|
</datalist>
|
||||||
|
|
||||||
|
<transition name="options-anim">
|
||||||
|
<div class="options_wrapper" v-if="showOptions">
|
||||||
<div class="options_content">
|
<div class="options_content">
|
||||||
<div class="content_select">
|
<h1 class="option-title">{{ $t('options.search-title') }}</h1>
|
||||||
<select-box
|
<div class="search_content">
|
||||||
:itemList="translatedSorterOptions"
|
<div class="search" v-for="(_, propName) in searchersValues" :key="propName">
|
||||||
:defaultItemIndex="0"
|
<label v-if="propName == 'search-date'" for="date">{{ $t('options.search-date') }}</label>
|
||||||
@selected="onSorterChange"
|
|
||||||
:prefix="$t('journal.sort-prefix')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content_search">
|
|
||||||
<div class="search-box" v-for="(value, propName) in searchersValues" :key="propName">
|
|
||||||
<input
|
|
||||||
class="search-input"
|
|
||||||
:placeholder="$t(`journal.${propName}`)"
|
|
||||||
v-model="searchersValues[propName]"
|
|
||||||
@keydown.enter="onInputSearch"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<img class="search-exit" :src="getIcon('exit')" alt="exit-icon" @click="onInputClear(propName)" />
|
|
||||||
</div>
|
|
||||||
<!-- <div class="search-box">
|
|
||||||
<input
|
|
||||||
class="search-input"
|
|
||||||
v-model="searchedTrain"
|
|
||||||
:placeholder="$t('journal.search-train')"
|
|
||||||
@keydown.enter="search"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="clearTrain" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<input
|
<input
|
||||||
class="search-input"
|
class="search-input"
|
||||||
v-model="searchedDriver"
|
v-model="searchersValues[propName]"
|
||||||
:placeholder="$t('journal.search-driver')"
|
@keydown.enter="onSearchConfirm"
|
||||||
@keydown.enter="search"
|
@focus="preventKeyDown = true"
|
||||||
|
@blur="preventKeyDown = false"
|
||||||
|
:placeholder="$t(`options.${propName}`)"
|
||||||
|
:type="propName == 'search-date' ? 'date' : 'text'"
|
||||||
|
:min="propName == 'search-date' ? '2022-02-01' : undefined"
|
||||||
|
:list="propName.toString()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="clearDriver" />
|
<button class="search-exit" v-if="propName != 'search-date'">
|
||||||
</div> -->
|
<img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear(propName)" />
|
||||||
|
</button>
|
||||||
<action-button class="search-button" @click="onInputSearch">
|
|
||||||
{{ $t('journal.search') }}
|
|
||||||
</action-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="search_actions">
|
||||||
|
<button class="btn--action" @click="onResetButtonClick">
|
||||||
|
{{ $t('options.reset-button') }}
|
||||||
|
</button>
|
||||||
|
<button class="btn--action" @click="onSearchButtonConfirm">
|
||||||
|
{{ $t('options.search-button') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="option-title">{{ $t('options.sort-title') }}</h1>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<h1 class="option-title" v-if="filters.length != 0">{{ $t('options.filter-title') }}</h1>
|
||||||
<div class="options_filters">
|
<div class="options_filters">
|
||||||
<button
|
<button
|
||||||
v-for="filter in filters"
|
v-for="filter in filters"
|
||||||
class="journal-filter-option btn--option"
|
class="filter-option btn--option"
|
||||||
:class="{ checked: journalFilterActive.id === filter.id }"
|
:class="{ checked: journalFilterActive.id === filter.id }"
|
||||||
:id="filter.id"
|
:id="filter.id"
|
||||||
@click="onFilterChange(filter)"
|
@click="onFilterChange(filter)"
|
||||||
>
|
>
|
||||||
{{ $t(`journal.filter-${filter.id}`) }}
|
{{ $t(`options.filter-${filter.id}`) }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, inject, JournalFilter, PropType } from 'vue';
|
import axios from 'axios';
|
||||||
|
import { defineComponent, inject, PropType } from 'vue';
|
||||||
import imageMixin from '../../mixins/imageMixin';
|
import imageMixin from '../../mixins/imageMixin';
|
||||||
|
import keyMixin from '../../mixins/keyMixin';
|
||||||
|
import { DataStatus } from '../../scripts/enums/DataStatus';
|
||||||
|
import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData';
|
||||||
|
import { URLs } from '../../scripts/utils/apiURLs';
|
||||||
|
import { useStore } from '../../store/store';
|
||||||
|
import { JournalTimetableFilter } from '../../types/Journal/JournalTimetablesTypes';
|
||||||
import ActionButton from '../Global/ActionButton.vue';
|
import ActionButton from '../Global/ActionButton.vue';
|
||||||
import SelectBox from '../Global/SelectBox.vue';
|
import SelectBox from '../Global/SelectBox.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { SelectBox, ActionButton },
|
components: { SelectBox, ActionButton },
|
||||||
emits: ['onSorterChange', 'onInputChange', 'onFilterChange'],
|
emits: ['onSearchConfirm', 'onOptionsReset', 'onRefreshData'],
|
||||||
mixins: [imageMixin],
|
mixins: [imageMixin, keyMixin],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
sorterOptionIds: {
|
sorterOptionIds: {
|
||||||
@@ -83,178 +116,170 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
filters: {
|
filters: {
|
||||||
type: Array as PropType<JournalFilter[]>,
|
type: Array as PropType<JournalTimetableFilter[]>,
|
||||||
default: [],
|
default: [],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
dataStatus: {
|
||||||
|
type: Number as PropType<DataStatus>,
|
||||||
|
default: DataStatus.Initialized,
|
||||||
|
},
|
||||||
|
|
||||||
|
currentOptionsActive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showOptions: false,
|
||||||
|
|
||||||
|
driverSuggestions: [] as string[],
|
||||||
|
dispatcherSuggestions: [] as string[],
|
||||||
|
|
||||||
|
searchTimeout: 0,
|
||||||
|
store: useStore(),
|
||||||
|
|
||||||
|
DataStatus,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
return {
|
return {
|
||||||
searchersValues: inject('searchersValues') as { [key: string]: string },
|
searchersValues: inject('searchersValues') as { [key: string]: string },
|
||||||
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
|
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
|
||||||
journalFilterActive: inject('journalFilterActive') as JournalFilter,
|
journalFilterActive: inject('journalFilterActive') as JournalTimetableFilter,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
driverStatsName() {
|
||||||
|
return this.store.driverStatsName;
|
||||||
|
},
|
||||||
|
|
||||||
translatedSorterOptions() {
|
translatedSorterOptions() {
|
||||||
return this.$props.sorterOptionIds.map((id) => ({
|
return this.$props.sorterOptionIds.map((id) => ({
|
||||||
id,
|
id,
|
||||||
value: this.$t(`journal.option-${id}`),
|
value: this.$t(`options.sort-${id}`),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
async driverStatsName(value: string) {
|
||||||
|
await this.fetchDriverStats();
|
||||||
|
this.store.currentStatsTab = value ? 'driver' : 'daily';
|
||||||
|
},
|
||||||
|
|
||||||
|
async 'searchersValues.search-driver'(value: string | undefined) {
|
||||||
|
clearTimeout(this.searchTimeout);
|
||||||
|
|
||||||
|
if (!value || value == '') return;
|
||||||
|
if (value.length < 3) return;
|
||||||
|
|
||||||
|
this.startSearchTimeout('driver', value);
|
||||||
|
},
|
||||||
|
|
||||||
|
async 'searchersValues.search-dispatcher'(value: string | undefined) {
|
||||||
|
if (!value || value == '') return;
|
||||||
|
if (value.length < 3) return;
|
||||||
|
|
||||||
|
this.startSearchTimeout('dispatcher', value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
async fetchDriverStats() {
|
||||||
|
this.store.driverStatsData = undefined;
|
||||||
|
|
||||||
|
if (!this.store.driverStatsName) {
|
||||||
|
this.store.driverStatsStatus = DataStatus.Initialized;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.store.driverStatsStatus = DataStatus.Loading;
|
||||||
|
|
||||||
|
const statsData: DriverStatsAPIData = await (
|
||||||
|
await axios.get(`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
this.store.driverStatsData = statsData;
|
||||||
|
this.store.driverStatsStatus = DataStatus.Loaded;
|
||||||
|
} catch (error) {
|
||||||
|
this.store.driverStatsStatus = DataStatus.Error;
|
||||||
|
console.error('Ups! Wystąpił błąd przy próbie pobrania statystyk maszynisty! :/');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshData() {
|
||||||
|
this.$emit('onRefreshData');
|
||||||
|
},
|
||||||
|
|
||||||
|
startSearchTimeout(type: 'driver' | 'dispatcher', value: string) {
|
||||||
|
if (this[`${type}Suggestions`].includes(value)) return;
|
||||||
|
|
||||||
|
window.clearTimeout(this.searchTimeout);
|
||||||
|
|
||||||
|
this.searchTimeout = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const suggestions: string[] = await (
|
||||||
|
await axios.get(`${URLs.stacjownikAPI}/api/get${type}Suggestions?name=${value}`)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
this[`${type}Suggestions`] = suggestions;
|
||||||
|
} catch (error) {
|
||||||
|
this[`${type}Suggestions`] = [];
|
||||||
|
}
|
||||||
|
}, 450);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Override keyMixin function
|
||||||
|
onKeyDownFunction() {
|
||||||
|
this.showOptions = !this.showOptions;
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.showOptions) (this.$refs['button'] as HTMLButtonElement)?.focus();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
focusEnd() {
|
||||||
|
console.log('focus end');
|
||||||
|
},
|
||||||
|
|
||||||
onSorterChange(item: { id: string | number; value: string }) {
|
onSorterChange(item: { id: string | number; value: string }) {
|
||||||
this.sorterActive.id = item.id;
|
this.sorterActive.id = item.id;
|
||||||
this.sorterActive.dir = -1;
|
this.sorterActive.dir = -1;
|
||||||
|
this.$emit('onSearchConfirm');
|
||||||
this.$emit('onSorterChange');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onFilterChange(filter: JournalFilter) {
|
onFilterChange(filter: JournalTimetableFilter) {
|
||||||
this.journalFilterActive = filter;
|
this.journalFilterActive = filter;
|
||||||
this.$emit('onFilterChange');
|
this.$emit('onSearchConfirm');
|
||||||
},
|
|
||||||
|
|
||||||
onInputSearch() {
|
|
||||||
this.$emit('onInputChange');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onInputClear(id: any) {
|
onInputClear(id: any) {
|
||||||
this.searchersValues[id] = '';
|
this.searchersValues[id] = '';
|
||||||
this.onInputSearch();
|
this.$emit('onSearchConfirm');
|
||||||
|
},
|
||||||
|
|
||||||
|
onSearchConfirm() {
|
||||||
|
this.$emit('onSearchConfirm');
|
||||||
|
},
|
||||||
|
|
||||||
|
onSearchButtonConfirm() {
|
||||||
|
this.showOptions = false;
|
||||||
|
this.$emit('onSearchConfirm');
|
||||||
|
},
|
||||||
|
|
||||||
|
onResetButtonClick() {
|
||||||
|
this.$emit('onOptionsReset');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/responsive';
|
@import '../../styles/filters_options.scss';
|
||||||
@import '../../styles/option.scss';
|
|
||||||
|
|
||||||
.options {
|
|
||||||
&_wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
&_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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&_filters {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin: 0.5em 0 0 0;
|
|
||||||
|
|
||||||
.journal-filter-option {
|
|
||||||
margin: 0 0.25em 0 0;
|
|
||||||
|
|
||||||
&#abandoned {
|
|
||||||
color: salmon;
|
|
||||||
}
|
|
||||||
|
|
||||||
&#fulfilled {
|
|
||||||
color: lightgreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
&#active {
|
|
||||||
color: lightblue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<div class="journal-stats" v-show="!store.isOffline">
|
||||||
|
<div class="tabs">
|
||||||
|
<button
|
||||||
|
v-for="tab in data.tabs"
|
||||||
|
class="btn--filled"
|
||||||
|
:data-selected="tab.name == store.currentStatsTab && areStatsOpen"
|
||||||
|
:data-inactive="tab.inactive"
|
||||||
|
@click="onTabButtonClick(tab.name)"
|
||||||
|
>
|
||||||
|
{{ $t(tab.titlePath) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-tab" v-show="areStatsOpen">
|
||||||
|
<keep-alive>
|
||||||
|
<JournalDailyStats v-if="store.currentStatsTab == 'daily'" ref="dailyStatsComp" />
|
||||||
|
<JournalDriverStats v-else-if="store.currentStatsTab == 'driver'" />
|
||||||
|
</keep-alive>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, KeepAlive, onActivated, onDeactivated, reactive, Ref, ref, watch } from 'vue';
|
||||||
|
import { useStore } from '../../store/store';
|
||||||
|
import JournalDailyStats from './DailyStats.vue';
|
||||||
|
import JournalDriverStats from './JournalDriverStats.vue';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
type TStatTab = 'daily' | 'driver';
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const dailyStatsComp: Ref<InstanceType<typeof JournalDailyStats> | null> = ref(null);
|
||||||
|
|
||||||
|
const lastDailyStatsOpen = ref(false);
|
||||||
|
const areStatsOpen = ref(false);
|
||||||
|
const lastClickedTab = ref('daily');
|
||||||
|
|
||||||
|
let data = reactive({
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'daily',
|
||||||
|
titlePath: 'journal.daily-stats-title',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'driver',
|
||||||
|
titlePath: 'journal.driver-stats-title',
|
||||||
|
inactive: true,
|
||||||
|
},
|
||||||
|
] as { name: TStatTab; titlePath: string; inactive?: boolean }[],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
function onTabButtonClick(tab: TStatTab) {
|
||||||
|
if (lastClickedTab.value == tab || !areStatsOpen.value) areStatsOpen.value = !areStatsOpen.value;
|
||||||
|
|
||||||
|
if (tab == 'daily') lastDailyStatsOpen.value = areStatsOpen.value;
|
||||||
|
|
||||||
|
store.currentStatsTab = tab;
|
||||||
|
lastClickedTab.value = tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
dailyStatsComp.value?.startFetchingDailyStats();
|
||||||
|
});
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
dailyStatsComp.value?.stopFetchingDailyStats();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
computed(() => store.driverStatsData),
|
||||||
|
(statsData) => {
|
||||||
|
data.tabs[1].inactive = statsData ? false : true;
|
||||||
|
|
||||||
|
lastClickedTab.value = statsData ? 'driver' : 'daily';
|
||||||
|
if (statsData) areStatsOpen.value = true;
|
||||||
|
if (!statsData) areStatsOpen.value = lastDailyStatsOpen.value;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../styles/JournalStats.scss';
|
||||||
|
@import '../../styles/variables.scss';
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0.5em 0.75em;
|
||||||
|
|
||||||
|
&[data-inactive='true'] {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-selected='true'] {
|
||||||
|
color: $accentCol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -1,439 +0,0 @@
|
|||||||
<template>
|
|
||||||
<section class="journal-timetables">
|
|
||||||
<keep-alive>
|
|
||||||
<DriverStats v-if="statsCardOpen" @close-card="closeCard" />
|
|
||||||
</keep-alive>
|
|
||||||
|
|
||||||
<div class="journal-wrapper">
|
|
||||||
<div class="journal_top-bar">
|
|
||||||
<JournalOptions
|
|
||||||
@on-input-change="search"
|
|
||||||
@on-filter-change="search"
|
|
||||||
@on-sorter-change="search"
|
|
||||||
:sorter-option-ids="['timetableId', 'beginDate', 'distance', 'total-stops']"
|
|
||||||
:filters="journalTimetableFilters"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="journal-list">
|
|
||||||
<div class="list-wrapper" ref="scrollElement">
|
|
||||||
<transition name="warning" mode="out-in">
|
|
||||||
<div :key="historyDataStatus.status">
|
|
||||||
<Loading v-if="isDataLoading || isDataInit" />
|
|
||||||
|
|
||||||
<div v-else-if="isDataError" class="journal_warning error">
|
|
||||||
{{ $t('app.error') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="journal_warning" v-else-if="historyList.length == 0">
|
|
||||||
{{ $t('app.no-result') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul v-else>
|
|
||||||
<transition-group name="journal-list-anim">
|
|
||||||
<li v-for="(item, i) in historyList" class="journal_item" :key="item.timetableId">
|
|
||||||
<div class="journal_item-top">
|
|
||||||
<span>
|
|
||||||
<span
|
|
||||||
tabindex="0"
|
|
||||||
@click="navigateToTimetable(item)"
|
|
||||||
@keydown.enter="navigateToTimetable(item)"
|
|
||||||
style="cursor: pointer"
|
|
||||||
>
|
|
||||||
<b class="text--primary">{{ item.trainCategoryCode }} </b>
|
|
||||||
<b>{{ item.trainNo }}</b>
|
|
||||||
| <span>{{ item.driverName }}</span> |
|
|
||||||
<span class="text--grayed">#{{ item.timetableId }}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<b>{{ item.route.replace('|', ' - ') }}</b>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr style="margin: 0.25em 0" />
|
|
||||||
|
|
||||||
<div class="scenery-list">
|
|
||||||
<span
|
|
||||||
v-for="(scenery, i) in getSceneryList(item)"
|
|
||||||
:key="scenery.name"
|
|
||||||
:class="{ confirmed: scenery.confirmed }"
|
|
||||||
>
|
|
||||||
{{ i > 0 ? ' > ' : '' }} {{ scenery.name }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="schedule-dates">
|
|
||||||
<!-- Data odjazdu ze stacji początkowej -->
|
|
||||||
<b>{{ item.route.split('|')[0] }}:</b>
|
|
||||||
<s v-if="item.beginDate != item.scheduledBeginDate" class="text--grayed">
|
|
||||||
{{ localeTime(item.beginDate, $i18n.locale) }}
|
|
||||||
</s>
|
|
||||||
<span>{{ localeTime(item.scheduledBeginDate, $i18n.locale) }} </span>•
|
|
||||||
|
|
||||||
<!-- Data przyjazdu na stację końcową / porzucenia -->
|
|
||||||
<b v-if="(item.fulfilled && item.terminated) || !item.terminated">
|
|
||||||
{{ item.route.split('|').slice(-1)[0] }}:
|
|
||||||
</b>
|
|
||||||
<i v-else>{{ $t('journal.timetable-abandoned') }} </i>
|
|
||||||
|
|
||||||
<s v-if="item.endDate != item.scheduledEndDate && item.terminated" class="text--grayed">
|
|
||||||
{{ localeTime(item.fulfilled ? item.endDate : item.scheduledEndDate, $i18n.locale) }}
|
|
||||||
</s>
|
|
||||||
<span
|
|
||||||
>{{ localeTime(item.fulfilled ? item.scheduledEndDate : item.endDate, $i18n.locale) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<b
|
|
||||||
class="journal_item-status"
|
|
||||||
:class="{
|
|
||||||
fulfilled: item.fulfilled || item.currentDistance >= item.routeDistance * 0.9,
|
|
||||||
terminated: item.terminated && !item.fulfilled,
|
|
||||||
active: !item.terminated,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
!item.terminated
|
|
||||||
? $t('journal.timetable-active')
|
|
||||||
: item.fulfilled || item.currentDistance >= item.routeDistance * 0.9
|
|
||||||
? $t('journal.timetable-fulfilled')
|
|
||||||
: $t('journal.timetable-abandoned')
|
|
||||||
}}
|
|
||||||
</b>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-top: 1em">
|
|
||||||
<div>
|
|
||||||
{{ $t('journal.timetable-day') }} <b>{{ localeDay(item.beginDate, $i18n.locale) }}</b>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Nick dyżurnego -->
|
|
||||||
<div v-if="item.authorName">
|
|
||||||
<b class="text--grayed">{{ $t('journal.dispatcher-name') }} </b>
|
|
||||||
<router-link
|
|
||||||
class="dispatcher-link"
|
|
||||||
:to="`/journal/dispatchers?dispatcherName=${item.authorName}`"
|
|
||||||
>{{ item.authorName }}</router-link
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-top: 1em">
|
|
||||||
<div>
|
|
||||||
<b>{{ $t('journal.route-length') }}</b>
|
|
||||||
{{ !item.fulfilled ? item.currentDistance + ' /' : '' }}
|
|
||||||
{{ item.routeDistance }} km
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<b>{{ $t('journal.station-count') }}</b>
|
|
||||||
{{ item.confirmedStopsCount }} /
|
|
||||||
{{ item.allStopsCount }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</transition-group>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
|
|
||||||
<div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, JournalFilter, provide, reactive, Ref, ref } from 'vue';
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
import DriverStats from './DriverStats.vue';
|
|
||||||
import Loading from '../Global/Loading.vue';
|
|
||||||
import { journalTimetableFilters } from '../../data/journalFilters';
|
|
||||||
import dateMixin from '../../mixins/dateMixin';
|
|
||||||
import routerMixin from '../../mixins/routerMixin';
|
|
||||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
|
||||||
import { JournalFilterType } from '../../scripts/enums/JournalFilterType';
|
|
||||||
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
|
|
||||||
import { URLs } from '../../scripts/utils/apiURLs';
|
|
||||||
import { useStore } from '../../store/store';
|
|
||||||
import JournalOptions from './JournalOptions.vue';
|
|
||||||
|
|
||||||
const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`;
|
|
||||||
|
|
||||||
type JournalTimetableSearcher = {
|
|
||||||
[key in 'search-driver' | 'search-train']: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: { DriverStats, Loading, JournalOptions },
|
|
||||||
mixins: [dateMixin, routerMixin],
|
|
||||||
|
|
||||||
name: 'JournalTimetables',
|
|
||||||
|
|
||||||
props: {
|
|
||||||
timetableId: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data: () => ({
|
|
||||||
currentQuery: '',
|
|
||||||
scrollDataLoaded: true,
|
|
||||||
scrollNoMoreData: false,
|
|
||||||
|
|
||||||
showReturnButton: false,
|
|
||||||
statsCardOpen: false,
|
|
||||||
|
|
||||||
journalTimetableFilters,
|
|
||||||
}),
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
const historyDataStatus: Ref<{ status: DataStatus; error: string | null }> = ref({
|
|
||||||
status: DataStatus.Loading,
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const sorterActive = ref({ id: 'timetableId', dir: -1 });
|
|
||||||
const journalFilterActive = ref(journalTimetableFilters[0]);
|
|
||||||
|
|
||||||
const searchersValues = reactive({
|
|
||||||
'search-train': '',
|
|
||||||
'search-driver': '',
|
|
||||||
} as JournalTimetableSearcher);
|
|
||||||
|
|
||||||
const countFromIndex = ref(0);
|
|
||||||
const countLimit = 15;
|
|
||||||
|
|
||||||
provide('searchersValues', searchersValues);
|
|
||||||
provide('sorterActive', sorterActive);
|
|
||||||
provide('journalFilterActive', journalFilterActive);
|
|
||||||
|
|
||||||
const scrollElement: Ref<HTMLElement | null> = ref(null);
|
|
||||||
|
|
||||||
return {
|
|
||||||
historyList: ref([]) as Ref<TimetableHistory[]>,
|
|
||||||
historyDataStatus,
|
|
||||||
|
|
||||||
isDataLoading: computed(() => historyDataStatus.value.status === DataStatus.Loading),
|
|
||||||
isDataError: computed(() => historyDataStatus.value.status === DataStatus.Error),
|
|
||||||
isDataInit: computed(() => historyDataStatus.value.status === DataStatus.Initialized),
|
|
||||||
|
|
||||||
sorterActive,
|
|
||||||
journalFilterActive,
|
|
||||||
searchersValues,
|
|
||||||
|
|
||||||
countFromIndex,
|
|
||||||
countLimit,
|
|
||||||
|
|
||||||
scrollElement,
|
|
||||||
maxCount: ref(15),
|
|
||||||
store: useStore(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
activated() {
|
|
||||||
window.addEventListener('scroll', this.handleScroll);
|
|
||||||
|
|
||||||
if (this.timetableId) {
|
|
||||||
this.searchersValues['search-train'] = `#${this.timetableId}`;
|
|
||||||
this.search();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
if (!this.timetableId) this.search();
|
|
||||||
},
|
|
||||||
|
|
||||||
deactivated() {
|
|
||||||
window.removeEventListener('scroll', this.handleScroll);
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
navigateToTimetable(historyItem: TimetableHistory) {
|
|
||||||
if (historyItem.terminated) return;
|
|
||||||
|
|
||||||
this.navigateTo('/trains', {
|
|
||||||
trainNo: historyItem.trainNo,
|
|
||||||
driverName: historyItem.driverName,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
closeCard() {
|
|
||||||
this.statsCardOpen = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
getSceneryList(historyItem: TimetableHistory) {
|
|
||||||
return historyItem.sceneriesString
|
|
||||||
.split('%')
|
|
||||||
.map((name, i) => ({ name, confirmed: i < historyItem.confirmedStopsCount }));
|
|
||||||
},
|
|
||||||
|
|
||||||
handleScroll() {
|
|
||||||
this.showReturnButton = window.scrollY > window.innerHeight;
|
|
||||||
|
|
||||||
const element = this.$refs.scrollElement as HTMLElement;
|
|
||||||
|
|
||||||
if (
|
|
||||||
element.getBoundingClientRect().bottom * 0.85 < window.innerHeight &&
|
|
||||||
this.scrollDataLoaded &&
|
|
||||||
!this.scrollNoMoreData &&
|
|
||||||
this.historyDataStatus.status == DataStatus.Loaded
|
|
||||||
)
|
|
||||||
this.addHistoryData();
|
|
||||||
},
|
|
||||||
|
|
||||||
scrollToTop() {
|
|
||||||
window.scrollTo({ top: 0 });
|
|
||||||
},
|
|
||||||
|
|
||||||
search() {
|
|
||||||
this.fetchHistoryData({
|
|
||||||
searchers: this.searchersValues,
|
|
||||||
filter: this.journalFilterActive,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.scrollNoMoreData = false;
|
|
||||||
this.scrollDataLoaded = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
async addHistoryData() {
|
|
||||||
this.scrollDataLoaded = false;
|
|
||||||
|
|
||||||
const countFrom = this.historyList.length;
|
|
||||||
|
|
||||||
const responseData: TimetableHistory[] = await (
|
|
||||||
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
|
|
||||||
).data;
|
|
||||||
|
|
||||||
if (!responseData) return;
|
|
||||||
|
|
||||||
if (responseData.length == 0) {
|
|
||||||
this.scrollNoMoreData = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.historyList.push(...responseData);
|
|
||||||
this.scrollDataLoaded = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchHistoryData(
|
|
||||||
props: {
|
|
||||||
searchers?: JournalTimetableSearcher;
|
|
||||||
filter?: JournalFilter;
|
|
||||||
} = {}
|
|
||||||
) {
|
|
||||||
this.historyDataStatus.status = DataStatus.Loading;
|
|
||||||
|
|
||||||
const queries: string[] = [];
|
|
||||||
|
|
||||||
const driver = props.searchers?.['search-driver'].trim();
|
|
||||||
const train = props.searchers?.['search-train'].trim();
|
|
||||||
|
|
||||||
if (driver) queries.push(`driverName=${driver}`);
|
|
||||||
if (train) queries.push(train.startsWith('#') ? `timetableId=${train.replace('#', '')}` : `trainNo=${train}`);
|
|
||||||
|
|
||||||
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
|
|
||||||
if (this.sorterActive.id == 'distance') queries.push('sortBy=routeDistance');
|
|
||||||
else if (this.sorterActive.id == 'total-stops') queries.push('sortBy=allStopsCount');
|
|
||||||
else if (this.sorterActive.id == 'beginDate') queries.push('sortBy=beginDate');
|
|
||||||
else queries.push('sortBy=timetableId');
|
|
||||||
|
|
||||||
queries.push('countLimit=15');
|
|
||||||
|
|
||||||
switch (props.filter?.id) {
|
|
||||||
case JournalFilterType.abandoned:
|
|
||||||
queries.push('fulfilled=0', 'terminated=1');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case JournalFilterType.active:
|
|
||||||
queries.push('terminated=0');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case JournalFilterType.fulfilled:
|
|
||||||
queries.push('fulfilled=1');
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentQuery = queries.join('&');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const responseData: TimetableHistory[] = await (
|
|
||||||
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}`)
|
|
||||||
).data;
|
|
||||||
|
|
||||||
if (!responseData) {
|
|
||||||
this.historyDataStatus.status = DataStatus.Error;
|
|
||||||
this.historyDataStatus.error = 'Brak danych!';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!responseData) return;
|
|
||||||
|
|
||||||
// Response data exists
|
|
||||||
this.historyList = responseData;
|
|
||||||
|
|
||||||
// Stats display
|
|
||||||
this.store.driverStatsName =
|
|
||||||
this.historyList.length > 0 && this.searchersValues['search-driver'].trim()
|
|
||||||
? this.historyList[0].driverName
|
|
||||||
: '';
|
|
||||||
|
|
||||||
this.historyDataStatus.status = DataStatus.Loaded;
|
|
||||||
} catch (error) {
|
|
||||||
this.historyDataStatus.status = DataStatus.Error;
|
|
||||||
this.historyDataStatus.error = 'Ups! Coś poszło nie tak!';
|
|
||||||
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '../../styles/JournalSection.scss';
|
|
||||||
|
|
||||||
.journal_item {
|
|
||||||
&-top {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
padding: 0.2em 0;
|
|
||||||
|
|
||||||
.scenery-list {
|
|
||||||
span {
|
|
||||||
color: #adadad;
|
|
||||||
|
|
||||||
&.confirmed {
|
|
||||||
color: #a3eba3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-status {
|
|
||||||
&.terminated {
|
|
||||||
color: salmon;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.fulfilled {
|
|
||||||
color: lightgreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
color: lightblue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dispatcher-link {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
<template>
|
||||||
|
<transition-group class="journal-list" tag="ul" name="list-anim">
|
||||||
|
<li
|
||||||
|
v-for="{ timetable, sceneryList, ...item } in computedTimetableHistory"
|
||||||
|
class="journal_item"
|
||||||
|
:key="timetable.id"
|
||||||
|
>
|
||||||
|
<div class="journal_item-info">
|
||||||
|
<div class="info-general">
|
||||||
|
<span
|
||||||
|
class="general-train"
|
||||||
|
tabindex="0"
|
||||||
|
@click="showTimetable(timetable)"
|
||||||
|
@keydown.enter="showTimetable(timetable)"
|
||||||
|
style="cursor: pointer"
|
||||||
|
>
|
||||||
|
<span class="text--grayed">#{{ timetable.id }}</span>
|
||||||
|
<span>
|
||||||
|
<strong class="text--primary">
|
||||||
|
{{ timetable.trainCategoryCode }}
|
||||||
|
</strong>
|
||||||
|
<strong> {{ timetable.trainNo }}</strong>
|
||||||
|
</span>
|
||||||
|
•
|
||||||
|
<strong
|
||||||
|
v-if="timetable.driverLevel !== null"
|
||||||
|
class="level-badge driver"
|
||||||
|
:style="calculateExpStyle(timetable.driverLevel, timetable.driverIsSupporter)"
|
||||||
|
>
|
||||||
|
{{ timetable.driverLevel < 2 ? 'L' : `${timetable.driverLevel}` }}
|
||||||
|
</strong>
|
||||||
|
|
||||||
|
<strong>{{ timetable.driverName }}</strong>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="general-time">
|
||||||
|
<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"> ></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>
|
||||||
|
•
|
||||||
|
<span>
|
||||||
|
<b>{{ $t('journal.station-count') }}</b>
|
||||||
|
{{ timetable.confirmedStopsCount }} /
|
||||||
|
{{ timetable.allStopsCount }}
|
||||||
|
</span>
|
||||||
|
<span class="text--grayed" v-if="!timetable.fulfilled && timetable.currentSceneryName">
|
||||||
|
•
|
||||||
|
<b>
|
||||||
|
{{ $t(`journal.${timetable.terminated ? 'last-seen-at' : 'currently-at'}`) }}
|
||||||
|
{{ timetable.currentSceneryName.replace(/.[a-zA-Z0-9]+.sc/, '') }}
|
||||||
|
</b>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- Nick dyżurnego -->
|
||||||
|
<div v-if="timetable.authorName">
|
||||||
|
<b class="text--grayed">{{ $t('journal.dispatcher-name') }} </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>
|
||||||
|
</transition-group>
|
||||||
|
</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 styleMixin from '../../mixins/styleMixin';
|
||||||
|
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
timetableHistory: {
|
||||||
|
type: Array as PropType<TimetableHistory[]>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [dateMixin, imageMixin, modalTrainMixin, styleMixin],
|
||||||
|
|
||||||
|
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) return;
|
||||||
|
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/animations.scss';
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-general {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
gap: 0.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-route {
|
||||||
|
margin: 0.25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-extended {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-train {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
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-general {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.info-extended {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-route {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--show {
|
||||||
|
margin: 1em auto 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,25 +5,31 @@
|
|||||||
<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="item in dispatcherHistoryList">
|
||||||
<div>
|
<router-link class="item-general" :to="`/journal/dispatchers?dispatcherName=${item.dispatcherName}`">
|
||||||
<router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`">
|
<span class="text--grayed">#{{ item.stationHash }} </span>
|
||||||
<span class="text--grayed">#{{ historyItem.stationHash }} </span>
|
<b
|
||||||
<b>{{ historyItem.dispatcherName }}</b>
|
v-if="item.dispatcherLevel !== null"
|
||||||
|
class="level-badge dispatcher"
|
||||||
|
:style="calculateExpStyle(item.dispatcherLevel, item.dispatcherIsSupporter)"
|
||||||
|
>
|
||||||
|
{{ item.dispatcherLevel >= 2 ? item.dispatcherLevel : 'L' }}
|
||||||
|
</b>
|
||||||
|
|
||||||
|
<b>{{ item.dispatcherName }}</b>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="historyItem.timestampTo">
|
<div v-if="item.timestampTo">
|
||||||
<b>{{ $d(historyItem.timestampFrom) }}</b>
|
<b>{{ $d(item.timestampFrom) }}</b>
|
||||||
|
|
||||||
{{ timestampToString(historyItem.timestampFrom) }}
|
{{ timestampToString(item.timestampFrom) }}
|
||||||
- {{ timestampToString(historyItem.timestampTo) }} ({{ calculateDuration(historyItem.currentDuration) }})
|
- {{ timestampToString(item.timestampTo) }} ({{ calculateDuration(item.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(item.timestampFrom) }}</b>
|
||||||
({{ calculateDuration(historyItem.currentDuration) }})
|
({{ calculateDuration(item.currentDuration) }})
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -31,7 +37,6 @@
|
|||||||
</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';
|
||||||
@@ -40,10 +45,11 @@ import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIDa
|
|||||||
import Station from '../../scripts/interfaces/Station';
|
import Station from '../../scripts/interfaces/Station';
|
||||||
import { URLs } from '../../scripts/utils/apiURLs';
|
import { URLs } from '../../scripts/utils/apiURLs';
|
||||||
import Loading from '../Global/Loading.vue';
|
import Loading from '../Global/Loading.vue';
|
||||||
|
import styleMixin from '../../mixins/styleMixin';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'SceneryDispatchersHistory',
|
name: 'SceneryDispatchersHistory',
|
||||||
mixins: [dateMixin],
|
mixins: [dateMixin, styleMixin],
|
||||||
props: {
|
props: {
|
||||||
station: {
|
station: {
|
||||||
type: Object as PropType<Station>,
|
type: Object as PropType<Station>,
|
||||||
@@ -56,7 +62,7 @@ export default defineComponent({
|
|||||||
dataStatus: DataStatus.Loading,
|
dataStatus: DataStatus.Loading,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
activated() {
|
||||||
this.fetchAPIData();
|
this.fetchAPIData();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -67,8 +73,6 @@ export default defineComponent({
|
|||||||
|
|
||||||
this.dispatcherHistoryList = historyAPIData;
|
this.dispatcherHistoryList = historyAPIData;
|
||||||
this.dataStatus = DataStatus.Loaded;
|
this.dataStatus = DataStatus.Loaded;
|
||||||
|
|
||||||
console.log(this.dispatcherHistoryList);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
@@ -99,13 +103,20 @@ export default defineComponent({
|
|||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item-general {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
.dispatcher-online {
|
.dispatcher-online {
|
||||||
color: springgreen;
|
color: springgreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include smallScreen {
|
@include smallScreen {
|
||||||
.history-list {
|
.history-list {
|
||||||
font-size: 1.2em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
.list-item {
|
.list-item {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -113,4 +124,3 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -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" target="_blank">
|
||||||
{{ 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,15 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="scenery-info">
|
<div class="scenery-info">
|
||||||
<section v-if="!timetableOnly">
|
<section v-if="!timetableOnly">
|
||||||
<div class="info-general" v-if="station.generalInfo">
|
<div class="scenery-info-general" v-if="station.generalInfo">
|
||||||
<scenery-info-icons :station="station" />
|
<scenery-info-icons :station="station" />
|
||||||
|
|
||||||
<div class="general-list">
|
<div class="scenery-general-list">
|
||||||
<span>
|
<span>
|
||||||
<b>{{ $t('availability.title') }}:</b> {{ $t(`availability.${station.generalInfo.availability}`) }}
|
<b>{{ $t('availability.title') }}:</b> {{ $t(`availability.${station.generalInfo.availability}`) }}
|
||||||
|
|
||||||
<span v-if="station.generalInfo.reqLevel > -1">
|
<span v-if="station.generalInfo.reqLevel > -1">
|
||||||
- {{ $tc('scenery.req-level', station.generalInfo.reqLevel, { lvl: station.generalInfo.reqLevel }) }}
|
- {{ $t('scenery.req-level', { lvl: station.generalInfo.reqLevel }, station.generalInfo.reqLevel) }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -26,26 +26,32 @@
|
|||||||
</span>
|
</span>
|
||||||
<span v-if="station.generalInfo.project">
|
<span v-if="station.generalInfo.project">
|
||||||
• <b>{{ $t('scenery.project-title') }}: </b>
|
• <b>{{ $t('scenery.project-title') }}: </b>
|
||||||
<b style="color: salmon">{{ station.generalInfo.project }}</b>
|
<a
|
||||||
|
style="color: salmon; text-decoration: underline; font-weight: bold"
|
||||||
|
:href="station.generalInfo.projectUrl"
|
||||||
|
target="_blank"
|
||||||
|
>{{ station.generalInfo.project }}</a
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<scenery-info-routes :station="station" />
|
<scenery-info-routes :station="station" />
|
||||||
|
|
||||||
<div class="scenery-authors" v-if="station.generalInfo.authors && station.generalInfo.authors.length > 0">
|
<div class="scenery-authors" v-if="station.generalInfo.authors && station.generalInfo.authors.length > 0">
|
||||||
<b> {{ $tc('scenery.authors-title', station.generalInfo.authors.length) }}: </b>
|
<b>
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
'scenery.authors-title',
|
||||||
|
{ authors: station.generalInfo.authors.length },
|
||||||
|
station.generalInfo.authors.length
|
||||||
|
)
|
||||||
|
}}:
|
||||||
|
</b>
|
||||||
{{ station.generalInfo.authors.join(', ') }}
|
{{ station.generalInfo.authors.join(', ') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
|
||||||
<div class="scenery-topic" v-if="station.generalInfo.url">
|
|
||||||
<a :href="station.generalInfo.url" target="_blank">
|
|
||||||
> {{ $t('scenery.forum-topic', { name: station.name }) }} <
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin: 2em 0; height: 2px; background-color: white" />
|
<div style="margin: 2em 0; height: 2px; background-color: white"></div>
|
||||||
|
|
||||||
<!-- info dispatcher -->
|
<!-- info dispatcher -->
|
||||||
<scenery-info-dispatcher :station="station" :onlineFrom="onlineFrom" />
|
<scenery-info-dispatcher :station="station" :onlineFrom="onlineFrom" />
|
||||||
@@ -72,7 +78,6 @@ import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue';
|
|||||||
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
|
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
|
||||||
import Station from '../../scripts/interfaces/Station';
|
import Station from '../../scripts/interfaces/Station';
|
||||||
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
SceneryInfoDispatcher,
|
SceneryInfoDispatcher,
|
||||||
@@ -109,7 +114,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;
|
||||||
@@ -125,12 +130,11 @@ h3.section-header {
|
|||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-general {
|
.scenery-info-general {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.general-list {
|
.scenery-general-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="info-dispatcher">
|
<section class="info-dispatcher">
|
||||||
<div class="dispatcher" v-if="station.onlineInfo">
|
<div class="dispatcher" v-if="station.onlineInfo">
|
||||||
<span class="dispatcher_level" :style="calculateExpStyle(station.onlineInfo.dispatcherExp)">
|
<span
|
||||||
|
class="dispatcher_level"
|
||||||
|
:style="calculateExpStyle(station.onlineInfo.dispatcherExp, station.onlineInfo.dispatcherIsSupporter)"
|
||||||
|
>
|
||||||
{{ station.onlineInfo.dispatcherExp > 1 ? station.onlineInfo.dispatcherExp : 'L' }}
|
{{ station.onlineInfo.dispatcherExp > 1 ? station.onlineInfo.dispatcherExp : 'L' }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -64,6 +67,7 @@ export default defineComponent({
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
.dispatcher {
|
.dispatcher {
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
@@ -82,17 +86,15 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
&_name {
|
&_name {
|
||||||
margin-right: 0.4em;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
margin-right: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&_likes {
|
&_likes {
|
||||||
img {
|
img {
|
||||||
height: 0.7em;
|
height: 0.7em;
|
||||||
margin-right: 0.25em;
|
margin: 0 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
margin-right: 1.5em;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
<img
|
<img
|
||||||
v-if="!station.generalInfo"
|
v-if="!station.generalInfo"
|
||||||
class="icon-info"
|
class="icon-info"
|
||||||
:src="getImage('unknown.png')"
|
:src="getIcon('unknown')"
|
||||||
alt="icon-unknown"
|
alt="icon-unknown"
|
||||||
:title="$t('desc.unknown')"
|
:title="$t('desc.unknown')"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,12 +4,10 @@
|
|||||||
<b>{{ $t('scenery.one-way-routes') }}</b>
|
<b>{{ $t('scenery.one-way-routes') }}</b>
|
||||||
|
|
||||||
<ul class="routes-list">
|
<ul class="routes-list">
|
||||||
<li
|
<li v-for="route in station.generalInfo.routes.oneWay">
|
||||||
v-for="route in station.generalInfo.routes.oneWay"
|
<span :class="{ 'no-catenary': !route.catenary, internal: route.isInternal }"> {{ route.name }}</span>
|
||||||
:class="{ 'no-catenary': !route.catenary, internal: route.isInternal }"
|
<span v-if="route.speed" class="speed">{{ route.speed }}</span>
|
||||||
>
|
<span v-if="route.SBL" class="sbl">SBL</span>
|
||||||
{{ route.name }}
|
|
||||||
<b v-if="route.SBL">SBL</b>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,41 +16,13 @@
|
|||||||
<b>{{ $t('scenery.two-way-routes') }}</b>
|
<b>{{ $t('scenery.two-way-routes') }}</b>
|
||||||
|
|
||||||
<ul class="routes-list">
|
<ul class="routes-list">
|
||||||
<li
|
<li v-for="route in station.generalInfo.routes.twoWay">
|
||||||
v-for="route in station.generalInfo.routes.twoWay"
|
<span :class="{ 'no-catenary': !route.catenary, internal: route.isInternal }">{{ route.name }}</span>
|
||||||
:class="{ 'no-catenary': !route.catenary, internal: route.isInternal }"
|
<span v-if="route.speed" class="speed">{{ route.speed }}</span>
|
||||||
>
|
<span v-if="route.SBL" class="sbl">SBL</span>
|
||||||
{{ route.name }} <b v-if="route.SBL">SBL</b>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div
|
|
||||||
class="route-info"
|
|
||||||
:class="{ 'no-catenary': !route.catenary, internal: route.isInternal }"
|
|
||||||
v-for="route in [...station.generalInfo.routes.oneWay, ...station.generalInfo.routes.twoWay].filter(
|
|
||||||
(route) => route.name != '-'
|
|
||||||
)"
|
|
||||||
:key="route.name"
|
|
||||||
:title="`Szlak ${route.name}: ${route.isInternal ? 'wewnętrzny' : 'zewnętrzny'}, ${
|
|
||||||
route.tracks == 2 ? 'dwutorowy' : 'jednotorowy'
|
|
||||||
}, ${route.catenary ? 'zelektryfikowany' : 'niezelektryfikowany'} z ${route.SBL ? 'SBL' : 'PBL'} ${
|
|
||||||
route.TWB ? 'i blokadą dwukierunkową' : ''
|
|
||||||
}`"
|
|
||||||
> -->
|
|
||||||
<!-- <span class="track-name">
|
|
||||||
<b>{{ route.name }}</b>
|
|
||||||
</span> -->
|
|
||||||
<!--
|
|
||||||
<span class="track-specs">
|
|
||||||
{{ route.tracks }}tor
|
|
||||||
<img v-if="route.catenary" :src="icons.trackCatenary" alt="icon track catenary" />
|
|
||||||
<img v-else :src="icons.trackNoCatenary" alt="icon track no catenary" />
|
|
||||||
|
|
||||||
<img v-if="route.TWB" :src="icons.trackTWB" alt="icon track twb" />
|
|
||||||
<img v-if="route.SBL" :src="icons.trackSBL" alt="icon track sbl" />
|
|
||||||
</span> -->
|
|
||||||
<!-- </div> -->
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -91,12 +61,16 @@ export default defineComponent({
|
|||||||
ul.routes-list {
|
ul.routes-list {
|
||||||
margin: 0.45em 0.25em;
|
margin: 0.45em 0.25em;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
background-color: #007599;
|
margin: 0.5em 0.25em;
|
||||||
|
|
||||||
|
span {
|
||||||
padding: 0.2em 0.25em;
|
padding: 0.2em 0.25em;
|
||||||
margin-left: 0.25em;
|
background-color: #007599;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
&.no-catenary {
|
&.no-catenary {
|
||||||
background-color: #686868;
|
background-color: #686868;
|
||||||
@@ -106,8 +80,28 @@ ul.routes-list {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
b {
|
&.speed {
|
||||||
|
background-color: #404040;
|
||||||
|
color: #cfcfcf;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sbl {
|
||||||
color: var(--clr-primary);
|
color: var(--clr-primary);
|
||||||
|
background-color: #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-radius: 0 0.5em 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-radius: 0.5em 0 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:only-child {
|
||||||
|
border-radius: 0.5em;
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,21 +13,22 @@
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="timetable-checkpoints" v-if="station && station.generalInfo?.checkpoints">
|
<div class="timetable-checkpoints" v-if="station && station.generalInfo?.checkpoints">
|
||||||
|
<span v-for="(cp, i) in station.generalInfo.checkpoints" :key="i">
|
||||||
|
{{ (i > 0 && '•') || '' }}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-for="cp in station.generalInfo.checkpoints"
|
|
||||||
:key="cp.checkpointName"
|
:key="cp.checkpointName"
|
||||||
class="checkpoint_item btn btn--text"
|
class="checkpoint_item"
|
||||||
:class="{ current: selectedCheckpoint === cp.checkpointName }"
|
:class="{ current: selectedCheckpoint === cp.checkpointName }"
|
||||||
@click="selectCheckpoint(cp)"
|
@click="selectCheckpoint(cp)"
|
||||||
>
|
>
|
||||||
{{ cp.checkpointName }}
|
{{ cp.checkpointName }}
|
||||||
</button>
|
</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="timetable-list">
|
<div class="timetable-list">
|
||||||
<!-- <transition name="scenery-timetable-list-anim" mode="out-in"> -->
|
|
||||||
<!-- <div :key="store.dataStatuses.trains + selectedCheckpoint" class="scenery-timetable-list"> -->
|
|
||||||
<div style="padding-bottom: 5em" v-if="store.dataStatuses.trains == 0 && computedScheduledTrains.length == 0">
|
<div style="padding-bottom: 5em" v-if="store.dataStatuses.trains == 0 && computedScheduledTrains.length == 0">
|
||||||
<Loading />
|
<Loading />
|
||||||
</div>
|
</div>
|
||||||
@@ -40,10 +41,11 @@
|
|||||||
{{ $t('scenery.no-timetables') }}
|
{{ $t('scenery.no-timetables') }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<transition-group name="list-anim">
|
||||||
<div
|
<div
|
||||||
class="timetable-item"
|
class="timetable-item"
|
||||||
v-for="(scheduledTrain, i) in computedScheduledTrains"
|
v-for="(scheduledTrain, i) in computedScheduledTrains"
|
||||||
:key="i + 1"
|
:key="scheduledTrain.trainId"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId)"
|
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId)"
|
||||||
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId)"
|
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId)"
|
||||||
@@ -60,23 +62,15 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
|
||||||
<span style="color: white">
|
<span>
|
||||||
{{ scheduledTrain.driverName }}
|
{{ scheduledTrain.driverName }}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
<span class="general-status">
|
|
||||||
<span :class="scheduledTrain.stopStatus">
|
|
||||||
{{ $t(`timetables.${scheduledTrain.stopStatus}`) }}
|
|
||||||
<span v-if="scheduledTrain.stopStatus == 'arriving'"> {{ scheduledTrain.prevStationName }}</span>
|
|
||||||
<span v-if="scheduledTrain.stopStatus.startsWith('departed')">{{
|
|
||||||
scheduledTrain.nextStationName
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div class="info-route">
|
<div class="info-route">
|
||||||
<strong>{{ scheduledTrain.beginsAt }} - {{ scheduledTrain.terminatesAt }}</strong>
|
<strong>{{ scheduledTrain.beginsAt }} - {{ scheduledTrain.terminatesAt }}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ScheduledTrainStatus :scheduledTrain="scheduledTrain" />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -99,7 +93,8 @@
|
|||||||
|
|
||||||
<span>
|
<span>
|
||||||
{{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }}
|
{{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }}
|
||||||
({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : '' }}{{ scheduledTrain.stopInfo.arrivalDelay }})
|
({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : ''
|
||||||
|
}}{{ scheduledTrain.stopInfo.arrivalDelay }})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
@@ -118,11 +113,15 @@
|
|||||||
<span class="arrow"></span>
|
<span class="arrow"></span>
|
||||||
|
|
||||||
<span class="stop-line">
|
<span class="stop-line">
|
||||||
|
<span>
|
||||||
{{ scheduledTrain.arrivingLine }}
|
{{ scheduledTrain.arrivingLine }}
|
||||||
{{ scheduledTrain.arrivingLine && scheduledTrain.departureLine && '>' }}
|
</span>
|
||||||
|
<span></span>
|
||||||
|
<span>
|
||||||
{{ scheduledTrain.departureLine }}
|
{{ scheduledTrain.departureLine }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
<span class="schedule-departure">
|
<span class="schedule-departure">
|
||||||
<span class="departure-time terminates" v-if="scheduledTrain.stopInfo.terminatesHere">
|
<span class="departure-time terminates" v-if="scheduledTrain.stopInfo.terminatesHere">
|
||||||
@@ -150,9 +149,8 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- </transition> -->
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -169,11 +167,13 @@ import Station from '../../scripts/interfaces/Station';
|
|||||||
import { useStore } from '../../store/store';
|
import { useStore } from '../../store/store';
|
||||||
import imageMixin from '../../mixins/imageMixin';
|
import imageMixin from '../../mixins/imageMixin';
|
||||||
import modalTrainMixin from '../../mixins/modalTrainMixin';
|
import modalTrainMixin from '../../mixins/modalTrainMixin';
|
||||||
|
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
|
||||||
|
import ScheduledTrain from '../../scripts/interfaces/ScheduledTrain';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'SceneryTimetable',
|
name: 'SceneryTimetable',
|
||||||
|
|
||||||
components: { SelectBox, Loading, TrainModal },
|
components: { SelectBox, Loading, TrainModal, ScheduledTrainStatus },
|
||||||
|
|
||||||
mixins: [dateMixin, routerMixin, imageMixin, modalTrainMixin],
|
mixins: [dateMixin, routerMixin, imageMixin, modalTrainMixin],
|
||||||
|
|
||||||
@@ -182,11 +182,14 @@ export default defineComponent({
|
|||||||
type: Object as PropType<Station>,
|
type: Object as PropType<Station>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
timetableOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
listOpen: false,
|
listOpen: false,
|
||||||
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setup(props) {
|
setup(props) {
|
||||||
@@ -250,6 +253,10 @@ export default defineComponent({
|
|||||||
selectCheckpoint(cp: { checkpointName: string }) {
|
selectCheckpoint(cp: { checkpointName: string }) {
|
||||||
this.selectedCheckpoint = cp.checkpointName;
|
this.selectedCheckpoint = cp.checkpointName;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showTimetableOnlyView() {
|
||||||
|
this.$router.push(`${this.$route.fullPath}&timetableOnly=1`);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -265,12 +272,7 @@ export default defineComponent({
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/responsive.scss';
|
@import '../../styles/responsive.scss';
|
||||||
@import '../../styles/variables.scss';
|
@import '../../styles/variables.scss';
|
||||||
|
@import '../../styles/animations.scss';
|
||||||
// .scenery-timetable {
|
|
||||||
// height: 85vh;
|
|
||||||
// max-height: 900px;
|
|
||||||
// min-height: 450px;
|
|
||||||
// }
|
|
||||||
|
|
||||||
.scenery-timetable {
|
.scenery-timetable {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -293,7 +295,7 @@ export default defineComponent({
|
|||||||
h3 {
|
h3 {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 1.4em;
|
font-size: 1.3em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,12 +306,14 @@ export default defineComponent({
|
|||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
margin: 0.5em auto;
|
margin: 0.5em auto;
|
||||||
padding: 0 0.5em;
|
padding: 0.5em;
|
||||||
max-width: 1100px;
|
max-width: 1100px;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
gap: 0 0.5em;
|
gap: 2em 0.5em;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
background: #353535;
|
background: #353535;
|
||||||
|
|
||||||
@@ -324,9 +328,6 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-general {
|
&-general {
|
||||||
padding: 0.5rem 0;
|
|
||||||
border-radius: 10px;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -337,6 +338,10 @@ export default defineComponent({
|
|||||||
&-schedule {
|
&-schedule {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(30px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(30px, 1fr));
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,17 +356,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 {
|
button.checkpoint_item {
|
||||||
font-weight: bold;
|
color: #aaa;
|
||||||
color: $accentCol;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:last-child)::after {
|
.checkpoint_item.current {
|
||||||
margin: 0 0.5em;
|
font-weight: bold;
|
||||||
content: '•';
|
color: $accentCol;
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,7 +404,6 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.info-route {
|
.info-route {
|
||||||
margin-top: 0.5em;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,38 +419,6 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.general-status {
|
|
||||||
text-align: right;
|
|
||||||
|
|
||||||
span.arriving {
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.departed {
|
|
||||||
color: lime;
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
&-away {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #5ecc5e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span.stopped {
|
|
||||||
color: #ffa600;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.online {
|
|
||||||
color: gold;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.terminated {
|
|
||||||
color: salmon;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule {
|
.schedule {
|
||||||
&-arrival,
|
&-arrival,
|
||||||
&-stop,
|
&-stop,
|
||||||
@@ -458,23 +428,40 @@ export default defineComponent({
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
margin: 0 0.3rem;
|
margin: 0 0.3rem;
|
||||||
font-size: 1.1em;
|
font-size: 1.15em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-stop {
|
&-stop {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
font-size: 0.85em;
|
font-size: 0.9em;
|
||||||
|
|
||||||
padding: 0.3em 0;
|
padding: 0.3em 0;
|
||||||
|
|
||||||
.stop-line {
|
.stop-line {
|
||||||
margin-top: 0.25em;
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
span {
|
||||||
|
width: 65px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
span:first-child {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
span:last-child {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.stop-time {
|
.stop-time {
|
||||||
transform: translateY(-0.25em);
|
position: absolute;
|
||||||
|
transform: translateY(-15px);
|
||||||
|
|
||||||
|
color: $accentCol;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -484,38 +471,9 @@ export default defineComponent({
|
|||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scenery-timetable-list-anim {
|
@include smallScreen {
|
||||||
&-enter-from,
|
.timetable-item {
|
||||||
&-leave-to {
|
grid-template-columns: 1fr;
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-enter-active {
|
|
||||||
transition: all 100ms ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-leave-active {
|
|
||||||
transition: all 100ms ease-out 100ms;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include smallScreen() {
|
|
||||||
.timetable {
|
|
||||||
&-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
font-size: 1.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-general {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-schedule {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,8 +2,46 @@
|
|||||||
<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>
|
<table v-else-if="sceneryHistoryList.length">
|
||||||
<ul class="history-list" v-else>
|
<thead>
|
||||||
|
<th>{{ $t('scenery.timetables-history-id') }}</th>
|
||||||
|
<th>{{ $t('scenery.timetables-history-number')}}</th>
|
||||||
|
<th>{{ $t('scenery.timetables-history-route')}}</th>
|
||||||
|
<th>{{ $t('scenery.timetables-history-driver')}}</th>
|
||||||
|
<th>{{ $t('scenery.timetables-history-author')}}</th>
|
||||||
|
<th>{{ $t('scenery.timetables-history-date')}}</th>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="historyItem in sceneryHistoryList" @click="test">
|
||||||
|
<td>
|
||||||
|
<router-link :to="`/journal/timetables?timetableId=${historyItem.id}`">#{{ historyItem.id }}</router-link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<b class="text--primary">{{ historyItem.trainCategoryCode }}</b> <br />
|
||||||
|
{{ historyItem.trainNo }}
|
||||||
|
</td>
|
||||||
|
<td>{{ historyItem.route.replace('|', ' -> ') }}</td>
|
||||||
|
<td>{{ historyItem.driverName }}</td>
|
||||||
|
<td>
|
||||||
|
<router-link
|
||||||
|
v-if="historyItem.authorName"
|
||||||
|
:to="`/journal/dispatchers?dispatcherName=${historyItem.authorName}`"
|
||||||
|
>{{ historyItem.authorName }}
|
||||||
|
</router-link>
|
||||||
|
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b>
|
||||||
|
{{ localeTime(historyItem.beginDate, $i18n.locale) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="list-warning" v-else>{{ $t('scenery.history-list-empty') }}</div>
|
||||||
|
|
||||||
|
<!-- <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>
|
||||||
@@ -11,29 +49,26 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<router-link :to="`/journal/timetables?timetableId=${historyItem.timetableId}`">
|
<router-link :to="`/journal/timetables?timetableId=${historyItem.id}`">
|
||||||
<span class="text--grayed"> #{{ historyItem.timetableId }} </span>
|
<span class="text--grayed"> #{{ historyItem.id }} </span>
|
||||||
<b class="text--primary"> {{ historyItem.trainCategoryCode }} {{ historyItem.trainNo }}</b>
|
<b class="text--primary"> {{ 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>
|
<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> -->
|
|
||||||
</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';
|
||||||
@@ -58,7 +93,7 @@ export default defineComponent({
|
|||||||
dataStatus: DataStatus.Loading,
|
dataStatus: DataStatus.Loading,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
activated() {
|
||||||
this.fetchAPIData();
|
this.fetchAPIData();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -73,6 +108,10 @@ export default defineComponent({
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
test() {
|
||||||
|
console.log('test');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: { Loading },
|
components: { Loading },
|
||||||
});
|
});
|
||||||
@@ -92,27 +131,34 @@ export default defineComponent({
|
|||||||
padding: 0 0.5em;
|
padding: 0 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-item {
|
table {
|
||||||
display: grid;
|
width: 100%;
|
||||||
grid-template-columns: 1fr 2fr 2fr 1fr;
|
border-collapse: collapse;
|
||||||
gap: 1em;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
background-color: #353535;
|
thead {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background-color: #222222;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
margin: 0.5em 0;
|
}
|
||||||
|
|
||||||
line-height: 1.5em;
|
tr {
|
||||||
|
background-color: #353535;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 0.75em;
|
||||||
|
border-bottom: solid 5px #111;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include smallScreen {
|
@include smallScreen {
|
||||||
.history-list {
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
|
||||||
.list-item {
|
.list-item {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
font-size: 1.05em;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<template>
|
||||||
|
<div class="general-status">
|
||||||
|
<span :class="scheduledTrain.stopStatus">
|
||||||
|
<span v-if="scheduledTrain.stopStatus == 'arriving'">
|
||||||
|
<span v-if="scheduledTrain.prevDepartureLine">({{ scheduledTrain.prevDepartureLine }})</span>
|
||||||
|
{{ scheduledTrain.prevStationName }}
|
||||||
|
><span v-if="scheduledTrain.nextArrivalLine"> ({{ scheduledTrain.nextArrivalLine }}) </span>
|
||||||
|
{{ scheduledTrain.nextStationName || '---' }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else-if="scheduledTrain.stopStatus == 'departed'">
|
||||||
|
>> <span v-if="scheduledTrain.nextArrivalLine"> ({{ scheduledTrain.nextArrivalLine }}) </span>
|
||||||
|
{{ scheduledTrain.nextStationName }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else-if="scheduledTrain.stopStatus == 'departed-away'">
|
||||||
|
>>>
|
||||||
|
<span v-if="scheduledTrain.nextArrivalLine"> ({{ scheduledTrain.nextArrivalLine }}) </span>
|
||||||
|
{{ scheduledTrain.nextStationName }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else-if="scheduledTrain.stopStatus == 'online'">
|
||||||
|
>
|
||||||
|
<span v-if="scheduledTrain.nextArrivalLine">
|
||||||
|
({{ scheduledTrain.nextArrivalLine }}) {{ scheduledTrain.nextStationName }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="!scheduledTrain.nextStationName">{{ $t('timetables.end') }}</span>
|
||||||
|
<span v-else>{{ scheduledTrain.nextStationName }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else-if="scheduledTrain.stopStatus == 'stopped'">
|
||||||
|
>
|
||||||
|
<span v-if="scheduledTrain.nextArrivalLine"> ({{ scheduledTrain.nextArrivalLine }}) </span>
|
||||||
|
{{ scheduledTrain.nextStationName }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else-if="scheduledTrain.stopStatus == 'terminated'">X {{ $t('timetables.terminated') }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType } from 'vue';
|
||||||
|
import ScheduledTrain from '../../scripts/interfaces/ScheduledTrain';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
scheduledTrain: {
|
||||||
|
type: Object as PropType<ScheduledTrain>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.general-status {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
|
||||||
|
span.arriving {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.departed {
|
||||||
|
color: lime;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&-away {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #5ecc5e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span.stopped {
|
||||||
|
color: #ffa600;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.online {
|
||||||
|
color: gold;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.terminated {
|
||||||
|
color: salmon;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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,64 +50,50 @@ $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;
|
||||||
|
|
||||||
&::before {
|
|
||||||
box-shadow: 0 0 6px 1px $accessCol;
|
box-shadow: 0 0 6px 1px $accessCol;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.control {
|
&.control {
|
||||||
background-color: $controlCol;
|
background-color: $controlCol;
|
||||||
|
|
||||||
&::before {
|
|
||||||
box-shadow: 0 0 6px 1px $controlCol;
|
box-shadow: 0 0 6px 1px $controlCol;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.signals {
|
&.signals {
|
||||||
background-color: $signalCol;
|
background-color: $signalCol;
|
||||||
|
|
||||||
&::before {
|
|
||||||
box-shadow: 0 0 6px 1px $signalCol;
|
box-shadow: 0 0 6px 1px $signalCol;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.routes {
|
&.routes {
|
||||||
background-color: $routesCol;
|
background-color: $routesCol;
|
||||||
|
|
||||||
&::before {
|
|
||||||
box-shadow: 0 0 6px 1px $routesCol;
|
box-shadow: 0 0 6px 1px $routesCol;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.status {
|
&.status {
|
||||||
background-color: $statusCol;
|
background-color: $statusCol;
|
||||||
|
|
||||||
&::before {
|
|
||||||
box-shadow: 0 0 6px 1px $statusCol;
|
box-shadow: 0 0 6px 1px $statusCol;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.save {
|
&.save {
|
||||||
background-color: $saveCol;
|
background-color: $saveCol;
|
||||||
|
|
||||||
&::before {
|
|
||||||
box-shadow: 0 0 6px 1px $saveCol;
|
box-shadow: 0 0 6px 1px $saveCol;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.troll {
|
&.troll {
|
||||||
background-color: firebrick;
|
background-color: firebrick;
|
||||||
|
|
||||||
&::before {
|
|
||||||
box-shadow: 0 0 6px 1px firebrick;
|
box-shadow: 0 0 6px 1px firebrick;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.mode {
|
&.mode {
|
||||||
background-color: lightgreen;
|
background-color: lightgreen;
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,20 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="filter-card" v-click-outside="closeCard">
|
<section class="filter-card" v-click-outside="closeCard" @keydown.esc="closeCard">
|
||||||
<div class="card_btn">
|
<div class="card_controls">
|
||||||
<button class="btn btn--option" @click="toggleCard">
|
<button class="btn--filled btn--image" @click="toggleCard">
|
||||||
<img class="button_icon" :src="getIcon('filter2')" alt="icon-filter" />
|
<img class="button_icon" :src="getIcon('filter2')" alt="filter icon" />
|
||||||
{{ $t('options.filters') }}
|
{{ $t('options.filters') }} [F]
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<label for="scenery-search">
|
||||||
|
<input
|
||||||
|
id="scenery-search"
|
||||||
|
list="sceneries"
|
||||||
|
:placeholder="$t('sceneries.scenery-search')"
|
||||||
|
@focus="preventKeyDown = true"
|
||||||
|
@blur="preventKeyDown = false"
|
||||||
|
v-model="chosenSearchScenery"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<datalist id="sceneries">
|
||||||
|
<option v-for="scenery in sortedStationList" :value="scenery.name"></option>
|
||||||
|
</datalist>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<transition name="card-anim">
|
<transition name="card-anim">
|
||||||
<div class="card" v-if="isVisible">
|
<div class="card" v-if="isVisible" tabindex="0" ref="cardEl">
|
||||||
<div class="card_content">
|
<div class="card_content">
|
||||||
<div class="card_title flex">{{ $t('filters.title') }}</div>
|
<div class="card_title flex">{{ $t('filters.title') }}</div>
|
||||||
|
|
||||||
<section class="card_options">
|
<section class="card_options">
|
||||||
<filter-option
|
<filter-option
|
||||||
v-for="(option, i) in inputs.options"
|
v-for="(option, i) in filterStore.inputs.options"
|
||||||
:option="option"
|
:option="option"
|
||||||
:key="i"
|
:key="i"
|
||||||
@optionChange="handleChange"
|
@optionChange="handleChange"
|
||||||
@@ -23,7 +38,7 @@
|
|||||||
<section class="card_timestamp" style="text-align: center">
|
<section class="card_timestamp" style="text-align: center">
|
||||||
<div>{{ $t('filters.minimum-hours-title') }}</div>
|
<div>{{ $t('filters.minimum-hours-title') }}</div>
|
||||||
<span class="clock">
|
<span class="clock">
|
||||||
<button @click="subHour">-</button>
|
<button class="btn--action" @click="subHour">-</button>
|
||||||
<span>{{
|
<span>{{
|
||||||
minimumHours == 0
|
minimumHours == 0
|
||||||
? $t('filters.now')
|
? $t('filters.now')
|
||||||
@@ -31,7 +46,7 @@
|
|||||||
? minimumHours + $t('filters.hour')
|
? minimumHours + $t('filters.hour')
|
||||||
: $t('filters.no-limit')
|
: $t('filters.no-limit')
|
||||||
}}</span>
|
}}</span>
|
||||||
<button @click="addHour">+</button>
|
<button class="btn--action" @click="addHour">+</button>
|
||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -42,11 +57,13 @@
|
|||||||
name="authors"
|
name="authors"
|
||||||
v-model="authorsInputValue"
|
v-model="authorsInputValue"
|
||||||
@input="handleAuthorsInput"
|
@input="handleAuthorsInput"
|
||||||
|
@focus="preventKeyDown = true"
|
||||||
|
@blur="preventKeyDown = false"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card_sliders">
|
<section class="card_sliders">
|
||||||
<div class="slider" v-for="(slider, i) in inputs.sliders" :key="i">
|
<div class="slider" v-for="(slider, i) in filterStore.inputs.sliders" :key="i">
|
||||||
<input
|
<input
|
||||||
class="slider-input"
|
class="slider-input"
|
||||||
type="range"
|
type="range"
|
||||||
@@ -65,23 +82,13 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card_actions">
|
<section class="card_actions">
|
||||||
<div>
|
<div class="action-buttons">
|
||||||
<filter-option
|
<button class="btn--action" style="width: 100%" @click="saveFilters" :data-selected="saveOptions">
|
||||||
@optionChange="saveFilters"
|
{{ $t('filters.save') }}
|
||||||
:option="{
|
</button>
|
||||||
id: 'save',
|
|
||||||
name: 'save',
|
<button class="btn--action" @click="resetFilters">{{ $t('filters.reset') }}</button>
|
||||||
section: 'mode',
|
<button class="btn--action" @click="closeCard">{{ $t('filters.close') }}</button>
|
||||||
value: saveOptions,
|
|
||||||
defaultValue: true,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<action-button class="outlined" @click="resetFilters">
|
|
||||||
{{ $t('filters.reset') }}
|
|
||||||
</action-button>
|
|
||||||
<action-button class="outlined" @click="closeCard">{{ $t('filters.close') }}</action-button>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,11 +98,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import { defineComponent, inject } from 'vue';
|
import { defineComponent, inject } from 'vue';
|
||||||
import inputData from '../../data/options.json';
|
|
||||||
import imageMixin from '../../mixins/imageMixin';
|
import imageMixin from '../../mixins/imageMixin';
|
||||||
|
import keyMixin from '../../mixins/keyMixin';
|
||||||
|
import routerMixin from '../../mixins/routerMixin';
|
||||||
import StorageManager from '../../scripts/managers/storageManager';
|
import StorageManager from '../../scripts/managers/storageManager';
|
||||||
|
import { useStationFiltersStore } from '../../store/stationFiltersStore';
|
||||||
import { useStore } from '../../store/store';
|
import { useStore } from '../../store/store';
|
||||||
|
|
||||||
import ActionButton from '../Global/ActionButton.vue';
|
import ActionButton from '../Global/ActionButton.vue';
|
||||||
@@ -103,12 +111,9 @@ import FilterOption from './FilterOption.vue';
|
|||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { ActionButton, FilterOption },
|
components: { ActionButton, FilterOption },
|
||||||
emits: ['changeFilterValue', 'invertFilters', 'resetFilters'],
|
mixins: [imageMixin, keyMixin, routerMixin],
|
||||||
mixins: [imageMixin],
|
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
|
||||||
inputs: { ...inputData },
|
|
||||||
saveOptions: false,
|
saveOptions: false,
|
||||||
STORAGE_KEY: 'options_saved',
|
STORAGE_KEY: 'options_saved',
|
||||||
|
|
||||||
@@ -118,15 +123,18 @@ export default defineComponent({
|
|||||||
currentRegion: { id: '', value: '' },
|
currentRegion: { id: '', value: '' },
|
||||||
|
|
||||||
delayInputTimer: -1,
|
delayInputTimer: -1,
|
||||||
|
chosenSearchScenery: '',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const isVisible = inject('isFilterCardVisible');
|
const isVisible = inject('isFilterCardVisible');
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
const filterStore = useStationFiltersStore();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isVisible,
|
isVisible,
|
||||||
store,
|
store,
|
||||||
|
filterStore,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -142,9 +150,39 @@ export default defineComponent({
|
|||||||
this.currentRegion = this.store.region;
|
this.currentRegion = this.store.region;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
sortedStationList() {
|
||||||
|
return this.store.stationList
|
||||||
|
.filter((s) => s.name.toLocaleLowerCase().includes(this.chosenSearchScenery.toLocaleLowerCase()))
|
||||||
|
.sort((s1, s2) => (s1.name > s2.name ? 1 : -1));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
chosenSearchScenery(value: string) {
|
||||||
|
const chosenStation = this.store.stationList.find(({ name }) => name == value);
|
||||||
|
|
||||||
|
if (chosenStation) {
|
||||||
|
this.$router.push(`/scenery?station=${chosenStation.name.replace(/ /g, '_')}`);
|
||||||
|
this.chosenSearchScenery = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isVisible(value: boolean) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (value) (this.$refs['cardEl'] as HTMLDivElement).focus();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
// Override keyMixin function
|
||||||
|
onKeyDownFunction() {
|
||||||
|
this.isVisible = !this.isVisible;
|
||||||
|
},
|
||||||
|
|
||||||
handleChange(change: { name: string; value: boolean }) {
|
handleChange(change: { name: string; value: boolean }) {
|
||||||
this.$emit('changeFilterValue', {
|
this.filterStore.changeFilterValue({
|
||||||
name: change.name,
|
name: change.name,
|
||||||
value: !change.value,
|
value: !change.value,
|
||||||
});
|
});
|
||||||
@@ -155,7 +193,7 @@ export default defineComponent({
|
|||||||
handleInput(e: Event) {
|
handleInput(e: Event) {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
|
|
||||||
this.$emit('changeFilterValue', {
|
this.filterStore.changeFilterValue({
|
||||||
name: target.name,
|
name: target.name,
|
||||||
value: target.value,
|
value: target.value,
|
||||||
});
|
});
|
||||||
@@ -172,7 +210,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
changeNumericFilterValue(name: string, value: number, saveToStorage = false) {
|
changeNumericFilterValue(name: string, value: number, saveToStorage = false) {
|
||||||
this.$emit('changeFilterValue', {
|
this.filterStore.changeFilterValue({
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
@@ -192,17 +230,8 @@ export default defineComponent({
|
|||||||
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
|
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
|
||||||
},
|
},
|
||||||
|
|
||||||
invertFilters() {
|
saveFilters() {
|
||||||
this.inputs.options.forEach((option) => {
|
this.saveOptions = !this.saveOptions;
|
||||||
option.value = !option.value;
|
|
||||||
StorageManager.setBooleanValue(option.name, option.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$emit('invertFilters');
|
|
||||||
},
|
|
||||||
|
|
||||||
saveFilters(change: { value: any }) {
|
|
||||||
this.saveOptions = change.value;
|
|
||||||
|
|
||||||
if (!this.saveOptions) {
|
if (!this.saveOptions) {
|
||||||
StorageManager.unregisterStorage(this.STORAGE_KEY);
|
StorageManager.unregisterStorage(this.STORAGE_KEY);
|
||||||
@@ -211,28 +240,16 @@ export default defineComponent({
|
|||||||
|
|
||||||
StorageManager.registerStorage(this.STORAGE_KEY);
|
StorageManager.registerStorage(this.STORAGE_KEY);
|
||||||
|
|
||||||
this.inputs.options.forEach((option) => StorageManager.setBooleanValue(option.name, option.value));
|
this.filterStore.inputs.options.forEach((option) => StorageManager.setBooleanValue(option.name, !option.value));
|
||||||
|
this.filterStore.inputs.sliders.forEach((slider) => StorageManager.setNumericValue(slider.name, slider.value));
|
||||||
this.inputs.sliders.forEach((slider) => StorageManager.setNumericValue(slider.name, slider.value));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
resetFilters() {
|
resetFilters() {
|
||||||
this.inputs.options.forEach((option) => {
|
|
||||||
option.value = option.defaultValue;
|
|
||||||
StorageManager.setBooleanValue(option.name, option.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.inputs.sliders.forEach((slider) => {
|
|
||||||
slider.value = slider.defaultValue;
|
|
||||||
StorageManager.setNumericValue(slider.name, slider.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.authorsInputValue = '';
|
this.authorsInputValue = '';
|
||||||
|
|
||||||
this.minimumHours = 0;
|
this.minimumHours = 0;
|
||||||
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
|
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
|
||||||
|
this.filterStore.resetFilters();
|
||||||
this.$emit('resetFilters');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
closeCard() {
|
closeCard() {
|
||||||
@@ -264,28 +281,24 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
&_btn {
|
&_controls {
|
||||||
button {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
gap: 0.5em;
|
||||||
|
|
||||||
padding: 0.5em 1em;
|
input {
|
||||||
border-radius: 0.75em 0.75em 0 0;
|
border-radius: 0.5em 0.5em 0 0;
|
||||||
|
height: 100%;
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 1.3em;
|
|
||||||
margin-right: 0.25em;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&_content {
|
&_content {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-rows: 70px 1fr 100px 50px auto;
|
flex-direction: column;
|
||||||
min-height: 0;
|
gap: 1em;
|
||||||
max-height: 100vh;
|
|
||||||
|
max-height: 90vh;
|
||||||
|
|
||||||
|
padding: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&_title {
|
&_title {
|
||||||
@@ -293,8 +306,6 @@ export default defineComponent({
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: $accentCol;
|
color: $accentCol;
|
||||||
|
|
||||||
margin: 0.5em 0;
|
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,31 +353,17 @@ 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;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
span {
|
||||||
min-width: 100px;
|
min-width: 120px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: $accentCol;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border: none;
|
padding: 0.2em 0.6em;
|
||||||
outline: none;
|
|
||||||
background: none;
|
|
||||||
padding: 0 0.45em;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
font-size: 1.35em;
|
|
||||||
|
|
||||||
&:focus,
|
|
||||||
&:hover {
|
|
||||||
color: $accentCol;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -389,22 +386,33 @@ export default defineComponent({
|
|||||||
input {
|
input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
|
border: 1px solid white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&_actions {
|
&_actions {
|
||||||
margin-top: 1em;
|
.filter-option {
|
||||||
|
max-width: 50%;
|
||||||
display: flex;
|
margin: 0 auto;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
button {
|
|
||||||
margin: 1em 0.25em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.option {
|
.action-buttons {
|
||||||
font-size: 1.1em;
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
margin-top: 0.5em;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 50%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0.5em;
|
||||||
|
|
||||||
|
&[data-selected='true'] {
|
||||||
|
background-color: lightgreen;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -435,8 +443,13 @@ export default defineComponent({
|
|||||||
min-width: 25%;
|
min-width: 25%;
|
||||||
max-width: 120px;
|
max-width: 120px;
|
||||||
|
|
||||||
|
&:focus-visible ~ * {
|
||||||
|
color: gold;
|
||||||
|
}
|
||||||
|
|
||||||
&::-webkit-slider-thumb {
|
&::-webkit-slider-thumb {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
|||||||
@@ -100,7 +100,10 @@
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="station_dispatcher-exp">
|
<td class="station_dispatcher-exp">
|
||||||
<span v-if="station.onlineInfo" :style="calculateExpStyle(station.onlineInfo.dispatcherExp)">
|
<span
|
||||||
|
v-if="station.onlineInfo"
|
||||||
|
:style="calculateExpStyle(station.onlineInfo.dispatcherExp, station.onlineInfo.dispatcherIsSupporter)"
|
||||||
|
>
|
||||||
{{ 2 > station.onlineInfo.dispatcherExp ? 'L' : station.onlineInfo.dispatcherExp }}
|
{{ 2 > station.onlineInfo.dispatcherExp ? 'L' : station.onlineInfo.dispatcherExp }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -182,7 +185,7 @@
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="station_info" v-else>
|
<td class="station_info" v-else>
|
||||||
<img class="icon-info" :src="getImage('unknown.png')" alt="icon-unknown" :title="$t('desc.unknown')" />
|
<img class="icon-info" :src="getIcon('unknown')" alt="icon-unknown" :title="$t('desc.unknown')" />
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="station_users" :class="{ inactive: !station.onlineInfo }">
|
<td class="station_users" :class="{ inactive: !station.onlineInfo }">
|
||||||
@@ -230,6 +233,7 @@ import stationInfoMixin from '../../mixins/stationInfoMixin';
|
|||||||
import styleMixin from '../../mixins/styleMixin';
|
import styleMixin from '../../mixins/styleMixin';
|
||||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
import { DataStatus } from '../../scripts/enums/DataStatus';
|
||||||
import Station from '../../scripts/interfaces/Station';
|
import Station from '../../scripts/interfaces/Station';
|
||||||
|
import { useStationFiltersStore } from '../../store/stationFiltersStore';
|
||||||
import { useStore } from '../../store/store';
|
import { useStore } from '../../store/store';
|
||||||
import Loading from '../Global/Loading.vue';
|
import Loading from '../Global/Loading.vue';
|
||||||
|
|
||||||
@@ -239,48 +243,58 @@ export default defineComponent({
|
|||||||
type: Array as () => Station[],
|
type: Array as () => Station[],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
sorterActive: {
|
|
||||||
type: Object as () => {
|
|
||||||
index: number;
|
|
||||||
dir: number;
|
|
||||||
},
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
setFocusedStation: { type: Function, required: true },
|
|
||||||
changeSorter: { type: Function, required: true },
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
components: { Loading },
|
||||||
mixins: [styleMixin, dateMixin, stationInfoMixin, returnBtnMixin, imageMixin],
|
mixins: [styleMixin, dateMixin, stationInfoMixin, returnBtnMixin, imageMixin],
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
headIds: ['station', 'min-lvl', 'status', 'dispatcher', 'dispatcher-lvl', 'routes', 'general'],
|
headIds: ['station', 'min-lvl', 'status', 'dispatcher', 'dispatcher-lvl', 'routes', 'general'],
|
||||||
headIconsIds: ['user', 'spawn', 'timetable'],
|
headIconsIds: ['user', 'spawn', 'timetable'],
|
||||||
lastSelectedStationName: '',
|
lastSelectedStationName: '',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
sorterActive() {
|
||||||
|
return this.stationFiltersStore.sorterActive;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
const stationFiltersStore = useStationFiltersStore();
|
||||||
|
|
||||||
const isDataLoaded = computed(() => {
|
const isDataLoaded = computed(() => {
|
||||||
return store.dataStatuses.sceneries != DataStatus.Loading;
|
return store.dataStatuses.sceneries != DataStatus.Loading;
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
isDataLoaded,
|
isDataLoaded,
|
||||||
|
stationFiltersStore,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
setScenery(name: string) {
|
setScenery(name: string) {
|
||||||
const station = this.stations.find((station) => station.name === name);
|
const station = this.stations.find((station) => station.name === name);
|
||||||
if (!station) return;
|
if (!station) return;
|
||||||
|
|
||||||
this.lastSelectedStationName = station.name;
|
this.lastSelectedStationName = station.name;
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: 'SceneryView',
|
name: 'SceneryView',
|
||||||
query: { station: station.name.replaceAll(' ', '_') },
|
query: { station: station.name.replaceAll(' ', '_') },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
openForumSite(e: Event, url: string | undefined) {
|
openForumSite(e: Event, url: string | undefined) {
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
changeSorter(i: number) {
|
||||||
|
this.stationFiltersStore.changeSorter(i);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: { Loading },
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -289,7 +303,7 @@ export default defineComponent({
|
|||||||
@import '../../styles/variables.scss';
|
@import '../../styles/variables.scss';
|
||||||
@import '../../styles/icons.scss';
|
@import '../../styles/icons.scss';
|
||||||
|
|
||||||
$rowCol: #4b4b4b;
|
$rowCol: #424242;
|
||||||
|
|
||||||
.change-anim {
|
.change-anim {
|
||||||
&-enter-active,
|
&-enter-active,
|
||||||
@@ -328,7 +342,7 @@ table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
thead tr {
|
thead tr {
|
||||||
background-color: $primaryCol;
|
background-color: $bgCol;
|
||||||
}
|
}
|
||||||
|
|
||||||
thead th {
|
thead th {
|
||||||
@@ -338,7 +352,7 @@ table {
|
|||||||
min-width: 75px;
|
min-width: 75px;
|
||||||
|
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
background-color: $primaryCol;
|
background-color: $bgCol;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -2,17 +2,23 @@
|
|||||||
<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>
|
<b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
|
||||||
<span class="timetable-id" v-if="train.timetableData">#{{ train.timetableData.timetableId }}</span>
|
<span class="timetable-id" v-if="train.timetableData">#{{ train.timetableData.timetableId }}</span>
|
||||||
|
|
||||||
<span class="timetable_warnings">
|
<span class="timetable_warnings" v-if="train.timetableData?.TWR || train.timetableData?.SKR">
|
||||||
<span class="train-badge twr" v-if="train.timetableData?.TWR">TWR</span>
|
<span class="train-badge twr" v-if="train.timetableData?.TWR">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 }} </strong>
|
|
||||||
<strong>{{ train.trainNo }}</strong>
|
<strong>
|
||||||
<span> | {{ train.driverName }} </span>
|
<span v-if="train.timetableData" class="text--primary">{{ train.timetableData.category }} </span>
|
||||||
</span>
|
<span class="train-number">{{ train.trainNo }}</span>
|
||||||
|
</strong>
|
||||||
|
<span>•</span>
|
||||||
|
<b class="level-badge driver" :style="calculateExpStyle(train.driverLevel, train.isSupporter)">
|
||||||
|
{{ train.driverLevel < 2 ? 'L' : `${train.driverLevel}` }}
|
||||||
|
</b>
|
||||||
|
<span>{{ train.driverName }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="timetable_route" v-if="train.timetableData">
|
<div class="timetable_route" v-if="train.timetableData">
|
||||||
@@ -35,9 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="timetable_progress" style="margin-top: 0.5em" v-if="train.timetableData">
|
<div class="timetable_progress" style="margin-top: 0.5em" v-if="train.timetableData">
|
||||||
<!-- <span> </span> -->
|
|
||||||
<span class="timetable_progress-bar">
|
<span class="timetable_progress-bar">
|
||||||
<!-- {{ confirmedPercentage(train.timetableData.followingStops) }}% -->
|
|
||||||
<span class="bar-bg"></span>
|
<span class="bar-bg"></span>
|
||||||
<span
|
<span
|
||||||
class="bar-fg"
|
class="bar-fg"
|
||||||
@@ -89,6 +93,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import imageMixin from '../../mixins/imageMixin';
|
import imageMixin from '../../mixins/imageMixin';
|
||||||
|
import styleMixin from '../../mixins/styleMixin';
|
||||||
import trainInfoMixin from '../../mixins/trainInfoMixin';
|
import trainInfoMixin from '../../mixins/trainInfoMixin';
|
||||||
import Train from '../../scripts/interfaces/Train';
|
import Train from '../../scripts/interfaces/Train';
|
||||||
|
|
||||||
@@ -105,12 +110,14 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [trainInfoMixin, imageMixin],
|
mixins: [trainInfoMixin, imageMixin, styleMixin],
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/responsive.scss';
|
@import '../../styles/responsive.scss';
|
||||||
|
@import '../../styles/badge.scss';
|
||||||
|
|
||||||
|
|
||||||
.image-warning {
|
.image-warning {
|
||||||
height: 1em;
|
height: 1em;
|
||||||
@@ -144,10 +151,18 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timetable-id {
|
.timetable-id {
|
||||||
margin-right: 0.3em;
|
|
||||||
color: #d2d2d2;
|
color: #d2d2d2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.warning-timeout {
|
||||||
|
background-color: #be3728;
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
padding: 0 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
.timetable_stops {
|
.timetable_stops {
|
||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
}
|
}
|
||||||
@@ -156,16 +171,20 @@ export default defineComponent({
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
gap: 0.25em;
|
||||||
|
margin-right: 1.5em;
|
||||||
}
|
}
|
||||||
.train-status-badges {
|
.train-status-badges {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
gap: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.train-badge {
|
.train-badge {
|
||||||
padding: 0.15em 0.35em;
|
padding: 0.1em 0.2em;
|
||||||
margin-right: 0.3em;
|
border-radius: 0.2em;
|
||||||
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
@@ -179,7 +198,14 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.offline {
|
&.offline {
|
||||||
background-color: #b83b2d;
|
background-color: #9c362b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.train-driver {
|
||||||
|
&.supporter {
|
||||||
|
color: orange;
|
||||||
|
text-shadow: orange 0 0 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +217,9 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timetable_warnings {
|
.timetable_warnings {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.2em;
|
||||||
|
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,10 +282,6 @@ export default defineComponent({
|
|||||||
|
|
||||||
.train-stats {
|
.train-stats {
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
|
|
||||||
img {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.train_general {
|
.train_general {
|
||||||
|
|||||||
@@ -1,267 +1,208 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="train-options">
|
<div class="filters-options" @keydown.esc="showOptions = false">
|
||||||
<div class="options_wrapper">
|
<div class="bg" v-if="showOptions" @click="showOptions = false"></div>
|
||||||
|
|
||||||
|
<button class="filter-button btn--filled btn--image" @click="toggleShowOptions" ref="button">
|
||||||
|
<img :src="getIcon('filter2')" alt="Open filters" />
|
||||||
|
{{ $t('options.filters') }} [F]
|
||||||
|
<span class="active-indicator" v-if="currentOptionsActive"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<transition name="options-anim">
|
||||||
|
<div class="options_wrapper" v-if="showOptions">
|
||||||
<div class="options_content">
|
<div class="options_content">
|
||||||
<div class="content_select">
|
<h1 class="option-title">{{ $t('options.search-title') }}</h1>
|
||||||
<select-box
|
<div class="search_content">
|
||||||
:itemList="translatedSorterOptions"
|
<div class="search-box">
|
||||||
:defaultItemIndex="0"
|
<input
|
||||||
@selected="changeSorter"
|
class="search-input"
|
||||||
:prefix="$t('trains.sorter-prefix')"
|
ref="initFocusedElement"
|
||||||
|
@focus="preventKeyDown = true"
|
||||||
|
@blur="preventKeyDown = false"
|
||||||
|
:placeholder="$t(`options.search-train`)"
|
||||||
|
v-model="searchedTrain"
|
||||||
/>
|
/>
|
||||||
</div>
|
<button class="search-exit">
|
||||||
|
<img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear('train')" />
|
||||||
<div class="content_search">
|
</button>
|
||||||
<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>
|
||||||
|
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<input class="search-input" v-model="searchedDriver" :placeholder="$t('trains.search-driver')" />
|
<input
|
||||||
|
class="search-input"
|
||||||
<img class="search-exit" :src="getIcon('exit')" alt="exit-icon" @click="() => (searchedDriver = '')" />
|
@focus="preventKeyDown = true"
|
||||||
</div>
|
@blur="preventKeyDown = false"
|
||||||
</div>
|
: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>
|
||||||
|
|
||||||
<div class="filters">
|
<h1 class="option-title">{{ $t('options.sort-title') }}</h1>
|
||||||
<span
|
<div class="options_sorters">
|
||||||
:class="{ active: filter.isActive }"
|
<div v-for="opt in translatedSorterOptions">
|
||||||
class="filter"
|
<button
|
||||||
v-for="filter in filterList"
|
class="sort-option btn--option"
|
||||||
:key="filter.id"
|
:data-selected="opt.id == sorterActive.id"
|
||||||
tabindex="0"
|
@click="onSorterChange(opt)"
|
||||||
@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}`) }}
|
{{ opt.value.toUpperCase() }}
|
||||||
</span>
|
</button>
|
||||||
|
|
||||||
<span class="filter reset-btn" @click="resetFilters" tabindex="0">
|
|
||||||
{{ $t('trains.filter-reset') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h1 class="option-title" v-if="trainFilterList.length != 0">{{ $t('options.filter-title') }}</h1>
|
||||||
|
<div class="options_filters">
|
||||||
|
<div class="filter-option" v-for="filter in trainFilterList">
|
||||||
|
<button class="btn--option" :data-inactive="!filter.isActive" @click="onFilterChange(filter)">
|
||||||
|
{{ $t(`options.filter-${filter.id}`) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn--action" @click="clearAllFilters">{{ $t('options.filter-clear') }}</button>
|
||||||
|
<button class="btn--action" @click="resetAllFilters">{{ $t('options.filter-reset') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, inject, TrainFilter } from 'vue';
|
import { defineComponent, inject, PropType } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import imageMixin from '../../mixins/imageMixin';
|
import imageMixin from '../../mixins/imageMixin';
|
||||||
|
import keyMixin from '../../mixins/keyMixin';
|
||||||
|
import { TrainFilter } from '../../types/Trains/TrainOptionsTypes';
|
||||||
|
import ActionButton from '../Global/ActionButton.vue';
|
||||||
import SelectBox from '../Global/SelectBox.vue';
|
import SelectBox from '../Global/SelectBox.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { SelectBox },
|
components: { SelectBox, ActionButton },
|
||||||
emits: ['changeSearchedTrain', 'changeSearchedDriver', 'changeSorter'],
|
mixins: [imageMixin, keyMixin],
|
||||||
mixins: [imageMixin],
|
|
||||||
|
|
||||||
setup() {
|
props: {
|
||||||
const { t } = useI18n();
|
sorterOptionIds: {
|
||||||
|
type: Array as PropType<Array<string>>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
const sorterOptions = [
|
currentOptionsActive: {
|
||||||
{
|
type: Boolean,
|
||||||
id: 'distance',
|
default: false,
|
||||||
value: 'kilometraż',
|
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'progress',
|
|
||||||
value: 'przebyta trasa',
|
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'delay',
|
|
||||||
value: 'opóźnienie',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'mass',
|
|
||||||
value: 'masa',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'speed',
|
|
||||||
value: 'prędkość',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'length',
|
|
||||||
value: 'długość',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let filterList = inject('filterList') as TrainFilter[];
|
|
||||||
|
|
||||||
const translatedSorterOptions = computed(() =>
|
|
||||||
sorterOptions.map(({ id }) => ({
|
|
||||||
id,
|
|
||||||
value: t(`trains.option-${id}`),
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
|
data() {
|
||||||
return {
|
return {
|
||||||
translatedSorterOptions,
|
showOptions: false,
|
||||||
searchedTrain: inject('searchedTrain') as string,
|
lastSelectedFilter: null as TrainFilter | null,
|
||||||
searchedDriver: inject('searchedDriver') as string,
|
|
||||||
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
|
|
||||||
filterList,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
searchedTrain: inject('searchedTrain') as string,
|
||||||
|
searchedDriver: inject('searchedDriver') as string,
|
||||||
|
|
||||||
|
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
|
||||||
|
trainFilterList: inject('filterList') as TrainFilter[],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
translatedSorterOptions() {
|
||||||
|
return this.$props.sorterOptionIds.map((id) => ({
|
||||||
|
id,
|
||||||
|
value: this.$t(`options.sort-${id}`),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
changeSorter(item: { id: string | number; value: string }) {
|
// Override keyMixin function
|
||||||
|
onKeyDownFunction() {
|
||||||
|
this.toggleShowOptions();
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleShowOptions() {
|
||||||
|
this.showOptions = !this.showOptions;
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.showOptions) (this.$refs['button'] as HTMLButtonElement)?.focus();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onSorterChange(item: { id: string | number; value: string }) {
|
||||||
this.sorterActive.id = item.id;
|
this.sorterActive.id = item.id;
|
||||||
this.sorterActive.dir = -1;
|
this.sorterActive.dir = -1;
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleFilter(filter: TrainFilter) {
|
onFilterChange(filter: TrainFilter) {
|
||||||
|
// if (this.lastSelectedFilter?.id === filter.id)
|
||||||
|
// this.trainFilterList.forEach((tf) => (tf.isActive = filter.id === tf.id));
|
||||||
|
|
||||||
filter.isActive = !filter.isActive;
|
filter.isActive = !filter.isActive;
|
||||||
|
this.lastSelectedFilter = filter;
|
||||||
},
|
},
|
||||||
|
|
||||||
setFilterOnly(filter: TrainFilter) {
|
clearAllFilters() {
|
||||||
this.filterList.forEach((f) => (f.isActive = f.id == filter.id));
|
this.trainFilterList.forEach((filter) => {
|
||||||
|
filter.isActive = false;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
resetFilters() {
|
resetAllFilters() {
|
||||||
this.filterList.forEach((f) => (f.isActive = true));
|
this.trainFilterList.forEach((filter) => {
|
||||||
this.searchedDriver = "";
|
filter.isActive = true;
|
||||||
this.searchedTrain = "";
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onInputClear(id: 'driver' | 'train') {
|
||||||
|
if (id == 'driver') this.searchedDriver = '';
|
||||||
|
if (id == 'train') this.searchedTrain = '';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/responsive';
|
@import '../../styles/filters_options.scss';
|
||||||
|
|
||||||
.train-options {
|
.search_content > div {
|
||||||
@include smallScreen() {
|
margin: 0.5em auto;
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.options {
|
.search_content > button {
|
||||||
&_wrapper {
|
|
||||||
display: flex;
|
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;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
margin-top: 0.5em;
|
|
||||||
|
|
||||||
@include smallScreen() {
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter {
|
.filter-option {
|
||||||
background: #333;
|
button {
|
||||||
padding: 0.2em 0.25em;
|
color: white;
|
||||||
margin: 0.25em 0.25em 0 0;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
cursor: pointer;
|
&[data-disabled='true'] {
|
||||||
color: gray;
|
color: #888;
|
||||||
|
|
||||||
&.active {
|
|
||||||
color: var(--clr-primary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.reset-btn {
|
|
||||||
color: salmon;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include smallScreen() {
|
.filter-actions {
|
||||||
.journal-options {
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
|
||||||
.options {
|
|
||||||
&_wrapper {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&_content {
|
margin-top: 1em;
|
||||||
padding: 0 1em;
|
|
||||||
|
|
||||||
flex-direction: column;
|
button {
|
||||||
|
|
||||||
.content_select {
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content_search {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search {
|
|
||||||
&-box,
|
|
||||||
&-button {
|
|
||||||
margin: 0.5em 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-box {
|
|
||||||
width: 100%;
|
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>
|
|
||||||
@@ -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>
|
||||||
|
|
||||||
@@ -87,6 +89,7 @@ import dateMixin from '../../mixins/dateMixin';
|
|||||||
import imageMixin from '../../mixins/imageMixin';
|
import imageMixin from '../../mixins/imageMixin';
|
||||||
import Train from '../../scripts/interfaces/Train';
|
import Train from '../../scripts/interfaces/Train';
|
||||||
import TrainStop from '../../scripts/interfaces/TrainStop';
|
import TrainStop from '../../scripts/interfaces/TrainStop';
|
||||||
|
import { useStore } from '../../store/store';
|
||||||
import StopDate from '../Global/StopDate.vue';
|
import StopDate from '../Global/StopDate.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@@ -104,6 +107,8 @@ export default defineComponent({
|
|||||||
|
|
||||||
setup(props) {
|
setup(props) {
|
||||||
return {
|
return {
|
||||||
|
store: useStore(),
|
||||||
|
|
||||||
lastConfirmed: computed(() => {
|
lastConfirmed: computed(() => {
|
||||||
return props.train.timetableData!.followingStops.findIndex(
|
return props.train.timetableData!.followingStops.findIndex(
|
||||||
(stop, i, stops) => stop.confirmed && !stops[i + 1]?.confirmed && !stops[i + 1]?.stopped
|
(stop, i, stops) => stop.confirmed && !stops[i + 1]?.confirmed && !stops[i + 1]?.stopped
|
||||||
@@ -175,10 +180,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 +199,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 {
|
||||||
@@ -421,3 +427,4 @@ ul.stop_list > li.stop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,21 @@
|
|||||||
<div class="train-table">
|
<div class="train-table">
|
||||||
<transition name="anim" mode="out-in">
|
<transition name="anim" mode="out-in">
|
||||||
<div :key="store.dataStatuses.trains">
|
<div :key="store.dataStatuses.trains">
|
||||||
<Loading v-if="trains.length == 0 && store.dataStatuses.trains == 0" />
|
<div class="table-info" v-if="store.isOffline">
|
||||||
|
{{ $t('app.offline') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table-info no-trains" v-if="trains.length == 0 && store.dataStatuses.trains != 0">
|
<Loading v-else-if="trains.length == 0 && store.dataStatuses.trains == 0" />
|
||||||
|
|
||||||
|
<div class="table-info no-trains" v-else-if="trains.length == 0 && store.dataStatuses.trains != 0">
|
||||||
{{ $t('trains.no-trains') }}
|
{{ $t('trains.no-trains') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="train-list">
|
<!-- <div class="timeouts-warning" v-if="trainNumbersWithTimeouts.length == 0">
|
||||||
|
<b class="warning-timeout">?</b>
|
||||||
|
{{ $t('trains.timeout') }}
|
||||||
|
</div> -->
|
||||||
|
<transition-group name="list-anim" tag="ul" class="train-list" v-else>
|
||||||
<li
|
<li
|
||||||
class="train-row"
|
class="train-row"
|
||||||
v-for="train in currentTrains"
|
v-for="train in currentTrains"
|
||||||
@@ -18,46 +26,37 @@
|
|||||||
>
|
>
|
||||||
<TrainInfo :train="train" />
|
<TrainInfo :train="train" />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</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,58 +66,38 @@ 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>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/responsive.scss';
|
@import '../../styles/responsive.scss';
|
||||||
|
@import '../../styles/animations.scss';
|
||||||
|
|
||||||
.anim {
|
.anim {
|
||||||
&-enter-from,
|
&-enter-from,
|
||||||
@@ -139,11 +118,10 @@ export default defineComponent({
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
padding: 1em 0;
|
padding: 1em 0;
|
||||||
margin: 1em 0;
|
|
||||||
|
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
|
|
||||||
background: #333;
|
background: #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
img.train-image {
|
img.train-image {
|
||||||
@@ -156,11 +134,31 @@ 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;
|
position: relative;
|
||||||
|
|
||||||
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,5 +1,5 @@
|
|||||||
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[] = [
|
||||||
{
|
{
|
||||||
@@ -37,6 +37,10 @@ export const sorterOptions = [
|
|||||||
id: 'distance',
|
id: 'distance',
|
||||||
value: 'kilometraż',
|
value: 'kilometraż',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'id',
|
||||||
|
value: 'id rozkładu',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'progress',
|
id: 'progress',
|
||||||
value: 'przebyta trasa',
|
value: 'przebyta trasa',
|
||||||
@@ -56,5 +60,5 @@ export const sorterOptions = [
|
|||||||
{
|
{
|
||||||
id: 'length',
|
id: 'length',
|
||||||
value: 'długość',
|
value: 'długość',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
@@ -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[] = []
|
|
||||||
@@ -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": [
|
||||||
|
|||||||
+106
-56
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"general": {
|
||||||
|
"and": " and ",
|
||||||
|
"refresh": "REFRESH"
|
||||||
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"sceneries": "SCENERIES",
|
"sceneries": "SCENERIES",
|
||||||
"trains": "TRAINS",
|
"trains": "TRAINS",
|
||||||
@@ -8,15 +12,18 @@
|
|||||||
"error": "An error occured while loading data!",
|
"error": "An error occured while loading data!",
|
||||||
"no-result": "No results for current search!",
|
"no-result": "No results for current search!",
|
||||||
"migration-warning": "Stacjownik services will be unavailable 2/06/2022 between 1-3am (CEST time) due to the migration of API hostings!",
|
"migration-warning": "Stacjownik services will be unavailable 2/06/2022 between 1-3am (CEST time) due to the migration of API hostings!",
|
||||||
"migration-confirm": "Roger that!"
|
"migration-confirm": "Roger that!",
|
||||||
|
"offline": "App is in the offline mode!"
|
||||||
},
|
},
|
||||||
"update": {
|
"update": {
|
||||||
"title": "New Stacjownik version is available!",
|
"title": "New version of the app is available!",
|
||||||
"paragraph1": "Enjoy the application and may the green signal be with you!",
|
"paragraph1": "Enjoy the application and may the green signal be with you!",
|
||||||
"release-link": "Click here to browse version changelog (GitHub)",
|
"release-link": "Click here to browse version changelog (GitHub)",
|
||||||
"confirm-button": "Understood!"
|
"confirm-button": "UPDATE NOW",
|
||||||
|
"later-button": "LATER"
|
||||||
},
|
},
|
||||||
"data-status": {
|
"data-status": {
|
||||||
|
"S1-offline": "<b>S1 signal</b> <br> The app is working in offline mode!",
|
||||||
"S1a-connection": "<b>S1a signal</b> <br> Cannot connect with Stacjownik API service!",
|
"S1a-connection": "<b>S1a signal</b> <br> Cannot connect with Stacjownik API service!",
|
||||||
"S1a-sceneries": "<b>S1a signal</b> <br> Cannot load online stations data!",
|
"S1a-sceneries": "<b>S1a signal</b> <br> Cannot load online stations data!",
|
||||||
"S2": "<b>S2 signal</b> <br> All data loaded successfully!",
|
"S2": "<b>S2 signal</b> <br> All data loaded successfully!",
|
||||||
@@ -72,7 +79,53 @@
|
|||||||
},
|
},
|
||||||
"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-id": "timetable id",
|
||||||
|
|
||||||
|
"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 +169,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 +184,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 +204,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 +227,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 +238,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 +252,41 @@
|
|||||||
"online-since": "ONLINE SINCE",
|
"online-since": "ONLINE SINCE",
|
||||||
"duty-lasted": "The duty lasted",
|
"duty-lasted": "The duty lasted",
|
||||||
"minutes": "{minutes} mins",
|
"minutes": "{minutes} mins",
|
||||||
"hours": "{hours}h {minutes} mins"
|
"hours": "{hours}h {minutes} mins",
|
||||||
|
|
||||||
|
"stock-info": "STOCK INFO",
|
||||||
|
"stock-length": "Length",
|
||||||
|
"stock-mass": "Mass",
|
||||||
|
"stock-max-speed": "Maximum registered speed",
|
||||||
|
|
||||||
|
"load-data": "Load further data...",
|
||||||
|
|
||||||
|
"last-seen-at": "Last seen at",
|
||||||
|
"currently-at": "Currently at",
|
||||||
|
|
||||||
|
"stats-title": "DRIVING STATISTICS OF",
|
||||||
|
|
||||||
|
"stats-timetables": "TIMETABLES",
|
||||||
|
"stats-longest-timetable": "LONGEST TIMETABLE",
|
||||||
|
"stats-avg-timetable": "AVERAGE TIMETABLE LENGTH",
|
||||||
|
"stats-distance": "DISTANCE",
|
||||||
|
"stats-stations": "STATIONS",
|
||||||
|
|
||||||
|
"timetable-stats-total": "Today, dispatchers made so far {count} with total distance of {distance}",
|
||||||
|
"timetable-stats-longest": "The longest timetable today is #{id} made by {author} for {driver} - {distance}",
|
||||||
|
"timetable-stats-most-active": "The most active dispatcher today is {dispatcher} who created {count}",
|
||||||
|
"timetable-stats-most-active-many": "The most active dispatchers today are {dispatchers} who created {count} each",
|
||||||
|
|
||||||
|
"timetable-count": "timetable | timetables",
|
||||||
|
|
||||||
|
"daily-stats-title": "DAILY STATS",
|
||||||
|
"daily-stats-info": "Today's statistics are unavailable yet!",
|
||||||
|
|
||||||
|
"driver-stats-title": "DRIVER STATS",
|
||||||
|
"driver-stats-info": "Enter a proper nickname into filters [F] to see user's driving statistics!",
|
||||||
|
|
||||||
|
"stats-loading": "Fetching statistics...",
|
||||||
|
"stats-error": "Oops! An unexpected error occurred while trying to fetch statistics! :/"
|
||||||
},
|
},
|
||||||
"scenery": {
|
"scenery": {
|
||||||
"users": "PLAYERS ONLINE",
|
"users": "PLAYERS ONLINE",
|
||||||
@@ -266,6 +313,13 @@
|
|||||||
"timetable-author-title": "Issued by",
|
"timetable-author-title": "Issued by",
|
||||||
"timetable-author-unknown": "Author unknown",
|
"timetable-author-unknown": "Author unknown",
|
||||||
|
|
||||||
|
"timetables-history-id": "ID",
|
||||||
|
"timetables-history-number": "Number",
|
||||||
|
"timetables-history-route": "Route",
|
||||||
|
"timetables-history-driver": "Driver",
|
||||||
|
"timetables-history-author": "TT author",
|
||||||
|
"timetables-history-date": "Date",
|
||||||
|
|
||||||
"req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required",
|
"req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required",
|
||||||
"history-list-empty": "No recorded scenery history!",
|
"history-list-empty": "No recorded scenery history!",
|
||||||
|
|
||||||
@@ -281,12 +335,8 @@
|
|||||||
},
|
},
|
||||||
"timetables": {
|
"timetables": {
|
||||||
"timetable-only": "Switch to timetable-only view",
|
"timetable-only": "Switch to timetable-only view",
|
||||||
"online": "At station",
|
"end": "Timetable terminates here",
|
||||||
"departed": "Dispatched to:",
|
"terminated": "Timetable terminated",
|
||||||
"departed-away": "Departed to:",
|
|
||||||
"arriving": "Arriving from:",
|
|
||||||
"stopped": "Stopped",
|
|
||||||
"terminated": "Terminated",
|
|
||||||
"begins": "BEGINS HERE",
|
"begins": "BEGINS HERE",
|
||||||
"terminates": "TERMINATES\nHERE"
|
"terminates": "TERMINATES\nHERE"
|
||||||
},
|
},
|
||||||
|
|||||||
+107
-55
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"general": {
|
||||||
|
"and": " oraz ",
|
||||||
|
"refresh": "ODŚWIEŻ"
|
||||||
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"sceneries": "SCENERIE",
|
"sceneries": "SCENERIE",
|
||||||
"trains": "POCIĄGI",
|
"trains": "POCIĄGI",
|
||||||
@@ -8,17 +12,20 @@
|
|||||||
"error": "Wystąpił problem z załadowaniem danych!",
|
"error": "Wystąpił problem z załadowaniem danych!",
|
||||||
"no-result": "Brak wyników o podanych kryteriach!",
|
"no-result": "Brak wyników o podanych kryteriach!",
|
||||||
"migration-warning": "Usługi Stacjownika będą niedostępne w godzinach 1:00-3:00 2 czerwca 2022r. z powodu migracji hostingów API!",
|
"migration-warning": "Usługi Stacjownika będą niedostępne w godzinach 1:00-3:00 2 czerwca 2022r. z powodu migracji hostingów API!",
|
||||||
"migration-confirm": "Przyjąłem!"
|
"migration-confirm": "Przyjąłem!",
|
||||||
|
"offline": "Aplikacja w trybie offline!"
|
||||||
},
|
},
|
||||||
|
|
||||||
"update": {
|
"update": {
|
||||||
"title": "Nowa wersja Stacjownika jest dostępna!",
|
"title": "Nowa wersja Stacjownika jest dostępna!",
|
||||||
"paragraph1": "Miłego korzystania z aplikacji i niech S2 będzie z wami!",
|
"paragraph1": "Miłego korzystania z aplikacji i niech S2 będzie z wami!",
|
||||||
"release-link": "Kliknij, aby przejrzeć listę zmian (GitHub)",
|
"release-link": "Kliknij, aby przejrzeć listę zmian (GitHub)",
|
||||||
"confirm-button": "Przyjąłem!"
|
"confirm-button": "ZAKTUALIZUJ",
|
||||||
|
"later-button": "PÓŹNIEJ"
|
||||||
},
|
},
|
||||||
|
|
||||||
"data-status": {
|
"data-status": {
|
||||||
|
"S1-offline": "<b>Sygnał S1</b> <br> Aplikacja działa w trybie offline!",
|
||||||
"S1a-connection": "<b>Sygnał S1a</b> <br> Błąd podczas próby połączenia się z API Stacjownika!",
|
"S1a-connection": "<b>Sygnał S1a</b> <br> Błąd podczas próby połączenia się z API Stacjownika!",
|
||||||
"S1a-sceneries": "<b>Sygnał S1a</b> <br> Błąd podczas pobierania danych o sceneriach online!",
|
"S1a-sceneries": "<b>Sygnał S1a</b> <br> Błąd podczas pobierania danych o sceneriach online!",
|
||||||
"S2": "<b>Sygnał S2</b> <br> Pomyślnie załadowano dane!",
|
"S2": "<b>Sygnał S2</b> <br> Pomyślnie załadowano dane!",
|
||||||
@@ -74,7 +81,54 @@
|
|||||||
},
|
},
|
||||||
"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-id": "id rozkładu",
|
||||||
|
|
||||||
|
"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 +172,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 +187,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 +207,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 +230,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 +242,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 +256,41 @@
|
|||||||
"timetable-day": "Rozkład z dnia",
|
"timetable-day": "Rozkład z dnia",
|
||||||
"timetable-active": "AKTYWNY",
|
"timetable-active": "AKTYWNY",
|
||||||
"timetable-fulfilled": "WYPEŁNIONY",
|
"timetable-fulfilled": "WYPEŁNIONY",
|
||||||
"timetable-abandoned": "PORZUCONY"
|
"timetable-abandoned": "PORZUCONY",
|
||||||
|
|
||||||
|
"stock-info": "INFORMACJE O SKŁADZIE",
|
||||||
|
"stock-length": "Długość",
|
||||||
|
"stock-mass": "Masa",
|
||||||
|
"stock-max-speed": "Maks. zarejestrowana prędkość",
|
||||||
|
|
||||||
|
"load-data": "Pobierz dalszą historię...",
|
||||||
|
|
||||||
|
"stats-title": "STATYSTYKI MASZYNISTY",
|
||||||
|
|
||||||
|
"last-seen-at": "Ostatnio widziany na: ",
|
||||||
|
"currently-at": "Obecnie na scenerii: ",
|
||||||
|
|
||||||
|
"stats-timetables": "ROZKŁADY JAZDY",
|
||||||
|
"stats-longest-timetable": "NAJDŁUŻSZY RJ",
|
||||||
|
"stats-avg-timetable": "ŚREDNIA DŁUGOŚĆ RJ",
|
||||||
|
"stats-distance": "DYSTANS",
|
||||||
|
"stats-stations": "STACJE",
|
||||||
|
|
||||||
|
"timetable-stats-total": "Dyżurni stworzyli dziś {count} o łącznym dystansie {distance}",
|
||||||
|
"timetable-stats-longest": "Najdłuższym rozkładem jazdy jest dzisiaj #{id} stworzony przez dyżurnego {author} dla maszynisty {driver} - {distance}",
|
||||||
|
"timetable-stats-most-active": "Dzisiejszym najaktywniejszym dyżurnym jest {dispatcher}, który stworzył {count}",
|
||||||
|
"timetable-stats-most-active-many": "Dzisiejszymi najaktywniejszymi dyżurnymi są {dispatchers}, którzy stworzyli po {count}",
|
||||||
|
|
||||||
|
"timetable-count": "rozkład jazdy | rozkładów jazdy",
|
||||||
|
|
||||||
|
"daily-stats-title": "STATYSTYKI DNIA",
|
||||||
|
"daily-stats-info": "Dzisiejsze statystyki nie są jeszcze dostępne!",
|
||||||
|
|
||||||
|
"driver-stats-title": "STATYSTYKI GRACZA",
|
||||||
|
"driver-stats-info": "Wpisz nazwę użytkownika w filtrach [F], aby zobaczyć jego statystyki maszynisty!",
|
||||||
|
|
||||||
|
"stats-loading": "Pobieranie statystyk...",
|
||||||
|
"stats-error": "Ups! Wystąpił błąd podczas próby pobrania statystyk! :/"
|
||||||
},
|
},
|
||||||
"scenery": {
|
"scenery": {
|
||||||
"users": "GRACZE ONLINE",
|
"users": "GRACZE ONLINE",
|
||||||
@@ -268,6 +317,13 @@
|
|||||||
"timetable-author-title": "Wydany przez",
|
"timetable-author-title": "Wydany przez",
|
||||||
"timetable-author-unknown": "Autor nieznany",
|
"timetable-author-unknown": "Autor nieznany",
|
||||||
|
|
||||||
|
"timetables-history-id": "ID",
|
||||||
|
"timetables-history-number": "Numer",
|
||||||
|
"timetables-history-route": "Trasa",
|
||||||
|
"timetables-history-driver": "Maszynista",
|
||||||
|
"timetables-history-author": "Autor RJ",
|
||||||
|
"timetables-history-date": "Data",
|
||||||
|
|
||||||
"req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego",
|
"req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego",
|
||||||
"history-list-empty": "Brak historii dla tej scenerii!",
|
"history-list-empty": "Brak historii dla tej scenerii!",
|
||||||
|
|
||||||
@@ -283,12 +339,8 @@
|
|||||||
},
|
},
|
||||||
"timetables": {
|
"timetables": {
|
||||||
"timetable-only": "Wyodrębnij rozkłady jazdy",
|
"timetable-only": "Wyodrębnij rozkłady jazdy",
|
||||||
"online": "Na stacji",
|
"end": "Koniec rozkładu jazdy",
|
||||||
"departed": "Odprawiony do:",
|
"terminated": "Rozkład jazdy zakończony",
|
||||||
"departed-away": "Odjechał do:",
|
|
||||||
"arriving": "W drodze z:",
|
|
||||||
"stopped": "Postój",
|
|
||||||
"terminated": "Skończył bieg",
|
|
||||||
"begins": "ROZPOCZYNA\nBIEG",
|
"begins": "ROZPOCZYNA\nBIEG",
|
||||||
"terminates": "KOŃCZY BIEG"
|
"terminates": "KOŃCZY BIEG"
|
||||||
},
|
},
|
||||||
|
|||||||
+11
@@ -7,9 +7,11 @@ import plLang from './locales/pl.json';
|
|||||||
|
|
||||||
import { createI18n } from 'vue-i18n';
|
import { createI18n } from 'vue-i18n';
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
|
import { registerSW } from 'virtual:pwa-register';
|
||||||
|
|
||||||
const i18n = createI18n({
|
const i18n = createI18n({
|
||||||
locale: 'pl',
|
locale: 'pl',
|
||||||
|
legacy: false,
|
||||||
fallbackLocale: 'pl',
|
fallbackLocale: 'pl',
|
||||||
messages: {
|
messages: {
|
||||||
en: enLang,
|
en: enLang,
|
||||||
@@ -18,6 +20,15 @@ const i18n = createI18n({
|
|||||||
enableLegacy: false,
|
enableLegacy: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registerSW({
|
||||||
|
onRegistered(r) {
|
||||||
|
r &&
|
||||||
|
setInterval(() => {
|
||||||
|
r.update();
|
||||||
|
}, 60 * 60 * 1000);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const clickOutsideDirective: Directive = {
|
const clickOutsideDirective: Directive = {
|
||||||
mounted(el, binding) {
|
mounted(el, binding) {
|
||||||
el.clickOutsideEvent = (event: Event) => {
|
el.clickOutsideEvent = (event: Event) => {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -2,16 +2,12 @@ import { defineComponent } from 'vue';
|
|||||||
import { useStore } from '../store/store';
|
import { useStore } from '../store/store';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
data() {
|
||||||
return {
|
return {
|
||||||
store: useStore(),
|
store: useStore(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
|
||||||
console.log('Mixin mounted');
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
chosenTrain() {
|
chosenTrain() {
|
||||||
return this.store.trainList.find((train) => train.trainId == this.store.chosenModalTrainId);
|
return this.store.trainList.find((train) => train.trainId == this.store.chosenModalTrainId);
|
||||||
@@ -21,10 +17,15 @@ export default defineComponent({
|
|||||||
methods: {
|
methods: {
|
||||||
selectModalTrain(trainId: string) {
|
selectModalTrain(trainId: string) {
|
||||||
this.store.chosenModalTrainId = trainId;
|
this.store.chosenModalTrainId = trainId;
|
||||||
|
document.body.classList.add('no-scroll');
|
||||||
},
|
},
|
||||||
|
|
||||||
closeModal() {
|
closeModal() {
|
||||||
this.store.chosenModalTrainId = undefined;
|
this.store.chosenModalTrainId = undefined;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.classList.remove('no-scroll');
|
||||||
|
}, 150);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
activated() {
|
activated() {
|
||||||
window.addEventListener('scroll', this.handleScroll);
|
window.addEventListener('wheel', this.handleScroll);
|
||||||
},
|
},
|
||||||
|
|
||||||
deactivated() {
|
deactivated() {
|
||||||
window.removeEventListener('scroll', this.handleScroll);
|
window.removeEventListener('wheel', this.handleScroll);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,10 +5,16 @@ export default defineComponent({
|
|||||||
calculateExpStyle(exp: number, isSupporter = false): string {
|
calculateExpStyle(exp: number, isSupporter = false): string {
|
||||||
const bgColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 85%, 50%)`) : '#666';
|
const bgColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 85%, 50%)`) : '#666';
|
||||||
|
|
||||||
const fontColor = exp > 15 || exp == -1 ? 'white' : 'black';
|
const fontColor = exp > 14 || exp == -1 ? 'white' : 'black';
|
||||||
const boxShadow = isSupporter ? `box-shadow: 0 0 10px 2px ${bgColor};` : '';
|
const boxShadow = isSupporter ? `box-shadow: 0 0 6px 2px ${bgColor};` : '';
|
||||||
|
|
||||||
return `background-color: ${bgColor}; color: ${fontColor}; ${boxShadow}`;
|
return `background-color: ${bgColor}; color: ${fontColor}; ${boxShadow};`;
|
||||||
|
},
|
||||||
|
|
||||||
|
calculateTextExpStyle(exp: number, isSupporter = false): string {
|
||||||
|
const textColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 75%, 50%)`) : '#666';
|
||||||
|
|
||||||
|
return `color: ${textColor}; ${isSupporter ? 'text-shadow: 0 0 6px ' + textColor : ''};`;
|
||||||
},
|
},
|
||||||
|
|
||||||
statusClasses(occupiedTo: string) {
|
statusClasses(occupiedTo: string) {
|
||||||
@@ -41,6 +47,6 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return className;
|
return className;
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { useRegisterSW } from 'virtual:pwa-register/vue';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const { needRefresh, updateServiceWorker, offlineReady } = useRegisterSW({
|
||||||
|
immediate: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
needRefresh,
|
||||||
|
updateServiceWorker,
|
||||||
|
offlineReady,
|
||||||
|
};
|
||||||
|
};
|
||||||
+11
-21
@@ -1,6 +1,6 @@
|
|||||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
|
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
|
||||||
import JournalDispatchersVue from '../components/JournalView/JournalDispatchers.vue';
|
import JournalDispatchersVue from '../views/JournalDispatchers.vue';
|
||||||
import JournalTimetablesVue from '../components/JournalView/JournalTimetables.vue';
|
import JournalTimetablesVue from '../views/JournalTimetables.vue';
|
||||||
|
|
||||||
const routes: Array<RouteRecordRaw> = [
|
const routes: Array<RouteRecordRaw> = [
|
||||||
{
|
{
|
||||||
@@ -12,33 +12,19 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
path: '/trains',
|
path: '/trains',
|
||||||
name: 'TrainsView',
|
name: 'TrainsView',
|
||||||
component: () => import('../views/TrainsView.vue'),
|
component: () => import('../views/TrainsView.vue'),
|
||||||
props: (route) => ({ train: route.query.train, driver: route.query.driver }),
|
props: (route) => ({ train: route.query.train, driver: route.query.driver, trainId: route.query.trainId }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/scenery',
|
path: '/scenery',
|
||||||
name: 'SceneryView',
|
name: 'SceneryView',
|
||||||
component: () => import('../views/SceneryView.vue'),
|
component: () => import('../views/SceneryView.vue'),
|
||||||
props: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/journal',
|
path: '/journal',
|
||||||
name: 'JournalView',
|
redirect: '/journal/timetables'
|
||||||
component: () => import('../views/JournalView.vue'),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
name: 'JournalTimetables',
|
|
||||||
component: JournalTimetablesVue,
|
|
||||||
alias: '/timetables',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'dispatchers',
|
path: '/journal/timetables',
|
||||||
name: 'JournalDispatchers',
|
|
||||||
component: JournalDispatchersVue,
|
|
||||||
props: (route) => ({ sceneryName: route.query.sceneryName, dispatcherName: route.query.dispatcherName }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'timetables',
|
|
||||||
name: 'JournalTimetables',
|
name: 'JournalTimetables',
|
||||||
component: JournalTimetablesVue,
|
component: JournalTimetablesVue,
|
||||||
props: (route) => ({
|
props: (route) => ({
|
||||||
@@ -47,7 +33,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
timetableId: route.query.timetableId,
|
timetableId: route.query.timetableId,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
],
|
{
|
||||||
|
path: '/journal/dispatchers',
|
||||||
|
name: 'JournalDispatchers',
|
||||||
|
component: JournalDispatchersVue,
|
||||||
|
props: (route) => ({ sceneryName: route.query.sceneryName, dispatcherName: route.query.dispatcherName }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/:catchAll(.*)',
|
path: '/:catchAll(.*)',
|
||||||
@@ -59,7 +49,7 @@ const router = createRouter({
|
|||||||
scrollBehavior(to, from) {
|
scrollBehavior(to, from) {
|
||||||
if (to.name == 'SceneryView' && from.name) return { el: `.app_main` };
|
if (to.name == 'SceneryView' && from.name) return { el: `.app_main` };
|
||||||
|
|
||||||
if (from.name == 'SceneryView' && to.name == 'StationsView') return { el: `.last-selected`, top: 20 };
|
// if (from.name == 'SceneryView' && to.name == 'StationsView') return { el: `.last-selected`, top: 20 };
|
||||||
},
|
},
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes,
|
routes,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const enum DataStatus {
|
export enum DataStatus {
|
||||||
Initialized = -1,
|
Initialized = -1,
|
||||||
Loading = 0,
|
Loading = 0,
|
||||||
Error = 1,
|
Error = 1,
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ export default interface ScheduledTrain {
|
|||||||
arrivingLine: string | null;
|
arrivingLine: string | null;
|
||||||
departureLine: string | null;
|
departureLine: string | null;
|
||||||
|
|
||||||
|
prevDepartureLine: string | null;
|
||||||
|
nextArrivalLine: string | null;
|
||||||
|
|
||||||
|
signal: string;
|
||||||
|
connectedTrack: string;
|
||||||
|
|
||||||
stopLabel: string;
|
stopLabel: string;
|
||||||
stopStatus: string;
|
stopStatus: string;
|
||||||
stopStatusID: number;
|
stopStatusID: number;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export default interface Station {
|
|||||||
|
|
||||||
lines: string;
|
lines: string;
|
||||||
project: string;
|
project: string;
|
||||||
|
projectUrl?: string;
|
||||||
|
|
||||||
signalType: string;
|
signalType: string;
|
||||||
controlType: string;
|
controlType: string;
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
export default interface StationRoutes {
|
export default interface StationRoutes {
|
||||||
oneWay:
|
oneWay: {
|
||||||
{
|
|
||||||
name: string;
|
name: string;
|
||||||
catenary: boolean;
|
catenary: boolean;
|
||||||
SBL: boolean;
|
SBL: boolean;
|
||||||
TWB: boolean;
|
TWB: boolean;
|
||||||
isInternal: boolean;
|
isInternal: boolean;
|
||||||
tracks: number;
|
tracks: number;
|
||||||
|
speed: number;
|
||||||
|
length: number;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
twoWay: {
|
twoWay: {
|
||||||
@@ -16,6 +17,8 @@ export default interface StationRoutes {
|
|||||||
TWB: boolean;
|
TWB: boolean;
|
||||||
isInternal: boolean;
|
isInternal: boolean;
|
||||||
tracks: number;
|
tracks: number;
|
||||||
|
speed: number;
|
||||||
|
length: number;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
/* [catenary, noCatenary] */
|
/* [catenary, noCatenary] */
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export default interface Train {
|
|||||||
driverId: number;
|
driverId: number;
|
||||||
trainNo: number;
|
trainNo: number;
|
||||||
driverName: string;
|
driverName: string;
|
||||||
|
driverLevel: number;
|
||||||
currentStationName: string;
|
currentStationName: string;
|
||||||
currentStationHash: string;
|
currentStationHash: string;
|
||||||
locoURL: string;
|
locoURL: string;
|
||||||
@@ -19,9 +20,11 @@ export default interface Train {
|
|||||||
online: boolean;
|
online: boolean;
|
||||||
lastSeen: number;
|
lastSeen: number;
|
||||||
region: string;
|
region: string;
|
||||||
|
|
||||||
cars: string[];
|
cars: string[];
|
||||||
|
|
||||||
|
isTimeout: boolean;
|
||||||
|
isSupporter: boolean;
|
||||||
|
|
||||||
timetableData?: {
|
timetableData?: {
|
||||||
timetableId: number;
|
timetableId: number;
|
||||||
category: string;
|
category: string;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
export interface DispatcherHistory {
|
export interface DispatcherHistory {
|
||||||
|
id: string;
|
||||||
|
|
||||||
currentDuration: number;
|
currentDuration: number;
|
||||||
dispatcherId: number;
|
dispatcherId: number;
|
||||||
dispatcherName: string;
|
dispatcherName: string;
|
||||||
|
dispatcherLevel: number | null;
|
||||||
|
dispatcherIsSupporter: boolean;
|
||||||
isOnline: boolean;
|
isOnline: boolean;
|
||||||
lastOnlineTimestamp: number;
|
lastOnlineTimestamp: number;
|
||||||
region: string;
|
region: string;
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { TimetableHistory } from './TimetablesAPIData';
|
||||||
|
|
||||||
|
export interface ITimetablesDailyStats {
|
||||||
|
totalTimetables: number;
|
||||||
|
distanceSum: number;
|
||||||
|
distanceAvg: number;
|
||||||
|
|
||||||
|
timetableId: number;
|
||||||
|
timetableAuthor: string;
|
||||||
|
timetableDriver: string;
|
||||||
|
timetableRouteDistance: number;
|
||||||
|
|
||||||
|
mostActiveDispatchers: {
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITimetablesDailyStatsResponse {
|
||||||
|
totalTimetables: number;
|
||||||
|
distanceSum: number;
|
||||||
|
distanceAvg: number;
|
||||||
|
maxTimetable: TimetableHistory | null;
|
||||||
|
|
||||||
|
mostActiveDispatchers: {
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
export interface TimetableHistory {
|
export interface TimetableHistory {
|
||||||
|
id: number;
|
||||||
|
|
||||||
timetableId: number;
|
timetableId: number;
|
||||||
trainNo: number;
|
trainNo: number;
|
||||||
trainCategoryCode: string;
|
trainCategoryCode: string;
|
||||||
|
|
||||||
driverId: number;
|
driverId: number;
|
||||||
driverName: string;
|
driverName: string;
|
||||||
|
driverLevel: number | null;
|
||||||
|
driverIsSupporter: boolean;
|
||||||
|
|
||||||
route: string;
|
route: string;
|
||||||
twr: number;
|
twr: number;
|
||||||
skr: number;
|
skr: number;
|
||||||
@@ -26,6 +32,15 @@ export interface TimetableHistory {
|
|||||||
|
|
||||||
authorName?: string;
|
authorName?: string;
|
||||||
authorId?: number;
|
authorId?: number;
|
||||||
|
|
||||||
|
stockString?: string;
|
||||||
|
stockMass?: number;
|
||||||
|
stockLength?: number;
|
||||||
|
maxSpeed?: number;
|
||||||
|
|
||||||
|
hashesString?: string;
|
||||||
|
currentSceneryName?: string;
|
||||||
|
currentSceneryHash?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SceneryTimetableHistory {
|
export interface SceneryTimetableHistory {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default interface TrainAPIData {
|
|||||||
driverName: string;
|
driverName: string;
|
||||||
driverId: number;
|
driverId: number;
|
||||||
driverIsSupporter: boolean;
|
driverIsSupporter: boolean;
|
||||||
|
driverLevel?: number;
|
||||||
|
|
||||||
currentStationName: string;
|
currentStationName: string;
|
||||||
currentStationHash: string;
|
currentStationHash: string;
|
||||||
@@ -21,6 +22,7 @@ export default interface TrainAPIData {
|
|||||||
lastSeen: number;
|
lastSeen: number;
|
||||||
|
|
||||||
region: string;
|
region: string;
|
||||||
|
isTimeout: boolean;
|
||||||
|
|
||||||
timetable?: {
|
timetable?: {
|
||||||
timetableId: number;
|
timetableId: number;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
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;
|
||||||
@@ -17,19 +17,18 @@ function currentDelay(stops: TrainStop[] | undefined) {
|
|||||||
?.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;
|
||||||
@@ -49,18 +48,25 @@ function filterTrainList(trainList: Train[], searchedTrain: string, searchedDriv
|
|||||||
default:
|
default:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
return (searchedTrain.length > 0 ? train.trainNo.toString().startsWith(searchedTrain) : true) &&
|
|
||||||
(searchedDriver.length > 0 ? train.driverName.toLowerCase().startsWith(searchedDriver.toLowerCase()) : true) && isFiltered
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
(searchedTrain.length > 0 ? train.trainNo.toString().startsWith(searchedTrain) : true) &&
|
||||||
|
(searchedDriver.length > 0 ? train.driverName.toLowerCase().startsWith(searchedDriver.toLowerCase()) : true) &&
|
||||||
|
(!train.timetableData ? !train.online : 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 'id':
|
||||||
|
if ((a.timetableData?.timetableId || -1) > (b.timetableData?.timetableId || -1)) return sorterActive.dir;
|
||||||
|
|
||||||
|
return -sorterActive.dir;
|
||||||
|
|
||||||
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;
|
||||||
@@ -109,7 +115,6 @@ export function filteredTrainList(
|
|||||||
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,9 +1,5 @@
|
|||||||
export const URLs = {
|
export const URLs = {
|
||||||
stacjownikAPI:
|
stacjownikAPI:
|
||||||
import.meta.env.VITE_APP_API_DEV == 1 && !import.meta.env.PROD
|
import.meta.env.VITE_APP_API_DEV == 1 && !import.meta.env.PROD ? 'http://localhost:3000' : 'https://spythere.pl',
|
||||||
? 'http://localhost:3000'
|
|
||||||
: 'https://stacjownik.eu-4.evennode.com',
|
|
||||||
stacjownikAPIDev: 'localhost:3000',
|
stacjownikAPIDev: 'localhost:3000',
|
||||||
// trains: "https://api.td2.info.pl:9640/?method=getTrainsOnline",
|
|
||||||
// getTimetableURL: (trainNo: string | number, region = "eu") => `https://api.td2.info.pl:9640/?method=readFromSWDR&value=getTimetable%3B${trainNo}%3B${region}`
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -117,31 +117,37 @@ export function getScheduledTrain(train: Train, trainStopIndex: number, stationN
|
|||||||
let prevStationName = '',
|
let prevStationName = '',
|
||||||
nextStationName = '';
|
nextStationName = '';
|
||||||
|
|
||||||
|
let prevDepartureLine: string | null = null,
|
||||||
|
nextArrivalLine: string | null = null;
|
||||||
|
|
||||||
for (let i = trainStopIndex - 1; i >= 0; i--) {
|
for (let i = trainStopIndex - 1; i >= 0; i--) {
|
||||||
if (/strong|podg/g.test(followingStops[i].stopName)) {
|
if (/strong|podg/g.test(followingStops[i].stopName)) {
|
||||||
prevStationName = followingStops[i].stopNameRAW;
|
prevStationName = followingStops[i].stopNameRAW.replace(/,.*/g,"");
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = trainStopIndex + 1; i < followingStops.length; i++) {
|
for (let i = trainStopIndex + 1; i < followingStops.length; i++) {
|
||||||
if (/strong|podg/g.test(followingStops[i].stopName)) {
|
if (/strong|podg/g.test(followingStops[i].stopName)) {
|
||||||
nextStationName = followingStops[i].stopNameRAW;
|
nextStationName = followingStops[i].stopNameRAW.replace(/,.*/g,"");
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let departureLine: string | null = trainStop.departureLine;
|
let departureLine: string | null = null;
|
||||||
let arrivingLine: string | null = trainStop.arrivalLine;
|
let arrivingLine: string | null = null;
|
||||||
|
|
||||||
for (let i = trainStopIndex; i < followingStops.length; i++) {
|
for (let i = trainStopIndex; i < followingStops.length; i++) {
|
||||||
const currentStop = followingStops[i];
|
const currentStop = followingStops[i];
|
||||||
|
|
||||||
if (currentStop.departureLine == null) break;
|
if (currentStop.departureLine == null) continue;
|
||||||
|
|
||||||
if (!/-|_|it|sbl/gi.test(currentStop.departureLine)) {
|
if (!/-|_|it|sbl/gi.test(currentStop.departureLine)) {
|
||||||
departureLine = currentStop.departureLine;
|
departureLine = currentStop.departureLine;
|
||||||
|
nextArrivalLine = followingStops[i + 1]?.arrivalLine || null;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,10 +155,12 @@ export function getScheduledTrain(train: Train, trainStopIndex: number, stationN
|
|||||||
for (let i = trainStopIndex; i >= 0; i--) {
|
for (let i = trainStopIndex; i >= 0; i--) {
|
||||||
const currentStop = followingStops[i];
|
const currentStop = followingStops[i];
|
||||||
|
|
||||||
if (currentStop.arrivalLine == null) break;
|
if (currentStop.arrivalLine == null) continue;
|
||||||
|
|
||||||
if (!/-|_|it|sbl/gi.test(currentStop.arrivalLine)) {
|
if (!/-|_|it|sbl/gi.test(currentStop.arrivalLine)) {
|
||||||
arrivingLine = currentStop.arrivalLine;
|
arrivingLine = currentStop.arrivalLine;
|
||||||
|
prevDepartureLine = followingStops[i - 1]?.departureLine || null;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,6 +169,10 @@ export function getScheduledTrain(train: Train, trainStopIndex: number, stationN
|
|||||||
trainNo: train.trainNo,
|
trainNo: train.trainNo,
|
||||||
trainId: train.trainId,
|
trainId: train.trainId,
|
||||||
|
|
||||||
|
signal: train.signal,
|
||||||
|
connectedTrack: train.connectedTrack,
|
||||||
|
|
||||||
|
|
||||||
driverName: train.driverName,
|
driverName: train.driverName,
|
||||||
driverId: train.driverId,
|
driverId: train.driverId,
|
||||||
currentStationName: train.currentStationName,
|
currentStationName: train.currentStationName,
|
||||||
@@ -179,5 +191,8 @@ export function getScheduledTrain(train: Train, trainStopIndex: number, stationN
|
|||||||
|
|
||||||
arrivingLine,
|
arrivingLine,
|
||||||
departureLine,
|
departureLine,
|
||||||
|
|
||||||
|
nextArrivalLine,
|
||||||
|
prevDepartureLine,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
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';
|
||||||
|
import StorageManager from '../scripts/managers/storageManager';
|
||||||
|
import { useStore } from './store';
|
||||||
|
|
||||||
const sortStations = (a: Station, b: Station, sorter: { index: number; dir: number }) => {
|
const sortStations = (a: Station, b: Station, sorter: { index: number; dir: number }) => {
|
||||||
switch (sorter.index) {
|
switch (sorter.index) {
|
||||||
|
case 0:
|
||||||
|
return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
|
||||||
|
|
||||||
case 1:
|
case 1:
|
||||||
if ((a.generalInfo?.reqLevel || 0) > (b.generalInfo?.reqLevel || 0)) return sorter.dir;
|
if ((a.generalInfo?.reqLevel || 0) > (b.generalInfo?.reqLevel || 0)) return sorter.dir;
|
||||||
if ((a.generalInfo?.reqLevel || 0) < (b.generalInfo?.reqLevel || 0)) return -sorter.dir;
|
if ((a.generalInfo?.reqLevel || 0) < (b.generalInfo?.reqLevel || 0)) return -sorter.dir;
|
||||||
@@ -50,10 +56,10 @@ const sortStations = (a: Station, b: Station, sorter: { index: number; dir: numb
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
|
return a.name.localeCompare(b.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterStations = (station: Station, filters: Filter) => {
|
const filterStations = (station: Station, filters: Filter, isOffline = false) => {
|
||||||
const returnMode = false;
|
const returnMode = false;
|
||||||
|
|
||||||
if ((station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && filters['nonPublic'])
|
if ((station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && filters['nonPublic'])
|
||||||
@@ -91,7 +97,7 @@ const filterStations = (station: Station, filters: Filter) => {
|
|||||||
const routes = station.generalInfo.routes;
|
const routes = station.generalInfo.routes;
|
||||||
const availability = station.generalInfo.availability;
|
const availability = station.generalInfo.availability;
|
||||||
|
|
||||||
if (filters['abandoned'] && availability == 'abandoned') return returnMode;
|
if (filters['abandoned'] && availability == 'abandoned' && !station.onlineInfo) return returnMode;
|
||||||
|
|
||||||
if (availability == 'default' && filters['default']) return returnMode;
|
if (availability == 'default' && filters['default']) return returnMode;
|
||||||
if (
|
if (
|
||||||
@@ -183,8 +189,7 @@ const filterStations = (station: Station, filters: Filter) => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class StationFilterManager {
|
const filterInitStates: Filter = {
|
||||||
private filterInitStates: Filter = {
|
|
||||||
default: false,
|
default: false,
|
||||||
notDefault: false,
|
notDefault: false,
|
||||||
real: false,
|
real: false,
|
||||||
@@ -226,26 +231,17 @@ export default class StationFilterManager {
|
|||||||
onlineFromHours: 0,
|
onlineFromHours: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
private filters: Filter = { ...this.filterInitStates };
|
export const useStationFiltersStore = defineStore('stationFiltersStore', {
|
||||||
|
state() {
|
||||||
private sorter: { index: number; dir: number } = { index: 0, dir: 1 };
|
return {
|
||||||
|
inputs: inputData,
|
||||||
checkFilters() {
|
filters: { ...filterInitStates },
|
||||||
if (!StorageManager.isRegistered('options_saved')) return;
|
sorterActive: { index: 0, dir: 1 },
|
||||||
|
store: useStore(),
|
||||||
Object.keys(this.filterInitStates).forEach((filterKey) => {
|
};
|
||||||
if (StorageManager.isRegistered(filterKey)) return;
|
},
|
||||||
|
|
||||||
const filterType = typeof this.filterInitStates[filterKey];
|
|
||||||
|
|
||||||
if (filterType === 'boolean')
|
|
||||||
StorageManager.setBooleanValue(filterKey, !this.filterInitStates[filterKey] as boolean);
|
|
||||||
|
|
||||||
if (filterType === 'number')
|
|
||||||
StorageManager.setNumericValue(filterKey, this.filterInitStates[filterKey] as number);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
actions: {
|
||||||
getFilteredStationList(stationList: Station[], region: string): Station[] {
|
getFilteredStationList(stationList: Station[], region: string): Station[] {
|
||||||
return stationList
|
return stationList
|
||||||
.map((station) => {
|
.map((station) => {
|
||||||
@@ -255,38 +251,58 @@ export default class StationFilterManager {
|
|||||||
|
|
||||||
return station;
|
return station;
|
||||||
})
|
})
|
||||||
.filter((station) => filterStations(station, this.filters))
|
.filter((station) => filterStations(station, this.filters, this.store.isOffline))
|
||||||
.sort((a, b) => sortStations(a, b, this.sorter));
|
.sort((a, b) => sortStations(a, b, this.sorterActive));
|
||||||
}
|
},
|
||||||
|
|
||||||
changeFilterValue(filter: { name: string; value: number }) {
|
setupFilters() {
|
||||||
|
if (!StorageManager.isRegistered('options_saved')) return;
|
||||||
|
|
||||||
|
this.inputs.options.forEach((option) => {
|
||||||
|
if (!StorageManager.isRegistered(option.id)) return;
|
||||||
|
const savedValue = StorageManager.getBooleanValue(option.id);
|
||||||
|
|
||||||
|
this.filters[option.id] = savedValue;
|
||||||
|
option.value = !savedValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.inputs.sliders.forEach((slider) => {
|
||||||
|
if (!StorageManager.isRegistered(slider.name)) return;
|
||||||
|
const savedValue = StorageManager.getNumericValue(slider.name);
|
||||||
|
|
||||||
|
this.filters[slider.name] = savedValue;
|
||||||
|
slider.value = savedValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
changeFilterValue(filter: { name: string; value: any }) {
|
||||||
this.filters[filter.name] = filter.value;
|
this.filters[filter.name] = filter.value;
|
||||||
|
|
||||||
// if(filter.name == 'authors')
|
if (StorageManager.isRegistered('options_saved')) StorageManager.setValue(filter.name, filter.value);
|
||||||
}
|
},
|
||||||
|
|
||||||
resetFilters() {
|
resetFilters() {
|
||||||
this.filters = { ...this.filterInitStates };
|
this.filters = { ...filterInitStates };
|
||||||
}
|
|
||||||
|
|
||||||
invertFilters() {
|
this.inputs.options.forEach((option) => {
|
||||||
Object.keys(this.filters).forEach((prop) => {
|
option.value = option.defaultValue;
|
||||||
if (typeof this.filters[prop] !== 'boolean') return;
|
StorageManager.setBooleanValue(option.name, !option.defaultValue);
|
||||||
|
|
||||||
this.filters[prop] = !this.filters[prop];
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
this.inputs.sliders.forEach((slider) => {
|
||||||
|
slider.value = slider.defaultValue;
|
||||||
|
StorageManager.setNumericValue(slider.name, slider.defaultValue);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
changeSorter(index: number) {
|
changeSorter(index: number) {
|
||||||
if (index > 4 && index < 7) return;
|
if (index > 4 && index < 7) return;
|
||||||
|
|
||||||
if (index == this.sorter.index) this.sorter.dir = -1 * this.sorter.dir;
|
if (index == this.sorterActive.index) this.sorterActive.dir = -1 * this.sorterActive.dir;
|
||||||
else this.sorter.dir = 1;
|
else this.sorterActive.dir = 1;
|
||||||
|
|
||||||
this.sorter.index = index;
|
this.sorterActive.index = index;
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
getSorter() {
|
|
||||||
return this.sorter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+31
-8
@@ -24,6 +24,7 @@ export const useStore = defineStore('store', {
|
|||||||
|
|
||||||
stationList: [],
|
stationList: [],
|
||||||
trainList: [],
|
trainList: [],
|
||||||
|
routesList: [],
|
||||||
|
|
||||||
sceneryData: [],
|
sceneryData: [],
|
||||||
lastDispatcherStatuses: [],
|
lastDispatcherStatuses: [],
|
||||||
@@ -34,12 +35,14 @@ export const useStore = defineStore('store', {
|
|||||||
stationCount: 0,
|
stationCount: 0,
|
||||||
|
|
||||||
webSocket: undefined,
|
webSocket: undefined,
|
||||||
|
isOffline: false,
|
||||||
|
|
||||||
dispatcherStatsName: '',
|
dispatcherStatsName: '',
|
||||||
dispatcherStatsData: undefined,
|
dispatcherStatsData: undefined,
|
||||||
|
|
||||||
driverStatsName: '',
|
driverStatsName: '',
|
||||||
driverStatsData: undefined,
|
driverStatsData: undefined,
|
||||||
|
driverStatsStatus: DataStatus.Initialized,
|
||||||
|
|
||||||
chosenModalTrainId: undefined,
|
chosenModalTrainId: undefined,
|
||||||
|
|
||||||
@@ -51,6 +54,9 @@ export const useStore = defineStore('store', {
|
|||||||
trains: DataStatus.Loading,
|
trains: DataStatus.Loading,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
currentStatsTab: 'daily',
|
||||||
|
|
||||||
|
blockScroll: false,
|
||||||
listenerLaunched: false,
|
listenerLaunched: false,
|
||||||
} as StoreState),
|
} as StoreState),
|
||||||
|
|
||||||
@@ -93,6 +99,10 @@ export const useStore = defineStore('store', {
|
|||||||
cars: stock.slice(1),
|
cars: stock.slice(1),
|
||||||
|
|
||||||
lastSeen: train.lastSeen,
|
lastSeen: train.lastSeen,
|
||||||
|
isTimeout: train.isTimeout,
|
||||||
|
|
||||||
|
isSupporter: train.driverIsSupporter,
|
||||||
|
driverLevel: train.driverLevel,
|
||||||
|
|
||||||
timetableData: timetable
|
timetableData: timetable
|
||||||
? {
|
? {
|
||||||
@@ -106,8 +116,8 @@ export const useStore = defineStore('store', {
|
|||||||
sceneries: timetable.sceneries,
|
sceneries: timetable.sceneries,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
} as Train;
|
||||||
}) as Train[];
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getDispatcherStatus(onlineStationData: StationAPIData) {
|
getDispatcherStatus(onlineStationData: StationAPIData) {
|
||||||
@@ -216,6 +226,14 @@ export const useStore = defineStore('store', {
|
|||||||
const onlineStationNames: string[] = [];
|
const onlineStationNames: string[] = [];
|
||||||
const prevDispatcherStatuses: StoreState['lastDispatcherStatuses'] = [];
|
const prevDispatcherStatuses: StoreState['lastDispatcherStatuses'] = [];
|
||||||
|
|
||||||
|
if (this.isOffline) {
|
||||||
|
this.stationList.forEach((station) => {
|
||||||
|
station.onlineInfo = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.apiData.stations?.forEach((stationAPIData) => {
|
this.apiData.stations?.forEach((stationAPIData) => {
|
||||||
if (stationAPIData.region !== this.region.id || !stationAPIData.isOnline) return;
|
if (stationAPIData.region !== this.region.id || !stationAPIData.isOnline) return;
|
||||||
const station = this.stationList.find((s) => s.name === stationAPIData.stationName);
|
const station = this.stationList.find((s) => s.name === stationAPIData.stationName);
|
||||||
@@ -277,7 +295,8 @@ export const useStore = defineStore('store', {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.stationList = sceneryData.map((scenery) => ({
|
this.stationList = sceneryData.map((scenery) => {
|
||||||
|
return {
|
||||||
name: scenery.name,
|
name: scenery.name,
|
||||||
|
|
||||||
generalInfo: {
|
generalInfo: {
|
||||||
@@ -298,6 +317,8 @@ export const useStore = defineStore('store', {
|
|||||||
const catenary = specs2[1] == 'E';
|
const catenary = specs2[1] == 'E';
|
||||||
const SBL = specs2[2] == 'S';
|
const SBL = specs2[2] == 'S';
|
||||||
const TWB = specs2[3] ? true : false;
|
const TWB = specs2[3] ? true : false;
|
||||||
|
const speed = Number(routeString.split(':')[1]) || 0;
|
||||||
|
const length = Number(routeString.split(':')[2]) || 0;
|
||||||
|
|
||||||
const propName = twoWay
|
const propName = twoWay
|
||||||
? catenary
|
? catenary
|
||||||
@@ -314,6 +335,8 @@ export const useStore = defineStore('store', {
|
|||||||
catenary,
|
catenary,
|
||||||
isInternal,
|
isInternal,
|
||||||
tracks: twoWay ? 2 : 1,
|
tracks: twoWay ? 2 : 1,
|
||||||
|
length,
|
||||||
|
speed,
|
||||||
});
|
});
|
||||||
if (!isInternal) acc[propName].push(name);
|
if (!isInternal) acc[propName].push(name);
|
||||||
|
|
||||||
@@ -335,20 +358,19 @@ export const useStore = defineStore('store', {
|
|||||||
? scenery.checkpoints.split(';').map((sub) => ({ checkpointName: sub, scheduledTrains: [] }))
|
? scenery.checkpoints.split(';').map((sub) => ({ checkpointName: sub, scheduledTrains: [] }))
|
||||||
: [],
|
: [],
|
||||||
},
|
},
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
connectToWebsocket() {
|
connectToWebsocket() {
|
||||||
const socket = io(URLs.stacjownikAPI, {
|
const socket = io(URLs.stacjownikAPI, {
|
||||||
transports: ['websocket', 'polling'],
|
// transports: ['websocket', 'polling'],
|
||||||
rememberUpgrade: true,
|
rememberUpgrade: true,
|
||||||
reconnection: true,
|
reconnection: true,
|
||||||
timeout: 10000,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('connect_error', (err) => {
|
socket.on('connect_error', (err) => {
|
||||||
this.dataStatuses.connection = DataStatus.Error;
|
this.dataStatuses.connection = DataStatus.Error;
|
||||||
this.webSocket = undefined;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('UPDATE', (data: APIData) => {
|
socket.on('UPDATE', (data: APIData) => {
|
||||||
@@ -358,6 +380,8 @@ export const useStore = defineStore('store', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.emit('FETCH_DATA', {}, (data: APIData) => {
|
socket.emit('FETCH_DATA', {}, (data: APIData) => {
|
||||||
|
this.dataStatuses.connection = DataStatus.Loaded;
|
||||||
|
|
||||||
this.apiData = data;
|
this.apiData = data;
|
||||||
this.setOnlineData();
|
this.setOnlineData();
|
||||||
});
|
});
|
||||||
@@ -395,4 +419,3 @@ export const useStore = defineStore('store', {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+10
-3
@@ -1,12 +1,11 @@
|
|||||||
|
|
||||||
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 { DispatcherStatsAPIData } from '../scripts/interfaces/api/DispatcherStatsAPIData';
|
|
||||||
import { DriverStatsAPIData } from '../scripts/interfaces/api/DriverStatsAPIData';
|
|
||||||
import StationAPIData from '../scripts/interfaces/api/StationAPIData';
|
import StationAPIData from '../scripts/interfaces/api/StationAPIData';
|
||||||
import TrainAPIData from '../scripts/interfaces/api/TrainAPIData';
|
import TrainAPIData from '../scripts/interfaces/api/TrainAPIData';
|
||||||
import Station from '../scripts/interfaces/Station';
|
import Station from '../scripts/interfaces/Station';
|
||||||
import Train from '../scripts/interfaces/Train';
|
import Train from '../scripts/interfaces/Train';
|
||||||
|
import { DispatcherStatsAPIData } from '../scripts/interfaces/api/DispatcherStatsAPIData';
|
||||||
|
import { DriverStatsAPIData } from '../scripts/interfaces/api/DriverStatsAPIData';
|
||||||
|
|
||||||
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
|
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
|
||||||
|
|
||||||
@@ -24,15 +23,19 @@ export interface StoreState {
|
|||||||
stationCount: number;
|
stationCount: number;
|
||||||
|
|
||||||
webSocket?: Socket;
|
webSocket?: Socket;
|
||||||
|
isOffline: boolean;
|
||||||
|
|
||||||
dispatcherStatsName: string;
|
dispatcherStatsName: string;
|
||||||
dispatcherStatsData?: DispatcherStatsAPIData;
|
dispatcherStatsData?: DispatcherStatsAPIData;
|
||||||
|
|
||||||
driverStatsName: string;
|
driverStatsName: string;
|
||||||
driverStatsData?: DriverStatsAPIData;
|
driverStatsData?: DriverStatsAPIData;
|
||||||
|
driverStatsStatus: DataStatus;
|
||||||
|
|
||||||
chosenModalTrainId?: string;
|
chosenModalTrainId?: string;
|
||||||
|
|
||||||
|
currentStatsTab: 'daily' | 'driver';
|
||||||
|
|
||||||
dataStatuses: {
|
dataStatuses: {
|
||||||
connection: DataStatus;
|
connection: DataStatus;
|
||||||
sceneries: DataStatus;
|
sceneries: DataStatus;
|
||||||
@@ -42,12 +45,14 @@ export interface StoreState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
listenerLaunched: boolean;
|
listenerLaunched: boolean;
|
||||||
|
blockScroll: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface APIData {
|
export interface APIData {
|
||||||
stations?: StationAPIData[];
|
stations?: StationAPIData[];
|
||||||
dispatchers?: string[][];
|
dispatchers?: string[][];
|
||||||
trains?: TrainAPIData[];
|
trains?: TrainAPIData[];
|
||||||
|
connectedSocketCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StationJSONData {
|
export interface StationJSONData {
|
||||||
@@ -55,6 +60,7 @@ export interface StationJSONData {
|
|||||||
url: string;
|
url: string;
|
||||||
lines: string;
|
lines: string;
|
||||||
project: string;
|
project: string;
|
||||||
|
projectUrl: string;
|
||||||
|
|
||||||
reqLevel: number;
|
reqLevel: number;
|
||||||
|
|
||||||
@@ -64,6 +70,7 @@ export interface StationJSONData {
|
|||||||
SUP: boolean;
|
SUP: boolean;
|
||||||
|
|
||||||
routes: string;
|
routes: string;
|
||||||
|
|
||||||
checkpoints: string | null;
|
checkpoints: string | null;
|
||||||
authors?: string;
|
authors?: string;
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
@import 'responsive.scss';
|
@import 'responsive.scss';
|
||||||
|
@import 'animations.scss';
|
||||||
// Animations
|
|
||||||
.warning {
|
|
||||||
&-enter-from,
|
|
||||||
&-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-enter-active {
|
|
||||||
transition: all 150ms ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-leave-active {
|
|
||||||
transition: all 150ms ease-out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Styles
|
//Styles
|
||||||
|
|
||||||
.journal-wrapper {
|
.list_wrapper {
|
||||||
width: 1350px;
|
overflow-y: auto;
|
||||||
|
height: 90vh;
|
||||||
|
min-height: 550px;
|
||||||
|
|
||||||
|
padding-right: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-list {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal_wrapper {
|
||||||
|
max-width: 1350px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
padding: 1em 0;
|
padding: 1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,24 +38,33 @@
|
|||||||
|
|
||||||
.journal_item,
|
.journal_item,
|
||||||
.journal_warning {
|
.journal_warning {
|
||||||
background: #202020;
|
background-color: #1a1a1a;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
margin: 1em 0;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.journal_top-bar {
|
.journal_top-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.btn {
|
.btn--load-data {
|
||||||
padding: 0.5em 0.7em;
|
padding: 0.5em 1em;
|
||||||
|
display: flex;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
font-size: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include smallScreen() {
|
@include smallScreen() {
|
||||||
.journal-wrapper {
|
.list_wrapper {
|
||||||
font-size: 1.25em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.journal_top-bar {
|
.journal_top-bar {
|
||||||
@@ -63,3 +72,9 @@ button.btn {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (orientation: landscape) {
|
||||||
|
.list_wrapper {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
@import 'variables.scss';
|
||||||
|
@import 'responsive.scss';
|
||||||
|
|
||||||
|
.stats-tab {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
box-shadow: 0 0 5px 1px $accentCol;
|
||||||
|
|
||||||
|
padding: 1em;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-badge {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
span {
|
||||||
|
background-color: $accentCol;
|
||||||
|
color: black;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0.2em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
span:first-child {
|
||||||
|
background-color: #333;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include smallScreen {
|
||||||
|
.journal-stats {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-stats {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
.list-anim-move,
|
||||||
|
.list-anim-enter-active,
|
||||||
|
.list-anim-leave-active {
|
||||||
|
transition: all 250ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-anim-enter-from,
|
||||||
|
.list-anim-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-anim-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-anim {
|
||||||
|
&-enter-from,
|
||||||
|
&-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-enter-active {
|
||||||
|
transition: all 100ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-leave-active {
|
||||||
|
transition: all 100ms ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,3 +26,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.level-badge {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.9em;
|
||||||
|
|
||||||
|
&.driver {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 1.7em;
|
||||||
|
height: 1.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dispatcher {
|
||||||
|
border-radius: 0.25em;
|
||||||
|
|
||||||
|
width: 1.6em;
|
||||||
|
height: 1.6em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-badge {
|
||||||
|
padding: 0 0.5em;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&.eu {
|
||||||
|
background-color: forestgreen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,8 +28,6 @@
|
|||||||
|
|
||||||
width: 600px;
|
width: 600px;
|
||||||
|
|
||||||
padding: 0.5em 1em;
|
|
||||||
|
|
||||||
@include smallScreen {
|
@include smallScreen {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 80vh;
|
height: 80vh;
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
@import 'responsive.scss';
|
||||||
|
@import 'variables.scss';
|
||||||
|
@import 'search_box.scss';
|
||||||
|
|
||||||
|
.filters-options {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-button .active-indicator {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
background-color: lightgreen;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1.option-title {
|
||||||
|
position: relative;
|
||||||
|
font-size: 1.1em;
|
||||||
|
margin: 0.7em 0 0.25em 0;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
|
||||||
|
width: 50%;
|
||||||
|
height: 2px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-anim {
|
||||||
|
&-enter-from,
|
||||||
|
&-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-enter-active,
|
||||||
|
&-leave-active {
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options_wrapper {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
background-color: $bgCol;
|
||||||
|
box-shadow: 0 5px 10px 2px #0f0f0f;
|
||||||
|
|
||||||
|
width: 97%;
|
||||||
|
max-width: 500px;
|
||||||
|
|
||||||
|
padding: 1em;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options_sorters {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
padding: 0.25em 0.25em 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options_filters {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 0.5em 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-option,
|
||||||
|
.filter-option {
|
||||||
|
margin: 0.25em 0.25em 0.25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-option[data-selected='true'] {
|
||||||
|
color: $accentCol;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-option {
|
||||||
|
&#abandoned {
|
||||||
|
color: salmon;
|
||||||
|
}
|
||||||
|
|
||||||
|
&#fulfilled {
|
||||||
|
color: lightgreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
&#active {
|
||||||
|
color: lightblue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search_content {
|
||||||
|
.search {
|
||||||
|
margin: 0.5em auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search_actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
margin: 1em 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
.search-exit {
|
||||||
|
position: absolute;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
top: 50%;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include smallScreen() {
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
width: 75%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.options_wrapper {
|
||||||
|
font-size: 1.1em;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-option,
|
||||||
|
.sort-option {
|
||||||
|
margin: 0.25em 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options_filters,
|
||||||
|
.options_sorters {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
+93
-49
@@ -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,26 @@
|
|||||||
|
|
||||||
--clr-error: #df3e3e;
|
--clr-error: #df3e3e;
|
||||||
--clr-warning: #c59429;
|
--clr-warning: #c59429;
|
||||||
|
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
&-track {
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-thumb {
|
||||||
|
background-color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-corner {
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
@@ -25,28 +46,14 @@ body {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
font-family: 'Quicksand', sans-serif;
|
font-family: 'Quicksand', sans-serif;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
&.no-scroll {
|
||||||
|
overflow-y: hidden;
|
||||||
|
padding-right: 15px;
|
||||||
|
|
||||||
|
@include smallScreen() {
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
*:focus-visible {
|
|
||||||
outline: 1px solid white;
|
|
||||||
outline-offset: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 0.5rem;
|
|
||||||
height: 0.5rem;
|
|
||||||
|
|
||||||
&-track {
|
|
||||||
background: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-thumb {
|
|
||||||
border-radius: 1rem;
|
|
||||||
background: #777;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,12 +112,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 +136,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,44 +197,52 @@ ul {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
background: none;
|
background: none;
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1em;
|
|
||||||
|
|
||||||
&--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;
|
transition: all 100ms ease;
|
||||||
|
|
||||||
|
&[data-disabled='true'] {
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.checked {
|
&[data-inactive='true'] {
|
||||||
color: var(--clr-primary);
|
opacity: 0.55;
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--image {
|
button.btn--filled {
|
||||||
color: white;
|
background-color: #1a1a1a;
|
||||||
transition: color 0.3s;
|
border-radius: 0.25em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--option {
|
button.btn--action {
|
||||||
cursor: pointer;
|
background-color: #424242;
|
||||||
|
border-radius: 0.25em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #555;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.btn--option {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
|
|
||||||
border-radius: 0.25em;
|
|
||||||
padding: 0.25em 0.5em;
|
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
|
||||||
background-color: #3c3c3c;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.checked {
|
&.checked {
|
||||||
color: var(--clr-primary);
|
color: var(--clr-primary);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -228,8 +251,14 @@ ul {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&: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 +303,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,13 +4,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@mixin midScreen() {
|
@mixin midScreen() {
|
||||||
@media only screen and (max-width: 1150px) {
|
@media only screen and (max-width: 1150px) {
|
||||||
@content;
|
@content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin screenLandscape() {
|
||||||
|
@media only screen and (orientation: landscape) and (max-device-height: 450px) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@mixin bigScreen() {
|
@mixin bigScreen() {
|
||||||
@media only screen and (min-width: 2000px) {
|
@media only screen and (min-width: 2000px) {
|
||||||
@content;
|
@content;
|
||||||
|
|||||||
@@ -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,7 +1,7 @@
|
|||||||
$primaryCol: #2c2c2c;
|
$primaryCol: #2c2c2c;
|
||||||
$secondaryCol: #01e733;
|
$secondaryCol: #01e733;
|
||||||
|
|
||||||
$bgCol: #4d4d4d;
|
$bgCol: #1d1d1d;
|
||||||
$bgLigtherCol: #5b5b5b;
|
$bgLigtherCol: #5b5b5b;
|
||||||
|
|
||||||
$errorCol: #ff1919;
|
$errorCol: #ff1919;
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export type JournalDispatcherSearcher = {
|
||||||
|
[key in 'search-dispatcher' | 'search-station' | 'search-date']: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface JournalDispatcherSorter {
|
||||||
|
id: 'timestampFrom' | 'duration';
|
||||||
|
dir: -1 | 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { JournalFilterType } from '../../scripts/enums/JournalFilterType';
|
||||||
|
|
||||||
|
export type JournalTimetableSearchKey = 'search-driver' | 'search-train' | 'search-date' | 'search-dispatcher';
|
||||||
|
|
||||||
|
export type JournalTimetableSearchType = {
|
||||||
|
[key in JournalTimetableSearchKey]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface JournalTimetableFilter {
|
||||||
|
id: JournalFilterType;
|
||||||
|
filterSection: string;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JournalTimetableSorter {
|
||||||
|
id: 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
|
||||||
|
dir: -1 | 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { TrainFilterType } from "../../scripts/enums/TrainFilterType";
|
||||||
|
|
||||||
|
export interface TrainFilter {
|
||||||
|
id: TrainFilterType;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
<template>
|
||||||
|
<section class="journal-timetables">
|
||||||
|
<JournalHeader />
|
||||||
|
|
||||||
|
<div class="journal_wrapper">
|
||||||
|
<JournalOptions
|
||||||
|
@on-search-confirm="fetchHistoryData"
|
||||||
|
@on-options-reset="resetOptions"
|
||||||
|
@on-refresh-data="fetchHistoryData"
|
||||||
|
:sorter-option-ids="['timestampFrom', 'duration']"
|
||||||
|
:data-status="dataStatus"
|
||||||
|
:current-options-active="currentOptionsActive"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="list_wrapper" @scroll="handleScroll">
|
||||||
|
<transition name="status-anim" mode="out-in">
|
||||||
|
<div :key="dataStatus">
|
||||||
|
<div class="journal_warning" v-if="store.isOffline">
|
||||||
|
{{ $t('app.offline') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Loading v-else-if="dataStatus == DataStatus.Loading" />
|
||||||
|
|
||||||
|
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
|
||||||
|
{{ $t('app.error') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="journal_warning" v-else-if="historyList.length == 0">
|
||||||
|
{{ $t('app.no-result') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<JournalDispatchersList :dispatcherHistory="computedHistoryList" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn--option btn--load-data"
|
||||||
|
v-if="!scrollNoMoreData && scrollDataLoaded && computedHistoryList.length > 15"
|
||||||
|
@click="addHistoryData"
|
||||||
|
>
|
||||||
|
{{ $t('journal.load-data') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<div class="journal_warning" v-if="scrollNoMoreData">
|
||||||
|
{{ $t('journal.no-further-data') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="journal_warning" v-else-if="!scrollDataLoaded">
|
||||||
|
{{ $t('journal.loading-further-data') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, provide, reactive, Ref, ref } from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import ActionButton from '../components/Global/ActionButton.vue';
|
||||||
|
import JournalOptions from '../components/JournalView/JournalOptions.vue';
|
||||||
|
import DispatcherStats from '../components/JournalView/DispatcherStats.vue';
|
||||||
|
import SearchBox from '../components/Global/SearchBox.vue';
|
||||||
|
|
||||||
|
import Loading from '../components/Global/Loading.vue';
|
||||||
|
import { URLs } from '../scripts/utils/apiURLs';
|
||||||
|
import { DataStatus } from '../scripts/enums/DataStatus';
|
||||||
|
import { useStore } from '../store/store';
|
||||||
|
import JournalDispatchersList from '../components/JournalView/JournalDispatchersList.vue';
|
||||||
|
import { JournalDispatcherSearcher, JournalDispatcherSorter } from '../types/Journal/JournalDispatcherTypes';
|
||||||
|
import { DispatcherHistory } from '../scripts/interfaces/api/DispatchersAPIData';
|
||||||
|
import JournalHeader from '../components/JournalView/JournalHeader.vue';
|
||||||
|
import { LocationQuery } from 'vue-router';
|
||||||
|
|
||||||
|
const DISPATCHERS_API_URL = `${URLs.stacjownikAPI}/api/getDispatchers`;
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
SearchBox,
|
||||||
|
ActionButton,
|
||||||
|
JournalOptions,
|
||||||
|
DispatcherStats,
|
||||||
|
Loading,
|
||||||
|
JournalDispatchersList,
|
||||||
|
JournalHeader,
|
||||||
|
},
|
||||||
|
name: 'JournalDispatchers',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
sceneryName: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
dispatcherName: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
currentQuery: '',
|
||||||
|
currentQueryArray: [] as string[],
|
||||||
|
|
||||||
|
scrollDataLoaded: true,
|
||||||
|
scrollNoMoreData: false,
|
||||||
|
|
||||||
|
showReturnButton: false,
|
||||||
|
statsCardOpen: false,
|
||||||
|
currentOptionsActive: false,
|
||||||
|
|
||||||
|
dataStatus: DataStatus.Loading,
|
||||||
|
DataStatus,
|
||||||
|
|
||||||
|
historyList: [] as DispatcherHistory[],
|
||||||
|
}),
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
const sorterActive: JournalDispatcherSorter = reactive({ id: 'timestampFrom', dir: -1 });
|
||||||
|
const journalFilterActive = ref({});
|
||||||
|
|
||||||
|
const searchersValues = reactive({
|
||||||
|
'search-dispatcher': '',
|
||||||
|
'search-station': '',
|
||||||
|
'search-date': '',
|
||||||
|
} as JournalDispatcherSearcher);
|
||||||
|
|
||||||
|
const countFromIndex = ref(0);
|
||||||
|
const countLimit = 15;
|
||||||
|
|
||||||
|
provide('sorterActive', sorterActive);
|
||||||
|
provide('journalFilterActive', journalFilterActive);
|
||||||
|
provide('searchersValues', searchersValues);
|
||||||
|
|
||||||
|
const scrollElement: Ref<HTMLElement | null> = ref(null);
|
||||||
|
|
||||||
|
return {
|
||||||
|
store: useStore(),
|
||||||
|
|
||||||
|
sorterActive,
|
||||||
|
searchersValues,
|
||||||
|
|
||||||
|
countFromIndex,
|
||||||
|
countLimit,
|
||||||
|
|
||||||
|
scrollElement,
|
||||||
|
maxCount: ref(15),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
currentQueryArray(q: string[]) {
|
||||||
|
this.currentOptionsActive =
|
||||||
|
q.length > 2 || q.some((qv) => qv.startsWith('sortBy=') && qv.split('=')[1] != 'timestampFrom');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
computedHistoryList() {
|
||||||
|
return this.historyList.filter(
|
||||||
|
(doc) => doc.isOnline || (doc.currentDuration && doc.currentDuration > 10 * 60000)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeRouteUpdate(to, _) {
|
||||||
|
this.handleQueries(to.query);
|
||||||
|
this.fetchHistoryData();
|
||||||
|
},
|
||||||
|
|
||||||
|
activated() {
|
||||||
|
this.handleQueries(this.$route.query);
|
||||||
|
this.fetchHistoryData();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
handleScroll(e: Event) {
|
||||||
|
const listElement = e.target as HTMLElement;
|
||||||
|
const scrollTop = listElement.scrollTop;
|
||||||
|
const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
|
||||||
|
|
||||||
|
if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
|
||||||
|
|
||||||
|
if (scrollTop > elementHeight * 0.85) this.addHistoryData();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleQueries(query: LocationQuery) {
|
||||||
|
if ('sceneryName' in query) this.searchersValues['search-station'] = `${query.sceneryName}`;
|
||||||
|
if ('dispatcherName' in query) this.searchersValues['search-dispatcher'] = `${query.dispatcherName}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
setSearchers(date: string, station: string, dispatcher: string) {
|
||||||
|
this.searchersValues['search-date'] = date;
|
||||||
|
this.searchersValues['search-station'] = station;
|
||||||
|
this.searchersValues['search-dispatcher'] = dispatcher;
|
||||||
|
},
|
||||||
|
|
||||||
|
resetOptions() {
|
||||||
|
this.setSearchers('', '', '');
|
||||||
|
this.sorterActive.id = 'timestampFrom';
|
||||||
|
|
||||||
|
this.fetchHistoryData();
|
||||||
|
},
|
||||||
|
|
||||||
|
async addHistoryData() {
|
||||||
|
this.scrollDataLoaded = false;
|
||||||
|
|
||||||
|
const countFrom = this.historyList.length;
|
||||||
|
|
||||||
|
const responseData: DispatcherHistory[] = await (
|
||||||
|
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
if (!responseData) return;
|
||||||
|
|
||||||
|
if (responseData.length == 0) {
|
||||||
|
this.scrollNoMoreData = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.historyList.push(...responseData);
|
||||||
|
this.scrollDataLoaded = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchHistoryData() {
|
||||||
|
const queries: string[] = [];
|
||||||
|
|
||||||
|
const dispatcher = this.searchersValues['search-dispatcher'].trim();
|
||||||
|
const station = this.searchersValues['search-station'].trim();
|
||||||
|
const dateString = this.searchersValues['search-date'].trim();
|
||||||
|
|
||||||
|
const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
|
||||||
|
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
|
||||||
|
|
||||||
|
if (dispatcher) queries.push(`dispatcherName=${dispatcher}`);
|
||||||
|
if (station) queries.push(`stationName=${station}`);
|
||||||
|
if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
|
||||||
|
|
||||||
|
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
|
||||||
|
if (this.sorterActive.id == 'timestampFrom') queries.push('sortBy=timestampFrom');
|
||||||
|
else if (this.sorterActive.id == 'duration') queries.push('sortBy=currentDuration');
|
||||||
|
else queries.push('sortBy=timestampFrom');
|
||||||
|
|
||||||
|
queries.push('countLimit=30');
|
||||||
|
|
||||||
|
if (this.currentQuery != queries.join('&')) this.dataStatus = DataStatus.Loading;
|
||||||
|
|
||||||
|
this.currentQuery = queries.join('&');
|
||||||
|
this.currentQueryArray = queries;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const responseData: DispatcherHistory[] = await (
|
||||||
|
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}`)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
if (!responseData) {
|
||||||
|
this.dataStatus = DataStatus.Error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!responseData) return;
|
||||||
|
|
||||||
|
// Response data exists
|
||||||
|
this.historyList = responseData;
|
||||||
|
|
||||||
|
// Stats display
|
||||||
|
this.store.dispatcherStatsName =
|
||||||
|
this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim()
|
||||||
|
? this.historyList[0].dispatcherName
|
||||||
|
: '';
|
||||||
|
|
||||||
|
this.dataStatus = DataStatus.Loaded;
|
||||||
|
} catch (error) {
|
||||||
|
this.dataStatus = DataStatus.Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scrollNoMoreData = false;
|
||||||
|
this.scrollDataLoaded = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../styles/JournalSection.scss';
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
<template>
|
||||||
|
<section class="journal-timetables">
|
||||||
|
<JournalHeader />
|
||||||
|
|
||||||
|
<div class="journal_wrapper">
|
||||||
|
<JournalOptions
|
||||||
|
@on-search-confirm="fetchHistoryData"
|
||||||
|
@on-options-reset="resetOptions"
|
||||||
|
@on-refresh-data="fetchHistoryData"
|
||||||
|
:sorter-option-ids="['timetableId', 'beginDate', 'distance', 'total-stops']"
|
||||||
|
:filters="journalTimetableFilters"
|
||||||
|
:currentOptionsActive="currentOptionsActive"
|
||||||
|
:data-status="dataStatus"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<JournalStats />
|
||||||
|
|
||||||
|
<div class="list_wrapper" @scroll="handleScroll">
|
||||||
|
<transition name="status-anim" mode="out-in">
|
||||||
|
<div :key="dataStatus">
|
||||||
|
<div class="journal_warning" v-if="store.isOffline">
|
||||||
|
{{ $t('app.offline') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Loading v-else-if="dataStatus == DataStatus.Loading" />
|
||||||
|
|
||||||
|
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
|
||||||
|
{{ $t('app.error') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="timetableHistory.length == 0" class="journal_warning">
|
||||||
|
{{ $t('app.no-result') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<JournalTimetablesList :timetableHistory="timetableHistory" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn--option btn--load-data"
|
||||||
|
v-if="!scrollNoMoreData && scrollDataLoaded && timetableHistory.length >= 15"
|
||||||
|
@click="addHistoryData"
|
||||||
|
>
|
||||||
|
{{ $t('journal.load-data') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
|
||||||
|
<div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, provide, reactive, Ref, ref } from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import DriverStats from '../components/JournalView/JournalDriverStats.vue';
|
||||||
|
import Loading from '../components/Global/Loading.vue';
|
||||||
|
import { JournalTimetableSorter } from '../types/Journal/JournalTimetablesTypes';
|
||||||
|
import dateMixin from '../mixins/dateMixin';
|
||||||
|
import routerMixin from '../mixins/routerMixin';
|
||||||
|
import { DataStatus } from '../scripts/enums/DataStatus';
|
||||||
|
import { JournalFilterType } from '../scripts/enums/JournalFilterType';
|
||||||
|
import { TimetableHistory } from '../scripts/interfaces/api/TimetablesAPIData';
|
||||||
|
import { URLs } from '../scripts/utils/apiURLs';
|
||||||
|
import { useStore } from '../store/store';
|
||||||
|
import JournalOptions from '../components/JournalView/JournalOptions.vue';
|
||||||
|
import { JournalTimetableSearchType } from '../types/Journal/JournalTimetablesTypes';
|
||||||
|
import modalTrainMixin from '../mixins/modalTrainMixin';
|
||||||
|
import imageMixin from '../mixins/imageMixin';
|
||||||
|
import JournalTimetablesList from '../components/JournalView/JournalTimetablesList.vue';
|
||||||
|
import { journalTimetableFilters } from '../constants/Journal/JournalTimetablesConsts';
|
||||||
|
import JournalStats from '../components/JournalView/JournalStats.vue';
|
||||||
|
import JournalHeader from '../components/JournalView/JournalHeader.vue';
|
||||||
|
import { LocationQuery } from 'vue-router';
|
||||||
|
|
||||||
|
const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`;
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { DriverStats, Loading, JournalOptions, JournalTimetablesList, JournalStats, JournalHeader },
|
||||||
|
mixins: [dateMixin, routerMixin, modalTrainMixin, imageMixin],
|
||||||
|
|
||||||
|
name: 'JournalTimetables',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
timetableId: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
currentQuery: '',
|
||||||
|
currentQueryArray: [] as string[],
|
||||||
|
|
||||||
|
scrollDataLoaded: true,
|
||||||
|
scrollNoMoreData: false,
|
||||||
|
|
||||||
|
showReturnButton: false,
|
||||||
|
statsCardOpen: false,
|
||||||
|
currentOptionsActive: false,
|
||||||
|
|
||||||
|
timetableHistory: [] as TimetableHistory[],
|
||||||
|
journalTimetableFilters,
|
||||||
|
|
||||||
|
dataStatus: DataStatus.Loading,
|
||||||
|
dataErrorMessage: '',
|
||||||
|
|
||||||
|
DataStatus,
|
||||||
|
}),
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
const sorterActive: JournalTimetableSorter = reactive({ id: 'timetableId', dir: 1 });
|
||||||
|
const journalFilterActive = ref(journalTimetableFilters[0]);
|
||||||
|
|
||||||
|
const searchersValues = reactive({
|
||||||
|
'search-train': '',
|
||||||
|
'search-driver': '',
|
||||||
|
'search-dispatcher': '',
|
||||||
|
'search-date': '',
|
||||||
|
} as JournalTimetableSearchType);
|
||||||
|
|
||||||
|
const countFromIndex = ref(0);
|
||||||
|
const countLimit = 15;
|
||||||
|
|
||||||
|
provide('searchersValues', searchersValues);
|
||||||
|
provide('sorterActive', sorterActive);
|
||||||
|
provide('journalFilterActive', journalFilterActive);
|
||||||
|
|
||||||
|
const scrollElement: Ref<HTMLElement | null> = ref(null);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sorterActive,
|
||||||
|
journalFilterActive,
|
||||||
|
searchersValues,
|
||||||
|
|
||||||
|
countFromIndex,
|
||||||
|
countLimit,
|
||||||
|
|
||||||
|
scrollElement,
|
||||||
|
|
||||||
|
store: useStore(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
currentQueryArray(q: string[]) {
|
||||||
|
this.currentOptionsActive = q.length >= 2 || q.some((qv) => qv.startsWith('sortBy=') && qv.split('=')[1]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Handle route updates for route-links
|
||||||
|
beforeRouteUpdate(to, _) {
|
||||||
|
this.handleQueries(to.query);
|
||||||
|
this.fetchHistoryData();
|
||||||
|
},
|
||||||
|
|
||||||
|
activated() {
|
||||||
|
this.handleQueries(this.$route.query);
|
||||||
|
this.fetchHistoryData();
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
handleScroll(e: Event) {
|
||||||
|
const listElement = e.target as HTMLElement;
|
||||||
|
const scrollTop = listElement.scrollTop;
|
||||||
|
const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
|
||||||
|
|
||||||
|
if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
|
||||||
|
|
||||||
|
if (scrollTop > elementHeight * 0.85) this.addHistoryData();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleQueries(query: LocationQuery) {
|
||||||
|
if ('timetableId' in query) this.searchersValues['search-train'] = `#${query.timetableId}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
setSearchers(date: string, driver: string, train: string, dispatcher: string) {
|
||||||
|
this.searchersValues['search-date'] = date;
|
||||||
|
this.searchersValues['search-driver'] = driver;
|
||||||
|
this.searchersValues['search-train'] = train;
|
||||||
|
this.searchersValues['search-dispatcher'] = dispatcher;
|
||||||
|
},
|
||||||
|
|
||||||
|
resetOptions() {
|
||||||
|
this.setSearchers('', '', '', '');
|
||||||
|
|
||||||
|
this.journalFilterActive = this.journalTimetableFilters[0];
|
||||||
|
this.sorterActive.id = 'timetableId';
|
||||||
|
|
||||||
|
this.fetchHistoryData();
|
||||||
|
},
|
||||||
|
|
||||||
|
async addHistoryData() {
|
||||||
|
this.scrollDataLoaded = false;
|
||||||
|
|
||||||
|
const countFrom = this.timetableHistory.length;
|
||||||
|
|
||||||
|
const responseData: TimetableHistory[] = await (
|
||||||
|
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
if (!responseData) return;
|
||||||
|
|
||||||
|
if (responseData.length == 0) {
|
||||||
|
this.scrollNoMoreData = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timetableHistory.push(...responseData);
|
||||||
|
this.scrollDataLoaded = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchHistoryData() {
|
||||||
|
// if(this.dataStatus == DataStatus.Loading) return;
|
||||||
|
|
||||||
|
const queries: string[] = [];
|
||||||
|
|
||||||
|
const driverName = this.searchersValues['search-driver'].trim();
|
||||||
|
const trainNo = this.searchersValues['search-train'].trim();
|
||||||
|
const authorName = this.searchersValues['search-dispatcher'].trim();
|
||||||
|
const dateString = this.searchersValues['search-date'].trim();
|
||||||
|
|
||||||
|
const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
|
||||||
|
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
|
||||||
|
|
||||||
|
if (driverName) queries.push(`driverName=${driverName}`);
|
||||||
|
if (trainNo)
|
||||||
|
queries.push(trainNo.startsWith('#') ? `timetableId=${trainNo.replace('#', '')}` : `trainNo=${trainNo}`);
|
||||||
|
if (authorName) queries.push(`authorName=${authorName}`);
|
||||||
|
if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
|
||||||
|
|
||||||
|
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
|
||||||
|
if (this.sorterActive.id == 'distance') queries.push('sortBy=routeDistance');
|
||||||
|
else if (this.sorterActive.id == 'total-stops') queries.push('sortBy=allStopsCount');
|
||||||
|
else if (this.sorterActive.id == 'beginDate') queries.push('sortBy=beginDate');
|
||||||
|
// else queries.push('sortBy=timetableId');
|
||||||
|
|
||||||
|
queries.push('countLimit=15');
|
||||||
|
|
||||||
|
switch (this.journalFilterActive.id) {
|
||||||
|
case JournalFilterType.abandoned:
|
||||||
|
queries.push('fulfilled=0', 'terminated=1');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JournalFilterType.active:
|
||||||
|
queries.push('terminated=0');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JournalFilterType.fulfilled:
|
||||||
|
queries.push('fulfilled=1');
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentQuery != queries.join('&')) this.dataStatus = DataStatus.Loading;
|
||||||
|
|
||||||
|
this.currentQuery = queries.join('&');
|
||||||
|
this.currentQueryArray = queries;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const responseData: TimetableHistory[] = await (
|
||||||
|
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}`)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
if (!responseData) {
|
||||||
|
this.dataStatus = DataStatus.Error;
|
||||||
|
this.dataErrorMessage = 'Brak danych!';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!responseData) return;
|
||||||
|
|
||||||
|
// Response data exists
|
||||||
|
this.timetableHistory = responseData;
|
||||||
|
|
||||||
|
// Stats display
|
||||||
|
this.store.driverStatsName =
|
||||||
|
this.timetableHistory.length > 0 && this.searchersValues['search-driver'].trim()
|
||||||
|
? this.timetableHistory[0].driverName
|
||||||
|
: '';
|
||||||
|
|
||||||
|
this.dataStatus = DataStatus.Loaded;
|
||||||
|
} catch (error) {
|
||||||
|
this.dataStatus = DataStatus.Error;
|
||||||
|
this.dataErrorMessage = 'Ups! Coś poszło nie tak!';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scrollNoMoreData = false;
|
||||||
|
this.scrollDataLoaded = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../styles/JournalSection.scss';
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
<template>
|
|
||||||
<section class="journal-view">
|
|
||||||
<div class="journal-type-options">
|
|
||||||
<router-link class="router-link" active-class="route-active" to="/journal/timetables" exact>
|
|
||||||
{{ $t('journal.section-timetables') }}
|
|
||||||
</router-link>
|
|
||||||
•
|
|
||||||
<router-link class="router-link" active-class="route-active" to="/journal/dispatchers">
|
|
||||||
{{ $t('journal.section-dispatchers') }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="journal-section">
|
|
||||||
<router-view v-slot="{ Component }">
|
|
||||||
<keep-alive>
|
|
||||||
<component :is="Component" :key="$route.path" />
|
|
||||||
</keep-alive>
|
|
||||||
</router-view>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, ref } from 'vue';
|
|
||||||
import JournalDispatchers from '../components/JournalView/JournalDispatchers.vue';
|
|
||||||
import JournalTimetables from '../components/JournalView/JournalTimetables.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: { JournalDispatchers, JournalTimetables },
|
|
||||||
setup() {
|
|
||||||
const journalTypeChosen = ref('journalTimetables');
|
|
||||||
|
|
||||||
return {
|
|
||||||
activeJournalComponent: journalTypeChosen,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
changeJournalType(type: string) {
|
|
||||||
this.activeJournalComponent = type;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
activated() {
|
|
||||||
const query = this.$route.query;
|
|
||||||
|
|
||||||
if (query.view == 'dispatchers') this.activeJournalComponent = 'journalDispatchers';
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.journal-type-options {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
background-color: #2c2c2c;
|
|
||||||
max-width: 18em;
|
|
||||||
|
|
||||||
font-size: 1.2em;
|
|
||||||
margin: 0 auto;
|
|
||||||
|
|
||||||
border-radius: 0 0 0.5em 0.5em;
|
|
||||||
padding: 0.1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.journal-section > section {
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.router-link.active {
|
|
||||||
color: gold;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
+28
-10
@@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-73
@@ -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,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
computedStationList() {
|
||||||
|
const list = this.filterStore.getFilteredStationList(this.store.stationList, this.store.region.id);
|
||||||
|
|
||||||
|
return list;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
if (!StorageManager.isRegistered(this.STORAGE_KEY)) return;
|
this.filterStore.setupFilters();
|
||||||
|
// this.filterStore.inputs.options.forEach((option) => {
|
||||||
|
// const value = StorageManager.getBooleanValue(option.name);
|
||||||
|
// option.value = value;
|
||||||
|
// this.filterStore.changeFilterValue({ name: option.name, value: value });
|
||||||
|
// });
|
||||||
|
|
||||||
this.filterManager.checkFilters();
|
// this.filterStore.inputs.sliders.forEach((slider) => {
|
||||||
|
// const value = StorageManager.getNumericValue(slider.name);
|
||||||
this.inputs.options.forEach((option) => {
|
// slider.value = value;
|
||||||
const value = StorageManager.getBooleanValue(option.name);
|
// this.filterStore.changeFilterValue({ name: slider.name, value: value });
|
||||||
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 {
|
|
||||||
if (name == 'filter') {
|
|
||||||
this.filterCardOpen = !this.filterCardOpen;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
changeSorter(index: number) {
|
|
||||||
this.filterManager.changeSorter(index);
|
|
||||||
},
|
|
||||||
changeFilterValue(filter: { name: string; value: number }) {
|
|
||||||
this.filterManager.changeFilterValue(filter);
|
|
||||||
},
|
|
||||||
resetFilters() {
|
|
||||||
this.filterManager.resetFilters();
|
|
||||||
},
|
|
||||||
invertFilters() {
|
|
||||||
this.filterManager.invertFilters();
|
|
||||||
},
|
|
||||||
closeCard() {
|
|
||||||
this.filterCardOpen = false;
|
|
||||||
},
|
|
||||||
setFocusedStation(name: string) {
|
|
||||||
this.focusedStationName = this.focusedStationName == name ? '' : name;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+36
-27
@@ -1,9 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="trains-view">
|
<section class="trains-view">
|
||||||
<div class="wrapper">
|
<div class="trains_wrapper">
|
||||||
<div class="options-bar">
|
<TrainOptions
|
||||||
<train-options />
|
:sorter-option-ids="['distance', 'id', 'progress', 'delay', 'mass', 'speed', 'length']"
|
||||||
</div>
|
:current-options-active="currentOptionsActive"
|
||||||
|
/>
|
||||||
|
|
||||||
<TrainTable :trains="computedTrains" />
|
<TrainTable :trains="computedTrains" />
|
||||||
</div>
|
</div>
|
||||||
@@ -11,14 +12,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, ComputedRef, defineComponent, provide, reactive, ref, TrainFilter } from 'vue';
|
import { computed, ComputedRef, defineComponent, provide, reactive, ref, watch } from 'vue';
|
||||||
import TrainOptions from '../components/TrainsView/TrainOptions.vue';
|
import TrainOptions from '../components/TrainsView/TrainOptions.vue';
|
||||||
import TrainStats from '../components/TrainsView/TrainStats.vue';
|
import TrainStats from '../components/TrainsView/TrainStats.vue';
|
||||||
import TrainTable from '../components/TrainsView/TrainTable.vue';
|
import TrainTable from '../components/TrainsView/TrainTable.vue';
|
||||||
import { trainFilters } from '../data/trainOptions';
|
import { trainFilters } from '../constants/Trains/TrainOptionsConsts';
|
||||||
|
import modalTrainMixin from '../mixins/modalTrainMixin';
|
||||||
import Train from '../scripts/interfaces/Train';
|
import Train from '../scripts/interfaces/Train';
|
||||||
import { filteredTrainList } from '../scripts/managers/trainFilterManager';
|
import { filteredTrainList } from '../scripts/managers/trainFilterManager';
|
||||||
import { useStore } from '../store/store';
|
import { useStore } from '../store/store';
|
||||||
|
import { TrainFilter } from '../types/Trains/TrainOptionsTypes';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@@ -27,6 +30,8 @@ export default defineComponent({
|
|||||||
TrainOptions,
|
TrainOptions,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mixins: [modalTrainMixin],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
train: {
|
train: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -37,6 +42,11 @@ export default defineComponent({
|
|||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
trainId: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
@@ -45,10 +55,12 @@ export default defineComponent({
|
|||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
const initTrainFilters = [...trainFilters.map((f) => ({ ...f }))];
|
||||||
|
|
||||||
const sorterActive = ref({ id: 'distance', dir: -1 });
|
const sorterActive = reactive({ id: 'distance', dir: -1 });
|
||||||
const filterList = reactive([...trainFilters]) as TrainFilter[];
|
const filterList = reactive([...trainFilters]) as TrainFilter[];
|
||||||
const isTrainOptionsCardVisible = ref(false);
|
|
||||||
|
const currentOptionsActive = ref(false);
|
||||||
|
|
||||||
const searchedDriver = ref('');
|
const searchedDriver = ref('');
|
||||||
const searchedTrain = ref('');
|
const searchedTrain = ref('');
|
||||||
@@ -57,16 +69,15 @@ export default defineComponent({
|
|||||||
provide('searchedDriver', searchedDriver);
|
provide('searchedDriver', searchedDriver);
|
||||||
provide('sorterActive', sorterActive);
|
provide('sorterActive', sorterActive);
|
||||||
provide('filterList', filterList);
|
provide('filterList', filterList);
|
||||||
provide('isTrainOptionsCardVisible', isTrainOptionsCardVisible);
|
|
||||||
|
|
||||||
const computedTrains: ComputedRef<Train[]> = computed(() => {
|
const computedTrains: ComputedRef<Train[]> = computed(() => {
|
||||||
return filteredTrainList(
|
return filteredTrainList(store.trainList, searchedTrain.value, searchedDriver.value, sorterActive, filterList);
|
||||||
store.trainList,
|
});
|
||||||
searchedTrain.value,
|
|
||||||
searchedDriver.value,
|
watch([searchedTrain, searchedDriver, sorterActive, filterList], ([sT, sD, sA, fL]) => {
|
||||||
sorterActive.value,
|
const areFiltersActive = fL.some((f, i) => f.isActive !== initTrainFilters[i].isActive);
|
||||||
filterList
|
|
||||||
);
|
currentOptionsActive.value = sT.length > 0 || sD.length > 0 || sA.id != 'distance' || areFiltersActive;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -74,6 +85,8 @@ export default defineComponent({
|
|||||||
searchedTrain,
|
searchedTrain,
|
||||||
searchedDriver,
|
searchedDriver,
|
||||||
sorterActive,
|
sorterActive,
|
||||||
|
store,
|
||||||
|
currentOptionsActive,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -82,10 +95,12 @@ export default defineComponent({
|
|||||||
this.searchedTrain = this.train;
|
this.searchedTrain = this.train;
|
||||||
this.searchedDriver = this.driver || '';
|
this.searchedDriver = this.driver || '';
|
||||||
}
|
}
|
||||||
// if (this.train) {
|
|
||||||
// this.searchedTrain = this.train;
|
this.$nextTick(() => {
|
||||||
// if(this.x) this.searchedDriver = this.x;
|
if (this.trainId) {
|
||||||
// }
|
this.selectModalTrain(this.trainId);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -98,14 +113,8 @@ export default defineComponent({
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper {
|
.trains_wrapper {
|
||||||
margin: 1rem auto;
|
margin: 1rem auto;
|
||||||
max-width: 1350px;
|
max-width: 1350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include smallScreen {
|
|
||||||
.options-bar {
|
|
||||||
font-size: 1.25em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Vendored
-30
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"ESNext",
|
"ESNext",
|
||||||
"DOM"
|
"DOM"
|
||||||
],
|
],
|
||||||
|
"types": ["vite/client", "vite-plugin-pwa/client"],
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|||||||
+47
-27
@@ -1,34 +1,54 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import vue from '@vitejs/plugin-vue';
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
server: {
|
||||||
|
port: 5001,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
VitePWA({
|
||||||
|
registerType: 'prompt',
|
||||||
|
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ['**/*.{js,css,html,png,svg,jpg}'],
|
||||||
|
runtimeCaching: [
|
||||||
|
{
|
||||||
|
urlPattern: new RegExp('^https://spythere.pl/api/getSceneries', 'i'),
|
||||||
|
handler: 'NetworkFirst',
|
||||||
|
options: {
|
||||||
|
cacheName: 'sceneries-cache',
|
||||||
|
expiration: {
|
||||||
|
maxEntries: 1,
|
||||||
|
maxAgeSeconds: 60 * 60 * 24 * 7, // <== 7 days
|
||||||
|
},
|
||||||
|
cacheableResponse: {
|
||||||
|
statuses: [0, 200],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urlPattern: /^https:\/\/rj.td2.info.pl\/dist\/img\/thumbnails\/.*/i,
|
||||||
|
handler: 'CacheFirst',
|
||||||
|
options: {
|
||||||
|
cacheName: 'images-cache',
|
||||||
|
expiration: {
|
||||||
|
maxEntries: 100,
|
||||||
|
maxAgeSeconds: 60 * 60 * 24 * 60,
|
||||||
|
},
|
||||||
|
cacheableResponse: {
|
||||||
|
statuses: [0, 200, 404],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
devOptions: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// PWA
|
|
||||||
|
|
||||||
// VitePWA({
|
|
||||||
// registerType: 'autoUpdate',
|
|
||||||
// workbox: {
|
|
||||||
// globPatterns: ['**/*.{js,css,html,png,svg,img}'],
|
|
||||||
// runtimeCaching: [
|
|
||||||
// {
|
|
||||||
// urlPattern: new RegExp('^https://stacjownik.eu-4.evennode.com/api/getSceneries'),
|
|
||||||
// handler: 'NetworkFirst',
|
|
||||||
// options: {
|
|
||||||
// cacheName: 'sceneries-cache',
|
|
||||||
// expiration: {
|
|
||||||
// maxEntries: 200,
|
|
||||||
// maxAgeSeconds: 60 * 60 * 24 * 60, // <== 60 days
|
|
||||||
// },
|
|
||||||
// cacheableResponse: {
|
|
||||||
// statuses: [0, 200],
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// devOptions: {
|
|
||||||
// enabled: true,
|
|
||||||
// },
|
|
||||||
// }),
|
|
||||||
|
|||||||
Reference in New Issue
Block a user