mirror of
https://github.com/Spythere/stacjownik.git
synced 2026-05-03 05:18:11 +00:00
Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd7d1b0bb0 | |||
| 68934a89a4 | |||
| b88a240ec1 | |||
| eaa34f3359 | |||
| 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 |
@@ -0,0 +1,20 @@
|
||||
# This file was auto-generated by the Firebase CLI
|
||||
# https://github.com/firebase/firebase-tools
|
||||
|
||||
name: Deploy to Firebase Hosting on merge
|
||||
'on':
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
build_and_deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: npm ci && npm run build
|
||||
- uses: FirebaseExtended/action-hosting-deploy@v0
|
||||
with:
|
||||
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
||||
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STACJOWNIK_TD2 }}'
|
||||
channelId: live
|
||||
projectId: stacjownik-td2
|
||||
@@ -0,0 +1,17 @@
|
||||
# This file was auto-generated by the Firebase CLI
|
||||
# https://github.com/firebase/firebase-tools
|
||||
|
||||
name: Deploy to Firebase Hosting on PR
|
||||
'on': pull_request
|
||||
jobs:
|
||||
build_and_preview:
|
||||
if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: npm ci && npm run build
|
||||
- uses: FirebaseExtended/action-hosting-deploy@v0
|
||||
with:
|
||||
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
||||
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STACJOWNIK_TD2 }}'
|
||||
projectId: stacjownik-td2
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
/dev-dist
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
|
||||
+5
-2
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"hosting": {
|
||||
"public": "dist",
|
||||
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
||||
"ignore": [
|
||||
"firebase.json",
|
||||
"**/.*",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
@@ -10,4 +14,3 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-17
@@ -5,8 +5,8 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
|
||||
<meta name="keywords" content="Stacjownik, TD2, Train Driver 2, stacjownik-td2" />
|
||||
<meta name="description" content="Automatycznie odświeżana strona wyświetlająca stacje w Train Driver 2!" />
|
||||
<meta name="keywords" content="Stacjownik, TD2, Train Driver 2, stacjownik-td2, stacjownik, td2.info.pl" />
|
||||
<meta name="description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
|
||||
|
||||
<title>Stacjownik</title>
|
||||
|
||||
@@ -24,21 +24,7 @@
|
||||
<link rel="icon" href="favicon-16.png" sizes="16x16" type="image/png" />
|
||||
<link rel="icon" href="favicon.ico" />
|
||||
|
||||
<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>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@500;700&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -46,3 +32,4 @@
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
Generated
+11386
File diff suppressed because it is too large
Load Diff
+9
-8
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "stacjownik",
|
||||
"version": "1.10.8",
|
||||
"version": "1.12.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"deploy": "yarn build && firebase deploy --only hosting",
|
||||
"preview": "vite preview"
|
||||
"preview": "yarn build && vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.12.1",
|
||||
@@ -21,12 +21,13 @@
|
||||
"vue-router": "^4.0.0-0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.8.3",
|
||||
"@vitejs/plugin-vue": "^3.0.0",
|
||||
"axios": "^1.1.2",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^3.0.0",
|
||||
"vue-tsc": "^1.0.3"
|
||||
"@types/node": "^18.11.17",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"axios": "^1.2.1",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^4.0.3",
|
||||
"vite-plugin-pwa": "^0.14.0",
|
||||
"vue-tsc": "^1.0.18"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
||||
+7
-2
@@ -33,7 +33,8 @@
|
||||
.route {
|
||||
margin: 0 0.2em;
|
||||
|
||||
&-active {
|
||||
&-active,
|
||||
&[data-active='true'] {
|
||||
color: $accentCol;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -45,7 +46,11 @@
|
||||
font-size: 1rem;
|
||||
|
||||
@include smallScreen() {
|
||||
font-size: calc(0.55rem + 1vw);
|
||||
font-size: calc(0.55rem + 1.1vw);
|
||||
}
|
||||
|
||||
@include screenLandscape() {
|
||||
font-size: calc(0.45rem + 0.8vw);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+33
-3
@@ -6,11 +6,13 @@
|
||||
</keep-alive>
|
||||
</transition>
|
||||
|
||||
<UpdatePrompt />
|
||||
|
||||
<AppHeader :current-lang="currentLang" @change-lang="changeLang" />
|
||||
|
||||
<main class="app_main">
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive>
|
||||
<keep-alive exclude="JournalView">
|
||||
<component :is="Component" :key="$route.name" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
@@ -19,7 +21,8 @@
|
||||
<footer class="app_footer">
|
||||
©
|
||||
<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>
|
||||
</footer>
|
||||
@@ -27,7 +30,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, provide, ref, watch } from 'vue';
|
||||
import { computed, defineComponent, KeepAlive, provide, ref, watch } from 'vue';
|
||||
|
||||
import Clock from './components/App/Clock.vue';
|
||||
|
||||
@@ -41,6 +44,10 @@ import StorageManager from './scripts/managers/storageManager';
|
||||
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({
|
||||
components: {
|
||||
@@ -49,6 +56,7 @@ export default defineComponent({
|
||||
SelectBox,
|
||||
TrainModal,
|
||||
AppHeader,
|
||||
UpdatePrompt,
|
||||
},
|
||||
|
||||
mixins: [imageMixin],
|
||||
@@ -57,6 +65,8 @@ export default defineComponent({
|
||||
const store = useStore();
|
||||
store.connectToAPI();
|
||||
|
||||
const { offlineReady } = useCustomSW();
|
||||
|
||||
const isFilterCardVisible = ref(false);
|
||||
|
||||
provide('isFilterCardVisible', isFilterCardVisible);
|
||||
@@ -77,10 +87,30 @@ export default defineComponent({
|
||||
|
||||
currentLang: 'pl',
|
||||
releaseURL: '',
|
||||
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app',
|
||||
}),
|
||||
|
||||
created() {
|
||||
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() {
|
||||
|
||||
@@ -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 |
@@ -22,7 +22,9 @@
|
||||
<StatusIndicator />
|
||||
|
||||
<span class="header_brand">
|
||||
<img :src="getImage('stacjownik-header-logo.svg')" alt="Stacjownik" />
|
||||
<router-link to="/">
|
||||
<img :src="getImage('stacjownik-header-logo.svg')" alt="Stacjownik" />
|
||||
</router-link>
|
||||
</span>
|
||||
|
||||
<span class="header_info">
|
||||
@@ -48,7 +50,12 @@
|
||||
/
|
||||
<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">
|
||||
<router-link
|
||||
class="route"
|
||||
active-class="route-active"
|
||||
:data-active="$route.path.startsWith('/journal')"
|
||||
to="/journal"
|
||||
>
|
||||
{{ $t('app.journal') }}
|
||||
</router-link>
|
||||
</span>
|
||||
@@ -66,50 +73,51 @@ import StatusIndicator from './StatusIndicator.vue';
|
||||
import Clock from './Clock.vue';
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["changeLang"],
|
||||
mixins: [imageMixin],
|
||||
props: {
|
||||
currentLang: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emits: ['changeLang'],
|
||||
mixins: [imageMixin],
|
||||
props: {
|
||||
currentLang: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
setup() {
|
||||
},
|
||||
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 {
|
||||
store: useStore(),
|
||||
id: region.id,
|
||||
value: `${region.value} <div class='text--grayed'>${regionStationCount} / ${regionTrainCount}</div>`,
|
||||
selectedValue: region.value,
|
||||
};
|
||||
});
|
||||
},
|
||||
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 }
|
||||
},
|
||||
components: { SelectBox, StatusIndicator, Clock },
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@@ -128,6 +136,7 @@ export default defineComponent({
|
||||
.header {
|
||||
&_body {
|
||||
max-width: 21em;
|
||||
position: relative;
|
||||
|
||||
@include smallScreen {
|
||||
max-width: 18em;
|
||||
@@ -263,4 +272,4 @@ export default defineComponent({
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</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>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
||||
import { useStore } from '../../store/store';
|
||||
@@ -172,6 +171,7 @@ export default defineComponent({
|
||||
return {
|
||||
tooltipActive: false,
|
||||
indicator: {
|
||||
offline: false,
|
||||
status: DataStatus.Loading,
|
||||
message: 'data-status.S3',
|
||||
},
|
||||
@@ -193,6 +193,7 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
dataStatus: store.dataStatuses,
|
||||
store,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -206,6 +207,13 @@ export default defineComponent({
|
||||
const trainsDataStatus = statuses.trains;
|
||||
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) {
|
||||
this.setSignalStatus(connectionStatus);
|
||||
this.indicator.status = connectionStatus;
|
||||
@@ -252,6 +260,10 @@ export default defineComponent({
|
||||
this.orangeLight = false;
|
||||
this.redBottomLight = false;
|
||||
|
||||
if (status == DataStatus.Initialized) {
|
||||
this.redTopLight = true;
|
||||
}
|
||||
|
||||
if (status == DataStatus.Loaded) {
|
||||
this.greenLight = true;
|
||||
}
|
||||
@@ -291,9 +303,8 @@ export default defineComponent({
|
||||
|
||||
.status-indicator {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
left: 110%;
|
||||
bottom: 0;
|
||||
transform: translateX(12em);
|
||||
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>
|
||||
|
||||
@@ -20,7 +20,7 @@ export default defineComponent({
|
||||
.loading {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -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,94 +0,0 @@
|
||||
<template>
|
||||
<div class="journal-stats" v-if="store.driverStatsData?._sum.routeDistance != null">
|
||||
<h1>
|
||||
{{ $t('journal.stats-title') }} <span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
|
||||
</h1>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import axios from 'axios';
|
||||
import { computed, defineComponent, ref } 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,
|
||||
driverStatsName: computed(() => store.driverStatsName),
|
||||
};
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
test: Math.random(),
|
||||
lastDispatcherName: '',
|
||||
|
||||
lastTimetables: [] as TimetableHistory[],
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
driverStatsName(value: string) {
|
||||
this.fetchDispatcherStats();
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchDispatcherStats() {
|
||||
this.store.driverStatsData = undefined;
|
||||
|
||||
if (!this.store.driverStatsName) return;
|
||||
|
||||
const statsData: DriverStatsAPIData = await (
|
||||
await axios.get(`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`)
|
||||
).data;
|
||||
|
||||
this.store.driverStatsData = statsData;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/JournalStats.scss';
|
||||
</style>
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<ul class="journal-list">
|
||||
<!-- <transition-group name="journal-list-anim"> -->
|
||||
<li v-for="item in computedDispatcherHistory" :class="{ sticky: typeof item == 'string' }">
|
||||
<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>
|
||||
@@ -14,13 +17,25 @@
|
||||
@keydown.enter="navigateToScenery(item.stationName, item.isOnline)"
|
||||
tabindex="0"
|
||||
>
|
||||
<span>
|
||||
<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 class="like-count" v-if="item.dispatcherRate">
|
||||
<img :src="getIcon('like')" alt="like icon" />
|
||||
{{ item.dispatcherRate }}
|
||||
</span>
|
||||
</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' }) }}
|
||||
@@ -36,14 +51,15 @@
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<!-- </transition-group> -->
|
||||
</ul>
|
||||
</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';
|
||||
import imageMixin from '../../mixins/imageMixin';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -53,7 +69,7 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
|
||||
mixins: [dateMixin],
|
||||
mixins: [dateMixin, styleMixin, imageMixin],
|
||||
|
||||
computed: {
|
||||
computedDispatcherHistory() {
|
||||
@@ -86,18 +102,11 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/animations.scss';
|
||||
@import '../../styles/responsive.scss';
|
||||
@import '../../styles/badge.scss';
|
||||
@import '../../styles/JournalSection.scss';
|
||||
|
||||
.region-badge {
|
||||
padding: 0.1em 0.5em;
|
||||
border-radius: 0.5em;
|
||||
font-weight: bold;
|
||||
|
||||
&.eu {
|
||||
background-color: forestgreen;
|
||||
}
|
||||
}
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
li.sticky {
|
||||
position: sticky;
|
||||
@@ -108,9 +117,12 @@ li.sticky {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
flex-wrap: wrap;
|
||||
text-align: left;
|
||||
|
||||
gap: 0.5em 1em;
|
||||
|
||||
line-height: 1.7em;
|
||||
padding: 0.75em;
|
||||
|
||||
&.online {
|
||||
@@ -126,6 +138,18 @@ li.sticky {
|
||||
}
|
||||
}
|
||||
|
||||
.item-general {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.level-badge {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
.journal_day {
|
||||
margin-bottom: 1em;
|
||||
padding: 0.5em;
|
||||
@@ -143,14 +167,17 @@ li.sticky {
|
||||
}
|
||||
}
|
||||
|
||||
@include smallScreen() {
|
||||
.like-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
font-size: 1.2em;
|
||||
color: $accentCol;
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.journal_item {
|
||||
flex-direction: column;
|
||||
|
||||
span {
|
||||
margin-top: 0.25em;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="journal-stats">
|
||||
<span v-if="store.driverStatsData">
|
||||
<h3>
|
||||
{{ $t('journal.stats-title') }} <span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
|
||||
</h3>
|
||||
|
||||
<div class="info-stats">
|
||||
<span class="stat-badge">
|
||||
<span>{{ $t('journal.stats-timetables') }}</span>
|
||||
<span>{{ store.driverStatsData._count.fulfilled }} / {{ store.driverStatsData._count._all }}</span>
|
||||
</span>
|
||||
|
||||
<span class="stat-badge">
|
||||
<span>{{ $t('journal.stats-longest-timetable') }}</span>
|
||||
<span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span>
|
||||
</span>
|
||||
|
||||
<span class="stat-badge">
|
||||
<span>{{ $t('journal.stats-avg-timetable') }}</span>
|
||||
<span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
|
||||
</span>
|
||||
|
||||
<span class="stat-badge">
|
||||
<span>{{ $t('journal.stats-distance') }}</span>
|
||||
<span>
|
||||
{{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
|
||||
{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="stat-badge">
|
||||
<span>{{ $t('journal.stats-stations') }}</span>
|
||||
<span>
|
||||
{{ store.driverStatsData._sum.confirmedStopsCount }} /
|
||||
{{ store.driverStatsData._sum.allStopsCount }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<b v-else-if="store.driverStatsStatus == DataStatus.Loading">{{ $t('journal.stats-loading') }}</b>
|
||||
<b v-else-if="store.driverStatsStatus == DataStatus.Error">
|
||||
{{ $t('journal.stats-error ') }}
|
||||
</b>
|
||||
<b v-else>{{ $t('journal.driver-stats-info') }}</b>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
||||
import { useStore } from '../../store/store';
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
store: useStore(),
|
||||
DataStatus,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/JournalStats.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<section class="journal-header">
|
||||
<div class="journal-type-options">
|
||||
<router-link class="router-link" active-class="route-active" to="/journal/timetables" exact>
|
||||
{{ $t('journal.section-timetables') }}
|
||||
</router-link>
|
||||
•
|
||||
<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>
|
||||
@@ -2,10 +2,26 @@
|
||||
<div class="filters-options" @keydown.esc="showOptions = false">
|
||||
<div class="bg" v-if="showOptions" @click="showOptions = false"></div>
|
||||
|
||||
<button class="btn--filled btn--image" @click="showOptions = !showOptions" ref="button">
|
||||
<img :src="getIcon('filter2')" alt="Open filters" />
|
||||
{{ $t('options.filters') }} [F]
|
||||
</button>
|
||||
<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">
|
||||
@@ -17,26 +33,18 @@
|
||||
|
||||
<div class="search-box">
|
||||
<input
|
||||
v-if="propName == 'search-date'"
|
||||
class="search-input"
|
||||
id="date"
|
||||
type="date"
|
||||
min="2022-02-01"
|
||||
@keydown.enter="onSearchConfirm"
|
||||
v-model="searchersValues[propName]"
|
||||
/>
|
||||
|
||||
<input
|
||||
v-else
|
||||
class="search-input"
|
||||
@keydown.enter="onSearchConfirm"
|
||||
@focus="preventKeyDown = true"
|
||||
@blur="preventKeyDown = false"
|
||||
:placeholder="$t(`options.${propName}`)"
|
||||
v-model="searchersValues[propName]"
|
||||
:type="propName == 'search-date' ? 'date' : 'text'"
|
||||
:min="propName == 'search-date' ? '2022-02-01' : undefined"
|
||||
:list="propName.toString()"
|
||||
/>
|
||||
|
||||
<button class="search-exit">
|
||||
<button class="search-exit" v-if="propName != 'search-date'">
|
||||
<img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear(propName)" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -84,17 +92,21 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, inject, Prop, PropType } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { defineComponent, inject, PropType } from 'vue';
|
||||
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 SelectBox from '../Global/SelectBox.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { SelectBox, ActionButton },
|
||||
emits: ['onSearchConfirm', 'onOptionsReset'],
|
||||
emits: ['onSearchConfirm', 'onOptionsReset', 'onRefreshData'],
|
||||
mixins: [imageMixin, keyMixin],
|
||||
|
||||
props: {
|
||||
@@ -112,11 +124,23 @@ export default defineComponent({
|
||||
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,
|
||||
};
|
||||
},
|
||||
@@ -130,6 +154,10 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
computed: {
|
||||
driverStatsName() {
|
||||
return this.store.driverStatsName;
|
||||
},
|
||||
|
||||
translatedSorterOptions() {
|
||||
return this.$props.sorterOptionIds.map((id) => ({
|
||||
id,
|
||||
@@ -138,7 +166,75 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
|
||||
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: {
|
||||
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;
|
||||
|
||||
@@ -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,25 +1,39 @@
|
||||
<template>
|
||||
<ul class="journal-list">
|
||||
<transition-group class="journal-list" tag="ul" name="list-anim">
|
||||
<li
|
||||
v-for="{ timetable, sceneryList, ...item } in computedTimetableHistory"
|
||||
class="journal_item"
|
||||
:key="timetable.timetableId"
|
||||
:key="timetable.id"
|
||||
>
|
||||
<div class="journal_item-info">
|
||||
<div class="info-top">
|
||||
<div class="info-general">
|
||||
<span
|
||||
class="general-train"
|
||||
tabindex="0"
|
||||
@click="showTimetable(timetable)"
|
||||
@keydown.enter="showTimetable(timetable)"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<b class="text--primary">{{ timetable.trainCategoryCode }} </b>
|
||||
<b>{{ timetable.trainNo }}</b>
|
||||
| <span>{{ timetable.driverName }}</span> |
|
||||
<span class="text--grayed">#{{ timetable.timetableId }}</span>
|
||||
<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>
|
||||
<span class="general-time">
|
||||
<b class="info-date">{{ localeDay(timetable.beginDate, $i18n.locale) }}</b>
|
||||
<b
|
||||
class="info-status"
|
||||
@@ -39,26 +53,20 @@
|
||||
</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>
|
||||
@@ -72,8 +80,14 @@
|
||||
{{ 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>
|
||||
@@ -93,24 +107,20 @@
|
||||
|
||||
<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
|
||||
@@ -118,14 +128,13 @@
|
||||
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${car.split(':')[0]}.png`"
|
||||
:alt="car"
|
||||
/>
|
||||
|
||||
<div>{{ car.replace(/_/g, ' ').split(':')[0] }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</transition-group>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -133,6 +142,7 @@ 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({
|
||||
@@ -143,7 +153,7 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
|
||||
mixins: [dateMixin, imageMixin, modalTrainMixin],
|
||||
mixins: [dateMixin, imageMixin, modalTrainMixin, styleMixin],
|
||||
|
||||
computed: {
|
||||
computedTimetableHistory() {
|
||||
@@ -190,6 +200,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
showTimetable(timetable: TimetableHistory) {
|
||||
if (!timetable) return;
|
||||
if (timetable.terminated) return;
|
||||
|
||||
this.selectModalTrain(timetable.driverName + timetable.trainNo.toString());
|
||||
@@ -204,6 +215,7 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/animations.scss';
|
||||
@import '../../styles/variables.scss';
|
||||
@import '../../styles/responsive.scss';
|
||||
@import '../../styles/badge.scss';
|
||||
@@ -235,10 +247,14 @@ hr {
|
||||
}
|
||||
}
|
||||
|
||||
&-top {
|
||||
&-general {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
gap: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
&-route {
|
||||
@@ -250,6 +266,12 @@ hr {
|
||||
}
|
||||
}
|
||||
|
||||
.general-train {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
ul.stock-list {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
@@ -290,14 +312,9 @@ ul.stock-list {
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.info-top {
|
||||
.info-general {
|
||||
flex-direction: column;
|
||||
|
||||
span {
|
||||
margin: 0.1em auto;
|
||||
}
|
||||
}
|
||||
|
||||
.info-extended {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -5,25 +5,31 @@
|
||||
<div class="list-warning" v-else-if="dispatcherHistoryList.length == 0">{{ $t('scenery.history-list-empty') }}</div>
|
||||
|
||||
<ul class="history-list" v-else>
|
||||
<li class="list-item" v-for="historyItem in dispatcherHistoryList">
|
||||
<div>
|
||||
<router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`">
|
||||
<span class="text--grayed">#{{ historyItem.stationHash }} </span>
|
||||
<b>{{ historyItem.dispatcherName }}</b>
|
||||
</router-link>
|
||||
</div>
|
||||
<li class="list-item" v-for="item in dispatcherHistoryList">
|
||||
<router-link class="item-general" :to="`/journal/dispatchers?dispatcherName=${item.dispatcherName}`">
|
||||
<span class="text--grayed">#{{ item.stationHash }} </span>
|
||||
<b
|
||||
v-if="item.dispatcherLevel !== null"
|
||||
class="level-badge dispatcher"
|
||||
:style="calculateExpStyle(item.dispatcherLevel, item.dispatcherIsSupporter)"
|
||||
>
|
||||
{{ item.dispatcherLevel >= 2 ? item.dispatcherLevel : 'L' }}
|
||||
</b>
|
||||
|
||||
<div v-if="historyItem.timestampTo">
|
||||
<b>{{ $d(historyItem.timestampFrom) }}</b>
|
||||
<b>{{ item.dispatcherName }}</b>
|
||||
</router-link>
|
||||
|
||||
{{ timestampToString(historyItem.timestampFrom) }}
|
||||
- {{ timestampToString(historyItem.timestampTo) }} ({{ calculateDuration(historyItem.currentDuration) }})
|
||||
<div v-if="item.timestampTo">
|
||||
<b>{{ $d(item.timestampFrom) }}</b>
|
||||
|
||||
{{ timestampToString(item.timestampFrom) }}
|
||||
- {{ timestampToString(item.timestampTo) }} ({{ calculateDuration(item.currentDuration) }})
|
||||
</div>
|
||||
|
||||
<div class="dispatcher-online" v-else>
|
||||
{{ $t('journal.online-since') }}
|
||||
<b>{{ timestampToString(historyItem.timestampFrom) }}</b>
|
||||
({{ calculateDuration(historyItem.currentDuration) }})
|
||||
<b>{{ timestampToString(item.timestampFrom) }}</b>
|
||||
({{ calculateDuration(item.currentDuration) }})
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -39,10 +45,11 @@ import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIDa
|
||||
import Station from '../../scripts/interfaces/Station';
|
||||
import { URLs } from '../../scripts/utils/apiURLs';
|
||||
import Loading from '../Global/Loading.vue';
|
||||
import styleMixin from '../../mixins/styleMixin';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SceneryDispatchersHistory',
|
||||
mixins: [dateMixin],
|
||||
mixins: [dateMixin, styleMixin],
|
||||
props: {
|
||||
station: {
|
||||
type: Object as PropType<Station>,
|
||||
@@ -55,7 +62,7 @@ export default defineComponent({
|
||||
dataStatus: DataStatus.Loading,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
activated() {
|
||||
this.fetchAPIData();
|
||||
},
|
||||
methods: {
|
||||
@@ -96,6 +103,13 @@ export default defineComponent({
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.item-general {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.dispatcher-online {
|
||||
color: springgreen;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<section class="info-header">
|
||||
<a class="scenery-name" :href="station.generalInfo?.url">
|
||||
<a class="scenery-name" :href="station.generalInfo?.url" target="_blank">
|
||||
{{ station.name }}
|
||||
</a>
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div class="scenery-info">
|
||||
<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" />
|
||||
|
||||
<div class="general-list">
|
||||
<div class="scenery-general-list">
|
||||
<span>
|
||||
<b>{{ $t('availability.title') }}:</b> {{ $t(`availability.${station.generalInfo.availability}`) }}
|
||||
|
||||
<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>
|
||||
|
||||
@@ -26,26 +26,32 @@
|
||||
</span>
|
||||
<span v-if="station.generalInfo.project">
|
||||
• <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>
|
||||
</div>
|
||||
|
||||
<scenery-info-routes :station="station" />
|
||||
|
||||
<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(', ') }}
|
||||
</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 style="margin: 2em 0; height: 2px; background-color: white" />
|
||||
<div style="margin: 2em 0; height: 2px; background-color: white"></div>
|
||||
|
||||
<!-- info dispatcher -->
|
||||
<scenery-info-dispatcher :station="station" :onlineFrom="onlineFrom" />
|
||||
@@ -72,7 +78,6 @@ import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue';
|
||||
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
|
||||
import Station from '../../scripts/interfaces/Station';
|
||||
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
SceneryInfoDispatcher,
|
||||
@@ -125,11 +130,11 @@ h3.section-header {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.info-general {
|
||||
.scenery-info-general {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.general-list {
|
||||
.scenery-general-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<section class="info-dispatcher">
|
||||
<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' }}
|
||||
</span>
|
||||
|
||||
|
||||
@@ -1,114 +1,108 @@
|
||||
<template>
|
||||
<section class="info-routes" v-if="station.generalInfo">
|
||||
<div class="routes one-way" v-if="station.generalInfo.routes.oneWay.length > 0">
|
||||
<b>{{ $t('scenery.one-way-routes') }}</b>
|
||||
|
||||
<ul class="routes-list">
|
||||
<li
|
||||
v-for="route in station.generalInfo.routes.oneWay"
|
||||
:class="{ 'no-catenary': !route.catenary, internal: route.isInternal }"
|
||||
>
|
||||
{{ route.name }}
|
||||
<b v-if="route.SBL">SBL</b>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="routes two-way" v-if="station.generalInfo.routes.twoWay.length > 0">
|
||||
<b>{{ $t('scenery.two-way-routes') }}</b>
|
||||
|
||||
<ul class="routes-list">
|
||||
<li
|
||||
v-for="route in station.generalInfo.routes.twoWay"
|
||||
:class="{ 'no-catenary': !route.catenary, internal: route.isInternal }"
|
||||
>
|
||||
{{ route.name }} <b v-if="route.SBL">SBL</b>
|
||||
</li>
|
||||
</ul>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import Station from '../../../scripts/interfaces/Station';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
station: {
|
||||
type: Object as () => Station,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.info-routes {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.routes {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
padding: 0.25em;
|
||||
}
|
||||
|
||||
ul.routes-list {
|
||||
margin: 0.45em 0.25em;
|
||||
display: flex;
|
||||
|
||||
li {
|
||||
background-color: #007599;
|
||||
|
||||
padding: 0.2em 0.25em;
|
||||
margin-left: 0.25em;
|
||||
|
||||
&.no-catenary {
|
||||
background-color: #686868;
|
||||
}
|
||||
|
||||
&.internal {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
b {
|
||||
color: var(--clr-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<section class="info-routes" v-if="station.generalInfo">
|
||||
<div class="routes one-way" v-if="station.generalInfo.routes.oneWay.length > 0">
|
||||
<b>{{ $t('scenery.one-way-routes') }}</b>
|
||||
|
||||
<ul class="routes-list">
|
||||
<li v-for="route in station.generalInfo.routes.oneWay">
|
||||
<span :class="{ 'no-catenary': !route.catenary, internal: route.isInternal }"> {{ route.name }}</span>
|
||||
<span v-if="route.speed" class="speed">{{ route.speed }}</span>
|
||||
<span v-if="route.SBL" class="sbl">SBL</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="routes two-way" v-if="station.generalInfo.routes.twoWay.length > 0">
|
||||
<b>{{ $t('scenery.two-way-routes') }}</b>
|
||||
|
||||
<ul class="routes-list">
|
||||
<li v-for="route in station.generalInfo.routes.twoWay">
|
||||
<span :class="{ 'no-catenary': !route.catenary, internal: route.isInternal }">{{ route.name }}</span>
|
||||
<span v-if="route.speed" class="speed">{{ route.speed }}</span>
|
||||
<span v-if="route.SBL" class="sbl">SBL</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import Station from '../../../scripts/interfaces/Station';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
station: {
|
||||
type: Object as () => Station,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.info-routes {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.routes {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
padding: 0.25em;
|
||||
}
|
||||
|
||||
ul.routes-list {
|
||||
margin: 0.45em 0.25em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
li {
|
||||
margin: 0.5em 0.25em;
|
||||
|
||||
span {
|
||||
padding: 0.2em 0.25em;
|
||||
background-color: #007599;
|
||||
font-weight: bold;
|
||||
|
||||
&.no-catenary {
|
||||
background-color: #686868;
|
||||
}
|
||||
|
||||
&.internal {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&.speed {
|
||||
background-color: #404040;
|
||||
color: #cfcfcf;
|
||||
}
|
||||
|
||||
&.sbl {
|
||||
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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
{{ $t('scenery.no-timetables') }}
|
||||
</span>
|
||||
|
||||
<transition-group name="timetables-anim">
|
||||
<transition-group name="list-anim">
|
||||
<div
|
||||
class="timetable-item"
|
||||
v-for="(scheduledTrain, i) in computedScheduledTrains"
|
||||
@@ -272,22 +272,7 @@ export default defineComponent({
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/responsive.scss';
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
.timetables-anim-move,
|
||||
.timetables-anim-enter-active,
|
||||
.timetables-anim-leave-active {
|
||||
transition: all 250ms ease;
|
||||
}
|
||||
|
||||
.timetables-anim-enter-from,
|
||||
.timetables-anim-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
.timetables-anim-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
@import '../../styles/animations.scss';
|
||||
|
||||
.scenery-timetable {
|
||||
height: 100%;
|
||||
@@ -486,21 +471,6 @@ export default defineComponent({
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.scenery-timetable-list-anim {
|
||||
&-enter-from,
|
||||
&-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&-enter-active {
|
||||
transition: all 100ms ease-out;
|
||||
}
|
||||
|
||||
&-leave-active {
|
||||
transition: all 100ms ease-out 100ms;
|
||||
}
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.timetable-item {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -2,8 +2,46 @@
|
||||
<section class="scenery-timetables-history scenery-section">
|
||||
<Loading v-if="dataStatus != 2" />
|
||||
|
||||
<div class="list-warning" v-else-if="sceneryHistoryList.length == 0">{{ $t('scenery.history-list-empty') }}</div>
|
||||
<ul class="history-list" v-else>
|
||||
<table v-else-if="sceneryHistoryList.length">
|
||||
<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">
|
||||
<div>
|
||||
<b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b>
|
||||
@@ -11,24 +49,22 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<router-link :to="`/journal/timetables?timetableId=${historyItem.timetableId}`">
|
||||
<span class="text--grayed"> #{{ historyItem.timetableId }} </span>
|
||||
<router-link :to="`/journal/timetables?timetableId=${historyItem.id}`">
|
||||
<span class="text--grayed"> #{{ historyItem.id }} </span>
|
||||
<b class="text--primary"> {{ historyItem.trainCategoryCode }} {{ historyItem.trainNo }}</b>
|
||||
<div>{{ historyItem.driverName }}</div>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div>{{ historyItem.route.replace('|', ' -> ') }}</div>
|
||||
<!-- <div>{{ historyItem.routeDistance }} km</div> -->
|
||||
<div>
|
||||
{{ $t('scenery.timetable-author-title') }}:
|
||||
<b v-if="historyItem.authorName">{{ historyItem.authorName }}</b>
|
||||
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i>
|
||||
</div>
|
||||
|
||||
<!-- <div v-if="historyItem.authorId">{{ historyItem.authorName }}</div> -->
|
||||
</li>
|
||||
</ul>
|
||||
</ul> -->
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -57,7 +93,7 @@ export default defineComponent({
|
||||
dataStatus: DataStatus.Loading,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
activated() {
|
||||
this.fetchAPIData();
|
||||
},
|
||||
methods: {
|
||||
@@ -72,6 +108,10 @@ export default defineComponent({
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
|
||||
test() {
|
||||
console.log('test');
|
||||
},
|
||||
},
|
||||
components: { Loading },
|
||||
});
|
||||
@@ -91,17 +131,29 @@ export default defineComponent({
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr 2fr 1fr;
|
||||
gap: 1em;
|
||||
align-items: center;
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
background-color: #353535;
|
||||
padding: 0.5em;
|
||||
margin: 0.5em 0;
|
||||
thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: #222222;
|
||||
}
|
||||
|
||||
line-height: 1.5em;
|
||||
th {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
tr {
|
||||
background-color: #353535;
|
||||
border: none;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.75em;
|
||||
border-bottom: solid 5px #111;
|
||||
}
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
/>
|
||||
|
||||
<datalist id="sceneries">
|
||||
<option v-for="scenery in store.stationList" :value="scenery.name"></option>
|
||||
<option v-for="scenery in sortedStationList" :value="scenery.name"></option>
|
||||
</datalist>
|
||||
</label>
|
||||
</div>
|
||||
@@ -150,6 +150,14 @@ export default defineComponent({
|
||||
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);
|
||||
|
||||
@@ -100,7 +100,10 @@
|
||||
</td>
|
||||
|
||||
<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 }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -2,18 +2,23 @@
|
||||
<div class="train-info" tabindex="0">
|
||||
<section class="train-route">
|
||||
<div class="train_general">
|
||||
<span>
|
||||
<span class="timetable-id" v-if="train.timetableData">#{{ train.timetableData.timetableId }}</span>
|
||||
|
||||
<span class="timetable_warnings">
|
||||
<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>
|
||||
<strong v-if="train.timetableData">{{ train.timetableData.category }} </strong>
|
||||
<strong>{{ train.trainNo }}</strong>
|
||||
<span> | {{ train.driverName }} </span>
|
||||
<b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
|
||||
<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_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 skr" v-if="train.timetableData?.SKR">SKR</span>
|
||||
</span>
|
||||
|
||||
<strong>
|
||||
<span v-if="train.timetableData" class="text--primary">{{ train.timetableData.category }} </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 class="timetable_route" v-if="train.timetableData">
|
||||
@@ -36,9 +41,7 @@
|
||||
</div>
|
||||
|
||||
<div class="timetable_progress" style="margin-top: 0.5em" v-if="train.timetableData">
|
||||
<!-- <span> </span> -->
|
||||
<span class="timetable_progress-bar">
|
||||
<!-- {{ confirmedPercentage(train.timetableData.followingStops) }}% -->
|
||||
<span class="bar-bg"></span>
|
||||
<span
|
||||
class="bar-fg"
|
||||
@@ -90,6 +93,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import imageMixin from '../../mixins/imageMixin';
|
||||
import styleMixin from '../../mixins/styleMixin';
|
||||
import trainInfoMixin from '../../mixins/trainInfoMixin';
|
||||
import Train from '../../scripts/interfaces/Train';
|
||||
|
||||
@@ -106,12 +110,14 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
|
||||
mixins: [trainInfoMixin, imageMixin],
|
||||
mixins: [trainInfoMixin, imageMixin, styleMixin],
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/responsive.scss';
|
||||
@import '../../styles/badge.scss';
|
||||
|
||||
|
||||
.image-warning {
|
||||
height: 1em;
|
||||
@@ -145,19 +151,16 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.timetable-id {
|
||||
margin-right: 0.3em;
|
||||
color: #d2d2d2;
|
||||
}
|
||||
|
||||
.warning-timeout {
|
||||
background-color: #be3728;
|
||||
|
||||
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
border-radius: 50%;
|
||||
|
||||
padding: 0 0.25em;
|
||||
}
|
||||
|
||||
.timetable_stops {
|
||||
@@ -168,16 +171,20 @@ export default defineComponent({
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
gap: 0.25em;
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
.train-status-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.train-badge {
|
||||
padding: 0.15em 0.35em;
|
||||
margin-right: 0.3em;
|
||||
|
||||
padding: 0.1em 0.2em;
|
||||
border-radius: 0.2em;
|
||||
font-weight: bold;
|
||||
|
||||
font-size: 0.9em;
|
||||
@@ -191,7 +198,14 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
&.offline {
|
||||
background-color: #b83b2d;
|
||||
background-color: #9c362b;
|
||||
}
|
||||
}
|
||||
|
||||
.train-driver {
|
||||
&.supporter {
|
||||
color: orange;
|
||||
text-shadow: orange 0 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +217,9 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.timetable_warnings {
|
||||
display: flex;
|
||||
gap: 0.2em;
|
||||
|
||||
color: black;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<div class="filters-options" @keydown.esc="showOptions = false">
|
||||
<div class="bg" v-if="showOptions" @click="showOptions = false"></div>
|
||||
|
||||
<button class="btn--filled btn--image" @click="toggleShowOptions" ref="button">
|
||||
<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">
|
||||
@@ -56,7 +57,7 @@
|
||||
<h1 class="option-title" v-if="trainFilterList.length != 0">{{ $t('options.filter-title') }}</h1>
|
||||
<div class="options_filters">
|
||||
<div class="filter-option" v-for="filter in trainFilterList">
|
||||
<button class="btn--option" :data-disabled="!filter.isActive" @click="onFilterChange(filter)">
|
||||
<button class="btn--option" :data-inactive="!filter.isActive" @click="onFilterChange(filter)">
|
||||
{{ $t(`options.filter-${filter.id}`) }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -89,11 +90,17 @@ export default defineComponent({
|
||||
type: Array as PropType<Array<string>>,
|
||||
required: true,
|
||||
},
|
||||
|
||||
currentOptionsActive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
showOptions: false,
|
||||
lastSelectedFilter: null as TrainFilter | null,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -136,7 +143,11 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
onFilterChange(filter: TrainFilter) {
|
||||
// if (this.lastSelectedFilter?.id === filter.id)
|
||||
// this.trainFilterList.forEach((tf) => (tf.isActive = filter.id === tf.id));
|
||||
|
||||
filter.isActive = !filter.isActive;
|
||||
this.lastSelectedFilter = filter;
|
||||
},
|
||||
|
||||
clearAllFilters() {
|
||||
|
||||
@@ -89,6 +89,7 @@ import dateMixin from '../../mixins/dateMixin';
|
||||
import imageMixin from '../../mixins/imageMixin';
|
||||
import Train from '../../scripts/interfaces/Train';
|
||||
import TrainStop from '../../scripts/interfaces/TrainStop';
|
||||
import { useStore } from '../../store/store';
|
||||
import StopDate from '../Global/StopDate.vue';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -106,6 +107,8 @@ export default defineComponent({
|
||||
|
||||
setup(props) {
|
||||
return {
|
||||
store: useStore(),
|
||||
|
||||
lastConfirmed: computed(() => {
|
||||
return props.train.timetableData!.followingStops.findIndex(
|
||||
(stop, i, stops) => stop.confirmed && !stops[i + 1]?.confirmed && !stops[i + 1]?.stopped
|
||||
@@ -424,3 +427,4 @@ ul.stop_list > li.stop {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -2,18 +2,21 @@
|
||||
<div class="train-table">
|
||||
<transition name="anim" mode="out-in">
|
||||
<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') }}
|
||||
</div>
|
||||
|
||||
<div class="timeouts-warning" v-if="trainNumbersWithTimeouts.length != 0">
|
||||
<!-- <div class="timeouts-warning" v-if="trainNumbersWithTimeouts.length == 0">
|
||||
<b class="warning-timeout">?</b>
|
||||
{{ $t('trains.timeout') }}
|
||||
</div>
|
||||
|
||||
<ul class="train-list">
|
||||
</div> -->
|
||||
<transition-group name="list-anim" tag="ul" class="train-list" v-else>
|
||||
<li
|
||||
class="train-row"
|
||||
v-for="train in currentTrains"
|
||||
@@ -23,7 +26,7 @@
|
||||
>
|
||||
<TrainInfo :train="train" />
|
||||
</li>
|
||||
</ul>
|
||||
</transition-group>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@@ -94,6 +97,7 @@ export default defineComponent({
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/responsive.scss';
|
||||
@import '../../styles/animations.scss';
|
||||
|
||||
.anim {
|
||||
&-enter-from,
|
||||
@@ -154,8 +158,8 @@ img.train-image {
|
||||
|
||||
.train {
|
||||
&-list {
|
||||
overflow: auto;
|
||||
|
||||
position: relative;
|
||||
|
||||
@include smallScreen() {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,10 @@ export const sorterOptions = [
|
||||
id: 'distance',
|
||||
value: 'kilometraż',
|
||||
},
|
||||
{
|
||||
id: 'id',
|
||||
value: 'id rozkładu',
|
||||
},
|
||||
{
|
||||
id: 'progress',
|
||||
value: 'przebyta trasa',
|
||||
|
||||
+38
-4
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"general": {
|
||||
"and": " and ",
|
||||
"refresh": "REFRESH"
|
||||
},
|
||||
"app": {
|
||||
"sceneries": "SCENERIES",
|
||||
"trains": "TRAINS",
|
||||
@@ -8,15 +12,18 @@
|
||||
"error": "An error occured while loading data!",
|
||||
"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-confirm": "Roger that!"
|
||||
"migration-confirm": "Roger that!",
|
||||
"offline": "App is in the offline mode!"
|
||||
},
|
||||
"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!",
|
||||
"release-link": "Click here to browse version changelog (GitHub)",
|
||||
"confirm-button": "Understood!"
|
||||
"confirm-button": "UPDATE NOW",
|
||||
"later-button": "LATER"
|
||||
},
|
||||
"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-sceneries": "<b>S1a signal</b> <br> Cannot load online stations data!",
|
||||
"S2": "<b>S2 signal</b> <br> All data loaded successfully!",
|
||||
@@ -96,6 +103,7 @@
|
||||
"sort-timetable": "train no.",
|
||||
"sort-progress": "route progress",
|
||||
"sort-delay": "current delay",
|
||||
"sort-id": "timetable id",
|
||||
|
||||
"sort-total-stops": "total stops",
|
||||
"sort-beginDate": "date",
|
||||
@@ -253,13 +261,32 @@
|
||||
|
||||
"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"
|
||||
"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": {
|
||||
"users": "PLAYERS ONLINE",
|
||||
@@ -286,6 +313,13 @@
|
||||
"timetable-author-title": "Issued by",
|
||||
"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",
|
||||
"history-list-empty": "No recorded scenery history!",
|
||||
|
||||
|
||||
+38
-4
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"general": {
|
||||
"and": " oraz ",
|
||||
"refresh": "ODŚWIEŻ"
|
||||
},
|
||||
"app": {
|
||||
"sceneries": "SCENERIE",
|
||||
"trains": "POCIĄGI",
|
||||
@@ -8,17 +12,20 @@
|
||||
"error": "Wystąpił problem z załadowaniem danych!",
|
||||
"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-confirm": "Przyjąłem!"
|
||||
"migration-confirm": "Przyjąłem!",
|
||||
"offline": "Aplikacja w trybie offline!"
|
||||
},
|
||||
|
||||
"update": {
|
||||
"title": "Nowa wersja Stacjownika jest dostępna!",
|
||||
"paragraph1": "Miłego korzystania z aplikacji i niech S2 będzie z wami!",
|
||||
"release-link": "Kliknij, aby przejrzeć listę zmian (GitHub)",
|
||||
"confirm-button": "Przyjąłem!"
|
||||
"confirm-button": "ZAKTUALIZUJ",
|
||||
"later-button": "PÓŹNIEJ"
|
||||
},
|
||||
|
||||
"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-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!",
|
||||
@@ -97,7 +104,8 @@
|
||||
"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ść",
|
||||
@@ -259,11 +267,30 @@
|
||||
|
||||
"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"
|
||||
"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": {
|
||||
"users": "GRACZE ONLINE",
|
||||
@@ -290,6 +317,13 @@
|
||||
"timetable-author-title": "Wydany przez",
|
||||
"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",
|
||||
"history-list-empty": "Brak historii dla tej scenerii!",
|
||||
|
||||
|
||||
+11
@@ -7,9 +7,11 @@ import plLang from './locales/pl.json';
|
||||
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import { createPinia } from 'pinia';
|
||||
import { registerSW } from 'virtual:pwa-register';
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: 'pl',
|
||||
legacy: false,
|
||||
fallbackLocale: 'pl',
|
||||
messages: {
|
||||
en: enLang,
|
||||
@@ -18,6 +20,15 @@ const i18n = createI18n({
|
||||
enableLegacy: false,
|
||||
});
|
||||
|
||||
registerSW({
|
||||
onRegistered(r) {
|
||||
r &&
|
||||
setInterval(() => {
|
||||
r.update();
|
||||
}, 60 * 60 * 1000);
|
||||
},
|
||||
});
|
||||
|
||||
const clickOutsideDirective: Directive = {
|
||||
mounted(el, binding) {
|
||||
el.clickOutsideEvent = (event: Event) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { defineComponent } from 'vue';
|
||||
import { useStore } from '../store/store';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
data() {
|
||||
return {
|
||||
store: useStore(),
|
||||
};
|
||||
|
||||
@@ -4,11 +4,17 @@ export default defineComponent({
|
||||
methods: {
|
||||
calculateExpStyle(exp: number, isSupporter = false): string {
|
||||
const bgColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 85%, 50%)`) : '#666';
|
||||
|
||||
const fontColor = exp > 15 || exp == -1 ? 'white' : 'black';
|
||||
const boxShadow = isSupporter ? `box-shadow: 0 0 10px 2px ${bgColor};` : '';
|
||||
|
||||
return `background-color: ${bgColor}; color: ${fontColor}; ${boxShadow}`;
|
||||
|
||||
const fontColor = exp > 14 || exp == -1 ? 'white' : 'black';
|
||||
const boxShadow = isSupporter ? `box-shadow: 0 0 6px 2px ${bgColor};` : '';
|
||||
|
||||
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) {
|
||||
@@ -41,6 +47,6 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
+19
-28
@@ -1,6 +1,6 @@
|
||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
|
||||
import JournalDispatchersVue from '../components/JournalView/JournalDispatchers.vue';
|
||||
import JournalTimetablesVue from '../components/JournalView/JournalTimetables.vue';
|
||||
import JournalDispatchersVue from '../views/JournalDispatchers.vue';
|
||||
import JournalTimetablesVue from '../views/JournalTimetables.vue';
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
@@ -21,32 +21,23 @@ const routes: Array<RouteRecordRaw> = [
|
||||
},
|
||||
{
|
||||
path: '/journal',
|
||||
name: 'JournalView',
|
||||
component: () => import('../views/JournalView.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'JournalTimetables',
|
||||
component: JournalTimetablesVue,
|
||||
alias: '/timetables',
|
||||
},
|
||||
{
|
||||
path: 'dispatchers',
|
||||
name: 'JournalDispatchers',
|
||||
component: JournalDispatchersVue,
|
||||
props: (route) => ({ sceneryName: route.query.sceneryName, dispatcherName: route.query.dispatcherName }),
|
||||
},
|
||||
{
|
||||
path: 'timetables',
|
||||
name: 'JournalTimetables',
|
||||
component: JournalTimetablesVue,
|
||||
props: (route) => ({
|
||||
trainNo: route.query.trainNo,
|
||||
driverName: route.query.driverName,
|
||||
timetableId: route.query.timetableId,
|
||||
}),
|
||||
},
|
||||
],
|
||||
redirect: '/journal/timetables'
|
||||
},
|
||||
{
|
||||
path: '/journal/timetables',
|
||||
name: 'JournalTimetables',
|
||||
component: JournalTimetablesVue,
|
||||
props: (route) => ({
|
||||
trainNo: route.query.trainNo,
|
||||
driverName: route.query.driverName,
|
||||
timetableId: route.query.timetableId,
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/journal/dispatchers',
|
||||
name: 'JournalDispatchers',
|
||||
component: JournalDispatchersVue,
|
||||
props: (route) => ({ sceneryName: route.query.sceneryName, dispatcherName: route.query.dispatcherName }),
|
||||
},
|
||||
{
|
||||
path: '/:catchAll(.*)',
|
||||
|
||||
@@ -8,12 +8,13 @@ export default interface Station {
|
||||
generalInfo?: {
|
||||
name: string;
|
||||
url: string;
|
||||
|
||||
|
||||
reqLevel: number;
|
||||
// supportersOnly: boolean;
|
||||
|
||||
|
||||
lines: string;
|
||||
project: string;
|
||||
projectUrl?: string;
|
||||
|
||||
signalType: string;
|
||||
controlType: string;
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
export default interface StationRoutes {
|
||||
oneWay:
|
||||
{
|
||||
name: string;
|
||||
catenary: boolean;
|
||||
SBL: boolean;
|
||||
TWB: boolean;
|
||||
isInternal: boolean;
|
||||
tracks: number;
|
||||
}[];
|
||||
|
||||
twoWay: {
|
||||
name: string;
|
||||
catenary: boolean;
|
||||
SBL: boolean;
|
||||
TWB: boolean;
|
||||
isInternal: boolean;
|
||||
tracks: number;
|
||||
}[];
|
||||
|
||||
/* [catenary, noCatenary] */
|
||||
oneWayCatenaryRouteNames: string[];
|
||||
oneWayNoCatenaryRouteNames: string[];
|
||||
twoWayCatenaryRouteNames: string[];
|
||||
twoWayNoCatenaryRouteNames: string[];
|
||||
sblRouteNames: string[];
|
||||
}
|
||||
export default interface StationRoutes {
|
||||
oneWay: {
|
||||
name: string;
|
||||
catenary: boolean;
|
||||
SBL: boolean;
|
||||
TWB: boolean;
|
||||
isInternal: boolean;
|
||||
tracks: number;
|
||||
speed: number;
|
||||
length: number;
|
||||
}[];
|
||||
|
||||
twoWay: {
|
||||
name: string;
|
||||
catenary: boolean;
|
||||
SBL: boolean;
|
||||
TWB: boolean;
|
||||
isInternal: boolean;
|
||||
tracks: number;
|
||||
speed: number;
|
||||
length: number;
|
||||
}[];
|
||||
|
||||
/* [catenary, noCatenary] */
|
||||
oneWayCatenaryRouteNames: string[];
|
||||
oneWayNoCatenaryRouteNames: string[];
|
||||
twoWayCatenaryRouteNames: string[];
|
||||
twoWayNoCatenaryRouteNames: string[];
|
||||
sblRouteNames: string[];
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export default interface Train {
|
||||
driverId: number;
|
||||
trainNo: number;
|
||||
driverName: string;
|
||||
driverLevel: number;
|
||||
currentStationName: string;
|
||||
currentStationHash: string;
|
||||
locoURL: string;
|
||||
@@ -22,6 +23,7 @@ export default interface Train {
|
||||
cars: string[];
|
||||
|
||||
isTimeout: boolean;
|
||||
isSupporter: boolean;
|
||||
|
||||
timetableData?: {
|
||||
timetableId: number;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default interface TrainStop {
|
||||
export default interface TrainStop {
|
||||
stopName: string;
|
||||
stopNameRAW: string;
|
||||
stopType: string;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
export interface DispatcherHistory {
|
||||
id: string;
|
||||
|
||||
|
||||
currentDuration: number;
|
||||
dispatcherId: number;
|
||||
dispatcherName: string;
|
||||
dispatcherLevel: number | null;
|
||||
dispatcherRate: number;
|
||||
dispatcherIsSupporter: boolean;
|
||||
isOnline: boolean;
|
||||
lastOnlineTimestamp: number;
|
||||
region: string;
|
||||
@@ -11,4 +14,4 @@ export interface DispatcherHistory {
|
||||
stationName: string;
|
||||
timestampFrom: number;
|
||||
timestampTo?: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { TimetableHistory } from './TimetablesAPIData';
|
||||
|
||||
export interface ITimetablesDailyStats {
|
||||
totalTimetables: number;
|
||||
distanceSum: number;
|
||||
distanceAvg: number;
|
||||
|
||||
timetableId: number;
|
||||
timetableAuthor: string;
|
||||
timetableDriver: string;
|
||||
timetableRouteDistance: number;
|
||||
|
||||
mostActiveDispatchers: {
|
||||
name: string;
|
||||
count: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface ITimetablesDailyStatsResponse {
|
||||
totalTimetables: number;
|
||||
distanceSum: number;
|
||||
distanceAvg: number;
|
||||
maxTimetable: TimetableHistory | null;
|
||||
|
||||
mostActiveDispatchers: {
|
||||
name: string;
|
||||
count: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
export interface TimetableHistory {
|
||||
id: number;
|
||||
|
||||
timetableId: number;
|
||||
trainNo: number;
|
||||
trainCategoryCode: string;
|
||||
|
||||
driverId: number;
|
||||
driverName: string;
|
||||
driverLevel: number | null;
|
||||
driverIsSupporter: boolean;
|
||||
|
||||
route: string;
|
||||
twr: number;
|
||||
skr: number;
|
||||
|
||||
@@ -13,6 +13,7 @@ export default interface TrainAPIData {
|
||||
driverName: string;
|
||||
driverId: number;
|
||||
driverIsSupporter: boolean;
|
||||
driverLevel?: number;
|
||||
|
||||
currentStationName: string;
|
||||
currentStationHash: string;
|
||||
|
||||
@@ -1,115 +1,120 @@
|
||||
import { TrainFilter } from "../../types/Trains/TrainOptionsTypes";
|
||||
import { TrainFilterType } from "../enums/TrainFilterType";
|
||||
import Train from "../interfaces/Train";
|
||||
import TrainStop from "../interfaces/TrainStop";
|
||||
import { TrainFilter } from '../../types/Trains/TrainOptionsTypes';
|
||||
import { TrainFilterType } from '../enums/TrainFilterType';
|
||||
import Train from '../interfaces/Train';
|
||||
import TrainStop from '../interfaces/TrainStop';
|
||||
|
||||
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) {
|
||||
if (!stops) return -Infinity;
|
||||
if (!stops) return -Infinity;
|
||||
|
||||
const delay =
|
||||
stops.find((stop, i) => (i == 0 && !stop.confirmed) || (i > 0 && stops[i - 1].confirmed && !stop.confirmed))
|
||||
?.departureDelay || 0;
|
||||
const delay =
|
||||
stops.find((stop, i) => (i == 0 && !stop.confirmed) || (i > 0 && stops[i - 1].confirmed && !stop.confirmed))
|
||||
?.departureDelay || 0;
|
||||
|
||||
return delay;
|
||||
};
|
||||
return delay;
|
||||
}
|
||||
|
||||
function filterTrainList(trainList: Train[], searchedTrain: string, searchedDriver: string, filters: TrainFilter[]) {
|
||||
return trainList.filter(
|
||||
(train) => {
|
||||
const isFiltered = filters.every(f => {
|
||||
if (f.isActive) return true;
|
||||
|
||||
if (!train.timetableData) return filters.find(filter => filter.id == TrainFilterType.noTimetable)!.isActive;
|
||||
return trainList.filter((train) => {
|
||||
const isFiltered = filters.every((f) => {
|
||||
if (f.isActive) return true;
|
||||
|
||||
switch (f.id) {
|
||||
case TrainFilterType.comments:
|
||||
return !train.timetableData.followingStops.some(stop => stop.comments);
|
||||
|
||||
case TrainFilterType.twr:
|
||||
return !train.timetableData.TWR;
|
||||
|
||||
case TrainFilterType.skr:
|
||||
return !train.timetableData.SKR;
|
||||
|
||||
case TrainFilterType.passenger:
|
||||
return !/^[AMRE]\D{2}$/.test(train.timetableData.category);
|
||||
|
||||
case TrainFilterType.freight:
|
||||
return !train.timetableData.category.startsWith('T');
|
||||
|
||||
case TrainFilterType.other:
|
||||
return !/^[PXZL]\D{2}$/.test(train.timetableData.category);
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
})
|
||||
if (!train.timetableData) return filters.find((filter) => filter.id == TrainFilterType.noTimetable)!.isActive;
|
||||
|
||||
return (searchedTrain.length > 0 ? train.trainNo.toString().startsWith(searchedTrain) : true) &&
|
||||
(searchedDriver.length > 0 ? train.driverName.toLowerCase().startsWith(searchedDriver.toLowerCase()) : true) && isFiltered
|
||||
}
|
||||
switch (f.id) {
|
||||
case TrainFilterType.comments:
|
||||
return !train.timetableData.followingStops.some((stop) => stop.comments);
|
||||
|
||||
case TrainFilterType.twr:
|
||||
return !train.timetableData.TWR;
|
||||
|
||||
case TrainFilterType.skr:
|
||||
return !train.timetableData.SKR;
|
||||
|
||||
case TrainFilterType.passenger:
|
||||
return !/^[AMRE]\D{2}$/.test(train.timetableData.category);
|
||||
|
||||
case TrainFilterType.freight:
|
||||
return !train.timetableData.category.startsWith('T');
|
||||
|
||||
case TrainFilterType.other:
|
||||
return !/^[PXZL]\D{2}$/.test(train.timetableData.category);
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
(searchedTrain.length > 0 ? train.trainNo.toString().startsWith(searchedTrain) : true) &&
|
||||
(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 }) {
|
||||
return trainList.sort((a: Train, b: Train) => {
|
||||
switch (sorterActive.id) {
|
||||
case 'mass':
|
||||
if (a.mass > b.mass) return sorterActive.dir;
|
||||
return -sorterActive.dir;
|
||||
return trainList.sort((a: Train, b: Train) => {
|
||||
switch (sorterActive.id) {
|
||||
case 'id':
|
||||
if ((a.timetableData?.timetableId || -1) > (b.timetableData?.timetableId || -1)) return sorterActive.dir;
|
||||
|
||||
case 'distance':
|
||||
if ((a.timetableData?.routeDistance || -1) > (b.timetableData?.routeDistance || -1)) return sorterActive.dir;
|
||||
return -sorterActive.dir;
|
||||
|
||||
return -sorterActive.dir;
|
||||
case 'mass':
|
||||
if (a.mass > b.mass) return sorterActive.dir;
|
||||
return -sorterActive.dir;
|
||||
|
||||
case 'progress':
|
||||
if (confirmedPercentage(a.timetableData?.followingStops) > confirmedPercentage(b.timetableData?.followingStops))
|
||||
return sorterActive.dir;
|
||||
case 'distance':
|
||||
if ((a.timetableData?.routeDistance || -1) > (b.timetableData?.routeDistance || -1)) return sorterActive.dir;
|
||||
|
||||
return -sorterActive.dir;
|
||||
return -sorterActive.dir;
|
||||
|
||||
case 'delay':
|
||||
if (currentDelay(a.timetableData?.followingStops) > currentDelay(b.timetableData?.followingStops))
|
||||
return sorterActive.dir;
|
||||
case 'progress':
|
||||
if (confirmedPercentage(a.timetableData?.followingStops) > confirmedPercentage(b.timetableData?.followingStops))
|
||||
return sorterActive.dir;
|
||||
|
||||
return -sorterActive.dir;
|
||||
return -sorterActive.dir;
|
||||
|
||||
case 'speed':
|
||||
if (a.speed > b.speed) return sorterActive.dir;
|
||||
return -sorterActive.dir;
|
||||
case 'delay':
|
||||
if (currentDelay(a.timetableData?.followingStops) > currentDelay(b.timetableData?.followingStops))
|
||||
return sorterActive.dir;
|
||||
|
||||
case 'timetable':
|
||||
if (a.trainNo > b.trainNo) return sorterActive.dir;
|
||||
return -sorterActive.dir;
|
||||
return -sorterActive.dir;
|
||||
|
||||
case 'length':
|
||||
if (a.length > b.length) return sorterActive.dir;
|
||||
return -sorterActive.dir;
|
||||
case 'speed':
|
||||
if (a.speed > b.speed) return sorterActive.dir;
|
||||
return -sorterActive.dir;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
case 'timetable':
|
||||
if (a.trainNo > b.trainNo) return sorterActive.dir;
|
||||
return -sorterActive.dir;
|
||||
|
||||
return 0;
|
||||
});
|
||||
case 'length':
|
||||
if (a.length > b.length) return sorterActive.dir;
|
||||
return -sorterActive.dir;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
export function filteredTrainList(
|
||||
trainList: Train[],
|
||||
searchedTrain: string,
|
||||
searchedDriver: string,
|
||||
sorterActive: { id: string; dir: number },
|
||||
filters: TrainFilter[]
|
||||
trainList: Train[],
|
||||
searchedTrain: string,
|
||||
searchedDriver: string,
|
||||
sorterActive: { id: string; dir: number },
|
||||
filters: TrainFilter[]
|
||||
) {
|
||||
|
||||
const filtered = filterTrainList(trainList, searchedTrain, searchedDriver, filters);
|
||||
return [...sortTrainList(filtered, sorterActive)];
|
||||
};
|
||||
const filtered = filterTrainList(trainList, searchedTrain, searchedDriver, filters);
|
||||
return [...sortTrainList(filtered, sorterActive)];
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
export const URLs = {
|
||||
stacjownikAPI:
|
||||
import.meta.env.VITE_APP_API_DEV == 1 && !import.meta.env.PROD
|
||||
? 'http://localhost:3000'
|
||||
: 'https://stacjownik.eu-4.evennode.com',
|
||||
import.meta.env.VITE_APP_API_DEV == 1 && !import.meta.env.PROD ? 'http://localhost:3000' : 'https://spythere.pl',
|
||||
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}`
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import inputData from '../data/options.json';
|
||||
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 }) => {
|
||||
switch (sorter.index) {
|
||||
@@ -58,7 +59,7 @@ const sortStations = (a: Station, b: Station, sorter: { index: number; dir: numb
|
||||
return a.name.localeCompare(b.name);
|
||||
};
|
||||
|
||||
const filterStations = (station: Station, filters: Filter) => {
|
||||
const filterStations = (station: Station, filters: Filter, isOffline = false) => {
|
||||
const returnMode = false;
|
||||
|
||||
if ((station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && filters['nonPublic'])
|
||||
@@ -236,6 +237,7 @@ export const useStationFiltersStore = defineStore('stationFiltersStore', {
|
||||
inputs: inputData,
|
||||
filters: { ...filterInitStates },
|
||||
sorterActive: { index: 0, dir: 1 },
|
||||
store: useStore(),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -249,7 +251,7 @@ export const useStationFiltersStore = defineStore('stationFiltersStore', {
|
||||
|
||||
return station;
|
||||
})
|
||||
.filter((station) => filterStations(station, this.filters))
|
||||
.filter((station) => filterStations(station, this.filters, this.store.isOffline))
|
||||
.sort((a, b) => sortStations(a, b, this.sorterActive));
|
||||
},
|
||||
|
||||
@@ -303,3 +305,4 @@ export const useStationFiltersStore = defineStore('stationFiltersStore', {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+80
-60
@@ -17,7 +17,6 @@ import {
|
||||
} from '../scripts/utils/storeUtils';
|
||||
import { APIData, StationJSONData, StoreState } from './storeTypes';
|
||||
|
||||
|
||||
export const useStore = defineStore('store', {
|
||||
state: () =>
|
||||
({
|
||||
@@ -25,6 +24,7 @@ export const useStore = defineStore('store', {
|
||||
|
||||
stationList: [],
|
||||
trainList: [],
|
||||
routesList: [],
|
||||
|
||||
sceneryData: [],
|
||||
lastDispatcherStatuses: [],
|
||||
@@ -35,12 +35,14 @@ export const useStore = defineStore('store', {
|
||||
stationCount: 0,
|
||||
|
||||
webSocket: undefined,
|
||||
isOffline: false,
|
||||
|
||||
dispatcherStatsName: '',
|
||||
dispatcherStatsData: undefined,
|
||||
|
||||
driverStatsName: '',
|
||||
driverStatsData: undefined,
|
||||
driverStatsStatus: DataStatus.Initialized,
|
||||
|
||||
chosenModalTrainId: undefined,
|
||||
|
||||
@@ -52,9 +54,10 @@ export const useStore = defineStore('store', {
|
||||
trains: DataStatus.Loading,
|
||||
},
|
||||
|
||||
currentStatsTab: 'daily',
|
||||
|
||||
blockScroll: false,
|
||||
listenerLaunched: false,
|
||||
|
||||
} as StoreState),
|
||||
|
||||
actions: {
|
||||
@@ -98,6 +101,9 @@ export const useStore = defineStore('store', {
|
||||
lastSeen: train.lastSeen,
|
||||
isTimeout: train.isTimeout,
|
||||
|
||||
isSupporter: train.driverIsSupporter,
|
||||
driverLevel: train.driverLevel,
|
||||
|
||||
timetableData: timetable
|
||||
? {
|
||||
timetableId: timetable.timetableId,
|
||||
@@ -110,8 +116,8 @@ export const useStore = defineStore('store', {
|
||||
sceneries: timetable.sceneries,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}) as Train[];
|
||||
} as Train;
|
||||
});
|
||||
},
|
||||
|
||||
getDispatcherStatus(onlineStationData: StationAPIData) {
|
||||
@@ -220,6 +226,14 @@ export const useStore = defineStore('store', {
|
||||
const onlineStationNames: string[] = [];
|
||||
const prevDispatcherStatuses: StoreState['lastDispatcherStatuses'] = [];
|
||||
|
||||
if (this.isOffline) {
|
||||
this.stationList.forEach((station) => {
|
||||
station.onlineInfo = undefined;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.apiData.stations?.forEach((stationAPIData) => {
|
||||
if (stationAPIData.region !== this.region.id || !stationAPIData.isOnline) return;
|
||||
const station = this.stationList.find((s) => s.name === stationAPIData.stationName);
|
||||
@@ -281,78 +295,82 @@ export const useStore = defineStore('store', {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stationList = sceneryData.map((scenery) => ({
|
||||
name: scenery.name,
|
||||
this.stationList = sceneryData.map((scenery) => {
|
||||
return {
|
||||
name: scenery.name,
|
||||
|
||||
generalInfo: {
|
||||
...scenery,
|
||||
authors: scenery.authors?.split(',').map((a) => a.trim()),
|
||||
routes:
|
||||
scenery.routes
|
||||
?.split(';')
|
||||
.filter((routeString) => routeString)
|
||||
.reduce(
|
||||
(acc, routeString) => {
|
||||
const specs1 = routeString.split('_')[0];
|
||||
const isInternal = specs1.startsWith('!');
|
||||
const name = isInternal ? specs1.replace('!', '') : specs1;
|
||||
generalInfo: {
|
||||
...scenery,
|
||||
authors: scenery.authors?.split(',').map((a) => a.trim()),
|
||||
routes:
|
||||
scenery.routes
|
||||
?.split(';')
|
||||
.filter((routeString) => routeString)
|
||||
.reduce(
|
||||
(acc, routeString) => {
|
||||
const specs1 = routeString.split('_')[0];
|
||||
const isInternal = specs1.startsWith('!');
|
||||
const name = isInternal ? specs1.replace('!', '') : specs1;
|
||||
|
||||
const specs2 = routeString.split('_')[1].split('');
|
||||
const twoWay = specs2[0] == '2';
|
||||
const catenary = specs2[1] == 'E';
|
||||
const SBL = specs2[2] == 'S';
|
||||
const TWB = specs2[3] ? true : false;
|
||||
const specs2 = routeString.split('_')[1].split('');
|
||||
const twoWay = specs2[0] == '2';
|
||||
const catenary = specs2[1] == 'E';
|
||||
const SBL = specs2[2] == 'S';
|
||||
const TWB = specs2[3] ? true : false;
|
||||
const speed = Number(routeString.split(':')[1]) || 0;
|
||||
const length = Number(routeString.split(':')[2]) || 0;
|
||||
|
||||
const propName = twoWay
|
||||
? catenary
|
||||
? 'twoWayCatenaryRouteNames'
|
||||
: 'twoWayNoCatenaryRouteNames'
|
||||
: catenary
|
||||
? 'oneWayCatenaryRouteNames'
|
||||
: 'oneWayNoCatenaryRouteNames';
|
||||
const propName = twoWay
|
||||
? catenary
|
||||
? 'twoWayCatenaryRouteNames'
|
||||
: 'twoWayNoCatenaryRouteNames'
|
||||
: catenary
|
||||
? 'oneWayCatenaryRouteNames'
|
||||
: 'oneWayNoCatenaryRouteNames';
|
||||
|
||||
acc[twoWay ? 'twoWay' : 'oneWay'].push({
|
||||
name,
|
||||
SBL,
|
||||
TWB,
|
||||
catenary,
|
||||
isInternal,
|
||||
tracks: twoWay ? 2 : 1,
|
||||
});
|
||||
if (!isInternal) acc[propName].push(name);
|
||||
acc[twoWay ? 'twoWay' : 'oneWay'].push({
|
||||
name,
|
||||
SBL,
|
||||
TWB,
|
||||
catenary,
|
||||
isInternal,
|
||||
tracks: twoWay ? 2 : 1,
|
||||
length,
|
||||
speed,
|
||||
});
|
||||
if (!isInternal) acc[propName].push(name);
|
||||
|
||||
if (SBL) acc['sblRouteNames'].push(name);
|
||||
if (SBL) acc['sblRouteNames'].push(name);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
oneWay: [],
|
||||
twoWay: [],
|
||||
sblRouteNames: [],
|
||||
oneWayCatenaryRouteNames: [],
|
||||
oneWayNoCatenaryRouteNames: [],
|
||||
twoWayCatenaryRouteNames: [],
|
||||
twoWayNoCatenaryRouteNames: [],
|
||||
} as StationRoutes
|
||||
) || {},
|
||||
checkpoints: scenery.checkpoints
|
||||
? scenery.checkpoints.split(';').map((sub) => ({ checkpointName: sub, scheduledTrains: [] }))
|
||||
: [],
|
||||
},
|
||||
}));
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
oneWay: [],
|
||||
twoWay: [],
|
||||
sblRouteNames: [],
|
||||
oneWayCatenaryRouteNames: [],
|
||||
oneWayNoCatenaryRouteNames: [],
|
||||
twoWayCatenaryRouteNames: [],
|
||||
twoWayNoCatenaryRouteNames: [],
|
||||
} as StationRoutes
|
||||
) || {},
|
||||
checkpoints: scenery.checkpoints
|
||||
? scenery.checkpoints.split(';').map((sub) => ({ checkpointName: sub, scheduledTrains: [] }))
|
||||
: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
connectToWebsocket() {
|
||||
const socket = io(URLs.stacjownikAPI, {
|
||||
transports: ['websocket', 'polling'],
|
||||
// transports: ['websocket', 'polling'],
|
||||
rememberUpgrade: true,
|
||||
reconnection: true,
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
socket.on('connect_error', (err) => {
|
||||
this.dataStatuses.connection = DataStatus.Error;
|
||||
this.webSocket = undefined;
|
||||
});
|
||||
|
||||
socket.on('UPDATE', (data: APIData) => {
|
||||
@@ -362,6 +380,8 @@ export const useStore = defineStore('store', {
|
||||
});
|
||||
|
||||
socket.emit('FETCH_DATA', {}, (data: APIData) => {
|
||||
this.dataStatuses.connection = DataStatus.Loaded;
|
||||
|
||||
this.apiData = data;
|
||||
this.setOnlineData();
|
||||
});
|
||||
|
||||
@@ -23,15 +23,19 @@ export interface StoreState {
|
||||
stationCount: number;
|
||||
|
||||
webSocket?: Socket;
|
||||
isOffline: boolean;
|
||||
|
||||
dispatcherStatsName: string;
|
||||
dispatcherStatsData?: DispatcherStatsAPIData;
|
||||
|
||||
driverStatsName: string;
|
||||
driverStatsData?: DriverStatsAPIData;
|
||||
driverStatsStatus: DataStatus;
|
||||
|
||||
chosenModalTrainId?: string;
|
||||
|
||||
currentStatsTab: 'daily' | 'driver';
|
||||
|
||||
dataStatuses: {
|
||||
connection: DataStatus;
|
||||
sceneries: DataStatus;
|
||||
@@ -48,6 +52,7 @@ export interface APIData {
|
||||
stations?: StationAPIData[];
|
||||
dispatchers?: string[][];
|
||||
trains?: TrainAPIData[];
|
||||
connectedSocketCount: number;
|
||||
}
|
||||
|
||||
export interface StationJSONData {
|
||||
@@ -55,6 +60,7 @@ export interface StationJSONData {
|
||||
url: string;
|
||||
lines: string;
|
||||
project: string;
|
||||
projectUrl: string;
|
||||
|
||||
reqLevel: number;
|
||||
|
||||
@@ -64,8 +70,9 @@ export interface StationJSONData {
|
||||
SUP: boolean;
|
||||
|
||||
routes: string;
|
||||
|
||||
checkpoints: string | null;
|
||||
authors?: string;
|
||||
|
||||
availability: Availability;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,5 @@
|
||||
@import 'responsive.scss';
|
||||
|
||||
// Animations
|
||||
.warning {
|
||||
&-enter-from,
|
||||
&-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&-enter-active {
|
||||
transition: all 150ms 100ms ease-out;
|
||||
}
|
||||
|
||||
&-leave-active {
|
||||
transition: all 150ms 100ms ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
@import 'animations.scss';
|
||||
//Styles
|
||||
|
||||
.list_wrapper {
|
||||
@@ -26,10 +10,16 @@
|
||||
padding-right: 0.2em;
|
||||
}
|
||||
|
||||
.journal-list {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.journal_wrapper {
|
||||
max-width: 1350px;
|
||||
width: 100%;
|
||||
|
||||
margin: 0 auto;
|
||||
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
@@ -57,6 +47,11 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
|
||||
position: relative;
|
||||
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.btn--load-data {
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
@import 'variables.scss';
|
||||
@import 'responsive.scss';
|
||||
|
||||
.journal-stats {
|
||||
.stats-tab {
|
||||
background-color: #1a1a1a;
|
||||
box-shadow: 0 0 5px 1px $accentCol;
|
||||
|
||||
padding: 1em;
|
||||
margin-bottom: 1em;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.info-stats {
|
||||
@@ -40,3 +47,4 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
+58
-28
@@ -1,28 +1,58 @@
|
||||
.badge {
|
||||
font-weight: 600;
|
||||
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
|
||||
background: #585858;
|
||||
|
||||
margin: 0.25em;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
|
||||
&-none {
|
||||
font-weight: 600;
|
||||
|
||||
padding: 0.2em 0.4em;
|
||||
background: firebrick;
|
||||
|
||||
text-align: center;
|
||||
|
||||
@include smallScreen() {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
.badge {
|
||||
font-weight: 600;
|
||||
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
|
||||
background: #585858;
|
||||
|
||||
margin: 0.25em;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
|
||||
&-none {
|
||||
font-weight: 600;
|
||||
|
||||
padding: 0.2em 0.4em;
|
||||
background: firebrick;
|
||||
|
||||
text-align: center;
|
||||
|
||||
@include smallScreen() {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,24 @@
|
||||
@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;
|
||||
@@ -42,22 +60,16 @@ h1.option-title {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.filters-options {
|
||||
position: relative;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.options_wrapper {
|
||||
position: absolute;
|
||||
|
||||
background-color: $bgCol;
|
||||
box-shadow: 0 5px 10px 2px #0f0f0f;
|
||||
|
||||
width: 100%;
|
||||
width: 97%;
|
||||
max-width: 500px;
|
||||
|
||||
padding: 1em;
|
||||
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
|
||||
+19
-20
@@ -18,19 +18,21 @@
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-color: transparent;
|
||||
|
||||
&-track {
|
||||
border-radius: 0.5em;
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
&-thumb {
|
||||
border-radius: 0.5em;
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
&-corner {
|
||||
background-color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
@@ -43,11 +45,12 @@ body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Quicksand', sans-serif;
|
||||
font-weight: 500;
|
||||
overflow-y: scroll;
|
||||
|
||||
&.no-scroll {
|
||||
overflow-y: hidden;
|
||||
padding-right: 1rem;
|
||||
padding-right: 15px;
|
||||
|
||||
@include smallScreen() {
|
||||
padding: 0;
|
||||
@@ -79,22 +82,9 @@ body {
|
||||
|
||||
transition: opacity 0.3s;
|
||||
padding: 0.25em;
|
||||
|
||||
// @include smallScreen() {
|
||||
// right: 0;
|
||||
// left: 0;
|
||||
|
||||
// &::after {
|
||||
// left: 75%;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
&:hover > .content {
|
||||
// @include smallScreen() {
|
||||
// display: none;
|
||||
// }
|
||||
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -103,7 +93,6 @@ body {
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
// font-family: "Open Sans", sans-serif;
|
||||
border: none;
|
||||
font-family: 'Quicksand', sans-serif;
|
||||
font-size: 1em;
|
||||
@@ -207,10 +196,20 @@ button {
|
||||
padding: 0.25em 0.5em;
|
||||
|
||||
transition: all 100ms ease;
|
||||
|
||||
&[data-disabled='true'] {
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
&[data-inactive='true'] {
|
||||
opacity: 0.55;
|
||||
}
|
||||
}
|
||||
|
||||
button.btn--filled {
|
||||
background-color: #333;
|
||||
background-color: #1a1a1a;
|
||||
border-radius: 0.25em;
|
||||
|
||||
&:hover {
|
||||
|
||||
+16
-11
@@ -1,18 +1,23 @@
|
||||
@mixin smallScreen() {
|
||||
@media only screen and (max-width: 700px) {
|
||||
@content;
|
||||
}
|
||||
@media only screen and (max-width: 700px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mixin midScreen() {
|
||||
@media only screen and (max-width: 1150px) {
|
||||
@content;
|
||||
}
|
||||
@media only screen and (max-width: 1150px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin screenLandscape() {
|
||||
@media only screen and (orientation: landscape) and (max-device-height: 450px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin bigScreen() {
|
||||
@media only screen and (min-width: 2000px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 2000px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { JournalFilterType } from '../../scripts/enums/JournalFilterType';
|
||||
|
||||
export type JorunalTimetableSearchType = {
|
||||
[key in 'search-driver' | 'search-train' | 'search-date' | 'search-author']: string;
|
||||
export type JournalTimetableSearchKey = 'search-driver' | 'search-train' | 'search-date' | 'search-dispatcher';
|
||||
|
||||
export type JournalTimetableSearchType = {
|
||||
[key in JournalTimetableSearchKey]: string;
|
||||
};
|
||||
|
||||
export interface JournalTimetableFilter {
|
||||
@@ -11,6 +13,6 @@ export interface JournalTimetableFilter {
|
||||
}
|
||||
|
||||
export interface JournalTimetableSorter {
|
||||
id: 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
|
||||
id: 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
|
||||
dir: -1 | 1;
|
||||
}
|
||||
|
||||
+289
-264
@@ -1,264 +1,289 @@
|
||||
<template>
|
||||
<section class="journal-timetables">
|
||||
<div class="journal_wrapper">
|
||||
<JournalOptions
|
||||
@on-search-confirm="searchHistory"
|
||||
@on-options-reset="resetOptions"
|
||||
:sorter-option-ids="['timestampFrom', 'duration']"
|
||||
:data-status="dataStatus"
|
||||
/>
|
||||
|
||||
<div class="list_wrapper" @scroll="handleScroll">
|
||||
<!-- <transition name="warning" mode="out-in"> -->
|
||||
<!-- <div :key="dataStatus"> -->
|
||||
<Loading v-if="dataStatus == DataStatus.Initialized || dataStatus == DataStatus.Loading" />
|
||||
|
||||
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
|
||||
{{ $t('app.error') }}
|
||||
</div>
|
||||
|
||||
<div class="journal_warning" v-else-if="historyList.length == 0">
|
||||
{{ $t('app.no-result') }}
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<JournalDispatchersList :dispatcherHistory="computedHistoryList" />
|
||||
|
||||
<button
|
||||
class="btn btn--option btn--load-data"
|
||||
v-if="!scrollNoMoreData && scrollDataLoaded && computedHistoryList.length > 15"
|
||||
@click="addHistoryData"
|
||||
>
|
||||
{{ $t('journal.load-data') }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- </div>
|
||||
</transition> -->
|
||||
|
||||
<div class="journal_warning" v-if="scrollNoMoreData">
|
||||
{{ $t('journal.no-further-data') }}
|
||||
</div>
|
||||
|
||||
<div class="journal_warning" v-else-if="!scrollDataLoaded">
|
||||
{{ $t('journal.loading-further-data') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, provide, reactive, Ref, ref } from 'vue';
|
||||
import axios from 'axios';
|
||||
|
||||
import ActionButton from '../../components/Global/ActionButton.vue';
|
||||
import JournalOptions from '../../components/JournalView/JournalOptions.vue';
|
||||
import DispatcherStats from '../../components/JournalView/DispatcherStats.vue';
|
||||
import SearchBox from '../Global/SearchBox.vue';
|
||||
|
||||
import Loading from '../Global/Loading.vue';
|
||||
import { URLs } from '../../scripts/utils/apiURLs';
|
||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
||||
import { useStore } from '../../store/store';
|
||||
import JournalDispatchersList from './JournalDispatchersList.vue';
|
||||
import { JournalDispatcherSearcher, JournalDispatcherSorter } from '../../types/Journal/JournalDispatcherTypes';
|
||||
import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
|
||||
import { JournalTimetableFilter } from '../../types/Journal/JournalTimetablesTypes';
|
||||
|
||||
const DISPATCHERS_API_URL = `${URLs.stacjownikAPI}/api/getDispatchers`;
|
||||
|
||||
export default defineComponent({
|
||||
components: { SearchBox, ActionButton, JournalOptions, DispatcherStats, Loading, JournalDispatchersList },
|
||||
name: 'JournalDispatchers',
|
||||
|
||||
props: {
|
||||
sceneryName: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
|
||||
dispatcherName: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
currentQuery: '',
|
||||
scrollDataLoaded: true,
|
||||
scrollNoMoreData: false,
|
||||
|
||||
showReturnButton: false,
|
||||
statsCardOpen: false,
|
||||
|
||||
dataStatus: DataStatus.Initialized,
|
||||
DataStatus,
|
||||
|
||||
historyList: [] as DispatcherHistory[],
|
||||
}),
|
||||
|
||||
setup() {
|
||||
const sorterActive: JournalDispatcherSorter = reactive({ id: 'timestampFrom', dir: -1 });
|
||||
const journalFilterActive = ref({});
|
||||
|
||||
const searchersValues = reactive({
|
||||
'search-dispatcher': '',
|
||||
'search-station': '',
|
||||
'search-date': '',
|
||||
} as JournalDispatcherSearcher);
|
||||
|
||||
const countFromIndex = ref(0);
|
||||
const countLimit = 15;
|
||||
|
||||
provide('sorterActive', sorterActive);
|
||||
provide('journalFilterActive', journalFilterActive);
|
||||
provide('searchersValues', searchersValues);
|
||||
|
||||
const scrollElement: Ref<HTMLElement | null> = ref(null);
|
||||
|
||||
return {
|
||||
store: useStore(),
|
||||
|
||||
sorterActive,
|
||||
searchersValues,
|
||||
|
||||
countFromIndex,
|
||||
countLimit,
|
||||
|
||||
scrollElement,
|
||||
maxCount: ref(15),
|
||||
};
|
||||
},
|
||||
|
||||
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.searchHistory();
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (!this.sceneryName && !this.dispatcherName) {
|
||||
this.searchHistory();
|
||||
}
|
||||
},
|
||||
|
||||
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();
|
||||
},
|
||||
|
||||
resetOptions() {
|
||||
this.searchersValues['search-station'] = '';
|
||||
this.searchersValues['search-dispatcher'] = '';
|
||||
this.sorterActive.id = 'timestampFrom';
|
||||
|
||||
this.searchHistory();
|
||||
},
|
||||
|
||||
searchHistory() {
|
||||
this.fetchHistoryData({
|
||||
searchers: this.searchersValues,
|
||||
});
|
||||
|
||||
this.scrollNoMoreData = false;
|
||||
this.scrollDataLoaded = true;
|
||||
},
|
||||
|
||||
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(
|
||||
props: {
|
||||
searchers?: JournalDispatcherSearcher;
|
||||
filter?: JournalTimetableFilter;
|
||||
} = {}
|
||||
) {
|
||||
this.dataStatus = DataStatus.Loading;
|
||||
|
||||
const queries: string[] = [];
|
||||
|
||||
const dispatcher = props.searchers?.['search-dispatcher'].trim();
|
||||
const station = props.searchers?.['search-station'].trim();
|
||||
const dateString = props.searchers?.['search-date'].trim();
|
||||
const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
|
||||
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
|
||||
|
||||
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');
|
||||
|
||||
this.currentQuery = queries.join('&');
|
||||
|
||||
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;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/JournalSection.scss';
|
||||
</style>
|
||||
<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>
|
||||
|
||||
+304
-284
@@ -1,284 +1,304 @@
|
||||
<template>
|
||||
<section class="journal-timetables">
|
||||
|
||||
<div class="journal_wrapper">
|
||||
<JournalOptions
|
||||
@on-search-confirm="searchHistory"
|
||||
@on-options-reset="resetOptions"
|
||||
:sorter-option-ids="['timetableId', 'beginDate', 'distance', 'total-stops']"
|
||||
:filters="journalTimetableFilters"
|
||||
:data-status="dataStatus"
|
||||
/>
|
||||
|
||||
<DriverStats />
|
||||
<!-- <button @click="statsCardOpen = true">Stats</button> -->
|
||||
|
||||
<div class="list_wrapper" @scroll="handleScroll">
|
||||
<!-- <transition name="warning" mode="out-in"> -->
|
||||
<!-- <div :key="dataStatus"> -->
|
||||
<Loading v-if="dataStatus == DataStatus.Initialized || dataStatus == DataStatus.Loading" />
|
||||
|
||||
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
|
||||
{{ $t('app.error') }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="timetableHistory.length == 0" class="journal_warning">
|
||||
{{ $t('app.no-result') }}
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<JournalTimetablesList :timetableHistory="timetableHistory" />
|
||||
|
||||
<button
|
||||
class="btn btn--option btn--load-data"
|
||||
v-if="!scrollNoMoreData && scrollDataLoaded && timetableHistory.length >= 15"
|
||||
@click="addHistoryData"
|
||||
>
|
||||
{{ $t('journal.load-data') }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- </div> -->
|
||||
<!-- </transition> -->
|
||||
|
||||
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
|
||||
<div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, provide, reactive, Ref, ref } from 'vue';
|
||||
import axios from 'axios';
|
||||
|
||||
import DriverStats from './DriverStats.vue';
|
||||
import Loading from '../Global/Loading.vue';
|
||||
import { JournalTimetableFilter, JournalTimetableSorter } from '../../types/Journal/JournalTimetablesTypes';
|
||||
import dateMixin from '../../mixins/dateMixin';
|
||||
import routerMixin from '../../mixins/routerMixin';
|
||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
||||
import { JournalFilterType } from '../../scripts/enums/JournalFilterType';
|
||||
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
|
||||
import { URLs } from '../../scripts/utils/apiURLs';
|
||||
import { useStore } from '../../store/store';
|
||||
import JournalOptions from './JournalOptions.vue';
|
||||
import { JorunalTimetableSearchType } from '../../types/Journal/JournalTimetablesTypes';
|
||||
import modalTrainMixin from '../../mixins/modalTrainMixin';
|
||||
import imageMixin from '../../mixins/imageMixin';
|
||||
import JournalTimetablesList from './JournalTimetablesList.vue';
|
||||
import { journalTimetableFilters } from '../../constants/Journal/JournalTimetablesConsts';
|
||||
|
||||
const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`;
|
||||
|
||||
export default defineComponent({
|
||||
components: { DriverStats, Loading, JournalOptions, JournalTimetablesList },
|
||||
mixins: [dateMixin, routerMixin, modalTrainMixin, imageMixin],
|
||||
|
||||
name: 'JournalTimetables',
|
||||
|
||||
props: {
|
||||
timetableId: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
currentQuery: '',
|
||||
scrollDataLoaded: true,
|
||||
scrollNoMoreData: false,
|
||||
|
||||
showReturnButton: false,
|
||||
statsCardOpen: false,
|
||||
|
||||
timetableHistory: [] as TimetableHistory[],
|
||||
journalTimetableFilters,
|
||||
|
||||
dataStatus: DataStatus.Initialized,
|
||||
dataErrorMessage: '',
|
||||
|
||||
DataStatus,
|
||||
}),
|
||||
|
||||
setup() {
|
||||
const sorterActive: JournalTimetableSorter = reactive({ id: 'timetableId', dir: 1 });
|
||||
const journalFilterActive = ref(journalTimetableFilters[0]);
|
||||
|
||||
const searchersValues = reactive({
|
||||
'search-train': '',
|
||||
'search-driver': '',
|
||||
'search-author': '',
|
||||
'search-date': '',
|
||||
} as JorunalTimetableSearchType);
|
||||
|
||||
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(),
|
||||
};
|
||||
},
|
||||
|
||||
activated() {
|
||||
if (this.timetableId) {
|
||||
this.searchersValues['search-train'] = `#${this.timetableId}`;
|
||||
this.searchHistory();
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (!this.timetableId) this.searchHistory();
|
||||
},
|
||||
|
||||
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();
|
||||
},
|
||||
|
||||
resetOptions() {
|
||||
this.searchersValues['search-date'] = '';
|
||||
this.searchersValues['search-driver'] = '';
|
||||
this.searchersValues['search-train'] = '';
|
||||
this.searchersValues['search-author'] = '';
|
||||
|
||||
this.journalFilterActive = this.journalTimetableFilters[0];
|
||||
this.sorterActive.id = 'timetableId';
|
||||
|
||||
this.searchHistory();
|
||||
},
|
||||
|
||||
searchHistory() {
|
||||
this.fetchHistoryData({
|
||||
searchers: this.searchersValues,
|
||||
filter: this.journalFilterActive,
|
||||
});
|
||||
|
||||
this.scrollNoMoreData = false;
|
||||
this.scrollDataLoaded = true;
|
||||
},
|
||||
|
||||
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(
|
||||
props: {
|
||||
searchers?: JorunalTimetableSearchType;
|
||||
filter?: JournalTimetableFilter;
|
||||
} = {}
|
||||
) {
|
||||
this.dataStatus = DataStatus.Loading;
|
||||
|
||||
const queries: string[] = [];
|
||||
|
||||
const driverName = props.searchers?.['search-driver'].trim();
|
||||
const trainNo = props.searchers?.['search-train'].trim();
|
||||
const authorName = props.searchers?.['search-author'].trim();
|
||||
|
||||
const dateString = props.searchers?.['search-date'].trim();
|
||||
const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
|
||||
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
|
||||
|
||||
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 (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.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!';
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/JournalSection.scss';
|
||||
</style>
|
||||
<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>
|
||||
+17
-10
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<section class="trains-view">
|
||||
<div class="trains_wrapper">
|
||||
<TrainOptions :sorter-option-ids="['distance', 'progress', 'delay', 'mass', 'speed', 'length']" />
|
||||
<TrainOptions
|
||||
:sorter-option-ids="['distance', 'id', 'progress', 'delay', 'mass', 'speed', 'length']"
|
||||
:current-options-active="currentOptionsActive"
|
||||
/>
|
||||
|
||||
<TrainTable :trains="computedTrains" />
|
||||
</div>
|
||||
@@ -9,7 +12,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, ComputedRef, defineComponent, provide, reactive, ref } from 'vue';
|
||||
import { computed, ComputedRef, defineComponent, provide, reactive, ref, watch } from 'vue';
|
||||
import TrainOptions from '../components/TrainsView/TrainOptions.vue';
|
||||
import TrainStats from '../components/TrainsView/TrainStats.vue';
|
||||
import TrainTable from '../components/TrainsView/TrainTable.vue';
|
||||
@@ -52,10 +55,13 @@ export default defineComponent({
|
||||
|
||||
setup() {
|
||||
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 currentOptionsActive = ref(false);
|
||||
|
||||
const searchedDriver = ref('');
|
||||
const searchedTrain = ref('');
|
||||
|
||||
@@ -65,13 +71,13 @@ export default defineComponent({
|
||||
provide('filterList', filterList);
|
||||
|
||||
const computedTrains: ComputedRef<Train[]> = computed(() => {
|
||||
return filteredTrainList(
|
||||
store.trainList,
|
||||
searchedTrain.value,
|
||||
searchedDriver.value,
|
||||
sorterActive.value,
|
||||
filterList
|
||||
);
|
||||
return filteredTrainList(store.trainList, searchedTrain.value, searchedDriver.value, sorterActive, filterList);
|
||||
});
|
||||
|
||||
watch([searchedTrain, searchedDriver, sorterActive, filterList], ([sT, sD, sA, fL]) => {
|
||||
const areFiltersActive = fL.some((f, i) => f.isActive !== initTrainFilters[i].isActive);
|
||||
|
||||
currentOptionsActive.value = sT.length > 0 || sD.length > 0 || sA.id != 'distance' || areFiltersActive;
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -80,6 +86,7 @@ export default defineComponent({
|
||||
searchedDriver,
|
||||
sorterActive,
|
||||
store,
|
||||
currentOptionsActive,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
"ESNext",
|
||||
"DOM"
|
||||
],
|
||||
"types": ["vite/client"],
|
||||
"types": ["vite/client", "vite-plugin-pwa/client"],
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
|
||||
+47
-27
@@ -1,34 +1,54 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
|
||||
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