mirror of
https://github.com/Spythere/stacjownik.git
synced 2026-05-03 05:18:11 +00:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 255e07372e | |||
| 279bbfa4db | |||
| a5c829faf5 | |||
| 5fdfaeac5e | |||
| 9beb30e3d5 | |||
| 48582e2eea | |||
| 2e721fb8bf | |||
| f93c1fbfec | |||
| c06e7b6468 | |||
| 22a6d266cb | |||
| 5f8a16401b | |||
| c9be01aa29 | |||
| 4ec058b33c | |||
| 27a5d2a406 | |||
| 58169e26f6 | |||
| fee1f4bbd5 | |||
| 240817acc3 | |||
| db3be87dd8 | |||
| 1665134d6f | |||
| df289ab734 | |||
| f74440ba6f | |||
| a25dbe9fd5 | |||
| 4fff136d6b | |||
| d06f2d5d2e | |||
| 9f68d628d0 | |||
| d64b906dac | |||
| f3e193e68a | |||
| 5640ce9f2b | |||
| 50100eb2f9 | |||
| e478c510b2 | |||
| 7ea558642f | |||
| 493145f7f2 | |||
| 4f72535365 | |||
| 8e3bf80715 | |||
| 6da586d08a | |||
| be53b9c7fb | |||
| 94ed1160a1 | |||
| 859d8d2631 | |||
| 5f3abd73c5 | |||
| d71c8bb6f9 | |||
| a3db13d79c | |||
| 8cb3da66f2 | |||
| 6e07897ac0 | |||
| 726b859f5c | |||
| 651c60707a | |||
| d4fee84603 | |||
| 86539cdf23 | |||
| 69772460b8 | |||
| 6988a83355 | |||
| b6425564c8 | |||
| caf0a9b4c5 | |||
| bd5f433d6e | |||
| 8d9cc721d6 | |||
| cceeffe49d | |||
| fcb8357489 | |||
| ceffd8e675 | |||
| 5aa53521f7 | |||
| d8b559694b | |||
| c82ac04a91 | |||
| 284bdcbf2a | |||
| 7f4df98349 |
@@ -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
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
/dist
|
|
||||||
/dev-dist
|
/dev-dist
|
||||||
|
/dist
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
|
|||||||
+5
-2
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"hosting": {
|
"hosting": {
|
||||||
"public": "dist",
|
"public": "dist",
|
||||||
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
"ignore": [
|
||||||
|
"firebase.json",
|
||||||
|
"**/.*",
|
||||||
|
"**/node_modules/**"
|
||||||
|
],
|
||||||
"rewrites": [
|
"rewrites": [
|
||||||
{
|
{
|
||||||
"source": "**",
|
"source": "**",
|
||||||
@@ -10,4 +14,3 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-14
@@ -25,20 +25,6 @@
|
|||||||
<link rel="icon" href="favicon.ico" />
|
<link rel="icon" href="favicon.ico" />
|
||||||
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;700&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;700&display=swap" rel="stylesheet" />
|
||||||
|
|
||||||
<script src="https://www.gstatic.com/firebasejs/8.1.1/firebase-app.js"></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const firebaseConfig = {
|
|
||||||
apiKey: 'AIzaSyBI36X2-p7vU1flxoJdCEc0noByyTe1mpw',
|
|
||||||
authDomain: 'stacjownik-td2.firebaseapp.com',
|
|
||||||
databaseURL: 'https://stacjownik-td2.firebaseio.com',
|
|
||||||
projectId: 'stacjownik-td2',
|
|
||||||
storageBucket: 'stacjownik-td2.appspot.com',
|
|
||||||
};
|
|
||||||
|
|
||||||
firebase.initializeApp(firebaseConfig);
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Generated
+8609
-1019
File diff suppressed because it is too large
Load Diff
+10
-9
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "stacjownik",
|
"name": "stacjownik",
|
||||||
"version": "1.10.6",
|
"version": "1.11.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
"deploy": "yarn build && firebase deploy --only hosting",
|
"deploy": "yarn build && firebase deploy --only hosting",
|
||||||
"preview": "vite preview"
|
"preview": "yarn build && vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-js": "^3.12.1",
|
"core-js": "^3.12.1",
|
||||||
"dotenv": "^8.6.0",
|
"dotenv": "^16.0.3",
|
||||||
"firebase": "^9.8.1",
|
"firebase": "^9.8.1",
|
||||||
"howler": "^2.2.1",
|
"howler": "^2.2.1",
|
||||||
"pinia": "^2.0.14",
|
"pinia": "^2.0.14",
|
||||||
@@ -21,12 +21,13 @@
|
|||||||
"vue-router": "^4.0.0-0"
|
"vue-router": "^4.0.0-0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^17.0.35",
|
"@types/node": "^18.11.17",
|
||||||
"@vitejs/plugin-vue": "^3.0.0",
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^1.2.1",
|
||||||
"typescript": "^4.6.4",
|
"typescript": "^4.9.4",
|
||||||
"vite": "^3.0.0",
|
"vite": "^4.0.3",
|
||||||
"vue-tsc": "^0.38.4"
|
"vite-plugin-pwa": "^0.14.0",
|
||||||
|
"vue-tsc": "^1.0.18"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
|
|||||||
+3
-2
@@ -33,7 +33,8 @@
|
|||||||
.route {
|
.route {
|
||||||
margin: 0 0.2em;
|
margin: 0 0.2em;
|
||||||
|
|
||||||
&-active {
|
&-active,
|
||||||
|
&[data-active='true'] {
|
||||||
color: $accentCol;
|
color: $accentCol;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
@@ -45,7 +46,7 @@
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|
||||||
@include smallScreen() {
|
@include smallScreen() {
|
||||||
font-size: calc(0.55rem + 1vw);
|
font-size: calc(0.5rem + 1.3vw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+30
-2
@@ -6,11 +6,13 @@
|
|||||||
</keep-alive>
|
</keep-alive>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
|
<UpdatePrompt />
|
||||||
|
|
||||||
<AppHeader :current-lang="currentLang" @change-lang="changeLang" />
|
<AppHeader :current-lang="currentLang" @change-lang="changeLang" />
|
||||||
|
|
||||||
<main class="app_main">
|
<main class="app_main">
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<keep-alive>
|
<keep-alive exclude="JournalView">
|
||||||
<component :is="Component" :key="$route.name" />
|
<component :is="Component" :key="$route.name" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</router-view>
|
</router-view>
|
||||||
@@ -27,7 +29,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<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';
|
import Clock from './components/App/Clock.vue';
|
||||||
|
|
||||||
@@ -41,6 +43,10 @@ import StorageManager from './scripts/managers/storageManager';
|
|||||||
import imageMixin from './mixins/imageMixin';
|
import imageMixin from './mixins/imageMixin';
|
||||||
import AppHeader from './components/App/AppHeader.vue';
|
import AppHeader from './components/App/AppHeader.vue';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import UpdatePrompt from './components/App/UpdatePrompt.vue';
|
||||||
|
import { VERSION } from 'vue-i18n';
|
||||||
|
import { RouterView } from 'vue-router';
|
||||||
|
import useCustomSW from './mixins/useCustomSW';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@@ -49,6 +55,7 @@ export default defineComponent({
|
|||||||
SelectBox,
|
SelectBox,
|
||||||
TrainModal,
|
TrainModal,
|
||||||
AppHeader,
|
AppHeader,
|
||||||
|
UpdatePrompt,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [imageMixin],
|
mixins: [imageMixin],
|
||||||
@@ -57,6 +64,8 @@ export default defineComponent({
|
|||||||
const store = useStore();
|
const store = useStore();
|
||||||
store.connectToAPI();
|
store.connectToAPI();
|
||||||
|
|
||||||
|
const { offlineReady } = useCustomSW();
|
||||||
|
|
||||||
const isFilterCardVisible = ref(false);
|
const isFilterCardVisible = ref(false);
|
||||||
|
|
||||||
provide('isFilterCardVisible', isFilterCardVisible);
|
provide('isFilterCardVisible', isFilterCardVisible);
|
||||||
@@ -81,6 +90,25 @@ export default defineComponent({
|
|||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.loadLang();
|
this.loadLang();
|
||||||
|
|
||||||
|
this.store.isOffline = !window.navigator.onLine;
|
||||||
|
|
||||||
|
window.addEventListener('offline', () => {
|
||||||
|
this.store.isOffline = true;
|
||||||
|
|
||||||
|
this.store.apiData = {
|
||||||
|
stations: [],
|
||||||
|
dispatchers: [],
|
||||||
|
trains: [],
|
||||||
|
connectedSocketCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.store.setOnlineData();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('online', () => {
|
||||||
|
this.store.isOffline = false;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
<StatusIndicator />
|
<StatusIndicator />
|
||||||
|
|
||||||
<span class="header_brand">
|
<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>
|
||||||
|
|
||||||
<span class="header_info">
|
<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="/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') }}
|
{{ $t('app.journal') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
@@ -66,50 +73,51 @@ import StatusIndicator from './StatusIndicator.vue';
|
|||||||
import Clock from './Clock.vue';
|
import Clock from './Clock.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
emits: ["changeLang"],
|
emits: ['changeLang'],
|
||||||
mixins: [imageMixin],
|
mixins: [imageMixin],
|
||||||
props: {
|
props: {
|
||||||
currentLang: {
|
currentLang: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
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 {
|
return {
|
||||||
store: useStore(),
|
id: region.id,
|
||||||
|
value: `${region.value} <div class='text--grayed'>${regionStationCount} / ${regionTrainCount}</div>`,
|
||||||
|
selectedValue: region.value,
|
||||||
};
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
},
|
||||||
changeRegion(region: {
|
components: { SelectBox, StatusIndicator, Clock },
|
||||||
id: string;
|
|
||||||
value: string;
|
|
||||||
}) {
|
|
||||||
this.store.changeRegion(region);
|
|
||||||
},
|
|
||||||
changeLang(lang: string) {
|
|
||||||
this.$emit("changeLang", lang);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
onlineTrainsCount() {
|
|
||||||
return this.store.trainList.filter((train) => train.online).length;
|
|
||||||
},
|
|
||||||
onlineDispatchersCount() {
|
|
||||||
return this.store.stationList.filter((station) => station.onlineInfo && station.onlineInfo.region == this.store.region.id).length;
|
|
||||||
},
|
|
||||||
computedRegions() {
|
|
||||||
return options.regions.map((region) => {
|
|
||||||
const regionStationCount = this.store.apiData.stations?.filter((station) => station.region == region.id && station.isOnline).length || 0;
|
|
||||||
const regionTrainCount = this.store.apiData.trains?.filter((train) => train.region == region.id && train.online).length || 0;
|
|
||||||
return {
|
|
||||||
id: region.id,
|
|
||||||
value: `${region.value} <div class='text--grayed'>${regionStationCount} / ${regionTrainCount}</div>`,
|
|
||||||
selectedValue: region.value,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: { SelectBox, StatusIndicator, Clock }
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -128,6 +136,7 @@ export default defineComponent({
|
|||||||
.header {
|
.header {
|
||||||
&_body {
|
&_body {
|
||||||
max-width: 21em;
|
max-width: 21em;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
@include smallScreen {
|
@include smallScreen {
|
||||||
max-width: 18em;
|
max-width: 18em;
|
||||||
@@ -263,4 +272,4 @@ export default defineComponent({
|
|||||||
font-size: 0.9em;
|
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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
import { DataStatus } from '../../scripts/enums/DataStatus';
|
||||||
import { useStore } from '../../store/store';
|
import { useStore } from '../../store/store';
|
||||||
@@ -172,6 +171,7 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
tooltipActive: false,
|
tooltipActive: false,
|
||||||
indicator: {
|
indicator: {
|
||||||
|
offline: false,
|
||||||
status: DataStatus.Loading,
|
status: DataStatus.Loading,
|
||||||
message: 'data-status.S3',
|
message: 'data-status.S3',
|
||||||
},
|
},
|
||||||
@@ -193,6 +193,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
dataStatus: store.dataStatuses,
|
dataStatus: store.dataStatuses,
|
||||||
|
store,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -206,6 +207,13 @@ export default defineComponent({
|
|||||||
const trainsDataStatus = statuses.trains;
|
const trainsDataStatus = statuses.trains;
|
||||||
const dispatcherDataStatus = statuses.dispatchers;
|
const dispatcherDataStatus = statuses.dispatchers;
|
||||||
|
|
||||||
|
if (this.store.isOffline) {
|
||||||
|
this.setSignalStatus(DataStatus.Initialized);
|
||||||
|
this.indicator.status = DataStatus.Initialized;
|
||||||
|
this.indicator.message = 'data-status.S1-offline';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (connectionStatus == DataStatus.Error) {
|
if (connectionStatus == DataStatus.Error) {
|
||||||
this.setSignalStatus(connectionStatus);
|
this.setSignalStatus(connectionStatus);
|
||||||
this.indicator.status = connectionStatus;
|
this.indicator.status = connectionStatus;
|
||||||
@@ -252,6 +260,10 @@ export default defineComponent({
|
|||||||
this.orangeLight = false;
|
this.orangeLight = false;
|
||||||
this.redBottomLight = false;
|
this.redBottomLight = false;
|
||||||
|
|
||||||
|
if (status == DataStatus.Initialized) {
|
||||||
|
this.redTopLight = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (status == DataStatus.Loaded) {
|
if (status == DataStatus.Loaded) {
|
||||||
this.greenLight = true;
|
this.greenLight = true;
|
||||||
}
|
}
|
||||||
@@ -291,9 +303,8 @@ export default defineComponent({
|
|||||||
|
|
||||||
.status-indicator {
|
.status-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 110%;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
transform: translateX(12em);
|
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<div class="update-prompt">
|
||||||
|
<transition name="prompt-anim">
|
||||||
|
<div class="prompt_content" v-if="!hidePrompt && needRefresh">
|
||||||
|
<div>{{ $t('update.title') }}</div>
|
||||||
|
|
||||||
|
<div class="prompt_actions">
|
||||||
|
<button class="btn btn--filled" @click="updateServiceWorker(true)">{{ $t('update.confirm-button') }}</button>
|
||||||
|
<button class="btn btn--filled" @click="hidePrompt = true">{{ $t('update.later-button') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import useCustomSW from '../../mixins/useCustomSW';
|
||||||
|
|
||||||
|
const hidePrompt = ref(false);
|
||||||
|
const { needRefresh, updateServiceWorker } = useCustomSW();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../styles/variables.scss';
|
||||||
|
|
||||||
|
.update-prompt {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt_content {
|
||||||
|
margin: 1em;
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: black;
|
||||||
|
|
||||||
|
box-shadow: 0 0 10px 1px $accentCol;
|
||||||
|
border-radius: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt_actions {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 1em;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animation
|
||||||
|
.prompt-anim {
|
||||||
|
&-enter-active,
|
||||||
|
&-leave-active {
|
||||||
|
transition: all 120ms ease-in;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-enter-from,
|
||||||
|
&-leave-to {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ export default defineComponent({
|
|||||||
.loading {
|
.loading {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -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?.timetableId || 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>
|
|
||||||
STATYSTYKI MASZYNISTY <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>
|
|
||||||
@@ -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,11 +2,20 @@
|
|||||||
<div class="filters-options" @keydown.esc="showOptions = false">
|
<div class="filters-options" @keydown.esc="showOptions = false">
|
||||||
<div class="bg" v-if="showOptions" @click="showOptions = false"></div>
|
<div class="bg" v-if="showOptions" @click="showOptions = false"></div>
|
||||||
|
|
||||||
<button class="btn--filled btn--image" @click="showOptions = !showOptions" ref="button">
|
<button class="filter-button btn--filled btn--image" @click="showOptions = !showOptions" ref="button">
|
||||||
<img :src="getIcon('filter2')" alt="Open filters" />
|
<img :src="getIcon('filter2')" alt="Open filters" />
|
||||||
{{ $t('options.filters') }} [F]
|
{{ $t('options.filters') }} [F]
|
||||||
|
<span class="active-indicator" v-if="currentOptionsActive"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<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">
|
<transition name="options-anim">
|
||||||
<div class="options_wrapper" v-if="showOptions">
|
<div class="options_wrapper" v-if="showOptions">
|
||||||
<div class="options_content">
|
<div class="options_content">
|
||||||
@@ -17,26 +26,18 @@
|
|||||||
|
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<input
|
<input
|
||||||
v-if="propName == 'search-date'"
|
|
||||||
class="search-input"
|
class="search-input"
|
||||||
id="date"
|
|
||||||
type="date"
|
|
||||||
min="2022-02-01"
|
|
||||||
@keydown.enter="onSearchConfirm"
|
|
||||||
v-model="searchersValues[propName]"
|
v-model="searchersValues[propName]"
|
||||||
/>
|
|
||||||
|
|
||||||
<input
|
|
||||||
v-else
|
|
||||||
class="search-input"
|
|
||||||
@keydown.enter="onSearchConfirm"
|
@keydown.enter="onSearchConfirm"
|
||||||
@focus="preventKeyDown = true"
|
@focus="preventKeyDown = true"
|
||||||
@blur="preventKeyDown = false"
|
@blur="preventKeyDown = false"
|
||||||
:placeholder="$t(`options.${propName}`)"
|
: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)" />
|
<img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear(propName)" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,10 +85,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<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 imageMixin from '../../mixins/imageMixin';
|
||||||
import keyMixin from '../../mixins/keyMixin';
|
import keyMixin from '../../mixins/keyMixin';
|
||||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
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 { JournalTimetableFilter } from '../../types/Journal/JournalTimetablesTypes';
|
||||||
import ActionButton from '../Global/ActionButton.vue';
|
import ActionButton from '../Global/ActionButton.vue';
|
||||||
import SelectBox from '../Global/SelectBox.vue';
|
import SelectBox from '../Global/SelectBox.vue';
|
||||||
@@ -112,11 +117,23 @@ export default defineComponent({
|
|||||||
type: Number as PropType<DataStatus>,
|
type: Number as PropType<DataStatus>,
|
||||||
default: DataStatus.Initialized,
|
default: DataStatus.Initialized,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
currentOptionsActive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showOptions: false,
|
showOptions: false,
|
||||||
|
|
||||||
|
driverSuggestions: [] as string[],
|
||||||
|
dispatcherSuggestions: [] as string[],
|
||||||
|
|
||||||
|
searchTimeout: 0,
|
||||||
|
store: useStore(),
|
||||||
|
|
||||||
DataStatus,
|
DataStatus,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -130,6 +147,10 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
driverStatsName() {
|
||||||
|
return this.store.driverStatsName;
|
||||||
|
},
|
||||||
|
|
||||||
translatedSorterOptions() {
|
translatedSorterOptions() {
|
||||||
return this.$props.sorterOptionIds.map((id) => ({
|
return this.$props.sorterOptionIds.map((id) => ({
|
||||||
id,
|
id,
|
||||||
@@ -138,7 +159,71 @@ 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: {
|
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! :/');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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
|
// Override keyMixin function
|
||||||
onKeyDownFunction() {
|
onKeyDownFunction() {
|
||||||
this.showOptions = !this.showOptions;
|
this.showOptions = !this.showOptions;
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div class="journal-stats" v-show="!store.isOffline">
|
||||||
|
<div class="tabs">
|
||||||
|
<button
|
||||||
|
v-for="tab in data.tabs"
|
||||||
|
class="btn--filled"
|
||||||
|
:data-selected="tab.name == store.currentStatsTab && areStatsOpen"
|
||||||
|
:data-inactive="tab.inactive"
|
||||||
|
@click="onTabButtonClick(tab.name)"
|
||||||
|
>
|
||||||
|
{{ $t(tab.titlePath) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-tab" v-show="areStatsOpen">
|
||||||
|
<keep-alive>
|
||||||
|
<JournalDailyStats v-if="store.currentStatsTab == 'daily'" ref="dailyStatsComp" />
|
||||||
|
<JournalDriverStats v-else-if="store.currentStatsTab == 'driver'" />
|
||||||
|
</keep-alive>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, KeepAlive, onActivated, onDeactivated, reactive, Ref, ref, watch } from 'vue';
|
||||||
|
import { useStore } from '../../store/store';
|
||||||
|
import JournalDailyStats from './DailyStats.vue';
|
||||||
|
import JournalDriverStats from './JournalDriverStats.vue';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
type TStatTab = 'daily' | 'driver';
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const dailyStatsComp: Ref<InstanceType<typeof JournalDailyStats> | null> = ref(null);
|
||||||
|
|
||||||
|
const areStatsOpen = ref(true);
|
||||||
|
const lastClickedTab = ref('daily');
|
||||||
|
|
||||||
|
let data = reactive({
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'daily',
|
||||||
|
titlePath: 'journal.daily-stats-title',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'driver',
|
||||||
|
titlePath: 'journal.driver-stats-title',
|
||||||
|
inactive: true,
|
||||||
|
},
|
||||||
|
] as { name: TStatTab; titlePath: string; inactive?: boolean }[],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
function onTabButtonClick(tab: TStatTab) {
|
||||||
|
if (lastClickedTab.value == tab || !areStatsOpen.value) {
|
||||||
|
areStatsOpen.value = !areStatsOpen.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
store.currentStatsTab = tab;
|
||||||
|
lastClickedTab.value = tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
dailyStatsComp.value?.startFetchingDailyStats();
|
||||||
|
});
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
dailyStatsComp.value?.stopFetchingDailyStats();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
computed(() => store.driverStatsData),
|
||||||
|
(statsData) => {
|
||||||
|
data.tabs[1].inactive = statsData ? false : true;
|
||||||
|
|
||||||
|
lastClickedTab.value = statsData ? 'driver' : 'daily';
|
||||||
|
if (statsData) areStatsOpen.value = true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../styles/JournalStats.scss';
|
||||||
|
@import '../../styles/variables.scss';
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0.5em 0.75em;
|
||||||
|
|
||||||
|
&[data-inactive='true'] {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-selected='true'] {
|
||||||
|
color: $accentCol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -72,6 +72,13 @@
|
|||||||
{{ timetable.confirmedStopsCount }} /
|
{{ timetable.confirmedStopsCount }} /
|
||||||
{{ timetable.allStopsCount }}
|
{{ timetable.allStopsCount }}
|
||||||
</span>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Nick dyżurnego -->
|
<!-- Nick dyżurnego -->
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<b>{{ $t('availability.title') }}:</b> {{ $t(`availability.${station.generalInfo.availability}`) }}
|
<b>{{ $t('availability.title') }}:</b> {{ $t(`availability.${station.generalInfo.availability}`) }}
|
||||||
|
|
||||||
<span v-if="station.generalInfo.reqLevel > -1">
|
<span v-if="station.generalInfo.reqLevel > -1">
|
||||||
- {{ $tc('scenery.req-level', station.generalInfo.reqLevel, { lvl: station.generalInfo.reqLevel }) }}
|
- {{ $t('scenery.req-level', { lvl: station.generalInfo.reqLevel }, station.generalInfo.reqLevel) }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<scenery-info-routes :station="station" />
|
<scenery-info-routes :station="station" />
|
||||||
|
|
||||||
<div class="scenery-authors" v-if="station.generalInfo.authors && station.generalInfo.authors.length > 0">
|
<div class="scenery-authors" v-if="station.generalInfo.authors && station.generalInfo.authors.length > 0">
|
||||||
<b> {{ $tc('scenery.authors-title', station.generalInfo.authors.length) }}: </b>
|
<b> {{ $t('scenery.authors-title', { authors: station.generalInfo.authors.length }, station.generalInfo.authors.length) }}: </b>
|
||||||
{{ station.generalInfo.authors.join(', ') }}
|
{{ station.generalInfo.authors.join(', ') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -72,7 +72,6 @@ import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue';
|
|||||||
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
|
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
|
||||||
import Station from '../../scripts/interfaces/Station';
|
import Station from '../../scripts/interfaces/Station';
|
||||||
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
SceneryInfoDispatcher,
|
SceneryInfoDispatcher,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="info-dispatcher">
|
<section class="info-dispatcher">
|
||||||
<div class="dispatcher" v-if="station.onlineInfo">
|
<div class="dispatcher" v-if="station.onlineInfo">
|
||||||
<span class="dispatcher_level" :style="calculateExpStyle(station.onlineInfo.dispatcherExp)">
|
<span
|
||||||
|
class="dispatcher_level"
|
||||||
|
:style="calculateExpStyle(station.onlineInfo.dispatcherExp, station.onlineInfo.dispatcherIsSupporter)"
|
||||||
|
>
|
||||||
{{ station.onlineInfo.dispatcherExp > 1 ? station.onlineInfo.dispatcherExp : 'L' }}
|
{{ station.onlineInfo.dispatcherExp > 1 ? station.onlineInfo.dispatcherExp : 'L' }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,6 @@
|
|||||||
<span class="text--grayed">
|
<span class="text--grayed">
|
||||||
{{ station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed).length || '0' }}
|
{{ station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed).length || '0' }}
|
||||||
</span>
|
</span>
|
||||||
<!--
|
|
||||||
<button class="btn--image" v-if="!timetableOnly">
|
|
||||||
<a :href="`${$route.path}?station=${$route.query.station}&timetableOnly=1`">
|
|
||||||
<img :src="getIcon('view')" alt="View image" />
|
|
||||||
</a>
|
|
||||||
</button> -->
|
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="timetable-checkpoints" v-if="station && station.generalInfo?.checkpoints">
|
<div class="timetable-checkpoints" v-if="station && station.generalInfo?.checkpoints">
|
||||||
@@ -35,8 +29,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="timetable-list">
|
<div class="timetable-list">
|
||||||
<!-- <transition name="scenery-timetable-list-anim" mode="out-in"> -->
|
|
||||||
<!-- <div :key="store.dataStatuses.trains + selectedCheckpoint" class="scenery-timetable-list"> -->
|
|
||||||
<div style="padding-bottom: 5em" v-if="store.dataStatuses.trains == 0 && computedScheduledTrains.length == 0">
|
<div style="padding-bottom: 5em" v-if="store.dataStatuses.trains == 0 && computedScheduledTrains.length == 0">
|
||||||
<Loading />
|
<Loading />
|
||||||
</div>
|
</div>
|
||||||
@@ -49,119 +41,116 @@
|
|||||||
{{ $t('scenery.no-timetables') }}
|
{{ $t('scenery.no-timetables') }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div
|
<transition-group name="timetables-anim">
|
||||||
class="timetable-item"
|
<div
|
||||||
v-for="(scheduledTrain, i) in computedScheduledTrains"
|
class="timetable-item"
|
||||||
:key="i + 1"
|
v-for="(scheduledTrain, i) in computedScheduledTrains"
|
||||||
tabindex="0"
|
:key="scheduledTrain.trainId"
|
||||||
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId)"
|
tabindex="0"
|
||||||
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId)"
|
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId)"
|
||||||
>
|
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId)"
|
||||||
<span class="timetable-general">
|
>
|
||||||
<span class="general-info">
|
<span class="timetable-general">
|
||||||
<span class="info-number">
|
<span class="general-info">
|
||||||
<strong>{{ scheduledTrain.category }}</strong>
|
<span class="info-number">
|
||||||
{{ scheduledTrain.trainNo }}
|
<strong>{{ scheduledTrain.category }}</strong>
|
||||||
|
{{ scheduledTrain.trainNo }}
|
||||||
|
|
||||||
<span class="g-tooltip" v-if="scheduledTrain.stopInfo.comments">
|
<span class="g-tooltip" v-if="scheduledTrain.stopInfo.comments">
|
||||||
<img :src="getIcon('warning')" />
|
<img :src="getIcon('warning')" />
|
||||||
<span class="content" v-html="scheduledTrain.stopInfo.comments"> </span>
|
<span class="content" v-html="scheduledTrain.stopInfo.comments"> </span>
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
|
||||||
<span style="color: white">
|
|
||||||
{{ scheduledTrain.driverName }}
|
|
||||||
</span>
|
|
||||||
|
|
|
||||||
<span class="general-status">
|
|
||||||
<span :class="scheduledTrain.stopStatus">
|
|
||||||
{{ $t(`timetables.${scheduledTrain.stopStatus}`) }}
|
|
||||||
<span v-if="scheduledTrain.stopStatus == 'arriving'"> {{ scheduledTrain.prevStationName }}</span>
|
|
||||||
<span v-if="scheduledTrain.stopStatus.startsWith('departed')">{{
|
|
||||||
scheduledTrain.nextStationName
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div class="info-route">
|
|
||||||
<strong>{{ scheduledTrain.beginsAt }} - {{ scheduledTrain.terminatesAt }}</strong>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="timetable-schedule">
|
|
||||||
<span class="schedule-arrival">
|
|
||||||
<span class="arrival-time begins" v-if="scheduledTrain.stopInfo.beginsHere">
|
|
||||||
{{ $t('timetables.begins') }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="arrival-time" v-else>
|
|
||||||
<div v-if="scheduledTrain.stopInfo.arrivalDelay == 0">
|
|
||||||
<span>{{ timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<div>
|
|
||||||
<s style="margin-right: 0.2em" class="text--grayed">{{
|
|
||||||
timestampToString(scheduledTrain.stopInfo.arrivalTimestamp)
|
|
||||||
}}</s>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
{{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }}
|
|
||||||
({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : '' }}{{ scheduledTrain.stopInfo.arrivalDelay }})
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</span>
|
||||||
</span>
|
|
|
||||||
</span>
|
<span>
|
||||||
|
{{ scheduledTrain.driverName }}
|
||||||
<span class="schedule-stop">
|
|
||||||
<span class="stop-time">
|
|
||||||
<span v-if="scheduledTrain.stopInfo.stopTime">
|
|
||||||
{{ scheduledTrain.stopInfo.stopTime }}
|
|
||||||
{{ scheduledTrain.stopInfo.stopType || 'pt' }}
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span v-else> </span>
|
<div class="info-route">
|
||||||
</span>
|
<strong>{{ scheduledTrain.beginsAt }} - {{ scheduledTrain.terminatesAt }}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span class="arrow"></span>
|
<ScheduledTrainStatus :scheduledTrain="scheduledTrain" />
|
||||||
|
|
||||||
<span class="stop-line">
|
|
||||||
{{ scheduledTrain.arrivingLine }}
|
|
||||||
{{ scheduledTrain.arrivingLine && scheduledTrain.departureLine && '>' }}
|
|
||||||
{{ scheduledTrain.departureLine }}
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="schedule-departure">
|
<span class="timetable-schedule">
|
||||||
<span class="departure-time terminates" v-if="scheduledTrain.stopInfo.terminatesHere">
|
<span class="schedule-arrival">
|
||||||
{{ $t('timetables.terminates') }}
|
<span class="arrival-time begins" v-if="scheduledTrain.stopInfo.beginsHere">
|
||||||
</span>
|
{{ $t('timetables.begins') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
<span class="departure-time" v-else>
|
<span class="arrival-time" v-else>
|
||||||
<div v-if="scheduledTrain.stopInfo.departureDelay == 0">
|
<div v-if="scheduledTrain.stopInfo.arrivalDelay == 0">
|
||||||
<span>{{ timestampToString(scheduledTrain.stopInfo.departureTimestamp) }}</span>
|
<span>{{ timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) }}</span>
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<div>
|
|
||||||
<s style="margin-right: 0.2em" class="text--grayed">{{
|
|
||||||
timestampToString(scheduledTrain.stopInfo.departureTimestamp)
|
|
||||||
}}</s>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div>
|
||||||
|
<s style="margin-right: 0.2em" class="text--grayed">{{
|
||||||
|
timestampToString(scheduledTrain.stopInfo.arrivalTimestamp)
|
||||||
|
}}</s>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
{{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }}
|
{{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }}
|
||||||
({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : ''
|
({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : ''
|
||||||
}}{{ scheduledTrain.stopInfo.departureDelay }})
|
}}{{ scheduledTrain.stopInfo.arrivalDelay }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="schedule-stop">
|
||||||
|
<span class="stop-time">
|
||||||
|
<span v-if="scheduledTrain.stopInfo.stopTime">
|
||||||
|
{{ scheduledTrain.stopInfo.stopTime }}
|
||||||
|
{{ scheduledTrain.stopInfo.stopType || 'pt' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
|
<span v-else> </span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="arrow"></span>
|
||||||
|
|
||||||
|
<span class="stop-line">
|
||||||
|
<span>
|
||||||
|
{{ scheduledTrain.arrivingLine }}
|
||||||
|
</span>
|
||||||
|
<span></span>
|
||||||
|
<span>
|
||||||
|
{{ scheduledTrain.departureLine }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="schedule-departure">
|
||||||
|
<span class="departure-time terminates" v-if="scheduledTrain.stopInfo.terminatesHere">
|
||||||
|
{{ $t('timetables.terminates') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="departure-time" v-else>
|
||||||
|
<div v-if="scheduledTrain.stopInfo.departureDelay == 0">
|
||||||
|
<span>{{ timestampToString(scheduledTrain.stopInfo.departureTimestamp) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div>
|
||||||
|
<s style="margin-right: 0.2em" class="text--grayed">{{
|
||||||
|
timestampToString(scheduledTrain.stopInfo.departureTimestamp)
|
||||||
|
}}</s>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }}
|
||||||
|
({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : ''
|
||||||
|
}}{{ scheduledTrain.stopInfo.departureDelay }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- </transition> -->
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -178,11 +167,13 @@ import Station from '../../scripts/interfaces/Station';
|
|||||||
import { useStore } from '../../store/store';
|
import { useStore } from '../../store/store';
|
||||||
import imageMixin from '../../mixins/imageMixin';
|
import imageMixin from '../../mixins/imageMixin';
|
||||||
import modalTrainMixin from '../../mixins/modalTrainMixin';
|
import modalTrainMixin from '../../mixins/modalTrainMixin';
|
||||||
|
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
|
||||||
|
import ScheduledTrain from '../../scripts/interfaces/ScheduledTrain';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'SceneryTimetable',
|
name: 'SceneryTimetable',
|
||||||
|
|
||||||
components: { SelectBox, Loading, TrainModal },
|
components: { SelectBox, Loading, TrainModal, ScheduledTrainStatus },
|
||||||
|
|
||||||
mixins: [dateMixin, routerMixin, imageMixin, modalTrainMixin],
|
mixins: [dateMixin, routerMixin, imageMixin, modalTrainMixin],
|
||||||
|
|
||||||
@@ -282,11 +273,21 @@ export default defineComponent({
|
|||||||
@import '../../styles/responsive.scss';
|
@import '../../styles/responsive.scss';
|
||||||
@import '../../styles/variables.scss';
|
@import '../../styles/variables.scss';
|
||||||
|
|
||||||
// .scenery-timetable {
|
.timetables-anim-move,
|
||||||
// height: 85vh;
|
.timetables-anim-enter-active,
|
||||||
// max-height: 900px;
|
.timetables-anim-leave-active {
|
||||||
// min-height: 450px;
|
transition: all 250ms ease;
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
.timetables-anim-enter-from,
|
||||||
|
.timetables-anim-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetables-anim-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
.scenery-timetable {
|
.scenery-timetable {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -320,12 +321,14 @@ export default defineComponent({
|
|||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
margin: 0.5em auto;
|
margin: 0.5em auto;
|
||||||
padding: 0 0.5em;
|
padding: 0.5em;
|
||||||
max-width: 1100px;
|
max-width: 1100px;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
gap: 0 0.5em;
|
gap: 2em 0.5em;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
background: #353535;
|
background: #353535;
|
||||||
|
|
||||||
@@ -340,9 +343,6 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-general {
|
&-general {
|
||||||
padding: 0.5rem 0;
|
|
||||||
border-radius: 10px;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -353,6 +353,10 @@ export default defineComponent({
|
|||||||
&-schedule {
|
&-schedule {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(30px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(30px, 1fr));
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,7 +419,6 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.info-route {
|
.info-route {
|
||||||
margin-top: 0.5em;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,38 +434,6 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.general-status {
|
|
||||||
text-align: right;
|
|
||||||
|
|
||||||
span.arriving {
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.departed {
|
|
||||||
color: lime;
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
&-away {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #5ecc5e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span.stopped {
|
|
||||||
color: #ffa600;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.online {
|
|
||||||
color: gold;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.terminated {
|
|
||||||
color: salmon;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule {
|
.schedule {
|
||||||
&-arrival,
|
&-arrival,
|
||||||
&-stop,
|
&-stop,
|
||||||
@@ -472,23 +443,40 @@ export default defineComponent({
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
margin: 0 0.3rem;
|
margin: 0 0.3rem;
|
||||||
font-size: 1.1em;
|
font-size: 1.15em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-stop {
|
&-stop {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
font-size: 0.85em;
|
font-size: 0.9em;
|
||||||
|
|
||||||
padding: 0.3em 0;
|
padding: 0.3em 0;
|
||||||
|
|
||||||
.stop-line {
|
.stop-line {
|
||||||
margin-top: 0.25em;
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
span {
|
||||||
|
width: 65px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
span:first-child {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
span:last-child {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.stop-time {
|
.stop-time {
|
||||||
transform: translateY(-0.25em);
|
position: absolute;
|
||||||
|
transform: translateY(-15px);
|
||||||
|
|
||||||
|
color: $accentCol;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -513,21 +501,9 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include smallScreen() {
|
@include smallScreen {
|
||||||
.timetable {
|
.timetable-item {
|
||||||
&-item {
|
grid-template-columns: 1fr;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-general {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-schedule {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<template>
|
||||||
|
<div class="general-status">
|
||||||
|
<span :class="scheduledTrain.stopStatus">
|
||||||
|
<span v-if="scheduledTrain.stopStatus == 'arriving'">
|
||||||
|
<span v-if="scheduledTrain.prevDepartureLine">({{ scheduledTrain.prevDepartureLine }})</span>
|
||||||
|
{{ scheduledTrain.prevStationName }}
|
||||||
|
><span v-if="scheduledTrain.nextArrivalLine"> ({{ scheduledTrain.nextArrivalLine }}) </span>
|
||||||
|
{{ scheduledTrain.nextStationName || '---' }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else-if="scheduledTrain.stopStatus == 'departed'">
|
||||||
|
>> <span v-if="scheduledTrain.nextArrivalLine"> ({{ scheduledTrain.nextArrivalLine }}) </span>
|
||||||
|
{{ scheduledTrain.nextStationName }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else-if="scheduledTrain.stopStatus == 'departed-away'">
|
||||||
|
>>>
|
||||||
|
<span v-if="scheduledTrain.nextArrivalLine"> ({{ scheduledTrain.nextArrivalLine }}) </span>
|
||||||
|
{{ scheduledTrain.nextStationName }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else-if="scheduledTrain.stopStatus == 'online'">
|
||||||
|
>
|
||||||
|
<span v-if="scheduledTrain.nextArrivalLine">
|
||||||
|
({{ scheduledTrain.nextArrivalLine }}) {{ scheduledTrain.nextStationName }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="!scheduledTrain.nextStationName">{{ $t('timetables.end') }}</span>
|
||||||
|
<span v-else>{{ scheduledTrain.nextStationName }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else-if="scheduledTrain.stopStatus == 'stopped'">
|
||||||
|
>
|
||||||
|
<span v-if="scheduledTrain.nextArrivalLine"> ({{ scheduledTrain.nextArrivalLine }}) </span>
|
||||||
|
{{ scheduledTrain.nextStationName }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else-if="scheduledTrain.stopStatus == 'terminated'">X {{ $t('timetables.terminated') }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType } from 'vue';
|
||||||
|
import ScheduledTrain from '../../scripts/interfaces/ScheduledTrain';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
scheduledTrain: {
|
||||||
|
type: Object as PropType<ScheduledTrain>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.general-status {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
|
||||||
|
span.arriving {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.departed {
|
||||||
|
color: lime;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&-away {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #5ecc5e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span.stopped {
|
||||||
|
color: #ffa600;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.online {
|
||||||
|
color: gold;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.terminated {
|
||||||
|
color: salmon;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<datalist id="sceneries">
|
<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>
|
</datalist>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -150,6 +150,14 @@ export default defineComponent({
|
|||||||
this.currentRegion = this.store.region;
|
this.currentRegion = this.store.region;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
sortedStationList() {
|
||||||
|
return this.store.stationList
|
||||||
|
.filter((s) => s.name.toLocaleLowerCase().includes(this.chosenSearchScenery.toLocaleLowerCase()))
|
||||||
|
.sort((s1, s2) => (s1.name > s2.name ? 1 : -1));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
chosenSearchScenery(value: string) {
|
chosenSearchScenery(value: string) {
|
||||||
const chosenStation = this.store.stationList.find(({ name }) => name == value);
|
const chosenStation = this.store.stationList.find(({ name }) => name == value);
|
||||||
|
|||||||
@@ -100,7 +100,10 @@
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="station_dispatcher-exp">
|
<td class="station_dispatcher-exp">
|
||||||
<span v-if="station.onlineInfo" :style="calculateExpStyle(station.onlineInfo.dispatcherExp)">
|
<span
|
||||||
|
v-if="station.onlineInfo"
|
||||||
|
:style="calculateExpStyle(station.onlineInfo.dispatcherExp, station.onlineInfo.dispatcherIsSupporter)"
|
||||||
|
>
|
||||||
{{ 2 > station.onlineInfo.dispatcherExp ? 'L' : station.onlineInfo.dispatcherExp }}
|
{{ 2 > station.onlineInfo.dispatcherExp ? 'L' : station.onlineInfo.dispatcherExp }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -4,14 +4,18 @@
|
|||||||
<div class="train_general">
|
<div class="train_general">
|
||||||
<span>
|
<span>
|
||||||
<span class="timetable-id" v-if="train.timetableData">#{{ train.timetableData.timetableId }}</span>
|
<span class="timetable-id" v-if="train.timetableData">#{{ train.timetableData.timetableId }}</span>
|
||||||
|
|
||||||
<span class="timetable_warnings">
|
<span class="timetable_warnings">
|
||||||
<span class="train-badge twr" v-if="train.timetableData?.TWR">TWR</span>
|
<span class="train-badge twr" v-if="train.timetableData?.TWR">TWR</span>
|
||||||
<span class="train-badge skr" v-if="train.timetableData?.SKR">SKR</span>
|
<span class="train-badge skr" v-if="train.timetableData?.SKR">SKR</span>
|
||||||
</span>
|
</span>
|
||||||
<strong v-if="train.timetableData">{{ train.timetableData.category }} </strong>
|
<strong class="timetable-category" v-if="train.timetableData">
|
||||||
<strong>{{ train.trainNo }}</strong>
|
{{ train.timetableData.category }}
|
||||||
<span> | {{ train.driverName }} </span>
|
</strong>
|
||||||
|
<strong class="train-number"> {{ train.trainNo }}</strong>
|
||||||
|
|
|
||||||
|
<span class="train-driver" :class="{ supporter: train.isSupporter }">{{ train.driverName }}</span>
|
||||||
|
|
||||||
<b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
|
<b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,13 +155,15 @@ export default defineComponent({
|
|||||||
|
|
||||||
.warning-timeout {
|
.warning-timeout {
|
||||||
background-color: #be3728;
|
background-color: #be3728;
|
||||||
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
width: 1.25em;
|
width: 1.25em;
|
||||||
height: 1.25em;
|
height: 1.25em;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
||||||
|
margin-left: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timetable_stops {
|
.timetable_stops {
|
||||||
@@ -195,6 +201,13 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.train-driver {
|
||||||
|
&.supporter {
|
||||||
|
color: orange;
|
||||||
|
text-shadow: orange 0 0 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.timetable_route {
|
.timetable_route {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
<div class="filters-options" @keydown.esc="showOptions = false">
|
<div class="filters-options" @keydown.esc="showOptions = false">
|
||||||
<div class="bg" v-if="showOptions" @click="showOptions = false"></div>
|
<div class="bg" v-if="showOptions" @click="showOptions = false"></div>
|
||||||
|
|
||||||
<button class="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" />
|
<img :src="getIcon('filter2')" alt="Open filters" />
|
||||||
{{ $t('options.filters') }} [F]
|
{{ $t('options.filters') }} [F]
|
||||||
|
<span class="active-indicator" v-if="currentOptionsActive"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<transition name="options-anim">
|
<transition name="options-anim">
|
||||||
@@ -56,7 +57,7 @@
|
|||||||
<h1 class="option-title" v-if="trainFilterList.length != 0">{{ $t('options.filter-title') }}</h1>
|
<h1 class="option-title" v-if="trainFilterList.length != 0">{{ $t('options.filter-title') }}</h1>
|
||||||
<div class="options_filters">
|
<div class="options_filters">
|
||||||
<div class="filter-option" v-for="filter in trainFilterList">
|
<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}`) }}
|
{{ $t(`options.filter-${filter.id}`) }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,11 +90,17 @@ export default defineComponent({
|
|||||||
type: Array as PropType<Array<string>>,
|
type: Array as PropType<Array<string>>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
currentOptionsActive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showOptions: false,
|
showOptions: false,
|
||||||
|
lastSelectedFilter: null as TrainFilter | null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -136,7 +143,11 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onFilterChange(filter: TrainFilter) {
|
onFilterChange(filter: TrainFilter) {
|
||||||
|
// if (this.lastSelectedFilter?.id === filter.id)
|
||||||
|
// this.trainFilterList.forEach((tf) => (tf.isActive = filter.id === tf.id));
|
||||||
|
|
||||||
filter.isActive = !filter.isActive;
|
filter.isActive = !filter.isActive;
|
||||||
|
this.lastSelectedFilter = filter;
|
||||||
},
|
},
|
||||||
|
|
||||||
clearAllFilters() {
|
clearAllFilters() {
|
||||||
|
|||||||
@@ -2,18 +2,22 @@
|
|||||||
<div class="train-table">
|
<div class="train-table">
|
||||||
<transition name="anim" mode="out-in">
|
<transition name="anim" mode="out-in">
|
||||||
<div :key="store.dataStatuses.trains">
|
<div :key="store.dataStatuses.trains">
|
||||||
<Loading v-if="trains.length == 0 && store.dataStatuses.trains == 0" />
|
<div class="table-info" v-if="store.isOffline">
|
||||||
|
{{ $t('app.offline') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table-info no-trains" v-if="trains.length == 0 && store.dataStatuses.trains != 0">
|
<Loading v-else-if="trains.length == 0 && store.dataStatuses.trains == 0" />
|
||||||
|
|
||||||
|
<div class="table-info no-trains" v-else-if="trains.length == 0 && store.dataStatuses.trains != 0">
|
||||||
{{ $t('trains.no-trains') }}
|
{{ $t('trains.no-trains') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="timeouts-warning" v-if="trainNumbersWithTimeouts.length != 0">
|
<!-- <div class="timeouts-warning" v-if="trainNumbersWithTimeouts.length == 0">
|
||||||
<b class="warning-timeout">?</b>
|
<b class="warning-timeout">?</b>
|
||||||
{{ $t('trains.timeout') }}
|
{{ $t('trains.timeout') }}
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<ul class="train-list">
|
<ul class="train-list" v-else>
|
||||||
<li
|
<li
|
||||||
class="train-row"
|
class="train-row"
|
||||||
v-for="train in currentTrains"
|
v-for="train in currentTrains"
|
||||||
|
|||||||
+33
-10
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"general": {
|
||||||
|
"and": " and "
|
||||||
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"sceneries": "SCENERIES",
|
"sceneries": "SCENERIES",
|
||||||
"trains": "TRAINS",
|
"trains": "TRAINS",
|
||||||
@@ -8,15 +11,18 @@
|
|||||||
"error": "An error occured while loading data!",
|
"error": "An error occured while loading data!",
|
||||||
"no-result": "No results for current search!",
|
"no-result": "No results for current search!",
|
||||||
"migration-warning": "Stacjownik services will be unavailable 2/06/2022 between 1-3am (CEST time) due to the migration of API hostings!",
|
"migration-warning": "Stacjownik services will be unavailable 2/06/2022 between 1-3am (CEST time) due to the migration of API hostings!",
|
||||||
"migration-confirm": "Roger that!"
|
"migration-confirm": "Roger that!",
|
||||||
|
"offline": "App is in the offline mode!"
|
||||||
},
|
},
|
||||||
"update": {
|
"update": {
|
||||||
"title": "New Stacjownik version is available!",
|
"title": "New version of the app is available!",
|
||||||
"paragraph1": "Enjoy the application and may the green signal be with you!",
|
"paragraph1": "Enjoy the application and may the green signal be with you!",
|
||||||
"release-link": "Click here to browse version changelog (GitHub)",
|
"release-link": "Click here to browse version changelog (GitHub)",
|
||||||
"confirm-button": "Understood!"
|
"confirm-button": "UPDATE NOW",
|
||||||
|
"later-button": "LATER"
|
||||||
},
|
},
|
||||||
"data-status": {
|
"data-status": {
|
||||||
|
"S1-offline": "<b>S1 signal</b> <br> The app is working in offline mode!",
|
||||||
"S1a-connection": "<b>S1a signal</b> <br> Cannot connect with Stacjownik API service!",
|
"S1a-connection": "<b>S1a signal</b> <br> Cannot connect with Stacjownik API service!",
|
||||||
"S1a-sceneries": "<b>S1a signal</b> <br> Cannot load online stations data!",
|
"S1a-sceneries": "<b>S1a signal</b> <br> Cannot load online stations data!",
|
||||||
"S2": "<b>S2 signal</b> <br> All data loaded successfully!",
|
"S2": "<b>S2 signal</b> <br> All data loaded successfully!",
|
||||||
@@ -253,11 +259,32 @@
|
|||||||
|
|
||||||
"load-data": "Load further data...",
|
"load-data": "Load further data...",
|
||||||
|
|
||||||
|
"last-seen-at": "Last seen at",
|
||||||
|
"currently-at": "Currently at",
|
||||||
|
|
||||||
|
"stats-title": "DRIVING STATISTICS OF",
|
||||||
|
|
||||||
"stats-timetables": "TIMETABLES",
|
"stats-timetables": "TIMETABLES",
|
||||||
"stats-longest-timetable": "LONGEST TIMETABLE",
|
"stats-longest-timetable": "LONGEST TIMETABLE",
|
||||||
"stats-avg-timetable": "AVERAGE TIMETABLE LENGTH",
|
"stats-avg-timetable": "AVERAGE TIMETABLE LENGTH",
|
||||||
"stats-distance": "DISTANCE",
|
"stats-distance": "DISTANCE",
|
||||||
"stats-stations": "STATIONS"
|
"stats-stations": "STATIONS",
|
||||||
|
|
||||||
|
"timetable-stats-total": "Today, dispatchers made so far {count} with total distance of {distance}",
|
||||||
|
"timetable-stats-longest": "The longest timetable today is #{id} made by {author} for {driver} - {distance}",
|
||||||
|
"timetable-stats-most-active": "The most active dispatcher today is {dispatcher} who created {count}",
|
||||||
|
"timetable-stats-most-active-many": "The most active dispatchers today are {dispatchers} who created {count} each",
|
||||||
|
|
||||||
|
"timetable-count": "timetable | timetables",
|
||||||
|
|
||||||
|
"daily-stats-title": "DAILY STATS",
|
||||||
|
"daily-stats-info": "Today's statistics are unavailable yet!",
|
||||||
|
|
||||||
|
"driver-stats-title": "DRIVER STATS",
|
||||||
|
"driver-stats-info": "Enter a proper nickname into filters [F] to see user's driving statistics!",
|
||||||
|
|
||||||
|
"stats-loading": "Fetching statistics...",
|
||||||
|
"stats-error": "Oops! An unexpected error occurred while trying to fetch statistics! :/"
|
||||||
},
|
},
|
||||||
"scenery": {
|
"scenery": {
|
||||||
"users": "PLAYERS ONLINE",
|
"users": "PLAYERS ONLINE",
|
||||||
@@ -299,12 +326,8 @@
|
|||||||
},
|
},
|
||||||
"timetables": {
|
"timetables": {
|
||||||
"timetable-only": "Switch to timetable-only view",
|
"timetable-only": "Switch to timetable-only view",
|
||||||
"online": "At station",
|
"end": "Timetable terminates here",
|
||||||
"departed": "Dispatched to:",
|
"terminated": "Timetable terminated",
|
||||||
"departed-away": "Departed to:",
|
|
||||||
"arriving": "Arriving from:",
|
|
||||||
"stopped": "Stopped",
|
|
||||||
"terminated": "Terminated",
|
|
||||||
"begins": "BEGINS HERE",
|
"begins": "BEGINS HERE",
|
||||||
"terminates": "TERMINATES\nHERE"
|
"terminates": "TERMINATES\nHERE"
|
||||||
},
|
},
|
||||||
|
|||||||
+32
-9
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"general": {
|
||||||
|
"and": " oraz "
|
||||||
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"sceneries": "SCENERIE",
|
"sceneries": "SCENERIE",
|
||||||
"trains": "POCIĄGI",
|
"trains": "POCIĄGI",
|
||||||
@@ -8,17 +11,20 @@
|
|||||||
"error": "Wystąpił problem z załadowaniem danych!",
|
"error": "Wystąpił problem z załadowaniem danych!",
|
||||||
"no-result": "Brak wyników o podanych kryteriach!",
|
"no-result": "Brak wyników o podanych kryteriach!",
|
||||||
"migration-warning": "Usługi Stacjownika będą niedostępne w godzinach 1:00-3:00 2 czerwca 2022r. z powodu migracji hostingów API!",
|
"migration-warning": "Usługi Stacjownika będą niedostępne w godzinach 1:00-3:00 2 czerwca 2022r. z powodu migracji hostingów API!",
|
||||||
"migration-confirm": "Przyjąłem!"
|
"migration-confirm": "Przyjąłem!",
|
||||||
|
"offline": "Aplikacja w trybie offline!"
|
||||||
},
|
},
|
||||||
|
|
||||||
"update": {
|
"update": {
|
||||||
"title": "Nowa wersja Stacjownika jest dostępna!",
|
"title": "Nowa wersja Stacjownika jest dostępna!",
|
||||||
"paragraph1": "Miłego korzystania z aplikacji i niech S2 będzie z wami!",
|
"paragraph1": "Miłego korzystania z aplikacji i niech S2 będzie z wami!",
|
||||||
"release-link": "Kliknij, aby przejrzeć listę zmian (GitHub)",
|
"release-link": "Kliknij, aby przejrzeć listę zmian (GitHub)",
|
||||||
"confirm-button": "Przyjąłem!"
|
"confirm-button": "ZAKTUALIZUJ",
|
||||||
|
"later-button": "PÓŹNIEJ"
|
||||||
},
|
},
|
||||||
|
|
||||||
"data-status": {
|
"data-status": {
|
||||||
|
"S1-offline": "<b>Sygnał S1</b> <br> Aplikacja działa w trybie offline!",
|
||||||
"S1a-connection": "<b>Sygnał S1a</b> <br> Błąd podczas próby połączenia się z API Stacjownika!",
|
"S1a-connection": "<b>Sygnał S1a</b> <br> Błąd podczas próby połączenia się z API Stacjownika!",
|
||||||
"S1a-sceneries": "<b>Sygnał S1a</b> <br> Błąd podczas pobierania danych o sceneriach online!",
|
"S1a-sceneries": "<b>Sygnał S1a</b> <br> Błąd podczas pobierania danych o sceneriach online!",
|
||||||
"S2": "<b>Sygnał S2</b> <br> Pomyślnie załadowano dane!",
|
"S2": "<b>Sygnał S2</b> <br> Pomyślnie załadowano dane!",
|
||||||
@@ -257,11 +263,32 @@
|
|||||||
|
|
||||||
"load-data": "Pobierz dalszą historię...",
|
"load-data": "Pobierz dalszą historię...",
|
||||||
|
|
||||||
|
"stats-title": "STATYSTYKI MASZYNISTY",
|
||||||
|
|
||||||
|
"last-seen-at": "Ostatnio widziany na: ",
|
||||||
|
"currently-at": "Obecnie na scenerii: ",
|
||||||
|
|
||||||
"stats-timetables": "ROZKŁADY JAZDY",
|
"stats-timetables": "ROZKŁADY JAZDY",
|
||||||
"stats-longest-timetable": "NAJDŁUŻSZY RJ",
|
"stats-longest-timetable": "NAJDŁUŻSZY RJ",
|
||||||
"stats-avg-timetable": "ŚREDNIA DŁUGOŚĆ RJ",
|
"stats-avg-timetable": "ŚREDNIA DŁUGOŚĆ RJ",
|
||||||
"stats-distance": "DYSTANS",
|
"stats-distance": "DYSTANS",
|
||||||
"stats-stations": "STACJE"
|
"stats-stations": "STACJE",
|
||||||
|
|
||||||
|
"timetable-stats-total": "Dyżurni stworzyli dziś {count} o łącznym dystansie {distance}",
|
||||||
|
"timetable-stats-longest": "Najdłuższym rozkładem jazdy jest dzisiaj #{id} stworzony przez dyżurnego {author} dla maszynisty {driver} - {distance}",
|
||||||
|
"timetable-stats-most-active": "Dzisiejszym najaktywniejszym dyżurnym jest {dispatcher}, który stworzył {count}",
|
||||||
|
"timetable-stats-most-active-many": "Dzisiejszymi najaktywniejszymi dyżurnymi są {dispatchers}, którzy stworzyli po {count}",
|
||||||
|
|
||||||
|
"timetable-count": "rozkład jazdy | rozkładów jazdy",
|
||||||
|
|
||||||
|
"daily-stats-title": "STATYSTYKI DNIA",
|
||||||
|
"daily-stats-info": "Dzisiejsze statystyki nie są jeszcze dostępne!",
|
||||||
|
|
||||||
|
"driver-stats-title": "STATYSTYKI GRACZA",
|
||||||
|
"driver-stats-info": "Wpisz nazwę użytkownika w filtrach [F], aby zobaczyć jego statystyki maszynisty!",
|
||||||
|
|
||||||
|
"stats-loading": "Pobieranie statystyk...",
|
||||||
|
"stats-error": "Ups! Wystąpił błąd podczas próby pobrania statystyk! :/"
|
||||||
},
|
},
|
||||||
"scenery": {
|
"scenery": {
|
||||||
"users": "GRACZE ONLINE",
|
"users": "GRACZE ONLINE",
|
||||||
@@ -303,12 +330,8 @@
|
|||||||
},
|
},
|
||||||
"timetables": {
|
"timetables": {
|
||||||
"timetable-only": "Wyodrębnij rozkłady jazdy",
|
"timetable-only": "Wyodrębnij rozkłady jazdy",
|
||||||
"online": "Na stacji",
|
"end": "Koniec rozkładu jazdy",
|
||||||
"departed": "Odprawiony do:",
|
"terminated": "Rozkład jazdy zakończony",
|
||||||
"departed-away": "Odjechał do:",
|
|
||||||
"arriving": "W drodze z:",
|
|
||||||
"stopped": "Postój",
|
|
||||||
"terminated": "Skończył bieg",
|
|
||||||
"begins": "ROZPOCZYNA\nBIEG",
|
"begins": "ROZPOCZYNA\nBIEG",
|
||||||
"terminates": "KOŃCZY BIEG"
|
"terminates": "KOŃCZY BIEG"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { createPinia } from 'pinia';
|
|||||||
|
|
||||||
const i18n = createI18n({
|
const i18n = createI18n({
|
||||||
locale: 'pl',
|
locale: 'pl',
|
||||||
|
legacy: false,
|
||||||
fallbackLocale: 'pl',
|
fallbackLocale: 'pl',
|
||||||
messages: {
|
messages: {
|
||||||
en: enLang,
|
en: enLang,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { defineComponent } from 'vue';
|
|||||||
import { useStore } from '../store/store';
|
import { useStore } from '../store/store';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
data() {
|
||||||
return {
|
return {
|
||||||
store: useStore(),
|
store: useStore(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export default defineComponent({
|
|||||||
calculateExpStyle(exp: number, isSupporter = false): string {
|
calculateExpStyle(exp: number, isSupporter = false): string {
|
||||||
const bgColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 85%, 50%)`) : '#666';
|
const bgColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 85%, 50%)`) : '#666';
|
||||||
|
|
||||||
const fontColor = exp > 15 || exp == -1 ? 'white' : 'black';
|
const fontColor = exp > 14 || exp == -1 ? 'white' : 'black';
|
||||||
const boxShadow = isSupporter ? `box-shadow: 0 0 10px 2px ${bgColor};` : '';
|
const boxShadow = isSupporter ? `box-shadow: 0 0 10px 2px ${bgColor};` : '';
|
||||||
|
|
||||||
return `background-color: ${bgColor}; color: ${fontColor}; ${boxShadow}`;
|
return `background-color: ${bgColor}; color: ${fontColor}; ${boxShadow}`;
|
||||||
|
|||||||
@@ -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 { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
|
||||||
import JournalDispatchersVue from '../components/JournalView/JournalDispatchers.vue';
|
import JournalDispatchersVue from '../views/JournalDispatchers.vue';
|
||||||
import JournalTimetablesVue from '../components/JournalView/JournalTimetables.vue';
|
import JournalTimetablesVue from '../views/JournalTimetables.vue';
|
||||||
|
|
||||||
const routes: Array<RouteRecordRaw> = [
|
const routes: Array<RouteRecordRaw> = [
|
||||||
{
|
{
|
||||||
@@ -21,32 +21,23 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/journal',
|
path: '/journal',
|
||||||
name: 'JournalView',
|
redirect: '/journal/timetables'
|
||||||
component: () => import('../views/JournalView.vue'),
|
},
|
||||||
children: [
|
{
|
||||||
{
|
path: '/journal/timetables',
|
||||||
path: '',
|
name: 'JournalTimetables',
|
||||||
name: 'JournalTimetables',
|
component: JournalTimetablesVue,
|
||||||
component: JournalTimetablesVue,
|
props: (route) => ({
|
||||||
alias: '/timetables',
|
trainNo: route.query.trainNo,
|
||||||
},
|
driverName: route.query.driverName,
|
||||||
{
|
timetableId: route.query.timetableId,
|
||||||
path: 'dispatchers',
|
}),
|
||||||
name: 'JournalDispatchers',
|
},
|
||||||
component: JournalDispatchersVue,
|
{
|
||||||
props: (route) => ({ sceneryName: route.query.sceneryName, dispatcherName: route.query.dispatcherName }),
|
path: '/journal/dispatchers',
|
||||||
},
|
name: 'JournalDispatchers',
|
||||||
{
|
component: JournalDispatchersVue,
|
||||||
path: 'timetables',
|
props: (route) => ({ sceneryName: route.query.sceneryName, dispatcherName: route.query.dispatcherName }),
|
||||||
name: 'JournalTimetables',
|
|
||||||
component: JournalTimetablesVue,
|
|
||||||
props: (route) => ({
|
|
||||||
trainNo: route.query.trainNo,
|
|
||||||
driverName: route.query.driverName,
|
|
||||||
timetableId: route.query.timetableId,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/:catchAll(.*)',
|
path: '/:catchAll(.*)',
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ export default interface ScheduledTrain {
|
|||||||
arrivingLine: string | null;
|
arrivingLine: string | null;
|
||||||
departureLine: string | null;
|
departureLine: string | null;
|
||||||
|
|
||||||
|
prevDepartureLine: string | null;
|
||||||
|
nextArrivalLine: string | null;
|
||||||
|
|
||||||
|
signal: string;
|
||||||
|
connectedTrack: string;
|
||||||
|
|
||||||
stopLabel: string;
|
stopLabel: string;
|
||||||
stopStatus: string;
|
stopStatus: string;
|
||||||
stopStatusID: number;
|
stopStatusID: number;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export default interface Train {
|
|||||||
cars: string[];
|
cars: string[];
|
||||||
|
|
||||||
isTimeout: boolean;
|
isTimeout: boolean;
|
||||||
|
isSupporter: boolean;
|
||||||
|
|
||||||
timetableData?: {
|
timetableData?: {
|
||||||
timetableId: number;
|
timetableId: 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,5 @@
|
|||||||
export const URLs = {
|
export const URLs = {
|
||||||
stacjownikAPI:
|
stacjownikAPI:
|
||||||
import.meta.env.VITE_APP_API_DEV == 1 && !import.meta.env.PROD
|
import.meta.env.VITE_APP_API_DEV == 1 && !import.meta.env.PROD ? 'http://localhost:3000' : 'https://spythere.pl',
|
||||||
? 'http://localhost:3000'
|
|
||||||
: 'https://stacjownik.eu-4.evennode.com',
|
|
||||||
stacjownikAPIDev: 'localhost:3000',
|
stacjownikAPIDev: 'localhost:3000',
|
||||||
// trains: "https://api.td2.info.pl:9640/?method=getTrainsOnline",
|
|
||||||
// getTimetableURL: (trainNo: string | number, region = "eu") => `https://api.td2.info.pl:9640/?method=readFromSWDR&value=getTimetable%3B${trainNo}%3B${region}`
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -117,31 +117,37 @@ export function getScheduledTrain(train: Train, trainStopIndex: number, stationN
|
|||||||
let prevStationName = '',
|
let prevStationName = '',
|
||||||
nextStationName = '';
|
nextStationName = '';
|
||||||
|
|
||||||
|
let prevDepartureLine: string | null = null,
|
||||||
|
nextArrivalLine: string | null = null;
|
||||||
|
|
||||||
for (let i = trainStopIndex - 1; i >= 0; i--) {
|
for (let i = trainStopIndex - 1; i >= 0; i--) {
|
||||||
if (/strong|podg/g.test(followingStops[i].stopName)) {
|
if (/strong|podg/g.test(followingStops[i].stopName)) {
|
||||||
prevStationName = followingStops[i].stopNameRAW;
|
prevStationName = followingStops[i].stopNameRAW.replace(/,.*/g,"");
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = trainStopIndex + 1; i < followingStops.length; i++) {
|
for (let i = trainStopIndex + 1; i < followingStops.length; i++) {
|
||||||
if (/strong|podg/g.test(followingStops[i].stopName)) {
|
if (/strong|podg/g.test(followingStops[i].stopName)) {
|
||||||
nextStationName = followingStops[i].stopNameRAW;
|
nextStationName = followingStops[i].stopNameRAW.replace(/,.*/g,"");
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let departureLine: string | null = trainStop.departureLine;
|
let departureLine: string | null = null;
|
||||||
let arrivingLine: string | null = trainStop.arrivalLine;
|
let arrivingLine: string | null = null;
|
||||||
|
|
||||||
for (let i = trainStopIndex; i < followingStops.length; i++) {
|
for (let i = trainStopIndex; i < followingStops.length; i++) {
|
||||||
const currentStop = followingStops[i];
|
const currentStop = followingStops[i];
|
||||||
|
|
||||||
if (currentStop.departureLine == null) break;
|
if (currentStop.departureLine == null) continue;
|
||||||
|
|
||||||
if (!/-|_|it|sbl/gi.test(currentStop.departureLine)) {
|
if (!/-|_|it|sbl/gi.test(currentStop.departureLine)) {
|
||||||
departureLine = currentStop.departureLine;
|
departureLine = currentStop.departureLine;
|
||||||
|
nextArrivalLine = followingStops[i + 1]?.arrivalLine || null;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,10 +155,12 @@ export function getScheduledTrain(train: Train, trainStopIndex: number, stationN
|
|||||||
for (let i = trainStopIndex; i >= 0; i--) {
|
for (let i = trainStopIndex; i >= 0; i--) {
|
||||||
const currentStop = followingStops[i];
|
const currentStop = followingStops[i];
|
||||||
|
|
||||||
if (currentStop.arrivalLine == null) break;
|
if (currentStop.arrivalLine == null) continue;
|
||||||
|
|
||||||
if (!/-|_|it|sbl/gi.test(currentStop.arrivalLine)) {
|
if (!/-|_|it|sbl/gi.test(currentStop.arrivalLine)) {
|
||||||
arrivingLine = currentStop.arrivalLine;
|
arrivingLine = currentStop.arrivalLine;
|
||||||
|
prevDepartureLine = followingStops[i - 1]?.departureLine || null;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,7 +168,11 @@ export function getScheduledTrain(train: Train, trainStopIndex: number, stationN
|
|||||||
return {
|
return {
|
||||||
trainNo: train.trainNo,
|
trainNo: train.trainNo,
|
||||||
trainId: train.trainId,
|
trainId: train.trainId,
|
||||||
|
|
||||||
|
signal: train.signal,
|
||||||
|
connectedTrack: train.connectedTrack,
|
||||||
|
|
||||||
|
|
||||||
driverName: train.driverName,
|
driverName: train.driverName,
|
||||||
driverId: train.driverId,
|
driverId: train.driverId,
|
||||||
currentStationName: train.currentStationName,
|
currentStationName: train.currentStationName,
|
||||||
@@ -179,5 +191,8 @@ export function getScheduledTrain(train: Train, trainStopIndex: number, stationN
|
|||||||
|
|
||||||
arrivingLine,
|
arrivingLine,
|
||||||
departureLine,
|
departureLine,
|
||||||
|
|
||||||
|
nextArrivalLine,
|
||||||
|
prevDepartureLine,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import inputData from '../data/options.json';
|
|||||||
import Filter from '../scripts/interfaces/Filter';
|
import Filter from '../scripts/interfaces/Filter';
|
||||||
import Station from '../scripts/interfaces/Station';
|
import Station from '../scripts/interfaces/Station';
|
||||||
import StorageManager from '../scripts/managers/storageManager';
|
import StorageManager from '../scripts/managers/storageManager';
|
||||||
|
import { useStore } from './store';
|
||||||
|
|
||||||
const sortStations = (a: Station, b: Station, sorter: { index: number; dir: number }) => {
|
const sortStations = (a: Station, b: Station, sorter: { index: number; dir: number }) => {
|
||||||
switch (sorter.index) {
|
switch (sorter.index) {
|
||||||
@@ -58,7 +59,7 @@ const sortStations = (a: Station, b: Station, sorter: { index: number; dir: numb
|
|||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterStations = (station: Station, filters: Filter) => {
|
const filterStations = (station: Station, filters: Filter, isOffline = false) => {
|
||||||
const returnMode = false;
|
const returnMode = false;
|
||||||
|
|
||||||
if ((station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && filters['nonPublic'])
|
if ((station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && filters['nonPublic'])
|
||||||
@@ -236,6 +237,7 @@ export const useStationFiltersStore = defineStore('stationFiltersStore', {
|
|||||||
inputs: inputData,
|
inputs: inputData,
|
||||||
filters: { ...filterInitStates },
|
filters: { ...filterInitStates },
|
||||||
sorterActive: { index: 0, dir: 1 },
|
sorterActive: { index: 0, dir: 1 },
|
||||||
|
store: useStore(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -249,7 +251,7 @@ export const useStationFiltersStore = defineStore('stationFiltersStore', {
|
|||||||
|
|
||||||
return station;
|
return station;
|
||||||
})
|
})
|
||||||
.filter((station) => filterStations(station, this.filters))
|
.filter((station) => filterStations(station, this.filters, this.store.isOffline))
|
||||||
.sort((a, b) => sortStations(a, b, this.sorterActive));
|
.sort((a, b) => sortStations(a, b, this.sorterActive));
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -303,3 +305,4 @@ export const useStationFiltersStore = defineStore('stationFiltersStore', {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+16
-4
@@ -17,7 +17,6 @@ import {
|
|||||||
} from '../scripts/utils/storeUtils';
|
} from '../scripts/utils/storeUtils';
|
||||||
import { APIData, StationJSONData, StoreState } from './storeTypes';
|
import { APIData, StationJSONData, StoreState } from './storeTypes';
|
||||||
|
|
||||||
|
|
||||||
export const useStore = defineStore('store', {
|
export const useStore = defineStore('store', {
|
||||||
state: () =>
|
state: () =>
|
||||||
({
|
({
|
||||||
@@ -35,12 +34,14 @@ export const useStore = defineStore('store', {
|
|||||||
stationCount: 0,
|
stationCount: 0,
|
||||||
|
|
||||||
webSocket: undefined,
|
webSocket: undefined,
|
||||||
|
isOffline: false,
|
||||||
|
|
||||||
dispatcherStatsName: '',
|
dispatcherStatsName: '',
|
||||||
dispatcherStatsData: undefined,
|
dispatcherStatsData: undefined,
|
||||||
|
|
||||||
driverStatsName: '',
|
driverStatsName: '',
|
||||||
driverStatsData: undefined,
|
driverStatsData: undefined,
|
||||||
|
driverStatsStatus: DataStatus.Initialized,
|
||||||
|
|
||||||
chosenModalTrainId: undefined,
|
chosenModalTrainId: undefined,
|
||||||
|
|
||||||
@@ -52,9 +53,10 @@ export const useStore = defineStore('store', {
|
|||||||
trains: DataStatus.Loading,
|
trains: DataStatus.Loading,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
currentStatsTab: 'daily',
|
||||||
|
|
||||||
blockScroll: false,
|
blockScroll: false,
|
||||||
listenerLaunched: false,
|
listenerLaunched: false,
|
||||||
|
|
||||||
} as StoreState),
|
} as StoreState),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
@@ -98,6 +100,8 @@ export const useStore = defineStore('store', {
|
|||||||
lastSeen: train.lastSeen,
|
lastSeen: train.lastSeen,
|
||||||
isTimeout: train.isTimeout,
|
isTimeout: train.isTimeout,
|
||||||
|
|
||||||
|
isSupporter: train.driverIsSupporter,
|
||||||
|
|
||||||
timetableData: timetable
|
timetableData: timetable
|
||||||
? {
|
? {
|
||||||
timetableId: timetable.timetableId,
|
timetableId: timetable.timetableId,
|
||||||
@@ -220,6 +224,14 @@ export const useStore = defineStore('store', {
|
|||||||
const onlineStationNames: string[] = [];
|
const onlineStationNames: string[] = [];
|
||||||
const prevDispatcherStatuses: StoreState['lastDispatcherStatuses'] = [];
|
const prevDispatcherStatuses: StoreState['lastDispatcherStatuses'] = [];
|
||||||
|
|
||||||
|
if (this.isOffline) {
|
||||||
|
this.stationList.forEach((station) => {
|
||||||
|
station.onlineInfo = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.apiData.stations?.forEach((stationAPIData) => {
|
this.apiData.stations?.forEach((stationAPIData) => {
|
||||||
if (stationAPIData.region !== this.region.id || !stationAPIData.isOnline) return;
|
if (stationAPIData.region !== this.region.id || !stationAPIData.isOnline) return;
|
||||||
const station = this.stationList.find((s) => s.name === stationAPIData.stationName);
|
const station = this.stationList.find((s) => s.name === stationAPIData.stationName);
|
||||||
@@ -347,12 +359,11 @@ export const useStore = defineStore('store', {
|
|||||||
transports: ['websocket', 'polling'],
|
transports: ['websocket', 'polling'],
|
||||||
rememberUpgrade: true,
|
rememberUpgrade: true,
|
||||||
reconnection: true,
|
reconnection: true,
|
||||||
timeout: 10000,
|
timeout: 2000,
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('connect_error', (err) => {
|
socket.on('connect_error', (err) => {
|
||||||
this.dataStatuses.connection = DataStatus.Error;
|
this.dataStatuses.connection = DataStatus.Error;
|
||||||
this.webSocket = undefined;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('UPDATE', (data: APIData) => {
|
socket.on('UPDATE', (data: APIData) => {
|
||||||
@@ -363,6 +374,7 @@ export const useStore = defineStore('store', {
|
|||||||
|
|
||||||
socket.emit('FETCH_DATA', {}, (data: APIData) => {
|
socket.emit('FETCH_DATA', {}, (data: APIData) => {
|
||||||
this.apiData = data;
|
this.apiData = data;
|
||||||
|
this.dataStatuses.connection = DataStatus.Loaded;
|
||||||
this.setOnlineData();
|
this.setOnlineData();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -23,15 +23,19 @@ export interface StoreState {
|
|||||||
stationCount: number;
|
stationCount: number;
|
||||||
|
|
||||||
webSocket?: Socket;
|
webSocket?: Socket;
|
||||||
|
isOffline: boolean;
|
||||||
|
|
||||||
dispatcherStatsName: string;
|
dispatcherStatsName: string;
|
||||||
dispatcherStatsData?: DispatcherStatsAPIData;
|
dispatcherStatsData?: DispatcherStatsAPIData;
|
||||||
|
|
||||||
driverStatsName: string;
|
driverStatsName: string;
|
||||||
driverStatsData?: DriverStatsAPIData;
|
driverStatsData?: DriverStatsAPIData;
|
||||||
|
driverStatsStatus: DataStatus;
|
||||||
|
|
||||||
chosenModalTrainId?: string;
|
chosenModalTrainId?: string;
|
||||||
|
|
||||||
|
currentStatsTab: 'daily' | 'driver';
|
||||||
|
|
||||||
dataStatuses: {
|
dataStatuses: {
|
||||||
connection: DataStatus;
|
connection: DataStatus;
|
||||||
sceneries: DataStatus;
|
sceneries: DataStatus;
|
||||||
@@ -48,6 +52,7 @@ export interface APIData {
|
|||||||
stations?: StationAPIData[];
|
stations?: StationAPIData[];
|
||||||
dispatchers?: string[][];
|
dispatchers?: string[][];
|
||||||
trains?: TrainAPIData[];
|
trains?: TrainAPIData[];
|
||||||
|
connectedSocketCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StationJSONData {
|
export interface StationJSONData {
|
||||||
|
|||||||
@@ -30,6 +30,8 @@
|
|||||||
max-width: 1350px;
|
max-width: 1350px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
padding: 1em 0;
|
padding: 1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +59,11 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn--load-data {
|
.btn--load-data {
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
@import 'variables.scss';
|
@import 'variables.scss';
|
||||||
@import 'responsive.scss';
|
@import 'responsive.scss';
|
||||||
|
|
||||||
.journal-stats {
|
.stats-tab {
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
|
box-shadow: 0 0 5px 1px $accentCol;
|
||||||
|
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
margin-bottom: 1em;
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-stats {
|
.info-stats {
|
||||||
@@ -40,3 +47,4 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,19 @@
|
|||||||
@import 'variables.scss';
|
@import 'variables.scss';
|
||||||
@import 'search_box.scss';
|
@import 'search_box.scss';
|
||||||
|
|
||||||
|
.filters-options {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-button .active-indicator {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
background-color: lightgreen;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
h1.option-title {
|
h1.option-title {
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
@@ -42,22 +55,16 @@ h1.option-title {
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filters-options {
|
|
||||||
position: relative;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.options_wrapper {
|
.options_wrapper {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
background-color: $bgCol;
|
background-color: $bgCol;
|
||||||
box-shadow: 0 5px 10px 2px #0f0f0f;
|
box-shadow: 0 5px 10px 2px #0f0f0f;
|
||||||
|
|
||||||
width: 100%;
|
width: 97%;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
|
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
|
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-1
@@ -207,10 +207,20 @@ button {
|
|||||||
padding: 0.25em 0.5em;
|
padding: 0.25em 0.5em;
|
||||||
|
|
||||||
transition: all 100ms ease;
|
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 {
|
button.btn--filled {
|
||||||
background-color: #333;
|
background-color: #1a1a1a;
|
||||||
border-radius: 0.25em;
|
border-radius: 0.25em;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { JournalFilterType } from '../../scripts/enums/JournalFilterType';
|
import { JournalFilterType } from '../../scripts/enums/JournalFilterType';
|
||||||
|
|
||||||
export type JorunalTimetableSearchType = {
|
export type JournalTimetableSearchKey = 'search-driver' | 'search-train' | 'search-date' | 'search-dispatcher';
|
||||||
[key in 'search-driver' | 'search-train' | 'search-date' | 'search-author']: string;
|
|
||||||
|
export type JournalTimetableSearchType = {
|
||||||
|
[key in JournalTimetableSearchKey]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface JournalTimetableFilter {
|
export interface JournalTimetableFilter {
|
||||||
|
|||||||
+288
-264
@@ -1,264 +1,288 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="journal-timetables">
|
<section class="journal-timetables">
|
||||||
<div class="journal_wrapper">
|
<JournalHeader />
|
||||||
<JournalOptions
|
|
||||||
@on-search-confirm="searchHistory"
|
<div class="journal_wrapper">
|
||||||
@on-options-reset="resetOptions"
|
<JournalOptions
|
||||||
:sorter-option-ids="['timestampFrom', 'duration']"
|
@on-search-confirm="fetchHistoryData"
|
||||||
:data-status="dataStatus"
|
@on-options-reset="resetOptions"
|
||||||
/>
|
:sorter-option-ids="['timestampFrom', 'duration']"
|
||||||
|
:data-status="dataStatus"
|
||||||
<div class="list_wrapper" @scroll="handleScroll">
|
:current-options-active="currentOptionsActive"
|
||||||
<!-- <transition name="warning" mode="out-in"> -->
|
/>
|
||||||
<!-- <div :key="dataStatus"> -->
|
|
||||||
<Loading v-if="dataStatus == DataStatus.Initialized || dataStatus == DataStatus.Loading" />
|
<div class="list_wrapper" @scroll="handleScroll">
|
||||||
|
<!-- <transition name="warning" mode="out-in"> -->
|
||||||
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
|
<!-- <div :key="dataStatus"> -->
|
||||||
{{ $t('app.error') }}
|
<div class="journal_warning" v-if="store.isOffline">
|
||||||
</div>
|
{{ $t('app.offline') }}
|
||||||
|
</div>
|
||||||
<div class="journal_warning" v-else-if="historyList.length == 0">
|
|
||||||
{{ $t('app.no-result') }}
|
<Loading v-else-if="dataStatus == DataStatus.Initialized || dataStatus == DataStatus.Loading" />
|
||||||
</div>
|
|
||||||
|
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
|
||||||
<div v-else>
|
{{ $t('app.error') }}
|
||||||
<JournalDispatchersList :dispatcherHistory="computedHistoryList" />
|
</div>
|
||||||
|
|
||||||
<button
|
<div class="journal_warning" v-else-if="historyList.length == 0">
|
||||||
class="btn btn--option btn--load-data"
|
{{ $t('app.no-result') }}
|
||||||
v-if="!scrollNoMoreData && scrollDataLoaded && computedHistoryList.length > 15"
|
</div>
|
||||||
@click="addHistoryData"
|
|
||||||
>
|
<div v-else>
|
||||||
{{ $t('journal.load-data') }}
|
<JournalDispatchersList :dispatcherHistory="computedHistoryList" />
|
||||||
</button>
|
|
||||||
</div>
|
<button
|
||||||
<!-- </div>
|
class="btn btn--option btn--load-data"
|
||||||
</transition> -->
|
v-if="!scrollNoMoreData && scrollDataLoaded && computedHistoryList.length > 15"
|
||||||
|
@click="addHistoryData"
|
||||||
<div class="journal_warning" v-if="scrollNoMoreData">
|
>
|
||||||
{{ $t('journal.no-further-data') }}
|
{{ $t('journal.load-data') }}
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
<div class="journal_warning" v-else-if="!scrollDataLoaded">
|
<!-- </div>
|
||||||
{{ $t('journal.loading-further-data') }}
|
</transition> -->
|
||||||
</div>
|
|
||||||
</div>
|
<div class="journal_warning" v-if="scrollNoMoreData">
|
||||||
</div>
|
{{ $t('journal.no-further-data') }}
|
||||||
</section>
|
</div>
|
||||||
</template>
|
|
||||||
|
<div class="journal_warning" v-else-if="!scrollDataLoaded">
|
||||||
<script lang="ts">
|
{{ $t('journal.loading-further-data') }}
|
||||||
import { defineComponent, provide, reactive, Ref, ref } from 'vue';
|
</div>
|
||||||
import axios from 'axios';
|
</div>
|
||||||
|
</div>
|
||||||
import ActionButton from '../../components/Global/ActionButton.vue';
|
</section>
|
||||||
import JournalOptions from '../../components/JournalView/JournalOptions.vue';
|
</template>
|
||||||
import DispatcherStats from '../../components/JournalView/DispatcherStats.vue';
|
|
||||||
import SearchBox from '../Global/SearchBox.vue';
|
<script lang="ts">
|
||||||
|
import { defineComponent, provide, reactive, Ref, ref } from 'vue';
|
||||||
import Loading from '../Global/Loading.vue';
|
import axios from 'axios';
|
||||||
import { URLs } from '../../scripts/utils/apiURLs';
|
|
||||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
import ActionButton from '../components/Global/ActionButton.vue';
|
||||||
import { useStore } from '../../store/store';
|
import JournalOptions from '../components/JournalView/JournalOptions.vue';
|
||||||
import JournalDispatchersList from './JournalDispatchersList.vue';
|
import DispatcherStats from '../components/JournalView/DispatcherStats.vue';
|
||||||
import { JournalDispatcherSearcher, JournalDispatcherSorter } from '../../types/Journal/JournalDispatcherTypes';
|
import SearchBox from '../components/Global/SearchBox.vue';
|
||||||
import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
|
|
||||||
import { JournalTimetableFilter } from '../../types/Journal/JournalTimetablesTypes';
|
import Loading from '../components/Global/Loading.vue';
|
||||||
|
import { URLs } from '../scripts/utils/apiURLs';
|
||||||
const DISPATCHERS_API_URL = `${URLs.stacjownikAPI}/api/getDispatchers`;
|
import { DataStatus } from '../scripts/enums/DataStatus';
|
||||||
|
import { useStore } from '../store/store';
|
||||||
export default defineComponent({
|
import JournalDispatchersList from '../components/JournalView/JournalDispatchersList.vue';
|
||||||
components: { SearchBox, ActionButton, JournalOptions, DispatcherStats, Loading, JournalDispatchersList },
|
import { JournalDispatcherSearcher, JournalDispatcherSorter } from '../types/Journal/JournalDispatcherTypes';
|
||||||
name: 'JournalDispatchers',
|
import { DispatcherHistory } from '../scripts/interfaces/api/DispatchersAPIData';
|
||||||
|
import JournalHeader from '../components/JournalView/JournalHeader.vue';
|
||||||
props: {
|
import { LocationQuery } from 'vue-router';
|
||||||
sceneryName: {
|
|
||||||
type: String,
|
const DISPATCHERS_API_URL = `${URLs.stacjownikAPI}/api/getDispatchers`;
|
||||||
required: false,
|
|
||||||
},
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
dispatcherName: {
|
SearchBox,
|
||||||
type: String,
|
ActionButton,
|
||||||
required: false,
|
JournalOptions,
|
||||||
},
|
DispatcherStats,
|
||||||
},
|
Loading,
|
||||||
|
JournalDispatchersList,
|
||||||
data: () => ({
|
JournalHeader,
|
||||||
currentQuery: '',
|
},
|
||||||
scrollDataLoaded: true,
|
name: 'JournalDispatchers',
|
||||||
scrollNoMoreData: false,
|
|
||||||
|
props: {
|
||||||
showReturnButton: false,
|
sceneryName: {
|
||||||
statsCardOpen: false,
|
type: String,
|
||||||
|
required: false,
|
||||||
dataStatus: DataStatus.Initialized,
|
},
|
||||||
DataStatus,
|
|
||||||
|
dispatcherName: {
|
||||||
historyList: [] as DispatcherHistory[],
|
type: String,
|
||||||
}),
|
required: false,
|
||||||
|
},
|
||||||
setup() {
|
},
|
||||||
const sorterActive: JournalDispatcherSorter = reactive({ id: 'timestampFrom', dir: -1 });
|
|
||||||
const journalFilterActive = ref({});
|
data: () => ({
|
||||||
|
currentQuery: '',
|
||||||
const searchersValues = reactive({
|
currentQueryArray: [] as string[],
|
||||||
'search-dispatcher': '',
|
|
||||||
'search-station': '',
|
scrollDataLoaded: true,
|
||||||
'search-date': '',
|
scrollNoMoreData: false,
|
||||||
} as JournalDispatcherSearcher);
|
|
||||||
|
showReturnButton: false,
|
||||||
const countFromIndex = ref(0);
|
statsCardOpen: false,
|
||||||
const countLimit = 15;
|
currentOptionsActive: false,
|
||||||
|
|
||||||
provide('sorterActive', sorterActive);
|
dataStatus: DataStatus.Initialized,
|
||||||
provide('journalFilterActive', journalFilterActive);
|
DataStatus,
|
||||||
provide('searchersValues', searchersValues);
|
|
||||||
|
historyList: [] as DispatcherHistory[],
|
||||||
const scrollElement: Ref<HTMLElement | null> = ref(null);
|
}),
|
||||||
|
|
||||||
return {
|
setup() {
|
||||||
store: useStore(),
|
const sorterActive: JournalDispatcherSorter = reactive({ id: 'timestampFrom', dir: -1 });
|
||||||
|
const journalFilterActive = ref({});
|
||||||
sorterActive,
|
|
||||||
searchersValues,
|
const searchersValues = reactive({
|
||||||
|
'search-dispatcher': '',
|
||||||
countFromIndex,
|
'search-station': '',
|
||||||
countLimit,
|
'search-date': '',
|
||||||
|
} as JournalDispatcherSearcher);
|
||||||
scrollElement,
|
|
||||||
maxCount: ref(15),
|
const countFromIndex = ref(0);
|
||||||
};
|
const countLimit = 15;
|
||||||
},
|
|
||||||
|
provide('sorterActive', sorterActive);
|
||||||
computed: {
|
provide('journalFilterActive', journalFilterActive);
|
||||||
computedHistoryList() {
|
provide('searchersValues', searchersValues);
|
||||||
return this.historyList.filter(
|
|
||||||
(doc) => doc.isOnline || (doc.currentDuration && doc.currentDuration > 10 * 60000)
|
const scrollElement: Ref<HTMLElement | null> = ref(null);
|
||||||
);
|
|
||||||
},
|
return {
|
||||||
},
|
store: useStore(),
|
||||||
|
|
||||||
activated() {
|
sorterActive,
|
||||||
if (this.sceneryName || this.dispatcherName) {
|
searchersValues,
|
||||||
this.searchersValues['search-station'] = this.sceneryName?.toString() || '';
|
|
||||||
this.searchersValues['search-dispatcher'] = this.dispatcherName?.toString() || '';
|
countFromIndex,
|
||||||
this.searchHistory();
|
countLimit,
|
||||||
}
|
|
||||||
},
|
scrollElement,
|
||||||
|
maxCount: ref(15),
|
||||||
mounted() {
|
};
|
||||||
if (!this.sceneryName && !this.dispatcherName) {
|
},
|
||||||
this.searchHistory();
|
|
||||||
}
|
watch: {
|
||||||
},
|
currentQueryArray(q: string[]) {
|
||||||
|
this.currentOptionsActive =
|
||||||
methods: {
|
q.length > 2 || q.some((qv) => qv.startsWith('sortBy=') && qv.split('=')[1] != 'timestampFrom');
|
||||||
handleScroll(e: Event) {
|
},
|
||||||
const listElement = e.target as HTMLElement;
|
},
|
||||||
const scrollTop = listElement.scrollTop;
|
|
||||||
const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
|
computed: {
|
||||||
|
computedHistoryList() {
|
||||||
if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
|
return this.historyList.filter(
|
||||||
|
(doc) => doc.isOnline || (doc.currentDuration && doc.currentDuration > 10 * 60000)
|
||||||
if (scrollTop > elementHeight * 0.85) this.addHistoryData();
|
);
|
||||||
},
|
},
|
||||||
|
},
|
||||||
resetOptions() {
|
|
||||||
this.searchersValues['search-station'] = '';
|
beforeRouteUpdate(to, _) {
|
||||||
this.searchersValues['search-dispatcher'] = '';
|
this.handleQueries(to.query);
|
||||||
this.sorterActive.id = 'timestampFrom';
|
this.fetchHistoryData();
|
||||||
|
},
|
||||||
this.searchHistory();
|
|
||||||
},
|
activated() {
|
||||||
|
this.handleQueries(this.$route.query);
|
||||||
searchHistory() {
|
this.fetchHistoryData();
|
||||||
this.fetchHistoryData({
|
},
|
||||||
searchers: this.searchersValues,
|
|
||||||
});
|
methods: {
|
||||||
|
handleScroll(e: Event) {
|
||||||
this.scrollNoMoreData = false;
|
const listElement = e.target as HTMLElement;
|
||||||
this.scrollDataLoaded = true;
|
const scrollTop = listElement.scrollTop;
|
||||||
},
|
const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
|
||||||
|
|
||||||
async addHistoryData() {
|
if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
|
||||||
this.scrollDataLoaded = false;
|
|
||||||
|
if (scrollTop > elementHeight * 0.85) this.addHistoryData();
|
||||||
const countFrom = this.historyList.length;
|
},
|
||||||
|
|
||||||
const responseData: DispatcherHistory[] = await (
|
handleQueries(query: LocationQuery) {
|
||||||
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
|
if ('sceneryName' in query) this.searchersValues['search-station'] = `${query.sceneryName}`;
|
||||||
).data;
|
if ('dispatcherName' in query) this.searchersValues['search-dispatcher'] = `${query.dispatcherName}`;
|
||||||
|
},
|
||||||
if (!responseData) return;
|
|
||||||
|
setSearchers(date: string, station: string, dispatcher: string) {
|
||||||
if (responseData.length == 0) {
|
this.searchersValues['search-date'] = date;
|
||||||
this.scrollNoMoreData = true;
|
this.searchersValues['search-station'] = station;
|
||||||
return;
|
this.searchersValues['search-dispatcher'] = dispatcher;
|
||||||
}
|
},
|
||||||
|
|
||||||
this.historyList.push(...responseData);
|
resetOptions() {
|
||||||
this.scrollDataLoaded = true;
|
this.setSearchers('', '', '');
|
||||||
},
|
this.sorterActive.id = 'timestampFrom';
|
||||||
|
|
||||||
async fetchHistoryData(
|
this.fetchHistoryData();
|
||||||
props: {
|
},
|
||||||
searchers?: JournalDispatcherSearcher;
|
|
||||||
filter?: JournalTimetableFilter;
|
async addHistoryData() {
|
||||||
} = {}
|
this.scrollDataLoaded = false;
|
||||||
) {
|
|
||||||
this.dataStatus = DataStatus.Loading;
|
const countFrom = this.historyList.length;
|
||||||
|
|
||||||
const queries: string[] = [];
|
const responseData: DispatcherHistory[] = await (
|
||||||
|
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
|
||||||
const dispatcher = props.searchers?.['search-dispatcher'].trim();
|
).data;
|
||||||
const station = props.searchers?.['search-station'].trim();
|
|
||||||
const dateString = props.searchers?.['search-date'].trim();
|
if (!responseData) return;
|
||||||
const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
|
|
||||||
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
|
if (responseData.length == 0) {
|
||||||
|
this.scrollNoMoreData = true;
|
||||||
if (dispatcher) queries.push(`dispatcherName=${dispatcher}`);
|
return;
|
||||||
if (station) queries.push(`stationName=${station}`);
|
}
|
||||||
if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
|
|
||||||
|
this.historyList.push(...responseData);
|
||||||
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
|
this.scrollDataLoaded = true;
|
||||||
if (this.sorterActive.id == 'timestampFrom') queries.push('sortBy=timestampFrom');
|
},
|
||||||
else if (this.sorterActive.id == 'duration') queries.push('sortBy=currentDuration');
|
|
||||||
else queries.push('sortBy=timestampFrom');
|
async fetchHistoryData() {
|
||||||
|
const queries: string[] = [];
|
||||||
queries.push('countLimit=30');
|
|
||||||
|
const dispatcher = this.searchersValues['search-dispatcher'].trim();
|
||||||
this.currentQuery = queries.join('&');
|
const station = this.searchersValues['search-station'].trim();
|
||||||
|
const dateString = this.searchersValues['search-date'].trim();
|
||||||
try {
|
|
||||||
const responseData: DispatcherHistory[] = await (
|
const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
|
||||||
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}`)
|
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
|
||||||
).data;
|
|
||||||
|
if (dispatcher) queries.push(`dispatcherName=${dispatcher}`);
|
||||||
if (!responseData) {
|
if (station) queries.push(`stationName=${station}`);
|
||||||
this.dataStatus = DataStatus.Error;
|
if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
|
||||||
return;
|
|
||||||
}
|
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
|
||||||
|
if (this.sorterActive.id == 'timestampFrom') queries.push('sortBy=timestampFrom');
|
||||||
if (!responseData) return;
|
else if (this.sorterActive.id == 'duration') queries.push('sortBy=currentDuration');
|
||||||
|
else queries.push('sortBy=timestampFrom');
|
||||||
// Response data exists
|
|
||||||
this.historyList = responseData;
|
queries.push('countLimit=30');
|
||||||
|
|
||||||
// Stats display
|
if (this.currentQuery != queries.join('&')) this.dataStatus = DataStatus.Loading;
|
||||||
this.store.dispatcherStatsName =
|
|
||||||
this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim()
|
this.currentQuery = queries.join('&');
|
||||||
? this.historyList[0].dispatcherName
|
this.currentQueryArray = queries;
|
||||||
: '';
|
|
||||||
|
try {
|
||||||
this.dataStatus = DataStatus.Loaded;
|
const responseData: DispatcherHistory[] = await (
|
||||||
} catch (error) {
|
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}`)
|
||||||
this.dataStatus = DataStatus.Error;
|
).data;
|
||||||
}
|
|
||||||
},
|
if (!responseData) {
|
||||||
},
|
this.dataStatus = DataStatus.Error;
|
||||||
});
|
return;
|
||||||
</script>
|
}
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
if (!responseData) return;
|
||||||
@import '../../styles/JournalSection.scss';
|
|
||||||
</style>
|
// Response data exists
|
||||||
|
this.historyList = responseData;
|
||||||
|
|
||||||
|
// Stats display
|
||||||
|
this.store.dispatcherStatsName =
|
||||||
|
this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim()
|
||||||
|
? this.historyList[0].dispatcherName
|
||||||
|
: '';
|
||||||
|
|
||||||
|
this.dataStatus = DataStatus.Loaded;
|
||||||
|
} catch (error) {
|
||||||
|
this.dataStatus = DataStatus.Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scrollNoMoreData = false;
|
||||||
|
this.scrollDataLoaded = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../styles/JournalSection.scss';
|
||||||
|
</style>
|
||||||
|
|
||||||
+301
-284
@@ -1,284 +1,301 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="journal-timetables">
|
<section class="journal-timetables">
|
||||||
|
<JournalHeader />
|
||||||
<div class="journal_wrapper">
|
|
||||||
<JournalOptions
|
<div class="journal_wrapper">
|
||||||
@on-search-confirm="searchHistory"
|
<JournalOptions
|
||||||
@on-options-reset="resetOptions"
|
@on-search-confirm="fetchHistoryData"
|
||||||
:sorter-option-ids="['timetableId', 'beginDate', 'distance', 'total-stops']"
|
@on-options-reset="resetOptions"
|
||||||
:filters="journalTimetableFilters"
|
:sorter-option-ids="['timetableId', 'beginDate', 'distance', 'total-stops']"
|
||||||
:data-status="dataStatus"
|
:filters="journalTimetableFilters"
|
||||||
/>
|
:currentOptionsActive="currentOptionsActive"
|
||||||
|
:data-status="dataStatus"
|
||||||
<DriverStats />
|
/>
|
||||||
<!-- <button @click="statsCardOpen = true">Stats</button> -->
|
|
||||||
|
<JournalStats />
|
||||||
<div class="list_wrapper" @scroll="handleScroll">
|
|
||||||
<!-- <transition name="warning" mode="out-in"> -->
|
<div class="list_wrapper" @scroll="handleScroll">
|
||||||
<!-- <div :key="dataStatus"> -->
|
<!-- <transition name="warning" mode="out-in"> -->
|
||||||
<Loading v-if="dataStatus == DataStatus.Initialized || dataStatus == DataStatus.Loading" />
|
<!-- <div :key="dataStatus"> -->
|
||||||
|
<div class="journal_warning" v-if="store.isOffline">
|
||||||
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
|
{{ $t('app.offline') }}
|
||||||
{{ $t('app.error') }}
|
</div>
|
||||||
</div>
|
|
||||||
|
<Loading v-else-if="dataStatus == DataStatus.Initialized || dataStatus == DataStatus.Loading" />
|
||||||
<div v-else-if="timetableHistory.length == 0" class="journal_warning">
|
|
||||||
{{ $t('app.no-result') }}
|
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
|
||||||
</div>
|
{{ $t('app.error') }}
|
||||||
|
</div>
|
||||||
<div v-else>
|
|
||||||
<JournalTimetablesList :timetableHistory="timetableHistory" />
|
<div v-else-if="timetableHistory.length == 0" class="journal_warning">
|
||||||
|
{{ $t('app.no-result') }}
|
||||||
<button
|
</div>
|
||||||
class="btn btn--option btn--load-data"
|
|
||||||
v-if="!scrollNoMoreData && scrollDataLoaded && timetableHistory.length >= 15"
|
<div v-else>
|
||||||
@click="addHistoryData"
|
<JournalTimetablesList :timetableHistory="timetableHistory" />
|
||||||
>
|
|
||||||
{{ $t('journal.load-data') }}
|
<button
|
||||||
</button>
|
class="btn btn--option btn--load-data"
|
||||||
</div>
|
v-if="!scrollNoMoreData && scrollDataLoaded && timetableHistory.length >= 15"
|
||||||
<!-- </div> -->
|
@click="addHistoryData"
|
||||||
<!-- </transition> -->
|
>
|
||||||
|
{{ $t('journal.load-data') }}
|
||||||
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
|
</button>
|
||||||
<div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div>
|
</div>
|
||||||
</div>
|
<!-- </div> -->
|
||||||
</div>
|
<!-- </transition> -->
|
||||||
</section>
|
|
||||||
</template>
|
<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>
|
||||||
<script lang="ts">
|
</div>
|
||||||
import { defineComponent, provide, reactive, Ref, ref } from 'vue';
|
</div>
|
||||||
import axios from 'axios';
|
</section>
|
||||||
|
</template>
|
||||||
import DriverStats from './DriverStats.vue';
|
|
||||||
import Loading from '../Global/Loading.vue';
|
<script lang="ts">
|
||||||
import { JournalTimetableFilter, JournalTimetableSorter } from '../../types/Journal/JournalTimetablesTypes';
|
import { defineComponent, provide, reactive, Ref, ref, watch } from 'vue';
|
||||||
import dateMixin from '../../mixins/dateMixin';
|
import axios from 'axios';
|
||||||
import routerMixin from '../../mixins/routerMixin';
|
|
||||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
import DriverStats from '../components/JournalView/JournalDriverStats.vue';
|
||||||
import { JournalFilterType } from '../../scripts/enums/JournalFilterType';
|
import Loading from '../components/Global/Loading.vue';
|
||||||
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
|
import { JournalTimetableFilter, JournalTimetableSorter } from '../types/Journal/JournalTimetablesTypes';
|
||||||
import { URLs } from '../../scripts/utils/apiURLs';
|
import dateMixin from '../mixins/dateMixin';
|
||||||
import { useStore } from '../../store/store';
|
import routerMixin from '../mixins/routerMixin';
|
||||||
import JournalOptions from './JournalOptions.vue';
|
import { DataStatus } from '../scripts/enums/DataStatus';
|
||||||
import { JorunalTimetableSearchType } from '../../types/Journal/JournalTimetablesTypes';
|
import { JournalFilterType } from '../scripts/enums/JournalFilterType';
|
||||||
import modalTrainMixin from '../../mixins/modalTrainMixin';
|
import { TimetableHistory } from '../scripts/interfaces/api/TimetablesAPIData';
|
||||||
import imageMixin from '../../mixins/imageMixin';
|
import { URLs } from '../scripts/utils/apiURLs';
|
||||||
import JournalTimetablesList from './JournalTimetablesList.vue';
|
import { useStore } from '../store/store';
|
||||||
import { journalTimetableFilters } from '../../constants/Journal/JournalTimetablesConsts';
|
import JournalOptions from '../components/JournalView/JournalOptions.vue';
|
||||||
|
import { JournalTimetableSearchType } from '../types/Journal/JournalTimetablesTypes';
|
||||||
const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`;
|
import modalTrainMixin from '../mixins/modalTrainMixin';
|
||||||
|
import imageMixin from '../mixins/imageMixin';
|
||||||
export default defineComponent({
|
import JournalTimetablesList from '../components/JournalView/JournalTimetablesList.vue';
|
||||||
components: { DriverStats, Loading, JournalOptions, JournalTimetablesList },
|
import { journalTimetableFilters } from '../constants/Journal/JournalTimetablesConsts';
|
||||||
mixins: [dateMixin, routerMixin, modalTrainMixin, imageMixin],
|
import JournalStats from '../components/JournalView/JournalStats.vue';
|
||||||
|
import JournalHeader from '../components/JournalView/JournalHeader.vue';
|
||||||
name: 'JournalTimetables',
|
import { LocationQuery } from 'vue-router';
|
||||||
|
|
||||||
props: {
|
const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`;
|
||||||
timetableId: {
|
|
||||||
type: String,
|
export default defineComponent({
|
||||||
},
|
components: { DriverStats, Loading, JournalOptions, JournalTimetablesList, JournalStats, JournalHeader },
|
||||||
},
|
mixins: [dateMixin, routerMixin, modalTrainMixin, imageMixin],
|
||||||
|
|
||||||
data: () => ({
|
name: 'JournalTimetables',
|
||||||
currentQuery: '',
|
|
||||||
scrollDataLoaded: true,
|
props: {
|
||||||
scrollNoMoreData: false,
|
timetableId: {
|
||||||
|
type: String,
|
||||||
showReturnButton: false,
|
},
|
||||||
statsCardOpen: false,
|
},
|
||||||
|
|
||||||
timetableHistory: [] as TimetableHistory[],
|
data: () => ({
|
||||||
journalTimetableFilters,
|
currentQuery: '',
|
||||||
|
currentQueryArray: [] as string[],
|
||||||
dataStatus: DataStatus.Initialized,
|
|
||||||
dataErrorMessage: '',
|
scrollDataLoaded: true,
|
||||||
|
scrollNoMoreData: false,
|
||||||
DataStatus,
|
|
||||||
}),
|
showReturnButton: false,
|
||||||
|
statsCardOpen: false,
|
||||||
setup() {
|
currentOptionsActive: false,
|
||||||
const sorterActive: JournalTimetableSorter = reactive({ id: 'timetableId', dir: 1 });
|
|
||||||
const journalFilterActive = ref(journalTimetableFilters[0]);
|
timetableHistory: [] as TimetableHistory[],
|
||||||
|
journalTimetableFilters,
|
||||||
const searchersValues = reactive({
|
|
||||||
'search-train': '',
|
dataStatus: DataStatus.Initialized,
|
||||||
'search-driver': '',
|
dataErrorMessage: '',
|
||||||
'search-author': '',
|
|
||||||
'search-date': '',
|
DataStatus,
|
||||||
} as JorunalTimetableSearchType);
|
}),
|
||||||
|
|
||||||
const countFromIndex = ref(0);
|
setup() {
|
||||||
const countLimit = 15;
|
const sorterActive: JournalTimetableSorter = reactive({ id: 'timetableId', dir: 1 });
|
||||||
|
const journalFilterActive = ref(journalTimetableFilters[0]);
|
||||||
provide('searchersValues', searchersValues);
|
|
||||||
provide('sorterActive', sorterActive);
|
const searchersValues = reactive({
|
||||||
provide('journalFilterActive', journalFilterActive);
|
'search-train': '',
|
||||||
|
'search-driver': '',
|
||||||
const scrollElement: Ref<HTMLElement | null> = ref(null);
|
'search-dispatcher': '',
|
||||||
|
'search-date': '',
|
||||||
return {
|
} as JournalTimetableSearchType);
|
||||||
sorterActive,
|
|
||||||
journalFilterActive,
|
const countFromIndex = ref(0);
|
||||||
searchersValues,
|
const countLimit = 15;
|
||||||
|
|
||||||
countFromIndex,
|
provide('searchersValues', searchersValues);
|
||||||
countLimit,
|
provide('sorterActive', sorterActive);
|
||||||
|
provide('journalFilterActive', journalFilterActive);
|
||||||
scrollElement,
|
|
||||||
store: useStore(),
|
const scrollElement: Ref<HTMLElement | null> = ref(null);
|
||||||
};
|
|
||||||
},
|
return {
|
||||||
|
sorterActive,
|
||||||
activated() {
|
journalFilterActive,
|
||||||
if (this.timetableId) {
|
searchersValues,
|
||||||
this.searchersValues['search-train'] = `#${this.timetableId}`;
|
|
||||||
this.searchHistory();
|
countFromIndex,
|
||||||
}
|
countLimit,
|
||||||
},
|
|
||||||
|
scrollElement,
|
||||||
mounted() {
|
|
||||||
if (!this.timetableId) this.searchHistory();
|
store: useStore(),
|
||||||
},
|
};
|
||||||
|
},
|
||||||
methods: {
|
|
||||||
handleScroll(e: Event) {
|
watch: {
|
||||||
const listElement = e.target as HTMLElement;
|
currentQueryArray(q: string[]) {
|
||||||
const scrollTop = listElement.scrollTop;
|
this.currentOptionsActive =
|
||||||
const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
|
q.length > 2 || q.some((qv) => qv.startsWith('sortBy=') && qv.split('=')[1] != 'timetableId');
|
||||||
|
},
|
||||||
if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
|
},
|
||||||
|
|
||||||
if (scrollTop > elementHeight * 0.85) this.addHistoryData();
|
// Handle route updates for route-links
|
||||||
},
|
beforeRouteUpdate(to, _) {
|
||||||
|
this.handleQueries(to.query);
|
||||||
resetOptions() {
|
this.fetchHistoryData();
|
||||||
this.searchersValues['search-date'] = '';
|
},
|
||||||
this.searchersValues['search-driver'] = '';
|
|
||||||
this.searchersValues['search-train'] = '';
|
activated() {
|
||||||
this.searchersValues['search-author'] = '';
|
this.handleQueries(this.$route.query);
|
||||||
|
this.fetchHistoryData();
|
||||||
this.journalFilterActive = this.journalTimetableFilters[0];
|
},
|
||||||
this.sorterActive.id = 'timetableId';
|
|
||||||
|
methods: {
|
||||||
this.searchHistory();
|
handleScroll(e: Event) {
|
||||||
},
|
const listElement = e.target as HTMLElement;
|
||||||
|
const scrollTop = listElement.scrollTop;
|
||||||
searchHistory() {
|
const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
|
||||||
this.fetchHistoryData({
|
|
||||||
searchers: this.searchersValues,
|
if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
|
||||||
filter: this.journalFilterActive,
|
|
||||||
});
|
if (scrollTop > elementHeight * 0.85) this.addHistoryData();
|
||||||
|
},
|
||||||
this.scrollNoMoreData = false;
|
|
||||||
this.scrollDataLoaded = true;
|
handleQueries(query: LocationQuery) {
|
||||||
},
|
if ('timetableId' in query) this.searchersValues['search-train'] = `#${query.timetableId}`;
|
||||||
|
},
|
||||||
async addHistoryData() {
|
|
||||||
this.scrollDataLoaded = false;
|
setSearchers(date: string, driver: string, train: string, dispatcher: string) {
|
||||||
|
this.searchersValues['search-date'] = date;
|
||||||
const countFrom = this.timetableHistory.length;
|
this.searchersValues['search-driver'] = driver;
|
||||||
|
this.searchersValues['search-train'] = train;
|
||||||
const responseData: TimetableHistory[] = await (
|
this.searchersValues['search-dispatcher'] = dispatcher;
|
||||||
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
|
},
|
||||||
).data;
|
|
||||||
|
resetOptions() {
|
||||||
if (!responseData) return;
|
this.setSearchers('', '', '', '');
|
||||||
|
|
||||||
if (responseData.length == 0) {
|
this.journalFilterActive = this.journalTimetableFilters[0];
|
||||||
this.scrollNoMoreData = true;
|
this.sorterActive.id = 'timetableId';
|
||||||
return;
|
|
||||||
}
|
this.fetchHistoryData();
|
||||||
|
},
|
||||||
this.timetableHistory.push(...responseData);
|
|
||||||
this.scrollDataLoaded = true;
|
async addHistoryData() {
|
||||||
},
|
this.scrollDataLoaded = false;
|
||||||
|
|
||||||
async fetchHistoryData(
|
const countFrom = this.timetableHistory.length;
|
||||||
props: {
|
|
||||||
searchers?: JorunalTimetableSearchType;
|
const responseData: TimetableHistory[] = await (
|
||||||
filter?: JournalTimetableFilter;
|
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
|
||||||
} = {}
|
).data;
|
||||||
) {
|
|
||||||
this.dataStatus = DataStatus.Loading;
|
if (!responseData) return;
|
||||||
|
|
||||||
const queries: string[] = [];
|
if (responseData.length == 0) {
|
||||||
|
this.scrollNoMoreData = true;
|
||||||
const driverName = props.searchers?.['search-driver'].trim();
|
return;
|
||||||
const trainNo = props.searchers?.['search-train'].trim();
|
}
|
||||||
const authorName = props.searchers?.['search-author'].trim();
|
|
||||||
|
this.timetableHistory.push(...responseData);
|
||||||
const dateString = props.searchers?.['search-date'].trim();
|
this.scrollDataLoaded = true;
|
||||||
const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
|
},
|
||||||
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
|
|
||||||
|
async fetchHistoryData() {
|
||||||
if (driverName) queries.push(`driverName=${driverName}`);
|
const queries: string[] = [];
|
||||||
if (trainNo)
|
|
||||||
queries.push(trainNo.startsWith('#') ? `timetableId=${trainNo.replace('#', '')}` : `trainNo=${trainNo}`);
|
const driverName = this.searchersValues['search-driver'].trim();
|
||||||
if (authorName) queries.push(`authorName=${authorName}`);
|
const trainNo = this.searchersValues['search-train'].trim();
|
||||||
if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
|
const authorName = this.searchersValues['search-dispatcher'].trim();
|
||||||
|
const dateString = this.searchersValues['search-date'].trim();
|
||||||
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
|
|
||||||
if (this.sorterActive.id == 'distance') queries.push('sortBy=routeDistance');
|
const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
|
||||||
else if (this.sorterActive.id == 'total-stops') queries.push('sortBy=allStopsCount');
|
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
|
||||||
else if (this.sorterActive.id == 'beginDate') queries.push('sortBy=beginDate');
|
|
||||||
else queries.push('sortBy=timetableId');
|
if (driverName) queries.push(`driverName=${driverName}`);
|
||||||
|
if (trainNo)
|
||||||
queries.push('countLimit=15');
|
queries.push(trainNo.startsWith('#') ? `timetableId=${trainNo.replace('#', '')}` : `trainNo=${trainNo}`);
|
||||||
|
if (authorName) queries.push(`authorName=${authorName}`);
|
||||||
switch (props.filter?.id) {
|
if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
|
||||||
case JournalFilterType.abandoned:
|
|
||||||
queries.push('fulfilled=0', 'terminated=1');
|
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
|
||||||
break;
|
if (this.sorterActive.id == 'distance') queries.push('sortBy=routeDistance');
|
||||||
|
else if (this.sorterActive.id == 'total-stops') queries.push('sortBy=allStopsCount');
|
||||||
case JournalFilterType.active:
|
else if (this.sorterActive.id == 'beginDate') queries.push('sortBy=beginDate');
|
||||||
queries.push('terminated=0');
|
else queries.push('sortBy=timetableId');
|
||||||
break;
|
|
||||||
|
queries.push('countLimit=15');
|
||||||
case JournalFilterType.fulfilled:
|
|
||||||
queries.push('fulfilled=1');
|
switch (this.journalFilterActive.id) {
|
||||||
break;
|
case JournalFilterType.abandoned:
|
||||||
|
queries.push('fulfilled=0', 'terminated=1');
|
||||||
default:
|
break;
|
||||||
break;
|
|
||||||
}
|
case JournalFilterType.active:
|
||||||
|
queries.push('terminated=0');
|
||||||
this.currentQuery = queries.join('&');
|
break;
|
||||||
|
|
||||||
try {
|
case JournalFilterType.fulfilled:
|
||||||
const responseData: TimetableHistory[] = await (
|
queries.push('fulfilled=1');
|
||||||
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}`)
|
break;
|
||||||
).data;
|
|
||||||
|
default:
|
||||||
if (!responseData) {
|
break;
|
||||||
this.dataStatus = DataStatus.Error;
|
}
|
||||||
this.dataErrorMessage = 'Brak danych!';
|
|
||||||
return;
|
if (this.currentQuery != queries.join('&')) this.dataStatus = DataStatus.Loading;
|
||||||
}
|
|
||||||
|
this.currentQuery = queries.join('&');
|
||||||
if (!responseData) return;
|
this.currentQueryArray = queries;
|
||||||
|
|
||||||
// Response data exists
|
try {
|
||||||
this.timetableHistory = responseData;
|
const responseData: TimetableHistory[] = await (
|
||||||
|
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}`)
|
||||||
// Stats display
|
).data;
|
||||||
this.store.driverStatsName =
|
|
||||||
this.timetableHistory.length > 0 && this.searchersValues['search-driver'].trim()
|
if (!responseData) {
|
||||||
? this.timetableHistory[0].driverName
|
this.dataStatus = DataStatus.Error;
|
||||||
: '';
|
this.dataErrorMessage = 'Brak danych!';
|
||||||
|
return;
|
||||||
this.dataStatus = DataStatus.Loaded;
|
}
|
||||||
} catch (error) {
|
|
||||||
this.dataStatus = DataStatus.Error;
|
if (!responseData) return;
|
||||||
this.dataErrorMessage = 'Ups! Coś poszło nie tak!';
|
|
||||||
}
|
// Response data exists
|
||||||
},
|
this.timetableHistory = responseData;
|
||||||
},
|
|
||||||
});
|
// Stats display
|
||||||
</script>
|
this.store.driverStatsName =
|
||||||
|
this.timetableHistory.length > 0 && this.searchersValues['search-driver'].trim()
|
||||||
<style lang="scss" scoped>
|
? this.timetableHistory[0].driverName
|
||||||
@import '../../styles/JournalSection.scss';
|
: '';
|
||||||
</style>
|
|
||||||
|
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>
|
<template>
|
||||||
<section class="trains-view">
|
<section class="trains-view">
|
||||||
<div class="trains_wrapper">
|
<div class="trains_wrapper">
|
||||||
<TrainOptions :sorter-option-ids="['distance', 'progress', 'delay', 'mass', 'speed', 'length']" />
|
<TrainOptions
|
||||||
|
:sorter-option-ids="['distance', 'progress', 'delay', 'mass', 'speed', 'length']"
|
||||||
|
:current-options-active="currentOptionsActive"
|
||||||
|
/>
|
||||||
|
|
||||||
<TrainTable :trains="computedTrains" />
|
<TrainTable :trains="computedTrains" />
|
||||||
</div>
|
</div>
|
||||||
@@ -9,7 +12,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<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 TrainOptions from '../components/TrainsView/TrainOptions.vue';
|
||||||
import TrainStats from '../components/TrainsView/TrainStats.vue';
|
import TrainStats from '../components/TrainsView/TrainStats.vue';
|
||||||
import TrainTable from '../components/TrainsView/TrainTable.vue';
|
import TrainTable from '../components/TrainsView/TrainTable.vue';
|
||||||
@@ -52,10 +55,13 @@ export default defineComponent({
|
|||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
const initTrainFilters = [...trainFilters.map((f) => ({ ...f }))];
|
||||||
|
|
||||||
const sorterActive = ref({ id: 'distance', dir: -1 });
|
const sorterActive = reactive({ id: 'distance', dir: -1 });
|
||||||
const filterList = reactive([...trainFilters]) as TrainFilter[];
|
const filterList = reactive([...trainFilters]) as TrainFilter[];
|
||||||
|
|
||||||
|
const currentOptionsActive = ref(false);
|
||||||
|
|
||||||
const searchedDriver = ref('');
|
const searchedDriver = ref('');
|
||||||
const searchedTrain = ref('');
|
const searchedTrain = ref('');
|
||||||
|
|
||||||
@@ -65,13 +71,13 @@ export default defineComponent({
|
|||||||
provide('filterList', filterList);
|
provide('filterList', filterList);
|
||||||
|
|
||||||
const computedTrains: ComputedRef<Train[]> = computed(() => {
|
const computedTrains: ComputedRef<Train[]> = computed(() => {
|
||||||
return filteredTrainList(
|
return filteredTrainList(store.trainList, searchedTrain.value, searchedDriver.value, sorterActive, filterList);
|
||||||
store.trainList,
|
});
|
||||||
searchedTrain.value,
|
|
||||||
searchedDriver.value,
|
watch([searchedTrain, searchedDriver, sorterActive, filterList], ([sT, sD, sA, fL]) => {
|
||||||
sorterActive.value,
|
const areFiltersActive = fL.some((f, i) => f.isActive !== initTrainFilters[i].isActive);
|
||||||
filterList
|
|
||||||
);
|
currentOptionsActive.value = sT.length > 0 || sD.length > 0 || sA.id != 'distance' || areFiltersActive;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -80,6 +86,7 @@ export default defineComponent({
|
|||||||
searchedDriver,
|
searchedDriver,
|
||||||
sorterActive,
|
sorterActive,
|
||||||
store,
|
store,
|
||||||
|
currentOptionsActive,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
"ESNext",
|
"ESNext",
|
||||||
"DOM"
|
"DOM"
|
||||||
],
|
],
|
||||||
"types": ["vite/client"],
|
"types": ["vite/client", "vite-plugin-pwa/client"],
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|||||||
+46
-27
@@ -1,34 +1,53 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import vue from '@vitejs/plugin-vue';
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
server: {
|
||||||
|
port: 5001,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
VitePWA({
|
||||||
|
registerType: 'prompt',
|
||||||
|
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ['**/*.{js,css,html,png,svg,img}'],
|
||||||
|
runtimeCaching: [
|
||||||
|
{
|
||||||
|
urlPattern: new RegExp('^https://spythere.pl/api/getSceneries', 'i'),
|
||||||
|
handler: 'NetworkFirst',
|
||||||
|
options: {
|
||||||
|
cacheName: 'sceneries-cache',
|
||||||
|
expiration: {
|
||||||
|
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