Compare commits

..

61 Commits

Author SHA1 Message Date
Spythere deb7b68985 Merge branch 'development' 2023-01-01 03:02:11 +01:00
Spythere 633f05f690 fix: wyświetlanie poprawnych id RJ 2023-01-01 02:57:11 +01:00
Spythere 73828867da Merge wersji dev do produkcji (1.11.1)
Wersja 1.11.1
2022-12-31 18:30:08 +01:00
Spythere 75685c1e0e bump: 1.11.1 2022-12-31 18:22:39 +01:00
Spythere 496ff95236 fix: sortowanie RJ wg id z API 2022-12-31 18:21:32 +01:00
Spythere 7e25327832 feature: lvl dyżurnego w dzienniku 2022-12-30 17:39:21 +01:00
Spythere 272c9f50f8 fix: SW cache 2022-12-30 15:45:17 +01:00
Spythere 255e07372e Merge wersji dev do produkcji (1.11)
Wersja produkcyjna 1.11.0
2022-12-26 22:58:17 +01:00
Spythere 279bbfa4db fix: responsywność 2022-12-26 20:01:10 +01:00
Spythere a5c829faf5 Fix: wskaźnik ładowania dzienników 2022-12-26 19:37:52 +01:00
Spythere 5fdfaeac5e hotfix 2022-12-26 18:52:31 +01:00
Spythere 9beb30e3d5 Tłumaczenie monitu 2022-12-26 18:51:50 +01:00
Spythere 48582e2eea lock files sync 2022-12-26 18:45:39 +01:00
Spythere 2e721fb8bf PWA: tryb offline 2022-12-26 18:43:15 +01:00
Spythere f93c1fbfec PWA: tryb offline 2022-12-26 18:43:14 +01:00
Spythere c06e7b6468 Poprawka wyświetlania sumy dystansu 2022-12-26 13:54:30 +01:00
Spythere 22a6d266cb Aktualizacja danych z API 2022-12-26 13:50:48 +01:00
Spythere 5f8a16401b Update API 2022-12-25 23:35:10 +01:00
Spythere c9be01aa29 lock files 2022-12-23 20:26:54 +01:00
Spythere 4ec058b33c Konfiguracja PWA 2022-12-23 20:25:02 +01:00
Spythere 27a5d2a406 fix: tłumaczenie komunikatu 2022-12-22 18:50:09 +01:00
Spythere 58169e26f6 Feedback i stylistyka statystyk RJ 2022-12-22 01:45:43 +01:00
Spythere fee1f4bbd5 Usprawienie podpowiedzi filtrów 2022-12-22 01:36:38 +01:00
Spythere 240817acc3 Przekierowanie do strony głównej 2022-12-21 20:32:41 +01:00
Spythere db3be87dd8 Przystosowanie pod update API 2022-12-21 20:24:48 +01:00
Spythere 1665134d6f Fix odznaczenia filtrów pociągów 2022-12-21 19:34:42 +01:00
Spythere df289ab734 Wskaźnik aktywnych filtrów pociągów online 2022-12-21 19:07:23 +01:00
Spythere f74440ba6f Pogrubienie linku dziennika w headerze 2022-12-21 18:39:40 +01:00
Spythere a25dbe9fd5 Usunięcie firebase config z html 2022-12-21 18:27:27 +01:00
Spythere 4fff136d6b Poprawki reaktywności 2022-12-21 18:24:04 +01:00
Spythere d06f2d5d2e Optymalizacja pobierania danych 2022-12-21 18:10:54 +01:00
Spythere 9f68d628d0 Wskaźnik aktywnych filtrów dziennika DR 2022-12-21 15:51:13 +01:00
Spythere d64b906dac Wskaźnik aktywnych filtrów dziennika RJ 2022-12-21 15:45:03 +01:00
Spythere f3e193e68a Cleanup 2022-12-21 15:02:41 +01:00
Spythere 5640ce9f2b Fix routingu w dzienniku RJ 2022-12-21 15:02:25 +01:00
Spythere 50100eb2f9 Nawigacja 2022-12-20 21:51:40 +01:00
Spythere e478c510b2 Fix działania reaktywności linków 2022-12-20 21:31:59 +01:00
Spythere 7ea558642f Stylistyka statystyk 2022-12-20 21:11:47 +01:00
Spythere 493145f7f2 Fix pola daty 2022-12-20 16:59:59 +01:00
Spythere 4f72535365 Setup GitHub Actions & npm 2022-12-20 16:56:12 +01:00
Spythere 8e3bf80715 Fix logiki przycisków 2022-12-20 16:44:15 +01:00
Spythere 6da586d08a Stylistyka komponentów statystyk 2022-12-20 16:41:42 +01:00
Spythere be53b9c7fb Notka o lokacji pociągu nie pojawia się przy jej braku 2022-12-20 01:41:13 +01:00
Spythere 94ed1160a1 Poprawki 2022-12-20 01:38:08 +01:00
Spythere 859d8d2631 Train modal fix 2022-12-20 00:53:03 +01:00
Spythere 5f3abd73c5 Informacja o statystykach 2022-12-19 00:44:46 +01:00
Spythere d71c8bb6f9 Bump wersji 2022-12-18 23:43:23 +01:00
Spythere a3db13d79c Github Actions 2022-12-18 20:01:15 +01:00
Spythere 8cb3da66f2 Statystyki maszynistów 2022-12-18 19:54:13 +01:00
Spythere 6e07897ac0 Fix: bug routingu dzienników 2022-12-18 03:01:13 +01:00
Spythere 726b859f5c Poprawki tabów statystyk 2022-12-18 01:28:11 +01:00
Spythere 651c60707a Rework statystyk RJ 2022-12-17 20:45:59 +01:00
Spythere d4fee84603 Rework statystyk RJ 2022-12-17 20:45:53 +01:00
Spythere 86539cdf23 1.10.10: status scenerii w dzienniku RJ 2022-12-03 09:41:46 +01:00
Spythere 69772460b8 Poprawka w działaniu sortowania wyszukiwarki scenerii 2022-11-01 18:27:27 +01:00
Spythere 6988a83355 Zmiana API 2022-10-30 23:03:47 +01:00
Spythere b6425564c8 Bump wersji 2022-10-28 13:15:28 +02:00
Spythere caf0a9b4c5 Dodano sugestie wyszukiwania istniejących użytkowników w dziennikach 2022-10-28 13:15:07 +02:00
Spythere bd5f433d6e Update paczek 2022-10-26 15:27:28 +02:00
Spythere 8d9cc721d6 Poprawki stylów 2022-10-16 23:09:46 +02:00
Spythere cceeffe49d Świecące nicki i poziomy sponsorów 2022-10-14 23:15:50 +02:00
58 changed files with 15746 additions and 1387 deletions
@@ -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
View File
@@ -1,7 +1,7 @@
.DS_Store .DS_Store
node_modules node_modules
/dist
/dev-dist /dev-dist
/dist
# local env files # local env files
.env.local .env.local
+5 -2
View File
@@ -1,7 +1,11 @@
{ {
"hosting": { "hosting": {
"public": "dist", "public": "dist",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"], "ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [ "rewrites": [
{ {
"source": "**", "source": "**",
@@ -10,4 +14,3 @@
] ]
} }
} }
-14
View File
@@ -25,20 +25,6 @@
<link rel="icon" href="favicon.ico" /> <link rel="icon" href="favicon.ico" />
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;700&display=swap" rel="stylesheet" />
<script src="https://www.gstatic.com/firebasejs/8.1.1/firebase-app.js"></script>
<script>
const firebaseConfig = {
apiKey: 'AIzaSyBI36X2-p7vU1flxoJdCEc0noByyTe1mpw',
authDomain: 'stacjownik-td2.firebaseapp.com',
databaseURL: 'https://stacjownik-td2.firebaseio.com',
projectId: 'stacjownik-td2',
storageBucket: 'stacjownik-td2.appspot.com',
};
firebase.initializeApp(firebaseConfig);
</script>
</head> </head>
<body> <body>
+11386
View File
File diff suppressed because it is too large Load Diff
+9 -8
View File
@@ -1,12 +1,12 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.10.8", "version": "1.11.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"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",
@@ -21,12 +21,13 @@
"vue-router": "^4.0.0-0" "vue-router": "^4.0.0-0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.8.3", "@types/node": "^18.11.17",
"@vitejs/plugin-vue": "^3.0.0", "@vitejs/plugin-vue": "^4.0.0",
"axios": "^1.1.2", "axios": "^1.2.1",
"typescript": "^4.6.4", "typescript": "^4.9.4",
"vite": "^3.0.0", "vite": "^4.0.3",
"vue-tsc": "^1.0.3" "vite-plugin-pwa": "^0.14.0",
"vue-tsc": "^1.0.18"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
+3 -2
View File
@@ -33,7 +33,8 @@
.route { .route {
margin: 0 0.2em; margin: 0 0.2em;
&-active { &-active,
&[data-active='true'] {
color: $accentCol; color: $accentCol;
font-weight: bold; font-weight: bold;
} }
@@ -45,7 +46,7 @@
font-size: 1rem; font-size: 1rem;
@include smallScreen() { @include smallScreen() {
font-size: calc(0.55rem + 1vw); font-size: calc(0.5rem + 1.3vw);
} }
} }
+30 -2
View File
@@ -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() {
+52 -43
View File
@@ -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>
-31
View File
@@ -1,31 +0,0 @@
<template>
<div class="loading">{{message}}</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
props: ["message"],
});
</script>
<style lang="scss" scoped>
.loading {
display: flex;
justify-content: center;
align-items: center;
min-height: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: calc(0.75rem + 1vw);
color: #fdc62f;
}
</style>
+14 -3
View File
@@ -161,7 +161,6 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { DataStatus } from '../../scripts/enums/DataStatus'; import { DataStatus } from '../../scripts/enums/DataStatus';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
@@ -172,6 +171,7 @@ export default defineComponent({
return { return {
tooltipActive: false, tooltipActive: false,
indicator: { indicator: {
offline: false,
status: DataStatus.Loading, status: DataStatus.Loading,
message: 'data-status.S3', message: 'data-status.S3',
}, },
@@ -193,6 +193,7 @@ export default defineComponent({
return { return {
dataStatus: store.dataStatuses, dataStatus: store.dataStatuses,
store,
}; };
}, },
@@ -206,6 +207,13 @@ export default defineComponent({
const trainsDataStatus = statuses.trains; const trainsDataStatus = statuses.trains;
const dispatcherDataStatus = statuses.dispatchers; const dispatcherDataStatus = statuses.dispatchers;
if (this.store.isOffline) {
this.setSignalStatus(DataStatus.Initialized);
this.indicator.status = DataStatus.Initialized;
this.indicator.message = 'data-status.S1-offline';
return;
}
if (connectionStatus == DataStatus.Error) { if (connectionStatus == DataStatus.Error) {
this.setSignalStatus(connectionStatus); this.setSignalStatus(connectionStatus);
this.indicator.status = connectionStatus; this.indicator.status = connectionStatus;
@@ -252,6 +260,10 @@ export default defineComponent({
this.orangeLight = false; this.orangeLight = false;
this.redBottomLight = false; this.redBottomLight = false;
if (status == DataStatus.Initialized) {
this.redTopLight = true;
}
if (status == DataStatus.Loaded) { if (status == DataStatus.Loaded) {
this.greenLight = true; this.greenLight = true;
} }
@@ -291,9 +303,8 @@ export default defineComponent({
.status-indicator { .status-indicator {
position: absolute; position: absolute;
left: 50%; left: 110%;
bottom: 0; bottom: 0;
transform: translateX(12em);
z-index: 100; z-index: 100;
} }
+69
View File
@@ -0,0 +1,69 @@
<template>
<div class="update-prompt">
<transition name="prompt-anim">
<div class="prompt_content" v-if="!hidePrompt && needRefresh">
<div>{{ $t('update.title') }}</div>
<div class="prompt_actions">
<button class="btn btn--filled" @click="updateServiceWorker(true)">{{ $t('update.confirm-button') }}</button>
<button class="btn btn--filled" @click="hidePrompt = true">{{ $t('update.later-button') }}</button>
</div>
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import useCustomSW from '../../mixins/useCustomSW';
const hidePrompt = ref(false);
const { needRefresh, updateServiceWorker } = useCustomSW();
</script>
<style lang="scss" scoped>
@import '../../styles/variables.scss';
.update-prompt {
position: fixed;
bottom: 0;
right: 0;
z-index: 200;
}
.prompt_content {
margin: 1em;
padding: 1em;
font-weight: bold;
background-color: black;
box-shadow: 0 0 10px 1px $accentCol;
border-radius: 1em;
}
.prompt_actions {
display: flex;
margin-top: 1em;
gap: 0.5em;
button {
width: 100%;
}
}
// Animation
.prompt-anim {
&-enter-active,
&-leave-active {
transition: all 120ms ease-in;
transform: translateY(0);
}
&-enter-from,
&-leave-to {
transform: translateY(100%);
}
}
</style>
+1 -1
View File
@@ -20,7 +20,7 @@ export default defineComponent({
.loading { .loading {
position: absolute; position: absolute;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translate(-50%, -50%);
display: flex; display: flex;
justify-content: center; justify-content: center;
+182
View File
@@ -0,0 +1,182 @@
<template>
<section class="daily-stats">
<span :data-active="data.statsStatus">
<b v-if="data.statsStatus == DataStatus.Loading">
{{ $t('app.loading') }}
</b>
<b v-else-if="data.stats.distanceSum == null">
{{ $t('journal.daily-stats-info') }}
</b>
<span>
<div v-if="data.stats.totalTimetables">
&bull;
<i18n-t keypath="journal.timetable-stats-total">
<template #count>
<b class="text--primary">
{{ data.stats.totalTimetables }}
{{ $t('journal.timetable-count', data.stats.totalTimetables) }}
</b>
</template>
<template #distance>
<b class="text--primary"> {{ data.stats.distanceSum?.toFixed(2) }} km </b>
</template>
</i18n-t>
</div>
<div v-if="data.stats.timetableId">
&bull;
<i18n-t keypath="journal.timetable-stats-longest">
<template #id>
<router-link :to="`/journal/timetables?timetableId=${data.stats.timetableId}`">
<b>{{ data.stats.timetableId }}</b>
</router-link>
</template>
<template #author>
<router-link :to="`/journal/dispatchers?dispatcherName=${data.stats.timetableAuthor}`">
<b>{{ data.stats.timetableAuthor }}</b>
</router-link>
</template>
<template #driver>
<b>{{ data.stats.timetableDriver }}</b>
</template>
<template #distance>
<b class="text--primary">{{ data.stats.timetableRouteDistance }} km</b>
</template>
</i18n-t>
</div>
<div v-if="firstPlaceDispatchers.length == 1">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active">
<template #dispatcher>
<router-link :to="`/journal/dispatchers?dispatcherName=${firstPlaceDispatchers[0].name}`">
<b>{{ firstPlaceDispatchers[0].name }}</b>
</router-link>
</template>
<template #count>
<b class="text--primary">
{{ firstPlaceDispatchers[0].count }}
{{ $t('journal.timetable-count', firstPlaceDispatchers[0].count) }}
</b>
</template>
</i18n-t>
</div>
<div v-if="firstPlaceDispatchers.length > 1">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active-many">
<template #dispatchers>
<span v-for="(disp, i) in firstPlaceDispatchers">
<span v-if="i == firstPlaceDispatchers.length - 1"> {{ $t('general.and') }} </span>
<router-link :to="`/journal/dispatchers?dispatcherName=${disp.name}`">
<b>{{ disp.name }}</b>
</router-link>
<span v-if="i < firstPlaceDispatchers.length - 2">, </span>
</span>
</template>
<template #count>
<b class="text--primary">
{{ firstPlaceDispatchers[0].count }}
{{ $t('journal.timetable-count', firstPlaceDispatchers[0].count) }}
</b>
</template>
</i18n-t>
</div>
</span>
</span>
</section>
</template>
<script setup lang="ts">
import axios from 'axios';
import { computed, reactive, ref } from 'vue';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { ITimetablesDailyStats, ITimetablesDailyStatsResponse } from '../../scripts/interfaces/api/StatsAPIData';
import { URLs } from '../../scripts/utils/apiURLs';
const intervalId = ref(-1);
const data = reactive({
statsStatus: DataStatus.Loading,
stats: {
totalTimetables: 0,
distanceSum: 0,
distanceAvg: 0,
timetableAuthor: '',
timetableDriver: '',
timetableId: 0,
timetableRouteDistance: 0,
mostActiveDispatchers: [],
} as ITimetablesDailyStats,
});
const firstPlaceDispatchers = computed(() => {
if (data.stats.mostActiveDispatchers.length == 0) return [];
const maxCount = data.stats.mostActiveDispatchers[0].count;
return data.stats.mostActiveDispatchers.filter((disp) => disp.count === maxCount);
});
async function fetchDailyTimetableStats() {
try {
const {
distanceAvg,
distanceSum,
maxTimetable,
totalTimetables,
mostActiveDispatchers,
}: ITimetablesDailyStatsResponse = await (
await axios.get(`${URLs.stacjownikAPI}/api/getDailyTimetableStats`)
).data;
data.stats = {
totalTimetables,
distanceSum,
distanceAvg,
timetableAuthor: maxTimetable?.authorName || '',
timetableDriver: maxTimetable?.driverName || '',
timetableId: maxTimetable?.id || 0,
timetableRouteDistance: maxTimetable?.routeDistance || 0,
mostActiveDispatchers,
};
data.statsStatus = DataStatus.Loaded;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...');
data.statsStatus = DataStatus.Error;
}
}
function startFetchingDailyStats() {
fetchDailyTimetableStats();
intervalId.value = setInterval(fetchDailyTimetableStats, 60000);
}
function stopFetchingDailyStats() {
clearInterval(intervalId.value);
}
defineExpose({
startFetchingDailyStats,
stopFetchingDailyStats,
});
</script>
<style lang="scss" scoped>
.daily-stats {
text-align: left;
}
.daily-stats > span[data-active='0'] {
opacity: 0.75;
}
</style>
@@ -1,94 +0,0 @@
<template>
<div class="journal-stats" v-if="store.driverStatsData?._sum.routeDistance != null">
<h1>
{{ $t('journal.stats-title') }} <span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
</h1>
<div class="info-stats">
<span class="stat-badge">
<span>{{ $t('journal.stats-timetables') }}</span>
<span>{{ store.driverStatsData._count.fulfilled }} / {{ store.driverStatsData._count._all }}</span>
</span>
<span class="stat-badge">
<span>{{ $t('journal.stats-longest-timetable') }}</span>
<span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span>
</span>
<span class="stat-badge">
<span>{{ $t('journal.stats-avg-timetable') }}</span>
<span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
</span>
<span class="stat-badge">
<span>{{ $t('journal.stats-distance') }}</span>
<span>
{{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km
</span>
</span>
<span class="stat-badge">
<span>{{ $t('journal.stats-stations') }}</span>
<span>
{{ store.driverStatsData._sum.confirmedStopsCount }} /
{{ store.driverStatsData._sum.allStopsCount }}
</span>
</span>
</div>
</div>
</template>
<script lang="ts">
import axios from 'axios';
import { computed, defineComponent, ref } from 'vue';
import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData';
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
import { URLs } from '../../scripts/utils/apiURLs';
import { useStore } from '../../store/store';
export default defineComponent({
emits: ['closeCard'],
setup() {
const store = useStore();
return {
store,
driverStatsName: computed(() => store.driverStatsName),
};
},
data() {
return {
test: Math.random(),
lastDispatcherName: '',
lastTimetables: [] as TimetableHistory[],
};
},
watch: {
driverStatsName(value: string) {
this.fetchDispatcherStats();
},
},
methods: {
async fetchDispatcherStats() {
this.store.driverStatsData = undefined;
if (!this.store.driverStatsName) return;
const statsData: DriverStatsAPIData = await (
await axios.get(`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`)
).data;
this.store.driverStatsData = statsData;
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/JournalStats.scss';
</style>
@@ -15,6 +15,14 @@
tabindex="0" tabindex="0"
> >
<span> <span>
<b
v-if="item.dispatcherLevel !== null"
class="dispatcher-level"
:style="calculateExpStyle(item.dispatcherLevel, item.dispatcherIsSupporter)"
>
{{ item.dispatcherLevel >= 2 ? item.dispatcherLevel : 'L' }}
</b>
<b class="text--primary">{{ item.dispatcherName }}</b> &bull; <b>{{ item.stationName }}</b> <b class="text--primary">{{ item.dispatcherName }}</b> &bull; <b>{{ item.stationName }}</b>
<span class="text--grayed">&nbsp;#{{ item.stationHash }}&nbsp;</span> <span class="text--grayed">&nbsp;#{{ item.stationHash }}&nbsp;</span>
<span class="region-badge" :class="item.region">PL1</span> <span class="region-badge" :class="item.region">PL1</span>
@@ -44,6 +52,7 @@
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData'; import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
import styleMixin from '../../mixins/styleMixin';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -53,7 +62,7 @@ export default defineComponent({
}, },
}, },
mixins: [dateMixin], mixins: [dateMixin, styleMixin],
computed: { computed: {
computedDispatcherHistory() { computedDispatcherHistory() {
@@ -143,6 +152,18 @@ li.sticky {
} }
} }
.dispatcher-level {
display: inline-block;
text-align: center;
line-height: 150%;
width: 25px;
height: 25px;
margin-right: 0.5em;
border-radius: 0.25em;
}
@include smallScreen() { @include smallScreen() {
.journal_item { .journal_item {
flex-direction: column; flex-direction: column;
@@ -0,0 +1,67 @@
<template>
<div class="journal-stats">
<span v-if="store.driverStatsData">
<h3>
{{ $t('journal.stats-title') }} <span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
</h3>
<div class="info-stats">
<span class="stat-badge">
<span>{{ $t('journal.stats-timetables') }}</span>
<span>{{ store.driverStatsData._count.fulfilled }} / {{ store.driverStatsData._count._all }}</span>
</span>
<span class="stat-badge">
<span>{{ $t('journal.stats-longest-timetable') }}</span>
<span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span>
</span>
<span class="stat-badge">
<span>{{ $t('journal.stats-avg-timetable') }}</span>
<span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
</span>
<span class="stat-badge">
<span>{{ $t('journal.stats-distance') }}</span>
<span>
{{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km
</span>
</span>
<span class="stat-badge">
<span>{{ $t('journal.stats-stations') }}</span>
<span>
{{ store.driverStatsData._sum.confirmedStopsCount }} /
{{ store.driverStatsData._sum.allStopsCount }}
</span>
</span>
</div>
</span>
<b v-else-if="store.driverStatsStatus == DataStatus.Loading">{{ $t('journal.stats-loading') }}</b>
<b v-else-if="store.driverStatsStatus == DataStatus.Error">
{{ $t('journal.stats-error ') }}
</b>
<b v-else>{{ $t('journal.driver-stats-info') }}</b>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { useStore } from '../../store/store';
export default defineComponent({
data() {
return {
store: useStore(),
DataStatus,
};
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/JournalStats.scss';
</style>
@@ -0,0 +1,46 @@
<template>
<section class="journal-header">
<div class="journal-type-options">
<router-link class="router-link" active-class="route-active" to="/journal/timetables" exact>
{{ $t('journal.section-timetables') }}
</router-link>
&nbsp;&bull;&nbsp;
<router-link class="router-link" active-class="route-active" to="/journal/dispatchers">
{{ $t('journal.section-dispatchers') }}
</router-link>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({});
</script>
<style lang="scss" scoped>
.journal-type-options {
display: flex;
justify-content: center;
background-color: #2c2c2c;
max-width: 18em;
font-size: 1.2em;
margin: 0 auto;
border-radius: 0 0 0.5em 0.5em;
padding: 0.1em 0;
}
.journal-section > section {
height: 100%;
display: flex;
justify-content: center;
}
.router-link.active {
color: gold;
}
</style>
+99 -14
View File
@@ -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;
+110
View File
@@ -0,0 +1,110 @@
<template>
<div class="journal-stats" v-show="!store.isOffline">
<div class="tabs">
<button
v-for="tab in data.tabs"
class="btn--filled"
:data-selected="tab.name == store.currentStatsTab && areStatsOpen"
:data-inactive="tab.inactive"
@click="onTabButtonClick(tab.name)"
>
{{ $t(tab.titlePath) }}
</button>
</div>
<div class="stats-tab" v-show="areStatsOpen">
<keep-alive>
<JournalDailyStats v-if="store.currentStatsTab == 'daily'" ref="dailyStatsComp" />
<JournalDriverStats v-else-if="store.currentStatsTab == 'driver'" />
</keep-alive>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, KeepAlive, onActivated, onDeactivated, reactive, Ref, ref, watch } from 'vue';
import { useStore } from '../../store/store';
import JournalDailyStats from './DailyStats.vue';
import JournalDriverStats from './JournalDriverStats.vue';
// Types
type TStatTab = 'daily' | 'driver';
// Variables
const store = useStore();
const dailyStatsComp: Ref<InstanceType<typeof JournalDailyStats> | null> = ref(null);
const areStatsOpen = ref(true);
const lastClickedTab = ref('daily');
let data = reactive({
tabs: [
{
name: 'daily',
titlePath: 'journal.daily-stats-title',
},
{
name: 'driver',
titlePath: 'journal.driver-stats-title',
inactive: true,
},
] as { name: TStatTab; titlePath: string; inactive?: boolean }[],
});
// Methods
function onTabButtonClick(tab: TStatTab) {
if (lastClickedTab.value == tab || !areStatsOpen.value) {
areStatsOpen.value = !areStatsOpen.value;
}
store.currentStatsTab = tab;
lastClickedTab.value = tab;
}
onActivated(() => {
dailyStatsComp.value?.startFetchingDailyStats();
});
onDeactivated(() => {
dailyStatsComp.value?.stopFetchingDailyStats();
});
watch(
computed(() => store.driverStatsData),
(statsData) => {
data.tabs[1].inactive = statsData ? false : true;
lastClickedTab.value = statsData ? 'driver' : 'daily';
if (statsData) areStatsOpen.value = true;
}
);
</script>
<style lang="scss" scoped>
@import '../../styles/JournalStats.scss';
@import '../../styles/variables.scss';
.tabs {
position: relative;
display: flex;
gap: 0.5em;
margin-bottom: 0.5em;
button {
font-weight: bold;
padding: 0.5em 0.75em;
&[data-inactive='true'] {
color: gray;
}
&[data-selected='true'] {
color: $accentCol;
}
}
}
</style>
@@ -16,7 +16,7 @@
<b class="text--primary">{{ timetable.trainCategoryCode }}&nbsp;</b> <b class="text--primary">{{ timetable.trainCategoryCode }}&nbsp;</b>
<b>{{ timetable.trainNo }}</b> <b>{{ timetable.trainNo }}</b>
| <span>{{ timetable.driverName }}</span> | | <span>{{ timetable.driverName }}</span> |
<span class="text--grayed">#{{ timetable.timetableId }}</span> <span class="text--grayed">#{{ timetable.id }}</span>
</span> </span>
<span> <span>
@@ -72,6 +72,13 @@
{{ timetable.confirmedStopsCount }} / {{ timetable.confirmedStopsCount }} /
{{ timetable.allStopsCount }} {{ timetable.allStopsCount }}
</span> </span>
<span class="text--grayed" v-if="!timetable.fulfilled && timetable.currentSceneryName">
&bull;
<b>
{{ $t(`journal.${timetable.terminated ? 'last-seen-at' : 'currently-at'}`) }}
{{ timetable.currentSceneryName.replace(/.[a-zA-Z0-9]+.sc/, '') }}
</b>
</span>
</div> </div>
<!-- Nick dyżurnego --> <!-- Nick dyżurnego -->
+2 -3
View File
@@ -9,7 +9,7 @@
<b>{{ $t('availability.title') }}:</b> {{ $t(`availability.${station.generalInfo.availability}`) }} <b>{{ $t('availability.title') }}:</b> {{ $t(`availability.${station.generalInfo.availability}`) }}
<span v-if="station.generalInfo.reqLevel > -1"> <span v-if="station.generalInfo.reqLevel > -1">
- {{ $tc('scenery.req-level', station.generalInfo.reqLevel, { lvl: station.generalInfo.reqLevel }) }} - {{ $t('scenery.req-level', { lvl: station.generalInfo.reqLevel }, station.generalInfo.reqLevel) }}
</span> </span>
</span> </span>
@@ -33,7 +33,7 @@
<scenery-info-routes :station="station" /> <scenery-info-routes :station="station" />
<div class="scenery-authors" v-if="station.generalInfo.authors && station.generalInfo.authors.length > 0"> <div class="scenery-authors" v-if="station.generalInfo.authors && station.generalInfo.authors.length > 0">
<b> {{ $tc('scenery.authors-title', station.generalInfo.authors.length) }}: </b> <b> {{ $t('scenery.authors-title', { authors: station.generalInfo.authors.length }, station.generalInfo.authors.length) }}: </b>
{{ station.generalInfo.authors.join(', ') }} {{ station.generalInfo.authors.join(', ') }}
</div> </div>
@@ -72,7 +72,6 @@ import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue';
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue'; import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
import Station from '../../scripts/interfaces/Station'; import Station from '../../scripts/interfaces/Station';
export default defineComponent({ export default defineComponent({
components: { components: {
SceneryInfoDispatcher, SceneryInfoDispatcher,
@@ -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>
@@ -11,8 +11,8 @@
</div> </div>
<div> <div>
<router-link :to="`/journal/timetables?timetableId=${historyItem.timetableId}`"> <router-link :to="`/journal/timetables?timetableId=${historyItem.id}`">
<span class="text--grayed"> #{{ historyItem.timetableId }} </span> <span class="text--grayed"> #{{ historyItem.id }} </span>
<b class="text--primary">&nbsp;{{ historyItem.trainCategoryCode }} {{ historyItem.trainNo }}</b> <b class="text--primary">&nbsp;{{ historyItem.trainCategoryCode }} {{ historyItem.trainNo }}</b>
<div>{{ historyItem.driverName }}</div> <div>{{ historyItem.driverName }}</div>
</router-link> </router-link>
@@ -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);
+4 -1
View File
@@ -100,7 +100,10 @@
</td> </td>
<td class="station_dispatcher-exp"> <td class="station_dispatcher-exp">
<span v-if="station.onlineInfo" :style="calculateExpStyle(station.onlineInfo.dispatcherExp)"> <span
v-if="station.onlineInfo"
:style="calculateExpStyle(station.onlineInfo.dispatcherExp, station.onlineInfo.dispatcherIsSupporter)"
>
{{ 2 > station.onlineInfo.dispatcherExp ? 'L' : station.onlineInfo.dispatcherExp }} {{ 2 > station.onlineInfo.dispatcherExp ? 'L' : station.onlineInfo.dispatcherExp }}
</span> </span>
</td> </td>
+19 -6
View File
@@ -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 }}&nbsp;</strong> <strong class="timetable-category" v-if="train.timetableData">
<strong>{{ train.trainNo }}</strong> {{ train.timetableData.category }}
<span>&nbsp;| {{ train.driverName }}&nbsp;</span> </strong>
<strong class="train-number">&nbsp;{{ 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;
+13 -2
View File
@@ -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() {
+10 -6
View File
@@ -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"
+29 -4
View File
@@ -1,4 +1,7 @@
{ {
"general": {
"and": " and "
},
"app": { "app": {
"sceneries": "SCENERIES", "sceneries": "SCENERIES",
"trains": "TRAINS", "trains": "TRAINS",
@@ -8,15 +11,18 @@
"error": "An error occured while loading data!", "error": "An error occured while loading data!",
"no-result": "No results for current search!", "no-result": "No results for current search!",
"migration-warning": "Stacjownik services will be unavailable 2/06/2022 between 1-3am (CEST time) due to the migration of API hostings!", "migration-warning": "Stacjownik services will be unavailable 2/06/2022 between 1-3am (CEST time) due to the migration of API hostings!",
"migration-confirm": "Roger that!" "migration-confirm": "Roger that!",
"offline": "App is in the offline mode!"
}, },
"update": { "update": {
"title": "New Stacjownik version is available!", "title": "New version of the app is available!",
"paragraph1": "Enjoy the application and may the green signal be with you!", "paragraph1": "Enjoy the application and may the green signal be with you!",
"release-link": "Click here to browse version changelog (GitHub)", "release-link": "Click here to browse version changelog (GitHub)",
"confirm-button": "Understood!" "confirm-button": "UPDATE NOW",
"later-button": "LATER"
}, },
"data-status": { "data-status": {
"S1-offline": "<b>S1 signal</b> <br> The app is working in offline mode!",
"S1a-connection": "<b>S1a signal</b> <br> Cannot connect with Stacjownik API service!", "S1a-connection": "<b>S1a signal</b> <br> Cannot connect with Stacjownik API service!",
"S1a-sceneries": "<b>S1a signal</b> <br> Cannot load online stations data!", "S1a-sceneries": "<b>S1a signal</b> <br> Cannot load online stations data!",
"S2": "<b>S2 signal</b> <br> All data loaded successfully!", "S2": "<b>S2 signal</b> <br> All data loaded successfully!",
@@ -253,13 +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-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",
+28 -3
View File
@@ -1,4 +1,7 @@
{ {
"general": {
"and": " oraz "
},
"app": { "app": {
"sceneries": "SCENERIE", "sceneries": "SCENERIE",
"trains": "POCIĄGI", "trains": "POCIĄGI",
@@ -8,17 +11,20 @@
"error": "Wystąpił problem z załadowaniem danych!", "error": "Wystąpił problem z załadowaniem danych!",
"no-result": "Brak wyników o podanych kryteriach!", "no-result": "Brak wyników o podanych kryteriach!",
"migration-warning": "Usługi Stacjownika będą niedostępne w godzinach 1:00-3:00 2 czerwca 2022r. z powodu migracji hostingów API!", "migration-warning": "Usługi Stacjownika będą niedostępne w godzinach 1:00-3:00 2 czerwca 2022r. z powodu migracji hostingów API!",
"migration-confirm": "Przyjąłem!" "migration-confirm": "Przyjąłem!",
"offline": "Aplikacja w trybie offline!"
}, },
"update": { "update": {
"title": "Nowa wersja Stacjownika jest dostępna!", "title": "Nowa wersja Stacjownika jest dostępna!",
"paragraph1": "Miłego korzystania z aplikacji i niech S2 będzie z wami!", "paragraph1": "Miłego korzystania z aplikacji i niech S2 będzie z wami!",
"release-link": "Kliknij, aby przejrzeć listę zmian (GitHub)", "release-link": "Kliknij, aby przejrzeć listę zmian (GitHub)",
"confirm-button": "Przyjąłem!" "confirm-button": "ZAKTUALIZUJ",
"later-button": "PÓŹNIEJ"
}, },
"data-status": { "data-status": {
"S1-offline": "<b>Sygnał S1</b> <br> Aplikacja działa w trybie offline!",
"S1a-connection": "<b>Sygnał S1a</b> <br> Błąd podczas próby połączenia się z API Stacjownika!", "S1a-connection": "<b>Sygnał S1a</b> <br> Błąd podczas próby połączenia się z API Stacjownika!",
"S1a-sceneries": "<b>Sygnał S1a</b> <br> Błąd podczas pobierania danych o sceneriach online!", "S1a-sceneries": "<b>Sygnał S1a</b> <br> Błąd podczas pobierania danych o sceneriach online!",
"S2": "<b>Sygnał S2</b> <br> Pomyślnie załadowano dane!", "S2": "<b>Sygnał S2</b> <br> Pomyślnie załadowano dane!",
@@ -259,11 +265,30 @@
"stats-title": "STATYSTYKI MASZYNISTY", "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",
+1
View File
@@ -10,6 +10,7 @@ import { createPinia } from 'pinia';
const i18n = createI18n({ const i18n = createI18n({
locale: 'pl', locale: 'pl',
legacy: false,
fallbackLocale: 'pl', fallbackLocale: 'pl',
messages: { messages: {
en: enLang, en: enLang,
+1 -1
View File
@@ -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(),
}; };
+1 -1
View File
@@ -5,7 +5,7 @@ export default defineComponent({
calculateExpStyle(exp: number, isSupporter = false): string { calculateExpStyle(exp: number, isSupporter = false): string {
const bgColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 85%, 50%)`) : '#666'; const bgColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 85%, 50%)`) : '#666';
const fontColor = exp > 15 || exp == -1 ? 'white' : 'black'; const fontColor = exp > 14 || exp == -1 ? 'white' : 'black';
const boxShadow = isSupporter ? `box-shadow: 0 0 10px 2px ${bgColor};` : ''; const boxShadow = isSupporter ? `box-shadow: 0 0 10px 2px ${bgColor};` : '';
return `background-color: ${bgColor}; color: ${fontColor}; ${boxShadow}`; return `background-color: ${bgColor}; color: ${fontColor}; ${boxShadow}`;
+13
View File
@@ -0,0 +1,13 @@
import { useRegisterSW } from 'virtual:pwa-register/vue';
export default () => {
const { needRefresh, updateServiceWorker, offlineReady } = useRegisterSW({
immediate: true,
});
return {
needRefresh,
updateServiceWorker,
offlineReady,
};
};
+19 -28
View File
@@ -1,6 +1,6 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import JournalDispatchersVue from '../components/JournalView/JournalDispatchers.vue'; import JournalDispatchersVue from '../views/JournalDispatchers.vue';
import JournalTimetablesVue from '../components/JournalView/JournalTimetables.vue'; import JournalTimetablesVue from '../views/JournalTimetables.vue';
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ {
@@ -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(.*)',
+1
View File
@@ -22,6 +22,7 @@ export default interface Train {
cars: string[]; cars: string[];
isTimeout: boolean; isTimeout: boolean;
isSupporter: boolean;
timetableData?: { timetableData?: {
timetableId: number; timetableId: number;
@@ -4,6 +4,8 @@ export interface DispatcherHistory {
currentDuration: number; currentDuration: number;
dispatcherId: number; dispatcherId: number;
dispatcherName: string; dispatcherName: string;
dispatcherLevel: number | null;
dispatcherIsSupporter: boolean;
isOnline: boolean; isOnline: boolean;
lastOnlineTimestamp: number; lastOnlineTimestamp: number;
region: string; region: string;
@@ -0,0 +1,30 @@
import { TimetableHistory } from './TimetablesAPIData';
export interface ITimetablesDailyStats {
totalTimetables: number;
distanceSum: number;
distanceAvg: number;
timetableId: number;
timetableAuthor: string;
timetableDriver: string;
timetableRouteDistance: number;
mostActiveDispatchers: {
name: string;
count: number;
}[];
}
export interface ITimetablesDailyStatsResponse {
totalTimetables: number;
distanceSum: number;
distanceAvg: number;
maxTimetable: TimetableHistory | null;
mostActiveDispatchers: {
name: string;
count: number;
}[];
}
@@ -1,4 +1,6 @@
export interface TimetableHistory { export interface TimetableHistory {
id: number;
timetableId: number; timetableId: number;
trainNo: number; trainNo: number;
trainCategoryCode: string; trainCategoryCode: string;
+1 -1
View File
@@ -24,7 +24,7 @@ function filterTrainList(trainList: Train[], searchedTrain: string, searchedDriv
(train) => { (train) => {
const isFiltered = filters.every(f => { const isFiltered = filters.every(f => {
if (f.isActive) return true; if (f.isActive) return true;
if (!train.timetableData) return filters.find(filter => filter.id == TrainFilterType.noTimetable)!.isActive; if (!train.timetableData) return filters.find(filter => filter.id == TrainFilterType.noTimetable)!.isActive;
switch (f.id) { switch (f.id) {
+1 -5
View File
@@ -1,9 +1,5 @@
export const URLs = { export const URLs = {
stacjownikAPI: stacjownikAPI:
import.meta.env.VITE_APP_API_DEV == 1 && !import.meta.env.PROD import.meta.env.VITE_APP_API_DEV == 1 && !import.meta.env.PROD ? 'http://localhost:3000' : 'https://spythere.pl',
? 'http://localhost:3000'
: 'https://stacjownik.eu-4.evennode.com',
stacjownikAPIDev: 'localhost:3000', stacjownikAPIDev: 'localhost:3000',
// trains: "https://api.td2.info.pl:9640/?method=getTrainsOnline",
// getTimetableURL: (trainNo: string | number, region = "eu") => `https://api.td2.info.pl:9640/?method=readFromSWDR&value=getTimetable%3B${trainNo}%3B${region}`
}; };
+5 -2
View File
@@ -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
View File
@@ -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();
}); });
+5
View File
@@ -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 {
+7
View File
@@ -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 {
+10 -2
View File
@@ -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;
} }
} }
+14 -7
View File
@@ -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
View File
@@ -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 {
+5 -3
View File
@@ -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 {
@@ -11,6 +13,6 @@ export interface JournalTimetableFilter {
} }
export interface JournalTimetableSorter { export interface JournalTimetableSorter {
id: 'timetableId' | 'beginDate' | 'distance' | 'total-stops'; id: 'beginDate' | 'distance' | 'total-stops';
dir: -1 | 1; dir: -1 | 1;
} }
@@ -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>
@@ -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="[ '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: 'beginDate', 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] != 'beginDate');
},
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 = 'beginDate';
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>
-77
View File
@@ -1,77 +0,0 @@
<template>
<section class="journal-view">
<div class="journal-type-options">
<router-link class="router-link" active-class="route-active" to="/journal/timetables" exact>
{{ $t('journal.section-timetables') }}
</router-link>
&nbsp;&bull;&nbsp;
<router-link class="router-link" active-class="route-active" to="/journal/dispatchers">
{{ $t('journal.section-dispatchers') }}
</router-link>
</div>
<div class="journal-section">
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" :key="$route.path" />
</keep-alive>
</router-view>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import JournalDispatchers from '../components/JournalView/JournalDispatchers.vue';
import JournalTimetables from '../components/JournalView/JournalTimetables.vue';
export default defineComponent({
components: { JournalDispatchers, JournalTimetables },
setup() {
const journalTypeChosen = ref('journalTimetables');
return {
activeJournalComponent: journalTypeChosen,
};
},
methods: {
changeJournalType(type: string) {
this.activeJournalComponent = type;
},
},
activated() {
const query = this.$route.query;
if (query.view == 'dispatchers') this.activeJournalComponent = 'journalDispatchers';
},
});
</script>
<style lang="scss" scoped>
.journal-type-options {
display: flex;
justify-content: center;
background-color: #2c2c2c;
max-width: 18em;
font-size: 1.2em;
margin: 0 auto;
border-radius: 0 0 0.5em 0.5em;
padding: 0.1em 0;
}
.journal-section > section {
height: 100%;
display: flex;
justify-content: center;
}
.router-link.active {
color: gold;
}
</style>
+17 -10
View File
@@ -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
View File
@@ -14,7 +14,7 @@
"ESNext", "ESNext",
"DOM" "DOM"
], ],
"types": ["vite/client"], "types": ["vite/client", "vite-plugin-pwa/client"],
"skipLibCheck": true "skipLibCheck": true
}, },
"include": [ "include": [
+47 -27
View File
@@ -1,34 +1,54 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({ export default defineConfig({
plugins: [vue()], server: {
port: 5001,
},
plugins: [
vue(),
VitePWA({
registerType: 'prompt',
workbox: {
globPatterns: ['**/*.{js,css,html,png,svg,jpg}'],
runtimeCaching: [
{
urlPattern: new RegExp('^https://spythere.pl/api/getSceneries', 'i'),
handler: 'NetworkFirst',
options: {
cacheName: 'sceneries-cache',
expiration: {
maxEntries: 1,
maxAgeSeconds: 60 * 60 * 24 * 7, // <== 7 days
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
urlPattern: /^https:\/\/rj.td2.info.pl\/dist\/img\/thumbnails\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'images-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24 * 60,
},
cacheableResponse: {
statuses: [0, 200, 404],
},
},
},
],
},
devOptions: {
enabled: true,
},
}),
],
}); });
// PWA
// VitePWA({
// registerType: 'autoUpdate',
// workbox: {
// globPatterns: ['**/*.{js,css,html,png,svg,img}'],
// runtimeCaching: [
// {
// urlPattern: new RegExp('^https://stacjownik.eu-4.evennode.com/api/getSceneries'),
// handler: 'NetworkFirst',
// options: {
// cacheName: 'sceneries-cache',
// expiration: {
// maxEntries: 200,
// maxAgeSeconds: 60 * 60 * 24 * 60, // <== 60 days
// },
// cacheableResponse: {
// statuses: [0, 200],
// },
// },
// },
// ],
// },
// devOptions: {
// enabled: true,
// },
// }),
+2686 -423
View File
File diff suppressed because it is too large Load Diff