mirror of
https://github.com/Spythere/stacjownik.git
synced 2026-05-03 13:28:11 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 87631d1f74 | |||
| 86bb9fcc2e | |||
| b85e3bfe1d | |||
| dd15072813 | |||
| 2f8376c996 | |||
| 514723cf74 | |||
| 0995ce15bc | |||
| 3b3c3bda31 | |||
| 2027b85450 | |||
| 0c6b55146f | |||
| 3c728e3cfa | |||
| adce339392 | |||
| 00a4a840b0 | |||
| 1e705ea496 | |||
| e8ed36df16 | |||
| f4be32aa39 | |||
| e0d3d2585d |
@@ -50,11 +50,6 @@
|
|||||||
name="twitter:image"
|
name="twitter:image"
|
||||||
content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg"
|
content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Quicksand:wght@500;700&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "stacjownik",
|
"name": "stacjownik",
|
||||||
"version": "1.19.4",
|
"version": "1.20.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+35
-35
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<main class="app_main">
|
<main class="app_main">
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<keep-alive exclude="JournalView,SceneryView">
|
<keep-alive exclude="SceneryView">
|
||||||
<component :is="Component" :key="$route.name" />
|
<component :is="Component" :key="$route.name" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</router-view>
|
</router-view>
|
||||||
@@ -37,7 +37,6 @@ import { defineComponent, watch } from 'vue';
|
|||||||
import Clock from './components/App/Clock.vue';
|
import Clock from './components/App/Clock.vue';
|
||||||
|
|
||||||
import packageInfo from '.././package.json';
|
import packageInfo from '.././package.json';
|
||||||
import { regions } from './data/options.json';
|
|
||||||
|
|
||||||
import { useMainStore } from './store/mainStore';
|
import { useMainStore } from './store/mainStore';
|
||||||
import StatusIndicator from './components/App/StatusIndicator.vue';
|
import StatusIndicator from './components/App/StatusIndicator.vue';
|
||||||
@@ -46,6 +45,7 @@ import AppHeader from './components/App/AppHeader.vue';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import StorageManager from './managers/storageManager';
|
import StorageManager from './managers/storageManager';
|
||||||
import { useApiStore } from './store/apiStore';
|
import { useApiStore } from './store/apiStore';
|
||||||
|
import { Status } from './typings/common';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@@ -66,26 +66,10 @@ export default defineComponent({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.loadLang();
|
this.init();
|
||||||
this.apiStore.setupAPI();
|
|
||||||
|
|
||||||
this.store.isOffline = !window.navigator.onLine;
|
|
||||||
|
|
||||||
window.addEventListener('offline', () => {
|
|
||||||
this.store.isOffline = true;
|
|
||||||
this.apiStore.activeData = undefined;
|
|
||||||
|
|
||||||
this.apiStore.setDataStatuses();
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('online', () => {
|
|
||||||
this.store.isOffline = false;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.setReleaseURL();
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => this.store.blockScroll,
|
() => this.store.blockScroll,
|
||||||
(value) => {
|
(value) => {
|
||||||
@@ -95,23 +79,39 @@ export default defineComponent({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
|
||||||
'$route.query.region': {
|
|
||||||
immediate: true,
|
|
||||||
handler(regionQuery: string) {
|
|
||||||
if (regionQuery) {
|
|
||||||
this.store.region.id =
|
|
||||||
regions.find(
|
|
||||||
(reg) =>
|
|
||||||
reg.id == regionQuery.toLocaleLowerCase() ||
|
|
||||||
reg.value.toLocaleLowerCase() == regionQuery.toLocaleLowerCase()
|
|
||||||
)?.id || 'eu';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
init() {
|
||||||
|
this.loadLang();
|
||||||
|
this.setReleaseURL();
|
||||||
|
this.setupOfflineHandling();
|
||||||
|
|
||||||
|
this.apiStore.setupAPI();
|
||||||
|
},
|
||||||
|
|
||||||
|
setupOfflineHandling() {
|
||||||
|
this.store.isOffline = !window.navigator.onLine;
|
||||||
|
|
||||||
|
if (this.store.isOffline) this.handleOfflineMode();
|
||||||
|
|
||||||
|
window.addEventListener('offline', this.handleOfflineMode);
|
||||||
|
window.addEventListener('online', this.handleOnlineMode);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleOfflineMode() {
|
||||||
|
this.store.isOffline = true;
|
||||||
|
|
||||||
|
this.apiStore.stopActiveDataScheduler();
|
||||||
|
this.apiStore.activeData = undefined;
|
||||||
|
|
||||||
|
this.apiStore.dataStatuses.connection = Status.Data.Offline;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleOnlineMode() {
|
||||||
|
this.store.isOffline = false;
|
||||||
|
|
||||||
|
this.apiStore.setupAPI();
|
||||||
|
},
|
||||||
|
|
||||||
changeLang(lang: string) {
|
changeLang(lang: string) {
|
||||||
this.$i18n.locale = lang;
|
this.$i18n.locale = lang;
|
||||||
this.currentLang = lang;
|
this.currentLang = lang;
|
||||||
|
|||||||
@@ -240,9 +240,9 @@ export default defineComponent({
|
|||||||
const trainsDataStatus = statuses.trains;
|
const trainsDataStatus = statuses.trains;
|
||||||
const dispatcherDataStatus = statuses.dispatchers;
|
const dispatcherDataStatus = statuses.dispatchers;
|
||||||
|
|
||||||
if (this.store.isOffline) {
|
if (connectionStatus == Status.Data.Offline) {
|
||||||
this.setSignalStatus(Status.Data.Initialized);
|
this.setSignalStatus(Status.Data.Offline);
|
||||||
this.indicator.status = Status.Data.Initialized;
|
this.indicator.status = Status.Data.Offline;
|
||||||
this.indicator.message = 'data-status.S1-offline';
|
this.indicator.message = 'data-status.S1-offline';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -293,7 +293,7 @@ export default defineComponent({
|
|||||||
this.orangeLight = false;
|
this.orangeLight = false;
|
||||||
this.redBottomLight = false;
|
this.redBottomLight = false;
|
||||||
|
|
||||||
if (status == Status.Data.Initialized) {
|
if (status == Status.Data.Initialized || status == Status.Data.Offline) {
|
||||||
this.redTopLight = true;
|
this.redTopLight = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,21 @@ export default defineComponent({
|
|||||||
'store.region.id': {
|
'store.region.id': {
|
||||||
handler(regionId) {
|
handler(regionId) {
|
||||||
this.selectedItemIndex = this.regionList.findIndex((reg) => reg.id == regionId);
|
this.selectedItemIndex = this.regionList.findIndex((reg) => reg.id == regionId);
|
||||||
|
|
||||||
|
console.log('region id', regionId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'$route.query.region': {
|
||||||
|
immediate: true,
|
||||||
|
handler(regionQuery: string) {
|
||||||
|
if (regionQuery) {
|
||||||
|
this.store.region.id =
|
||||||
|
regionsJSON.find(
|
||||||
|
(reg) =>
|
||||||
|
reg.id == regionQuery.toLocaleLowerCase() ||
|
||||||
|
reg.value.toLocaleLowerCase() == regionQuery.toLocaleLowerCase()
|
||||||
|
)?.id || 'eu';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,160 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="stats_container" v-click-outside="() => (cardVisible = false)">
|
|
||||||
<button class="stats_button" @click="toggleCard">
|
|
||||||
Statystyki dyżurnego {{ store.dispatcherStatsName }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="stats_card" v-if="store.dispatcherStatsName && cardVisible">
|
|
||||||
<div>
|
|
||||||
<Loading v-if="!store.dispatcherStatsData" />
|
|
||||||
|
|
||||||
<div class="loading" v-else-if="!store.dispatcherStatsData._count._all">
|
|
||||||
Ten dyżurny nie ma jeszcze szczegółowych statystyk!
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else>
|
|
||||||
<h3>STATYSTYKI WYSTAWIONYCH ROZKŁADÓW</h3>
|
|
||||||
|
|
||||||
<div class="info-stats" v-if="store.dispatcherStatsData._count._all">
|
|
||||||
<span class="stat-badge">
|
|
||||||
<span>LICZBA</span>
|
|
||||||
<span>{{ store.dispatcherStatsData._count._all }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="stat-badge">
|
|
||||||
<span>SUMA (KM)</span>
|
|
||||||
<span>{{ store.dispatcherStatsData._sum.routeDistance.toFixed(2) }}km</span>
|
|
||||||
</span>
|
|
||||||
<span class="stat-badge">
|
|
||||||
<span>NAJDŁUŻSZY</span>
|
|
||||||
<span>{{ store.dispatcherStatsData._max.routeDistance.toFixed(2) }}km</span>
|
|
||||||
</span>
|
|
||||||
<span class="stat-badge">
|
|
||||||
<span>ŚREDNIO</span>
|
|
||||||
<span>{{ store.dispatcherStatsData._avg.routeDistance.toFixed(2) }}km</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3>OSTATNIE WYSTAWIONE ROZKŁADY</h3>
|
|
||||||
<div class="last-timetables">
|
|
||||||
<div class="timetable-row" v-for="timetable in timetables" :key="timetable.id">
|
|
||||||
#{{ timetable.timetableId }} |
|
|
||||||
<b>{{ timetable.trainCategoryCode }} {{ timetable.trainNo }}</b> |
|
|
||||||
{{ timetable.driverName }} ({{ timetable.routeDistance }}km)
|
|
||||||
<div>{{ timetable.route.replace('|', ' > ') }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import { useMainStore } from '../../store/mainStore';
|
|
||||||
import Loading from '../Global/Loading.vue';
|
|
||||||
import { API } from '../../typings/api';
|
|
||||||
import http from '../../http';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: { Loading },
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
const store = useMainStore();
|
|
||||||
|
|
||||||
return {
|
|
||||||
store
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
cardVisible: false,
|
|
||||||
lastDispatcherName: '',
|
|
||||||
timetables: [] as API.TimetableHistory.Response
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
toggleCard() {
|
|
||||||
if (!this.store.dispatcherStatsName) return;
|
|
||||||
|
|
||||||
this.cardVisible = !this.cardVisible;
|
|
||||||
if (this.cardVisible) this.fetchDispatcherStats();
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchDispatcherStats() {
|
|
||||||
if (this.lastDispatcherName != this.store.dispatcherStatsName) {
|
|
||||||
this.store.dispatcherStatsData = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const statsData: API.DispatcherStats.Response = await (
|
|
||||||
await http.get('api/getDispatcherInfo?name=${this.store.dispatcherStatsName}')
|
|
||||||
).data;
|
|
||||||
|
|
||||||
const timetables: API.TimetableHistory.Response = await (
|
|
||||||
await http.get('api/getTimetables?authorName=${this.store.dispatcherStatsName}')
|
|
||||||
).data;
|
|
||||||
|
|
||||||
this.timetables = timetables;
|
|
||||||
this.store.dispatcherStatsData = statsData;
|
|
||||||
this.lastDispatcherName = this.store.dispatcherStatsName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '../../styles/responsive.scss';
|
|
||||||
@import '../../styles/variables.scss';
|
|
||||||
|
|
||||||
.stats_container {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats_card {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 999;
|
|
||||||
top: 120%;
|
|
||||||
right: 0;
|
|
||||||
width: 500px;
|
|
||||||
max-width: 97vw;
|
|
||||||
min-height: 100px;
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
border-radius: 1em 0 1em 1em;
|
|
||||||
background-color: #222222f1;
|
|
||||||
box-shadow: 0 3px 10px 5px #131313;
|
|
||||||
padding: 1em 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.last-timetables {
|
|
||||||
max-height: 400px;
|
|
||||||
margin: 0.5em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timetable-row {
|
|
||||||
width: 95%;
|
|
||||||
margin: 0.5em auto;
|
|
||||||
padding: 0.5em;
|
|
||||||
|
|
||||||
background-color: #4d4d4d;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2.card-title {
|
|
||||||
font-size: 1.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2,
|
|
||||||
h3 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.last-timetables {
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,128 +1,165 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="daily-stats">
|
<section class="daily-stats">
|
||||||
<span :data-active="statsStatus">
|
<span :data-active="statsStatus">
|
||||||
<b v-if="statsStatus == Status.Data.Loading">
|
<span class="stats-list">
|
||||||
{{ $t('app.loading') }}
|
|
||||||
</b>
|
|
||||||
|
|
||||||
<span class="stats-list" v-else>
|
|
||||||
<h3>
|
<h3>
|
||||||
{{ $t('journal.daily-stats-title') }}
|
{{ $t('journal.daily-stats.title') }}
|
||||||
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
|
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<hr style="margin-bottom: 0.5em" />
|
<hr class="header-separator" />
|
||||||
|
|
||||||
<div v-if="stats.totalTimetables">
|
<b v-if="statsStatus == Status.Data.Loading">
|
||||||
•
|
{{ $t('app.loading') }}
|
||||||
<i18n-t keypath="journal.timetable-stats-total">
|
</b>
|
||||||
<template #count>
|
|
||||||
<b class="text--primary">
|
|
||||||
{{ stats.totalTimetables }}
|
|
||||||
{{ $t('journal.timetable-count', stats.totalTimetables) }}
|
|
||||||
</b>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #distance>
|
<b class="text--error" v-else-if="statsStatus == Status.Data.Error">
|
||||||
<b class="text--primary"> {{ stats.distanceSum?.toFixed(2) }} km</b>
|
{{ $t('journal.stats-error') }}
|
||||||
</template>
|
</b>
|
||||||
</i18n-t>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="stats.maxTimetable">
|
<b v-else-if="topDispatchers.length == 0">
|
||||||
•
|
{{ $t('journal.daily-stats.info') }}
|
||||||
<i18n-t keypath="journal.timetable-stats-longest">
|
</b>
|
||||||
<template #id>
|
|
||||||
<router-link :to="`/journal/timetables?timetableId=${stats.maxTimetable.id}`">
|
|
||||||
<b>{{ stats.maxTimetable.id }}</b>
|
|
||||||
</router-link>
|
|
||||||
</template>
|
|
||||||
<template #author>
|
|
||||||
<router-link
|
|
||||||
:to="`/journal/dispatchers?dispatcherName=${stats.maxTimetable.authorName}`"
|
|
||||||
>
|
|
||||||
<b>{{ stats.maxTimetable.authorName }}</b>
|
|
||||||
</router-link>
|
|
||||||
</template>
|
|
||||||
<template #driver>
|
|
||||||
<b class="text--primary">{{ stats.maxTimetable.driverName }}</b>
|
|
||||||
</template>
|
|
||||||
<template #distance>
|
|
||||||
<b class="text--primary">{{ stats.maxTimetable.routeDistance }} km</b>
|
|
||||||
</template>
|
|
||||||
</i18n-t>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="topDispatchers.length == 1">
|
<div v-else>
|
||||||
•
|
<div v-if="stats.totalTimetables">
|
||||||
<i18n-t keypath="journal.timetable-stats-most-active-dr">
|
•
|
||||||
<template #dispatcher>
|
<i18n-t keypath="journal.daily-stats.total">
|
||||||
<router-link :to="`/journal/dispatchers?dispatcherName=${topDispatchers[0].name}`">
|
<template #count>
|
||||||
<b>{{ topDispatchers[0].name }}</b>
|
<b class="text--primary">
|
||||||
</router-link>
|
{{ stats.totalTimetables }}
|
||||||
</template>
|
{{ $t('journal.daily-stats.count', stats.totalTimetables) }}
|
||||||
<template #count>
|
</b>
|
||||||
<b class="text--primary">
|
</template>
|
||||||
{{ topDispatchers[0].count }}
|
|
||||||
{{ $t('journal.timetable-count', topDispatchers[0].count) }}
|
|
||||||
</b>
|
|
||||||
</template>
|
|
||||||
</i18n-t>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="topDispatchers.length > 1">
|
<template #distance>
|
||||||
•
|
<b class="text--primary"> {{ stats.distanceSum?.toFixed(2) }} km</b>
|
||||||
<i18n-t keypath="journal.timetable-stats-most-active-dr-many">
|
</template>
|
||||||
<template #dispatchers>
|
</i18n-t>
|
||||||
<span v-for="(disp, i) in topDispatchers" :key="i">
|
</div>
|
||||||
<span v-if="i == topDispatchers.length - 1"> {{ $t('general.and') }} </span>
|
|
||||||
|
|
||||||
<router-link :to="`/journal/dispatchers?dispatcherName=${disp.name}`">
|
<div v-if="stats.maxTimetable">
|
||||||
<b>{{ disp.name }}</b>
|
•
|
||||||
|
<i18n-t keypath="journal.daily-stats.longest">
|
||||||
|
<template #id>
|
||||||
|
<router-link :to="`/journal/timetables?search-train=%23${stats.maxTimetable.id}`">
|
||||||
|
<b>{{ stats.maxTimetable.id }}</b>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
</template>
|
||||||
|
<template #author>
|
||||||
|
<router-link
|
||||||
|
:to="`/journal/timetables?search-dispatcher=${stats.maxTimetable.authorName}`"
|
||||||
|
>
|
||||||
|
<b>{{ stats.maxTimetable.authorName }}</b>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
<template #driver>
|
||||||
|
<b class="text--primary">{{ stats.maxTimetable.driverName }}</b>
|
||||||
|
</template>
|
||||||
|
<template #distance>
|
||||||
|
<b class="text--primary">{{ stats.maxTimetable.routeDistance }} km</b>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span v-if="i < topDispatchers.length - 2">, </span>
|
<div v-if="topDispatchers.length == 1">
|
||||||
</span>
|
•
|
||||||
</template>
|
<i18n-t keypath="journal.daily-stats.most-active-dr">
|
||||||
|
<template #dispatcher>
|
||||||
|
<router-link
|
||||||
|
:to="`/journal/dispatchers?search-dispatcher=${topDispatchers[0].name}`"
|
||||||
|
>
|
||||||
|
<b>{{ topDispatchers[0].name }}</b>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
<template #count>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ topDispatchers[0].count }}
|
||||||
|
{{ $t('journal.daily-stats.count', topDispatchers[0].count) }}
|
||||||
|
</b>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template #count>
|
<div v-if="topDispatchers.length > 1">
|
||||||
<b class="text--primary">
|
•
|
||||||
{{ topDispatchers[0].count }}
|
<i18n-t keypath="journal.daily-stats.most-active-dr-many">
|
||||||
{{ $t('journal.timetable-count', topDispatchers[0].count) }}
|
<template #dispatchers>
|
||||||
</b>
|
<span v-for="(disp, i) in topDispatchers" :key="i">
|
||||||
</template>
|
<span v-if="i == topDispatchers.length - 1"> {{ $t('general.and') }} </span>
|
||||||
</i18n-t>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="stats.longestDuties.length > 0">
|
<router-link :to="`/journal/dispatchers?search-dispatcher=${disp.name}`">
|
||||||
•
|
<b>{{ disp.name }}</b>
|
||||||
<i18n-t keypath="journal.timetable-stats-longest-duties">
|
</router-link>
|
||||||
<template #dispatcher>
|
|
||||||
<router-link
|
|
||||||
:to="`/journal/dispatchers?dispatcherName=${stats.longestDuties[0].name}`"
|
|
||||||
>
|
|
||||||
<b>{{ stats.longestDuties[0].name }}</b>
|
|
||||||
</router-link>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #station>{{ stats.longestDuties[0].station }}</template>
|
<span v-if="i < topDispatchers.length - 2">, </span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #duration>
|
<template #count>
|
||||||
{{ calculateDuration(stats.longestDuties[0].duration) }}
|
<b class="text--primary">
|
||||||
</template>
|
{{ topDispatchers[0].count }}
|
||||||
</i18n-t>
|
{{ $t('journal.daily-stats.count', topDispatchers[0].count) }}
|
||||||
</div>
|
</b>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="stats.mostActiveDrivers.length > 0">
|
<div v-if="stats.longestDuties.length > 0">
|
||||||
•
|
•
|
||||||
<i18n-t keypath="journal.timetable-stats-most-active-driver">
|
<i18n-t keypath="journal.daily-stats.longest-duties">
|
||||||
<template #driver>
|
<template #dispatcher>
|
||||||
<b class="text--primary">{{ stats.mostActiveDrivers[0].name }}</b>
|
<router-link
|
||||||
</template>
|
:to="`/journal/dispatchers?search-dispatcher=${stats.longestDuties[0].name}`"
|
||||||
<template #distance>
|
>
|
||||||
<b class="text--primary">{{ stats.mostActiveDrivers[0].distance.toFixed(2) }} km</b>
|
<b>{{ stats.longestDuties[0].name }}</b>
|
||||||
</template>
|
</router-link>
|
||||||
</i18n-t>
|
</template>
|
||||||
|
|
||||||
|
<template #station>{{ stats.longestDuties[0].station }}</template>
|
||||||
|
|
||||||
|
<template #duration>
|
||||||
|
{{ calculateDuration(stats.longestDuties[0].duration) }}
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="stats.mostActiveDrivers.length > 0">
|
||||||
|
•
|
||||||
|
<i18n-t keypath="journal.daily-stats.most-active-driver">
|
||||||
|
<template #driver>
|
||||||
|
<router-link
|
||||||
|
:to="`/journal/timetables?search-driver=${stats.mostActiveDrivers[0].name}`"
|
||||||
|
>
|
||||||
|
<b>{{ stats.mostActiveDrivers[0].name }}</b>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
<template #distance>
|
||||||
|
<b class="text--primary">{{ stats.mostActiveDrivers[0].distance.toFixed(2) }} km</b>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="section-separator" />
|
||||||
|
|
||||||
|
<div class="stats-badges">
|
||||||
|
<span
|
||||||
|
class="stat-badge"
|
||||||
|
v-for="key in [
|
||||||
|
'rippedSwitches',
|
||||||
|
'derailments',
|
||||||
|
'skippedStopSignals',
|
||||||
|
'radioStops',
|
||||||
|
'kills'
|
||||||
|
]"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
<span>{{ $t(`journal.daily-stats.${key}`) }}</span>
|
||||||
|
<span>{{
|
||||||
|
Object.entries(stats.globalDiff).find(([k, v]) => k == key)?.[1] || '--'
|
||||||
|
}}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -203,6 +240,8 @@ export default defineComponent({
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/responsive.scss';
|
@import '../../styles/responsive.scss';
|
||||||
|
@import '../../styles/JournalStats.scss';
|
||||||
|
@import '../../styles/badge.scss';
|
||||||
|
|
||||||
.daily-stats {
|
.daily-stats {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -215,6 +254,12 @@ export default defineComponent({
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stats-badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
@include smallScreen {
|
@include smallScreen {
|
||||||
h3 {
|
h3 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<div class="journal-stats dispatcher" v-if="dispatcherName && stats">
|
||||||
|
<span class="loading" v-if="!stats.issuedTimetables && !stats.services">
|
||||||
|
{{ $t('journal.dispatcher-stats.empty') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else>
|
||||||
|
<h3>
|
||||||
|
<i18n-t keypath="journal.dispatcher-stats.title">
|
||||||
|
<template #name>
|
||||||
|
<span class="text--primary">{{ dispatcherName.toUpperCase() }}</span>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<hr class="header-separator" />
|
||||||
|
|
||||||
|
<div class="info-stats">
|
||||||
|
<span class="stat-badge" v-if="stats.services">
|
||||||
|
<span>{{ $t('journal.dispatcher-stats.services-count') }}</span>
|
||||||
|
<span>{{ stats.services.count }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="stat-badge" v-if="stats.services">
|
||||||
|
<span>{{ $t('journal.dispatcher-stats.service-max') }}</span>
|
||||||
|
<span>{{ calculateDuration(stats.services.durationMax) }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="stat-badge" v-if="stats.services">
|
||||||
|
<span>{{ $t('journal.dispatcher-stats.service-avg') }}</span>
|
||||||
|
<span>{{ calculateDuration(stats.services.durationAvg) }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="section-separator" />
|
||||||
|
|
||||||
|
<div class="info-stats">
|
||||||
|
<span class="stat-badge" v-if="stats.issuedTimetables">
|
||||||
|
<span>{{ $t('journal.dispatcher-stats.timetables-count') }}</span>
|
||||||
|
<span>{{ stats.issuedTimetables.count }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="stat-badge" v-if="stats.issuedTimetables">
|
||||||
|
<span>{{ $t('journal.dispatcher-stats.timetables-sum') }}</span>
|
||||||
|
<span>{{ stats.issuedTimetables.distanceSum.toFixed(2) }}km</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="stat-badge" v-if="stats.issuedTimetables">
|
||||||
|
<span>{{ $t('journal.dispatcher-stats.timetables-max') }}</span>
|
||||||
|
<span>{{ stats.issuedTimetables.distanceMax.toFixed(2) }}km</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="stat-badge" v-if="stats.issuedTimetables">
|
||||||
|
<span>{{ $t('journal.dispatcher-stats.timetables-avg') }}</span>
|
||||||
|
<span>{{ stats.issuedTimetables.distanceAvg.toFixed(2) }}km</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import dateMixin from '../../../mixins/dateMixin';
|
||||||
|
import { useMainStore } from '../../../store/mainStore';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'journal-dispatcher-stats',
|
||||||
|
|
||||||
|
mixins: [dateMixin],
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
const store = useMainStore();
|
||||||
|
|
||||||
|
return {
|
||||||
|
stats: store.dispatcherStatsData,
|
||||||
|
dispatcherName: store.dispatcherStatsName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../../styles/JournalStats.scss';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="status-anim" mode="out-in">
|
||||||
|
<div :key="dataStatus">
|
||||||
|
<div class="journal_warning" v-if="store.isOffline">
|
||||||
|
{{ $t('app.offline') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Loading v-else-if="dataStatus == Status.Data.Loading" />
|
||||||
|
|
||||||
|
<div v-else-if="dataStatus == Status.Data.Error" class="journal_warning error">
|
||||||
|
{{ $t('app.error') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="journal_warning" v-else-if="dispatcherHistory.length == 0">
|
||||||
|
{{ $t('app.no-result') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<table class="dispatchers-table">
|
||||||
|
<thead>
|
||||||
|
<th>{{ $t('journal.history-name') }}</th>
|
||||||
|
<th>{{ $t('journal.history-hash') }}</th>
|
||||||
|
<th>{{ $t('journal.history-dispatcher') }}</th>
|
||||||
|
<th>{{ $t('journal.history-level') }}</th>
|
||||||
|
<th>{{ $t('journal.history-rate') }}</th>
|
||||||
|
<th>{{ $t('journal.history-region') }}</th>
|
||||||
|
<th>{{ $t('journal.history-date') }}</th>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<transition-group name="list-anim">
|
||||||
|
<tr v-for="historyItem in dispatcherHistory" :key="historyItem.id">
|
||||||
|
<td>
|
||||||
|
<router-link
|
||||||
|
:to="`/journal/dispatchers?search-station=${historyItem.stationName}`"
|
||||||
|
>
|
||||||
|
<b>{{ historyItem.stationName }}</b>
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td>#{{ historyItem.stationHash }}</td>
|
||||||
|
<td>
|
||||||
|
<router-link
|
||||||
|
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
|
||||||
|
>
|
||||||
|
<b
|
||||||
|
v-if="isDonator(historyItem.dispatcherName)"
|
||||||
|
class="text--donator"
|
||||||
|
:title="$t('donations.dispatcher-message')"
|
||||||
|
>
|
||||||
|
{{ historyItem.dispatcherName }}
|
||||||
|
</b>
|
||||||
|
|
||||||
|
<b v-else>
|
||||||
|
{{ historyItem.dispatcherName }}
|
||||||
|
</b>
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<b
|
||||||
|
v-if="historyItem.dispatcherLevel !== null"
|
||||||
|
class="level-badge dispatcher"
|
||||||
|
:style="
|
||||||
|
calculateExpStyle(
|
||||||
|
historyItem.dispatcherLevel,
|
||||||
|
historyItem.dispatcherIsSupporter
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
|
||||||
|
</b>
|
||||||
|
</td>
|
||||||
|
<td class="text--primary">
|
||||||
|
<b>{{ historyItem.dispatcherRate }}</b>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<b class="region-badge" :aria-describedby="historyItem.region">{{
|
||||||
|
regions.find((r) => r.id == historyItem.region)?.value || '???'
|
||||||
|
}}</b>
|
||||||
|
</td>
|
||||||
|
<td style="min-width: 200px" class="time">
|
||||||
|
<span v-if="historyItem.timestampTo" class="text--offline">
|
||||||
|
<b>{{ $d(historyItem.timestampFrom) }}</b>
|
||||||
|
{{ timestampToString(historyItem.timestampFrom) }}
|
||||||
|
- {{ timestampToString(historyItem.timestampTo) }} ({{
|
||||||
|
calculateDuration(historyItem.currentDuration)
|
||||||
|
}})
|
||||||
|
</span>
|
||||||
|
<span class="dispatcher-online" v-else>
|
||||||
|
<b class="text--online">
|
||||||
|
<router-link :to="`/scenery?station=${historyItem.stationName}`">{{
|
||||||
|
$t('journal.online-since')
|
||||||
|
}}</router-link>
|
||||||
|
{{ timestampToString(historyItem.timestampFrom) }}
|
||||||
|
</b>
|
||||||
|
({{ calculateDuration(historyItem.currentDuration) }})
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</transition-group>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<AddDataButton
|
||||||
|
:list="dispatcherHistory"
|
||||||
|
:scrollDataLoaded="scrollDataLoaded"
|
||||||
|
:scrollNoMoreData="scrollNoMoreData"
|
||||||
|
@addHistoryData="addHistoryData"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="journal_warning" v-if="scrollNoMoreData">
|
||||||
|
{{ $t('journal.no-further-data') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="journal_warning" v-else-if="!scrollDataLoaded">
|
||||||
|
{{ $t('journal.loading-further-data') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType } from 'vue';
|
||||||
|
import { regions } from '../../../data/options.json';
|
||||||
|
import { useMainStore } from '../../../store/mainStore';
|
||||||
|
import { API } from '../../../typings/api';
|
||||||
|
import { Status } from '../../../typings/common';
|
||||||
|
import Loading from '../../Global/Loading.vue';
|
||||||
|
import AddDataButton from '../../Global/AddDataButton.vue';
|
||||||
|
import dateMixin from '../../../mixins/dateMixin';
|
||||||
|
import donatorMixin from '../../../mixins/donatorMixin';
|
||||||
|
import styleMixin from '../../../mixins/styleMixin';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { Loading, AddDataButton },
|
||||||
|
|
||||||
|
mixins: [dateMixin, styleMixin, donatorMixin],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
dispatcherHistory: {
|
||||||
|
type: Array as PropType<API.DispatcherHistory.Response>,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
scrollNoMoreData: {
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
scrollDataLoaded: {
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
addHistoryData: {
|
||||||
|
type: Function as PropType<() => void>
|
||||||
|
},
|
||||||
|
dataStatus: {
|
||||||
|
type: Number as PropType<Status.Data>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
Status,
|
||||||
|
store: useMainStore(),
|
||||||
|
regions
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
computedDispatcherHistory() {
|
||||||
|
return this.dispatcherHistory.reduce(
|
||||||
|
(acc, historyItem, i) => {
|
||||||
|
if (this.isAnotherDay(i - 1, i))
|
||||||
|
acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL'));
|
||||||
|
acc.push(historyItem);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[] as (API.DispatcherHistory.Data | string)[]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
navigateToScenery(name: string, isOnline: boolean) {
|
||||||
|
if (!isOnline) return;
|
||||||
|
|
||||||
|
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
isAnotherDay(prevIndex: number, currIndex: number) {
|
||||||
|
if (currIndex == 0) return true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
new Date(this.dispatcherHistory[prevIndex].timestampFrom).getDate() !=
|
||||||
|
new Date(this.dispatcherHistory[currIndex].timestampFrom).getDate()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../../styles/animations.scss';
|
||||||
|
@import '../../../styles/responsive.scss';
|
||||||
|
@import '../../../styles/badge.scss';
|
||||||
|
@import '../../../styles/variables.scss';
|
||||||
|
@import '../../../styles/JournalSection.scss';
|
||||||
|
|
||||||
|
table.dispatchers-table {
|
||||||
|
--_bg-table: #111;
|
||||||
|
--_bg-head: #101010;
|
||||||
|
--_bg-row: #2f2f2f;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
margin-bottom: 1em;
|
||||||
|
|
||||||
|
thead {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background-color: var(--_bg-head);
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
background-color: var(--_bg-row);
|
||||||
|
border-bottom: 2px solid black;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 0.75em;
|
||||||
|
|
||||||
|
.level-badge {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
&--online {
|
||||||
|
color: springgreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--offline {
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<transition name="status-anim" mode="out-in">
|
|
||||||
<div :key="dataStatus">
|
|
||||||
<div class="journal_warning" v-if="store.isOffline">
|
|
||||||
{{ $t('app.offline') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Loading v-else-if="dataStatus == Status.Data.Loading" />
|
|
||||||
|
|
||||||
<div v-else-if="dataStatus == Status.Data.Error" class="journal_warning error">
|
|
||||||
{{ $t('app.error') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="journal_warning" v-else-if="dispatcherHistory.length == 0">
|
|
||||||
{{ $t('app.no-result') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else>
|
|
||||||
<table class="scenery-history-table">
|
|
||||||
<thead>
|
|
||||||
<th>{{ $t('journal.history-name') }}</th>
|
|
||||||
<th>{{ $t('journal.history-hash') }}</th>
|
|
||||||
<th>{{ $t('journal.history-dispatcher') }}</th>
|
|
||||||
<th>{{ $t('journal.history-level') }}</th>
|
|
||||||
<th>{{ $t('journal.history-rate') }}</th>
|
|
||||||
<th>{{ $t('journal.history-region') }}</th>
|
|
||||||
<th>{{ $t('journal.history-date') }}</th>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<transition-group name="list-anim">
|
|
||||||
<tr v-for="historyItem in dispatcherHistory" :key="historyItem.id">
|
|
||||||
<td>
|
|
||||||
<router-link
|
|
||||||
:to="`/journal/dispatchers?sceneryName=${historyItem.stationName}`"
|
|
||||||
>
|
|
||||||
<b>{{ historyItem.stationName }}</b>
|
|
||||||
</router-link>
|
|
||||||
</td>
|
|
||||||
<td>#{{ historyItem.stationHash }}</td>
|
|
||||||
<td>
|
|
||||||
<router-link
|
|
||||||
:to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`"
|
|
||||||
>
|
|
||||||
<b
|
|
||||||
v-if="isDonator(historyItem.dispatcherName)"
|
|
||||||
class="text--donator"
|
|
||||||
:title="$t('donations.dispatcher-message')"
|
|
||||||
>
|
|
||||||
{{ historyItem.dispatcherName }}
|
|
||||||
</b>
|
|
||||||
|
|
||||||
<b v-else>
|
|
||||||
{{ historyItem.dispatcherName }}
|
|
||||||
</b>
|
|
||||||
</router-link>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<b
|
|
||||||
v-if="historyItem.dispatcherLevel !== null"
|
|
||||||
class="level-badge dispatcher"
|
|
||||||
:style="
|
|
||||||
calculateExpStyle(
|
|
||||||
historyItem.dispatcherLevel,
|
|
||||||
historyItem.dispatcherIsSupporter
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
|
|
||||||
</b>
|
|
||||||
</td>
|
|
||||||
<td class="text--primary">
|
|
||||||
<b>{{ historyItem.dispatcherRate }}</b>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<b class="region-badge" :aria-describedby="historyItem.region">{{
|
|
||||||
regions.find((r) => r.id == historyItem.region)?.value || '???'
|
|
||||||
}}</b>
|
|
||||||
</td>
|
|
||||||
<td style="min-width: 200px" class="time">
|
|
||||||
<span v-if="historyItem.timestampTo" class="text--offline">
|
|
||||||
<b>{{ $d(historyItem.timestampFrom) }}</b>
|
|
||||||
{{ timestampToString(historyItem.timestampFrom) }}
|
|
||||||
- {{ timestampToString(historyItem.timestampTo) }} ({{
|
|
||||||
calculateDuration(historyItem.currentDuration)
|
|
||||||
}})
|
|
||||||
</span>
|
|
||||||
<span class="dispatcher-online" v-else>
|
|
||||||
<b class="text--online">
|
|
||||||
<router-link :to="`/scenery?station=${historyItem.stationName}`">{{
|
|
||||||
$t('journal.online-since')
|
|
||||||
}}</router-link>
|
|
||||||
{{ timestampToString(historyItem.timestampFrom) }}
|
|
||||||
</b>
|
|
||||||
({{ calculateDuration(historyItem.currentDuration) }})
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</transition-group>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<AddDataButton
|
|
||||||
:list="dispatcherHistory"
|
|
||||||
:scrollDataLoaded="scrollDataLoaded"
|
|
||||||
:scrollNoMoreData="scrollNoMoreData"
|
|
||||||
@addHistoryData="addHistoryData"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
|
|
||||||
<div class="journal_warning" v-if="scrollNoMoreData">
|
|
||||||
{{ $t('journal.no-further-data') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="journal_warning" v-else-if="!scrollDataLoaded">
|
|
||||||
{{ $t('journal.loading-further-data') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, PropType } from 'vue';
|
|
||||||
import dateMixin from '../../mixins/dateMixin';
|
|
||||||
import styleMixin from '../../mixins/styleMixin';
|
|
||||||
import { useMainStore } from '../../store/mainStore';
|
|
||||||
import Loading from '../Global/Loading.vue';
|
|
||||||
import { regions } from '../../data/options.json';
|
|
||||||
import AddDataButton from '../Global/AddDataButton.vue';
|
|
||||||
import { API } from '../../typings/api';
|
|
||||||
import { Status } from '../../typings/common';
|
|
||||||
import donatorMixin from '../../mixins/donatorMixin';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: { Loading, AddDataButton },
|
|
||||||
|
|
||||||
mixins: [dateMixin, styleMixin, donatorMixin],
|
|
||||||
|
|
||||||
props: {
|
|
||||||
dispatcherHistory: {
|
|
||||||
type: Array as PropType<API.DispatcherHistory.Response>,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
scrollNoMoreData: {
|
|
||||||
type: Boolean
|
|
||||||
},
|
|
||||||
scrollDataLoaded: {
|
|
||||||
type: Boolean
|
|
||||||
},
|
|
||||||
addHistoryData: {
|
|
||||||
type: Function as PropType<() => void>
|
|
||||||
},
|
|
||||||
dataStatus: {
|
|
||||||
type: Number as PropType<Status.Data>
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
Status,
|
|
||||||
store: useMainStore(),
|
|
||||||
regions
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
computedDispatcherHistory() {
|
|
||||||
console.log(this.dispatcherHistory.length);
|
|
||||||
|
|
||||||
return this.dispatcherHistory.reduce(
|
|
||||||
(acc, historyItem, i) => {
|
|
||||||
if (this.isAnotherDay(i - 1, i))
|
|
||||||
acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL'));
|
|
||||||
acc.push(historyItem);
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
[] as (API.DispatcherHistory.Data | string)[]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
navigateToScenery(name: string, isOnline: boolean) {
|
|
||||||
if (!isOnline) return;
|
|
||||||
|
|
||||||
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
isAnotherDay(prevIndex: number, currIndex: number) {
|
|
||||||
if (currIndex == 0) return true;
|
|
||||||
|
|
||||||
return (
|
|
||||||
new Date(this.dispatcherHistory[prevIndex].timestampFrom).getDate() !=
|
|
||||||
new Date(this.dispatcherHistory[currIndex].timestampFrom).getDate()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '../../styles/animations.scss';
|
|
||||||
@import '../../styles/responsive.scss';
|
|
||||||
@import '../../styles/badge.scss';
|
|
||||||
@import '../../styles/variables.scss';
|
|
||||||
@import '../../styles/JournalSection.scss';
|
|
||||||
|
|
||||||
table.scenery-history-table {
|
|
||||||
--_bg-table: #111;
|
|
||||||
--_bg-head: #101010;
|
|
||||||
--_bg-row: #2f2f2f;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
position: relative;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
margin-bottom: 1em;
|
|
||||||
|
|
||||||
thead {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
background-color: var(--_bg-head);
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr {
|
|
||||||
background-color: var(--_bg-row);
|
|
||||||
border-bottom: 2px solid black;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding: 0.75em;
|
|
||||||
|
|
||||||
.level-badge {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
&--online {
|
|
||||||
color: springgreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--offline {
|
|
||||||
color: #ddd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<h1 class="option-title">{{ $t('options.search-title') }}</h1>
|
<h1 class="option-title">{{ $t('options.search-title') }}</h1>
|
||||||
<div class="search_content">
|
<div class="search_content">
|
||||||
<div class="search" v-for="(_, propName) in searchersValues" :key="propName">
|
<div class="search" v-for="(_, propName) in searchersValues" :key="propName">
|
||||||
<label v-if="propName == 'search-date'" for="date">{{
|
<label v-if="propName == 'search-date'" for="search-date">{{
|
||||||
$t(`options.search-${optionsType}-date`)
|
$t(`options.search-${optionsType}-date`)
|
||||||
}}</label>
|
}}</label>
|
||||||
|
|
||||||
@@ -41,12 +41,13 @@
|
|||||||
<input
|
<input
|
||||||
class="search-input"
|
class="search-input"
|
||||||
v-model="searchersValues[propName]"
|
v-model="searchersValues[propName]"
|
||||||
@keydown.enter="onSearchConfirm"
|
@keydown.enter="searchConfirm"
|
||||||
@focus="preventKeyDown = true"
|
@focus="preventKeyDown = true"
|
||||||
@blur="preventKeyDown = false"
|
@blur="preventKeyDown = false"
|
||||||
:placeholder="$t(`options.${propName}`)"
|
:placeholder="$t(`options.${propName}`)"
|
||||||
:type="propName == 'search-date' ? 'date' : 'text'"
|
:type="propName == 'search-date' ? 'date' : 'text'"
|
||||||
:min="propName == 'search-date' ? '2022-02-01' : undefined"
|
:min="propName == 'search-date' ? '2022-02-01' : undefined"
|
||||||
|
:id="`${propName}`"
|
||||||
:list="propName.toString()"
|
:list="propName.toString()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -114,7 +115,6 @@ import { defineComponent, inject, PropType } from 'vue';
|
|||||||
import keyMixin from '../../mixins/keyMixin';
|
import keyMixin from '../../mixins/keyMixin';
|
||||||
import { useMainStore } from '../../store/mainStore';
|
import { useMainStore } from '../../store/mainStore';
|
||||||
import { Journal } from './typings';
|
import { Journal } from './typings';
|
||||||
import { API } from '../../typings/api';
|
|
||||||
import { Status } from '../../typings/common';
|
import { Status } from '../../typings/common';
|
||||||
import http from '../../http';
|
import http from '../../http';
|
||||||
|
|
||||||
@@ -181,10 +181,6 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
async 'store.driverStatsName'() {
|
|
||||||
await this.fetchDriverStats();
|
|
||||||
},
|
|
||||||
|
|
||||||
async 'searchersValues.search-driver'(value: string | undefined) {
|
async 'searchersValues.search-driver'(value: string | undefined) {
|
||||||
clearTimeout(this.searchTimeout);
|
clearTimeout(this.searchTimeout);
|
||||||
|
|
||||||
@@ -203,27 +199,34 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async fetchDriverStats() {
|
// filters & sorters from URL params
|
||||||
this.store.driverStatsData = undefined;
|
handleRouteParams() {
|
||||||
|
this.$router.push({
|
||||||
|
query: {
|
||||||
|
...this.$route.query,
|
||||||
|
'sorter-active':
|
||||||
|
this.sorterOptionIds.indexOf(`${this.sorterActive.id}`) != 0
|
||||||
|
? this.sorterActive.id
|
||||||
|
: undefined,
|
||||||
|
...Object.keys(this.searchersValues).reduce(
|
||||||
|
(acc, k) => {
|
||||||
|
const searchVal = this.searchersValues[k as Journal.TimetableSearchKey];
|
||||||
|
|
||||||
if (!this.store.driverStatsName) {
|
acc[k] = searchVal || undefined;
|
||||||
this.store.driverStatsStatus = Status.Data.Initialized;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
return acc;
|
||||||
this.store.driverStatsStatus = Status.Data.Loading;
|
},
|
||||||
|
{} as { [k: string]: string | undefined }
|
||||||
const statsData: API.DriverStats.Response = await (
|
),
|
||||||
await http.get(`api/getDriverInfo?name=${this.store.driverStatsName}`)
|
...this.filterList?.reduce(
|
||||||
).data;
|
(acc, f) => {
|
||||||
|
if (f.isActive) acc[f.filterSection] = f.default ? undefined : f.id;
|
||||||
this.store.driverStatsData = statsData;
|
return acc;
|
||||||
this.store.driverStatsStatus = Status.Data.Loaded;
|
},
|
||||||
} catch (error) {
|
{} as { [k: string]: string | undefined }
|
||||||
this.store.driverStatsStatus = Status.Data.Error;
|
)
|
||||||
console.error('Ups! Wystąpił błąd przy próbie pobrania statystyk maszynisty! :/');
|
}
|
||||||
}
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
refreshData() {
|
refreshData() {
|
||||||
@@ -245,7 +248,7 @@ export default defineComponent({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
this[`${type}Suggestions`] = [];
|
this[`${type}Suggestions`] = [];
|
||||||
}
|
}
|
||||||
}, 450);
|
}, 250);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Override keyMixin function
|
// Override keyMixin function
|
||||||
@@ -260,7 +263,7 @@ export default defineComponent({
|
|||||||
onSorterChange(item: { id: string | number; value: string }) {
|
onSorterChange(item: { id: string | number; value: string }) {
|
||||||
this.sorterActive.id = item.id;
|
this.sorterActive.id = item.id;
|
||||||
this.sorterActive.dir = -1;
|
this.sorterActive.dir = -1;
|
||||||
this.$emit('onSearchConfirm');
|
this.searchConfirm();
|
||||||
},
|
},
|
||||||
|
|
||||||
onFilterChange(filter: Journal.TimetableFilter) {
|
onFilterChange(filter: Journal.TimetableFilter) {
|
||||||
@@ -270,25 +273,27 @@ export default defineComponent({
|
|||||||
.forEach((f) => (f.isActive = false));
|
.forEach((f) => (f.isActive = false));
|
||||||
filter.isActive = true;
|
filter.isActive = true;
|
||||||
|
|
||||||
this.$emit('onSearchConfirm');
|
this.searchConfirm();
|
||||||
},
|
},
|
||||||
|
|
||||||
onInputClear(id: any) {
|
onInputClear(id: any) {
|
||||||
this.searchersValues[id] = '';
|
this.searchersValues[id] = '';
|
||||||
this.$emit('onSearchConfirm');
|
this.searchConfirm();
|
||||||
},
|
},
|
||||||
|
|
||||||
onSearchConfirm() {
|
searchConfirm() {
|
||||||
this.$emit('onSearchConfirm');
|
this.$emit('onSearchConfirm');
|
||||||
|
this.handleRouteParams();
|
||||||
},
|
},
|
||||||
|
|
||||||
onSearchButtonConfirm() {
|
onSearchButtonConfirm() {
|
||||||
this.showOptions = false;
|
this.showOptions = false;
|
||||||
this.$emit('onSearchConfirm');
|
this.searchConfirm();
|
||||||
},
|
},
|
||||||
|
|
||||||
onResetButtonClick() {
|
onResetButtonClick() {
|
||||||
this.$emit('onOptionsReset');
|
this.$emit('onOptionsReset');
|
||||||
|
this.handleRouteParams();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="journal-stats" v-if="!store.isOffline">
|
<div
|
||||||
<div class="stats-buttons">
|
class="journal-stats dropdown"
|
||||||
|
v-if="!mainStore.isOffline"
|
||||||
|
@keydown.esc="currentStatsTab = null"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="dropdown_background"
|
||||||
|
v-if="currentStatsTab !== null"
|
||||||
|
@click="currentStatsTab = null"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div class="actions-bar">
|
||||||
<button
|
<button
|
||||||
v-for="button in data.statsButtons"
|
v-for="button in statsButtons"
|
||||||
:key="button.name"
|
:key="button.tab"
|
||||||
class="btn--filled btn--image"
|
class="btn--filled btn--image"
|
||||||
:data-selected="button.name == currentStatsTab"
|
:data-selected="button.tab == currentStatsTab"
|
||||||
@click="onTabButtonClick(button.name)"
|
:data-disabled="button.disabled"
|
||||||
|
:disabled="button.disabled"
|
||||||
|
@click="onTabButtonClick(button.tab)"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="button.iconName"
|
v-if="button.iconName"
|
||||||
@@ -17,87 +29,57 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stats-tab" v-show="currentStatsTab !== null">
|
<transition name="dropdown-anim">
|
||||||
<keep-alive>
|
<div class="dropdown_wrapper" v-if="currentStatsTab !== null">
|
||||||
<JournalDailyStats v-if="currentStatsTab == 'journal-daily-stats'" />
|
<keep-alive>
|
||||||
<JournalDriverStats v-else-if="currentStatsTab == 'journal-driver-stats'" />
|
<component :is="currentStatsTab" :key="currentStatsTab"></component>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</div>
|
</div>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script lang="ts">
|
||||||
import { computed, onMounted, reactive, Ref, ref, watch } from 'vue';
|
import { defineComponent, PropType } from 'vue';
|
||||||
import { useMainStore } from '../../store/mainStore';
|
import { useMainStore } from '../../store/mainStore';
|
||||||
import JournalDailyStats from './JournalDailyStats.vue';
|
|
||||||
import JournalDriverStats from './JournalDriverStats.vue';
|
|
||||||
import StorageManager from '../../managers/storageManager';
|
import StorageManager from '../../managers/storageManager';
|
||||||
|
import { Journal } from './typings';
|
||||||
|
import JournalDailyStats from './JournalDailyStats.vue';
|
||||||
|
import JournalDispatcherStats from '../JournalView/JournalDispatchers/JournalDispatcherStats.vue';
|
||||||
|
import JournalDriverStats from '../JournalView/JournalTimetables/JournalDriverStats.vue';
|
||||||
|
|
||||||
export type JournalStatsTab = 'journal-driver-stats' | 'journal-daily-stats';
|
export default defineComponent({
|
||||||
|
components: { JournalDailyStats, JournalDriverStats, JournalDispatcherStats },
|
||||||
const store = useMainStore();
|
props: {
|
||||||
const currentStatsTab: Ref<JournalStatsTab | null> = ref(null);
|
statsButtons: {
|
||||||
|
type: Array as PropType<Journal.StatsButton[]>,
|
||||||
let data = reactive({
|
required: true
|
||||||
statsButtons: [
|
|
||||||
{
|
|
||||||
name: 'journal-daily-stats',
|
|
||||||
localeKey: 'journal.daily-stats-title',
|
|
||||||
iconName: 'stats'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'journal-driver-stats',
|
|
||||||
localeKey: 'journal.driver-stats-title',
|
|
||||||
iconName: 'user'
|
|
||||||
}
|
}
|
||||||
] as { name: JournalStatsTab; localeKey: string; iconName?: string }[]
|
},
|
||||||
});
|
data() {
|
||||||
|
return {
|
||||||
|
Journal,
|
||||||
|
mainStore: useMainStore(),
|
||||||
|
currentStatsTab: null as Journal.StatsTab | null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
function onTabButtonClick(tab: JournalStatsTab) {
|
methods: {
|
||||||
currentStatsTab.value = tab == currentStatsTab.value ? null : tab;
|
onTabButtonClick(tab: Journal.StatsTab) {
|
||||||
StorageManager.setStringValue('journalStatsTab', currentStatsTab.value ?? '');
|
this.currentStatsTab = tab == this.currentStatsTab ? null : tab;
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
StorageManager.setStringValue('journalStatsTab', this.currentStatsTab ?? '');
|
||||||
computed(() => store.driverStatsData),
|
}
|
||||||
(newData, prevData) => {
|
|
||||||
currentStatsTab.value =
|
|
||||||
JSON.stringify(prevData) !== JSON.stringify(newData) && newData !== undefined
|
|
||||||
? 'journal-driver-stats'
|
|
||||||
: currentStatsTab.value;
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const storedTab = StorageManager.getStringValue('journalStatsTab');
|
|
||||||
|
|
||||||
if (storedTab && storedTab !== '') currentStatsTab.value = storedTab as JournalStatsTab;
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/JournalStats.scss';
|
@import '../../styles/dropdown.scss';
|
||||||
|
@import '../../styles/dropdown_filters.scss';
|
||||||
@import '../../styles/variables.scss';
|
@import '../../styles/variables.scss';
|
||||||
|
|
||||||
.stats-buttons {
|
.dropdown_wrapper {
|
||||||
position: relative;
|
max-width: 100%;
|
||||||
|
|
||||||
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>
|
</style>
|
||||||
|
|||||||
+17
-20
@@ -1,14 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="journal-stats">
|
<div class="journal-stats driver" v-if="store.driverStatsData">
|
||||||
<span v-if="store.driverStatsData">
|
<span>
|
||||||
<h3>
|
<h3>
|
||||||
{{ $t('journal.stats-title') }}
|
<i18n-t keypath="journal.driver-stats.title">
|
||||||
<span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
|
<template #name>
|
||||||
|
<span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
<hr class="header-separator" />
|
||||||
|
|
||||||
<div class="info-stats">
|
<div class="info-stats">
|
||||||
<span class="stat-badge">
|
<span class="stat-badge">
|
||||||
<span>{{ $t('journal.stats-timetables') }}</span>
|
<span>{{ $t('journal.driver-stats.timetables') }}</span>
|
||||||
<span
|
<span
|
||||||
>{{ store.driverStatsData._count.fulfilled }} /
|
>{{ store.driverStatsData._count.fulfilled }} /
|
||||||
{{ store.driverStatsData._count._all }}</span
|
{{ store.driverStatsData._count._all }}</span
|
||||||
@@ -16,17 +21,17 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="stat-badge">
|
<span class="stat-badge">
|
||||||
<span>{{ $t('journal.stats-longest-timetable') }}</span>
|
<span>{{ $t('journal.driver-stats.longest-timetable') }}</span>
|
||||||
<span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span>
|
<span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="stat-badge">
|
<span class="stat-badge">
|
||||||
<span>{{ $t('journal.stats-avg-timetable') }}</span>
|
<span>{{ $t('journal.driver-stats.avg-timetable') }}</span>
|
||||||
<span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
|
<span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="stat-badge">
|
<span class="stat-badge">
|
||||||
<span>{{ $t('journal.stats-distance') }}</span>
|
<span>{{ $t('journal.driver-stats.distance') }}</span>
|
||||||
<span>
|
<span>
|
||||||
{{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
|
{{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
|
||||||
{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km
|
{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km
|
||||||
@@ -34,7 +39,7 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="stat-badge">
|
<span class="stat-badge">
|
||||||
<span>{{ $t('journal.stats-stations') }}</span>
|
<span>{{ $t('journal.driver-stats.stations') }}</span>
|
||||||
<span>
|
<span>
|
||||||
{{ store.driverStatsData._sum.confirmedStopsCount }} /
|
{{ store.driverStatsData._sum.confirmedStopsCount }} /
|
||||||
{{ store.driverStatsData._sum.allStopsCount }}
|
{{ store.driverStatsData._sum.allStopsCount }}
|
||||||
@@ -42,21 +47,13 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<b v-else-if="store.driverStatsStatus == Status.Data.Loading">{{
|
|
||||||
$t('journal.stats-loading')
|
|
||||||
}}</b>
|
|
||||||
<b v-else-if="store.driverStatsStatus == Status.Data.Error">
|
|
||||||
{{ $t('journal.stats-error ') }}
|
|
||||||
</b>
|
|
||||||
<b v-else>{{ $t('journal.driver-stats-info') }}</b>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { useMainStore } from '../../store/mainStore';
|
import { useMainStore } from '../../../store/mainStore';
|
||||||
import { Status } from '../../typings/common';
|
import { Status } from '../../../typings/common';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'journal-driver-stats',
|
name: 'journal-driver-stats',
|
||||||
@@ -71,5 +68,5 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../styles/JournalStats.scss';
|
@import '../../../styles/JournalStats.scss';
|
||||||
</style>
|
</style>
|
||||||
@@ -1,12 +1,5 @@
|
|||||||
export namespace Journal {
|
export namespace Journal {
|
||||||
export type DispatcherSearcher = {
|
export type DispatcherSearchKey = 'search-dispatcher' | 'search-station' | 'search-date';
|
||||||
[key in 'search-dispatcher' | 'search-station' | 'search-date']: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface DispatcherSorter {
|
|
||||||
id: 'timestampFrom' | 'duration';
|
|
||||||
dir: -1 | 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TimetableSearchKey =
|
export type TimetableSearchKey =
|
||||||
| 'search-driver'
|
| 'search-driver'
|
||||||
@@ -19,11 +12,29 @@ export namespace Journal {
|
|||||||
[key in TimetableSearchKey]: string;
|
[key in TimetableSearchKey]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DispatcherSearchType = {
|
||||||
|
[key in DispatcherSearchKey]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TimetableSorterKey = 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
|
||||||
|
export type DispatcherSorterKey = 'timestampFrom' | 'duration';
|
||||||
|
|
||||||
|
export interface DispatcherSorter {
|
||||||
|
id: DispatcherSorterKey;
|
||||||
|
dir: -1 | 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimetableSorter {
|
||||||
|
id: TimetableSorterKey;
|
||||||
|
dir: 'asc' | 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
export const enum TimetableFilterId {
|
export const enum TimetableFilterId {
|
||||||
|
ALL_STATUSES = 'all-statuses',
|
||||||
ACTIVE = 'active',
|
ACTIVE = 'active',
|
||||||
FULFILLED = 'fulfilled',
|
FULFILLED = 'fulfilled',
|
||||||
ABANDONED = 'abandoned',
|
ABANDONED = 'abandoned',
|
||||||
ALL = 'all',
|
ALL_SPECIALS = 'all-specials',
|
||||||
TWR = 'twr',
|
TWR = 'twr',
|
||||||
SKR = 'skr',
|
SKR = 'skr',
|
||||||
TWR_SKR = 'twr-skr'
|
TWR_SKR = 'twr-skr'
|
||||||
@@ -31,19 +42,26 @@ export namespace Journal {
|
|||||||
|
|
||||||
export enum FilterSection {
|
export enum FilterSection {
|
||||||
TIMETABLE_STATUS = 'timetable-status',
|
TIMETABLE_STATUS = 'timetable-status',
|
||||||
TWRSKR = 'twrskr'
|
SPECIAL = 'special'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimetableFilter {
|
export interface TimetableFilter {
|
||||||
id: TimetableFilterId;
|
id: TimetableFilterId;
|
||||||
filterSection: string;
|
filterSection: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
default: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TimetableSorterKey = 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
|
export enum StatsTab {
|
||||||
|
DRIVER_STATS = 'journal-driver-stats',
|
||||||
|
DISPATCHER_STATS = 'journal-dispatcher-stats',
|
||||||
|
DAILY_STATS = 'journal-daily-stats'
|
||||||
|
}
|
||||||
|
|
||||||
export interface TimetableSorter {
|
export interface StatsButton {
|
||||||
id: TimetableSorterKey;
|
tab: StatsTab;
|
||||||
dir: 'asc' | 'desc';
|
localeKey: string;
|
||||||
|
iconName: string;
|
||||||
|
disabled: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,9 @@
|
|||||||
<tr v-for="historyItem in historyList" :key="historyItem.id">
|
<tr v-for="historyItem in historyList" :key="historyItem.id">
|
||||||
<td>#{{ historyItem.stationHash }}</td>
|
<td>#{{ historyItem.stationHash }}</td>
|
||||||
<td>
|
<td>
|
||||||
<router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`">
|
<router-link
|
||||||
|
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
|
||||||
|
>
|
||||||
<b>{{ historyItem.dispatcherName }}</b>
|
<b>{{ historyItem.dispatcherName }}</b>
|
||||||
</router-link>
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
@@ -138,7 +140,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
navigateToHistory() {
|
navigateToHistory() {
|
||||||
this.$router.push(
|
this.$router.push(
|
||||||
`/journal/dispatchers?sceneryName=${this.station?.name || this.onlineScenery?.name}`
|
`/journal/dispatchers?search-station=${this.station?.name || this.onlineScenery?.name}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<router-link
|
<router-link
|
||||||
class="dispatcher_name"
|
class="dispatcher_name"
|
||||||
:to="`/journal/dispatchers?dispatcherName=${onlineScenery.dispatcherName}`"
|
:to="`/journal/dispatchers?search-dispatcher=${onlineScenery.dispatcherName}`"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="text--donator"
|
class="text--donator"
|
||||||
|
|||||||
@@ -14,13 +14,13 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="header_links" v-if="station">
|
<span class="header_links" v-if="station">
|
||||||
<a
|
<!-- <a
|
||||||
:href="`https://pragotron-td2.web.app/board?name=${station.name}`"
|
:href="`https://pragotron-td2.web.app/board?name=${station.name}`"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:title="$t('scenery.pragotron-link')"
|
:title="$t('scenery.pragotron-link')"
|
||||||
>
|
>
|
||||||
<img src="/images/icon-pragotron.svg" alt="icon-pragotron" />
|
<img src="/images/icon-pragotron.svg" alt="icon-pragotron" />
|
||||||
</a>
|
</a> -->
|
||||||
|
|
||||||
<a :href="tabliceZbiorczeHref" target="_blank" :title="$t('scenery.tablice-link')">
|
<a :href="tabliceZbiorczeHref" target="_blank" :title="$t('scenery.tablice-link')">
|
||||||
<img src="/images/icon-tablice.ico" alt="icon-tablice" />
|
<img src="/images/icon-tablice.ico" alt="icon-tablice" />
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
<transition-group name="list-anim">
|
<transition-group name="list-anim">
|
||||||
<div
|
<div
|
||||||
style="padding-bottom: 5em"
|
style="padding-bottom: 5em"
|
||||||
v-if="apiStore.dataStatuses.trains == 0 && computedScheduledTrains.length == 0"
|
v-if="apiStore.dataStatuses.connection == 0 && computedScheduledTrains.length == 0"
|
||||||
key="list-loading"
|
key="list-loading"
|
||||||
>
|
>
|
||||||
<Loading />
|
<Loading />
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="historyItem in historyList" :key="historyItem.id">
|
<tr v-for="historyItem in historyList" :key="historyItem.id">
|
||||||
<td>
|
<td>
|
||||||
<router-link :to="`/journal/timetables?timetableId=${historyItem.id}`">
|
<router-link :to="`/journal/timetables?search-train=%23${historyItem.id}`">
|
||||||
#{{ historyItem.id }}
|
#{{ historyItem.id }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
@@ -37,11 +37,16 @@
|
|||||||
{{ historyItem.trainNo }}
|
{{ historyItem.trainNo }}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ historyItem.route.replace('|', ' -> ') }}</td>
|
<td>{{ historyItem.route.replace('|', ' -> ') }}</td>
|
||||||
<td>{{ historyItem.driverName }}</td>
|
<td>
|
||||||
|
<router-link :to="`/journal/timetables?search-driver=${historyItem.driverName}`">
|
||||||
|
{{ historyItem.driverName }}
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="historyItem.authorName"
|
v-if="historyItem.authorName"
|
||||||
:to="`/journal/timetables?authorName=${historyItem.authorName}`"
|
:to="`/journal/timetables?search-dispatcher=${historyItem.authorName}`"
|
||||||
>{{ historyItem.authorName }}
|
>{{ historyItem.authorName }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i>
|
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i>
|
||||||
@@ -99,18 +104,20 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async fetchAPIData(countFrom = 0, countLimit = 15) {
|
async fetchAPIData() {
|
||||||
if (!this.station && !this.onlineScenery) {
|
if (!this.station && !this.onlineScenery) {
|
||||||
this.dataStatus = Status.Data.Loaded;
|
this.dataStatus = Status.Data.Loaded;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const requestString = `api/getTimetables?issuedFrom=${
|
const response: API.TimetableHistory.Response = await (
|
||||||
this.station?.name || this.onlineScenery?.name
|
await http.get('api/getTimetables', {
|
||||||
}&countFrom=${countFrom}&countLimit=${countLimit}`;
|
params: {
|
||||||
|
issuedFrom: this.station?.name
|
||||||
const response: API.TimetableHistory.Response = await (await http.get(requestString)).data;
|
}
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
this.historyList = response;
|
this.historyList = response;
|
||||||
|
|
||||||
@@ -121,9 +128,12 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
navigateToHistory() {
|
navigateToHistory() {
|
||||||
this.$router.push(
|
this.$router.push({
|
||||||
`/journal/timetables?issuedFrom=${this.station?.name || this.onlineScenery?.name}`
|
path: '/journal/timetables',
|
||||||
);
|
query: {
|
||||||
|
'search-issuedFrom': this.station?.name || this.onlineScenery?.name
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: { Loading }
|
components: { Loading }
|
||||||
|
|||||||
@@ -447,7 +447,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
.section-inputs {
|
.section-inputs {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -279,7 +279,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Loading v-if="!isDataLoaded && stations.length == 0" />
|
<Loading v-if="apiStore.dataStatuses.connection == Status.Loading" />
|
||||||
|
|
||||||
<div class="no-stations" v-else-if="stations.length == 0">
|
<div class="no-stations" v-else-if="stations.length == 0">
|
||||||
{{ $t('sceneries.no-stations') }}
|
{{ $t('sceneries.no-stations') }}
|
||||||
@@ -288,7 +288,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, computed, PropType } from 'vue';
|
import { defineComponent, PropType } from 'vue';
|
||||||
import dateMixin from '../../mixins/dateMixin';
|
import dateMixin from '../../mixins/dateMixin';
|
||||||
import stationInfoMixin from '../../mixins/stationInfoMixin';
|
import stationInfoMixin from '../../mixins/stationInfoMixin';
|
||||||
import styleMixin from '../../mixins/styleMixin';
|
import styleMixin from '../../mixins/styleMixin';
|
||||||
@@ -330,12 +330,8 @@ export default defineComponent({
|
|||||||
const apiStore = useApiStore();
|
const apiStore = useApiStore();
|
||||||
const stationFiltersStore = useStationFiltersStore();
|
const stationFiltersStore = useStationFiltersStore();
|
||||||
|
|
||||||
const isDataLoaded = computed(() => {
|
|
||||||
return apiStore.dataStatuses.sceneries != Status.Data.Loading;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDataLoaded,
|
Status: Status.Data,
|
||||||
stationFiltersStore,
|
stationFiltersStore,
|
||||||
mainStore,
|
mainStore,
|
||||||
apiStore
|
apiStore
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<hr style="margin: 0.5em 0" />
|
<hr style="margin: 0.5em 0" />
|
||||||
|
|
||||||
<div v-if="apiStore.dataStatuses.trains == Status.Loaded && regionTrains.length > 0">
|
<div v-if="apiStore.dataStatuses.connection == Status.Loaded && regionTrains.length > 0">
|
||||||
<div class="top-list general">
|
<div class="top-list general">
|
||||||
<transition-group tag="ul" name="stats-anim">
|
<transition-group tag="ul" name="stats-anim">
|
||||||
<li class="badge" key="timetable-count">
|
<li class="badge" key="timetable-count">
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="apiStore.dataStatuses.trains != Status.Loaded">
|
<div v-else-if="apiStore.dataStatuses.connection != Status.Loaded">
|
||||||
{{ $t('train-stats.stats-loading') }}
|
{{ $t('train-stats.stats-loading') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition name="status-anim" mode="out-in" tag="div" class="train-table">
|
<transition name="status-anim" mode="out-in" tag="div" class="train-table">
|
||||||
<div :key="apiStore.dataStatuses.trains">
|
<div :key="apiStore.dataStatuses.connection">
|
||||||
<div class="table-info" key="offline" v-if="store.isOffline">
|
<div class="table-info" key="offline" v-if="store.isOffline">
|
||||||
{{ $t('app.offline') }}
|
{{ $t('app.offline') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Loading v-else-if="trains.length == 0 && apiStore.dataStatuses.trains == 0" key="loading" />
|
<Loading v-else-if="apiStore.dataStatuses.connection == Status.Loading" key="loading" />
|
||||||
|
|
||||||
<div
|
<div class="table-info" key="no-trains" v-else-if="trains.length == 0">
|
||||||
class="table-info"
|
|
||||||
key="no-trains"
|
|
||||||
v-else-if="trains.length == 0 && apiStore.dataStatuses.trains != 0"
|
|
||||||
>
|
|
||||||
{{ $t('trains.no-trains') }}
|
{{ $t('trains.no-trains') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -64,6 +60,7 @@ export default defineComponent({
|
|||||||
searchedDriver,
|
searchedDriver,
|
||||||
store,
|
store,
|
||||||
apiStore,
|
apiStore,
|
||||||
|
Status: Status.Data,
|
||||||
sorterActive: inject('sorterActive') as {
|
sorterActive: inject('sorterActive') as {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
dir: number;
|
dir: number;
|
||||||
@@ -75,7 +72,7 @@ export default defineComponent({
|
|||||||
dataStatus() {
|
dataStatus() {
|
||||||
if (this.store.isOffline) return Status.Data.Offline;
|
if (this.store.isOffline) return Status.Data.Offline;
|
||||||
|
|
||||||
if (this.trains.length == 0 && this.apiStore.dataStatuses.trains == Status.Data.Loading)
|
if (this.trains.length == 0 && this.apiStore.dataStatuses.connection == Status.Data.Loading)
|
||||||
return Status.Data.Loading;
|
return Status.Data.Loading;
|
||||||
|
|
||||||
return Status.Data.Loaded;
|
return Status.Data.Loaded;
|
||||||
|
|||||||
+44
-23
@@ -144,7 +144,8 @@
|
|||||||
"filter-withComments": "COMMENTS",
|
"filter-withComments": "COMMENTS",
|
||||||
"filter-twr": "HIGH RISK CARGO",
|
"filter-twr": "HIGH RISK CARGO",
|
||||||
"filter-skr": "EXCEEDED GAUGE",
|
"filter-skr": "EXCEEDED GAUGE",
|
||||||
"filter-twr-skr": "ALL TYPES",
|
"filter-twr-skr": "BOTH TYPES",
|
||||||
|
"filter-all-specials": "ALL",
|
||||||
"filter-common": "NO WARNINGS",
|
"filter-common": "NO WARNINGS",
|
||||||
"filter-passenger": "PASSENGER",
|
"filter-passenger": "PASSENGER",
|
||||||
"filter-freight": "FREIGHT",
|
"filter-freight": "FREIGHT",
|
||||||
@@ -156,9 +157,9 @@
|
|||||||
"filter-clear": "CLEAR FILTERS",
|
"filter-clear": "CLEAR FILTERS",
|
||||||
|
|
||||||
"filter-section-timetable-status": "TIMETABLE STATUS",
|
"filter-section-timetable-status": "TIMETABLE STATUS",
|
||||||
"filter-section-twrskr": "WARNINGS",
|
"filter-section-special": "SPECIAL TYPE",
|
||||||
|
|
||||||
"filter-all": "ALL ENTRIES",
|
"filter-all-statuses": "ALL",
|
||||||
"filter-abandoned": "ABANDONED",
|
"filter-abandoned": "ABANDONED",
|
||||||
"filter-fulfilled": "FULFILLED",
|
"filter-fulfilled": "FULFILLED",
|
||||||
"filter-active": "ACTIVE"
|
"filter-active": "ACTIVE"
|
||||||
@@ -347,29 +348,49 @@
|
|||||||
"last-seen-at": "Last seen at",
|
"last-seen-at": "Last seen at",
|
||||||
"currently-at": "Currently at",
|
"currently-at": "Currently at",
|
||||||
|
|
||||||
"stats-title": "DRIVING STATISTICS OF",
|
"driver-stats": {
|
||||||
|
"button": "DRIVER STATS",
|
||||||
|
"title": "{name}'s DRIVER STATS",
|
||||||
|
"info": "Enter a proper nickname into filters [F] to see user's driving statistics!",
|
||||||
|
"timetables": "TIMETABLES",
|
||||||
|
"longest-timetable": "LONGEST TIMETABLE",
|
||||||
|
"avg-timetable": "AVERAGE TIMETABLE LENGTH",
|
||||||
|
"distance": "DISTANCE",
|
||||||
|
"stations": "STATIONS"
|
||||||
|
},
|
||||||
|
|
||||||
"stats-timetables": "TIMETABLES",
|
"daily-stats": {
|
||||||
"stats-longest-timetable": "LONGEST TIMETABLE",
|
"button": "DAILY STATS",
|
||||||
"stats-avg-timetable": "AVERAGE TIMETABLE LENGTH",
|
"title": "STATS OF THE DAY",
|
||||||
"stats-distance": "DISTANCE",
|
"info": "Today's statistics are unavailable yet!",
|
||||||
"stats-stations": "STATIONS",
|
"total": "Issued timetables: {count} (total distance: {distance})",
|
||||||
|
"longest": "The longest timetable: #{id} (made by {author} for {driver}, distance: {distance})",
|
||||||
|
"most-active-dr": "The most active dispatcher: {dispatcher} (created {count})",
|
||||||
|
"most-active-dr-many": "The most active dispatchers: {dispatchers} (created {count} each)",
|
||||||
|
"most-active-driver": "The most active driver: {driver} (total driven distance: {distance})",
|
||||||
|
"longest-duties": "The longest service: {dispatcher} at {station} (duration: {duration})",
|
||||||
|
"count": "timetable | timetables",
|
||||||
|
|
||||||
"timetable-stats-title": "Daily stats on {date}",
|
"rippedSwitches": "RIPPED SWITCHES",
|
||||||
"timetable-stats-total": "Issued timetables: {count} (total distance: {distance})",
|
"derailments": "DERAILMENTS",
|
||||||
"timetable-stats-longest": "The longest timetable: #{id} (made by {author} for {driver}, distance: {distance})",
|
"skippedStopSignals": "SKIPPED STOP SIGNALS",
|
||||||
"timetable-stats-most-active-dr": "The most active dispatcher: {dispatcher} (created {count})",
|
"radioStops": "RADIOSTOPS",
|
||||||
"timetable-stats-most-active-dr-many": "The most active dispatchers: {dispatchers} (created {count} each)",
|
"kills": "KILLS"
|
||||||
"timetable-stats-most-active-driver": "The most active driver: {driver} (total driven distance: {distance})",
|
},
|
||||||
"timetable-stats-longest-duties": "The longest service: {dispatcher} at {station} (duration: {duration})",
|
|
||||||
|
|
||||||
"timetable-count": "timetable | timetables",
|
"dispatcher-stats": {
|
||||||
|
"button": "DISPATCHER STATS",
|
||||||
"daily-stats-title": "DAILY STATS",
|
"title": "{name}'s DISPATCHER STATS",
|
||||||
"daily-stats-info": "Today's statistics are unavailable yet!",
|
"empty": "This user has no statistics saved yet!",
|
||||||
|
"info": "Enter a proper nickname into filters [F] to see user's dispatcher statistics!",
|
||||||
"driver-stats-title": "DRIVER STATS",
|
"services-count": "SERVICES",
|
||||||
"driver-stats-info": "Enter a proper nickname into filters [F] to see user's driving statistics!",
|
"service-max": "MAX SERVICE DURATION",
|
||||||
|
"service-avg": "AVG SERVICE DURATION",
|
||||||
|
"timetables-count": "ISSUED TIMETABLES",
|
||||||
|
"timetables-sum": "TIMETABLES DISTANCE SUM",
|
||||||
|
"timetables-max": "LONGEST TIMETABLE",
|
||||||
|
"timetables-avg": "AVG TIMETABLE DISTANCE"
|
||||||
|
},
|
||||||
|
|
||||||
"stats-loading": "Fetching statistics...",
|
"stats-loading": "Fetching statistics...",
|
||||||
"stats-error": "Oops! An unexpected error occurred while trying to fetch statistics! :/",
|
"stats-error": "Oops! An unexpected error occurred while trying to fetch statistics! :/",
|
||||||
|
|||||||
+43
-22
@@ -133,7 +133,8 @@
|
|||||||
"filter-noComments": "BEZ UWAG",
|
"filter-noComments": "BEZ UWAG",
|
||||||
"filter-twr": "WYS. RYZYKA",
|
"filter-twr": "WYS. RYZYKA",
|
||||||
"filter-skr": "SKRAJNIA",
|
"filter-skr": "SKRAJNIA",
|
||||||
"filter-twr-skr": "WSZYSTKIE",
|
"filter-twr-skr": "TWR/SKR",
|
||||||
|
"filter-all-statuses": "WSZYSTKIE",
|
||||||
"filter-common": "ZWYKŁE",
|
"filter-common": "ZWYKŁE",
|
||||||
"filter-passenger": "PASAŻERSKIE",
|
"filter-passenger": "PASAŻERSKIE",
|
||||||
"filter-freight": "TOWAROWE",
|
"filter-freight": "TOWAROWE",
|
||||||
@@ -145,9 +146,9 @@
|
|||||||
"filter-clear": "WYŁĄCZ FILTRY",
|
"filter-clear": "WYŁĄCZ FILTRY",
|
||||||
|
|
||||||
"filter-section-timetable-status": "STATUS ROZKŁADU JAZDY",
|
"filter-section-timetable-status": "STATUS ROZKŁADU JAZDY",
|
||||||
"filter-section-twrskr": "UWAGI",
|
"filter-section-special": "TYPY SPECJALNE",
|
||||||
|
|
||||||
"filter-all": "WSZYSTKIE",
|
"filter-all-specials": "WSZYSTKIE",
|
||||||
"filter-abandoned": "PORZUCONE",
|
"filter-abandoned": "PORZUCONE",
|
||||||
"filter-fulfilled": "WYPEŁNIONE",
|
"filter-fulfilled": "WYPEŁNIONE",
|
||||||
"filter-active": "AKTYWNE"
|
"filter-active": "AKTYWNE"
|
||||||
@@ -326,31 +327,51 @@
|
|||||||
|
|
||||||
"load-data": "Pobierz dalszą historię...",
|
"load-data": "Pobierz dalszą historię...",
|
||||||
|
|
||||||
"stats-title": "STATYSTYKI MASZYNISTY",
|
|
||||||
|
|
||||||
"last-seen-at": "Ostatnio widziany na: ",
|
"last-seen-at": "Ostatnio widziany na: ",
|
||||||
"currently-at": "Obecnie na scenerii: ",
|
"currently-at": "Obecnie na scenerii: ",
|
||||||
|
|
||||||
"stats-timetables": "ROZKŁADY JAZDY",
|
"driver-stats": {
|
||||||
"stats-longest-timetable": "NAJDŁUŻSZY RJ",
|
"button": "STAT. MASZYNISTY",
|
||||||
"stats-avg-timetable": "ŚREDNIA DŁUGOŚĆ RJ",
|
"title": "STATYSTYKI MASZYNISTY {name}",
|
||||||
"stats-distance": "DYSTANS",
|
"info": "Wpisz nazwę użytkownika w filtrach [F], aby zobaczyć jego statystyki maszynisty!",
|
||||||
"stats-stations": "STACJE",
|
"timetables": "ROZKŁADY JAZDY",
|
||||||
|
"longest-timetable": "NAJDŁUŻSZY RJ",
|
||||||
|
"avg-timetable": "ŚREDNIA DŁUGOŚĆ RJ",
|
||||||
|
"distance": "DYSTANS",
|
||||||
|
"stations": "STACJE"
|
||||||
|
},
|
||||||
|
|
||||||
"timetable-stats-total": "Stworzone rozkłady jazdy: {count} (łączny dystans: {distance})",
|
"daily-stats": {
|
||||||
"timetable-stats-longest": "Najdłuższy rozkład jazdy: #{id} (stworzony przez dyżurnego {author} dla maszynisty {driver} o dystansie {distance})",
|
"button": "STATYSTYKI DNIA",
|
||||||
"timetable-stats-most-active-dr": "Najaktywniejszy dyżurny: {dispatcher} (stworzył {count})",
|
"title": "STATYSTYKI DNIA",
|
||||||
"timetable-stats-most-active-dr-many": "Najaktywniejsi dyżurni: {dispatchers} (stworzyli po {count})",
|
"info": "Dzisiejsze statystyki nie są jeszcze dostępne!",
|
||||||
"timetable-stats-most-active-driver": "Najaktywniejszy maszynista: {driver} (łączny przejechany dystans: {distance})",
|
"total": "Stworzone rozkłady jazdy: {count} (łączny dystans: {distance})",
|
||||||
"timetable-stats-longest-duties": "Najdłuższa służba: {dispatcher} na scenerii {station} (czas trwania: {duration})",
|
"longest": "Najdłuższy rozkład jazdy: #{id} (stworzony przez dyżurnego {author} dla maszynisty {driver} o dystansie {distance})",
|
||||||
|
"most-active-dr": "Najaktywniejszy dyżurny: {dispatcher} (stworzył {count})",
|
||||||
|
"most-active-dr-many": "Najaktywniejsi dyżurni: {dispatchers} (stworzyli po {count})",
|
||||||
|
"most-active-driver": "Najaktywniejszy maszynista: {driver} (łączny przejechany dystans: {distance})",
|
||||||
|
"longest-duties": "Najdłuższa służba: {dispatcher} na scenerii {station} (czas trwania: {duration})",
|
||||||
|
"count": "rozkład jazdy | rozkładów jazdy",
|
||||||
|
|
||||||
"timetable-count": "rozkład jazdy | rozkładów jazdy",
|
"rippedSwitches": "ROZPRUTE ZWROTNICE",
|
||||||
|
"derailments": "WYKOLEJENIA",
|
||||||
|
"skippedStopSignals": "POMINIĘTE S1",
|
||||||
|
"radioStops": "RADIOSTOPY",
|
||||||
|
"kills": "POTRĄCENIA"
|
||||||
|
},
|
||||||
|
|
||||||
"daily-stats-title": "STATYSTYKI DNIA",
|
"dispatcher-stats": {
|
||||||
"daily-stats-info": "Dzisiejsze statystyki nie są jeszcze dostępne!",
|
"button": "STATYSTYKI DYŻURNEGO",
|
||||||
|
"title": "STATYSTYKI DYŻURNEGO {name}",
|
||||||
"driver-stats-title": "STATYSTYKI GRACZA",
|
"info": "Wpisz nazwę użytkownika w filtrach [F], aby zobaczyć jego statystyki dyżurnego!",
|
||||||
"driver-stats-info": "Wpisz nazwę użytkownika w filtrach [F], aby zobaczyć jego statystyki maszynisty!",
|
"services-count": "DYŻURY",
|
||||||
|
"service-max": "MAKS. CZAS DYŻURU",
|
||||||
|
"service-avg": "ŚREDNI CZAS DYŻURU",
|
||||||
|
"timetables-count": "WYSTAWIONE RJ",
|
||||||
|
"timetables-sum": "SUMA WYSTAWIONYCH RJ",
|
||||||
|
"timetables-max": "NAJDŁUŻSZY WYSTAWIONY RJ",
|
||||||
|
"timetables-avg": "ŚREDNIA WYSTAWIONYCH RJ"
|
||||||
|
},
|
||||||
|
|
||||||
"stats-loading": "Pobieranie statystyk...",
|
"stats-loading": "Pobieranie statystyk...",
|
||||||
"stats-error": "Ups! Wystąpił błąd podczas próby pobrania statystyk!",
|
"stats-error": "Ups! Wystąpił błąd podczas próby pobrania statystyk!",
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ export default defineComponent({
|
|||||||
mountObserver(actionFunction: () => void, target: Element) {
|
mountObserver(actionFunction: () => void, target: Element) {
|
||||||
this.observer = new IntersectionObserver(
|
this.observer = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
console.log(entries);
|
|
||||||
|
|
||||||
if (entries[0].intersectionRatio > 0.5) actionFunction();
|
if (entries[0].intersectionRatio > 0.5) actionFunction();
|
||||||
},
|
},
|
||||||
{ threshold: 0.2 }
|
{ threshold: 0.2 }
|
||||||
|
|||||||
+4
-6
@@ -18,7 +18,8 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
props: (route) => ({
|
props: (route) => ({
|
||||||
train: route.query.train,
|
train: route.query.train,
|
||||||
driver: route.query.driver,
|
driver: route.query.driver,
|
||||||
trainId: route.query.trainId
|
trainId: route.query.trainId,
|
||||||
|
region: route.query.region
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -39,9 +40,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: 'JournalTimetables',
|
name: 'JournalTimetables',
|
||||||
component: JournalTimetablesVue,
|
component: JournalTimetablesVue,
|
||||||
props: (route) => ({
|
props: (route) => ({
|
||||||
trainNo: route.query.trainNo,
|
region: route.query.region
|
||||||
driverName: route.query.driverName,
|
|
||||||
timetableId: route.query.timetableId
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -49,8 +48,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: 'JournalDispatchers',
|
name: 'JournalDispatchers',
|
||||||
component: JournalDispatchersVue,
|
component: JournalDispatchersVue,
|
||||||
props: (route) => ({
|
props: (route) => ({
|
||||||
sceneryName: route.query.sceneryName,
|
region: route.query.region
|
||||||
dispatcherName: route.query.dispatcherName
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+39
-19
@@ -18,7 +18,9 @@ export const useApiStore = defineStore('apiStore', {
|
|||||||
activeData: undefined as API.ActiveData.Response | undefined,
|
activeData: undefined as API.ActiveData.Response | undefined,
|
||||||
rollingStockData: undefined as API.RollingStock.Response | undefined,
|
rollingStockData: undefined as API.RollingStock.Response | undefined,
|
||||||
donatorsData: [] as API.Donators.Response,
|
donatorsData: [] as API.Donators.Response,
|
||||||
sceneryData: [] as StationJSONData[]
|
sceneryData: [] as StationJSONData[],
|
||||||
|
|
||||||
|
activeDataTimeout: undefined as number | undefined
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
@@ -28,22 +30,32 @@ export const useApiStore = defineStore('apiStore', {
|
|||||||
this.fetchDonatorsData();
|
this.fetchDonatorsData();
|
||||||
this.fetchStationsGeneralInfo();
|
this.fetchStationsGeneralInfo();
|
||||||
|
|
||||||
this.scheduleFetchActiveData();
|
if (this.activeDataTimeout === undefined) this.startActiveDataScheduler();
|
||||||
},
|
},
|
||||||
|
|
||||||
async setDataStatuses() {
|
// async setDataStatuses() {
|
||||||
if (!this.activeData?.activeSceneries) {
|
// if (!window.navigator.onLine) {
|
||||||
this.dataStatuses.sceneries = Status.Data.Error;
|
// this.dataStatuses.connection = Status.Data.Offline;
|
||||||
this.dataStatuses.trains = Status.Data.Error;
|
// this.dataStatuses.sceneries = Status.Data.Offline;
|
||||||
this.dataStatuses.dispatchers = Status.Data.Error;
|
// this.dataStatuses.trains = Status.Data.Offline;
|
||||||
|
// this.dataStatuses.dispatchers = Status.Data.Offline;
|
||||||
|
// this.dataStatuses.timetables = Status.Data.Offline;
|
||||||
|
// }
|
||||||
|
|
||||||
return;
|
// if (!this.activeData?.activeSceneries) {
|
||||||
}
|
// this.dataStatuses.connection = Status.Data.Loaded;
|
||||||
|
// this.dataStatuses.sceneries = Status.Data.Error;
|
||||||
|
// this.dataStatuses.trains = Status.Data.Error;
|
||||||
|
// this.dataStatuses.dispatchers = Status.Data.Error;
|
||||||
|
|
||||||
this.dataStatuses.sceneries = Status.Data.Loaded;
|
// return;
|
||||||
this.dataStatuses.trains = !this.activeData.trains ? Status.Data.Warning : Status.Data.Loaded;
|
// }
|
||||||
this.dataStatuses.dispatchers = Status.Data.Loaded;
|
|
||||||
},
|
// this.dataStatuses.connection = Status.Data.Loaded;
|
||||||
|
// this.dataStatuses.sceneries = Status.Data.Loaded;
|
||||||
|
// this.dataStatuses.trains = !this.activeData.trains ? Status.Data.Warning : Status.Data.Loaded;
|
||||||
|
// this.dataStatuses.dispatchers = Status.Data.Loaded;
|
||||||
|
// },
|
||||||
|
|
||||||
async fetchDonatorsData() {
|
async fetchDonatorsData() {
|
||||||
try {
|
try {
|
||||||
@@ -67,12 +79,16 @@ export const useApiStore = defineStore('apiStore', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async scheduleFetchActiveData() {
|
async startActiveDataScheduler() {
|
||||||
|
if (!window.navigator.onLine) {
|
||||||
|
this.dataStatuses.connection = Status.Data.Offline;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (import.meta.env.VITE_API_MODE === 'mock') {
|
if (import.meta.env.VITE_API_MODE === 'mock') {
|
||||||
const mockActiveData = await import('../data/mockActiveData.json');
|
const mockActiveData = await import('../data/mockActiveData.json');
|
||||||
this.dataStatuses.connection = Status.Data.Loaded;
|
this.dataStatuses.connection = Status.Data.Loaded;
|
||||||
this.activeData = mockActiveData;
|
this.activeData = mockActiveData;
|
||||||
this.setDataStatuses();
|
|
||||||
|
|
||||||
console.warn('Stacjownik działa w trybie mockowania danych z WS');
|
console.warn('Stacjownik działa w trybie mockowania danych z WS');
|
||||||
|
|
||||||
@@ -84,21 +100,24 @@ export const useApiStore = defineStore('apiStore', {
|
|||||||
|
|
||||||
this.activeData = data;
|
this.activeData = data;
|
||||||
this.dataStatuses.connection = Status.Data.Loaded;
|
this.dataStatuses.connection = Status.Data.Loaded;
|
||||||
|
|
||||||
this.setDataStatuses();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.dataStatuses.connection = Status.Data.Error;
|
this.dataStatuses.connection = Status.Data.Error;
|
||||||
console.error('Wystąpił błąd podczas pobierania danych online z API!');
|
console.error('Wystąpił błąd podczas pobierania danych online z API!');
|
||||||
} finally {
|
} finally {
|
||||||
setTimeout(
|
this.activeDataTimeout = window.setTimeout(
|
||||||
() => {
|
() => {
|
||||||
this.scheduleFetchActiveData();
|
this.startActiveDataScheduler();
|
||||||
},
|
},
|
||||||
~~(1000 * (Math.random() * (25 - 20) + 25))
|
~~(1000 * (Math.random() * (25 - 20) + 25))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async stopActiveDataScheduler() {
|
||||||
|
window.clearTimeout(this.activeDataTimeout);
|
||||||
|
this.activeDataTimeout = undefined;
|
||||||
|
},
|
||||||
|
|
||||||
async fetchStationsGeneralInfo() {
|
async fetchStationsGeneralInfo() {
|
||||||
const sceneryData: StationJSONData[] = (await http.get<StationJSONData[]>('api/getSceneries'))
|
const sceneryData: StationJSONData[] = (await http.get<StationJSONData[]>('api/getSceneries'))
|
||||||
.data;
|
.data;
|
||||||
@@ -108,6 +127,7 @@ export const useApiStore = defineStore('apiStore', {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.dataStatuses.sceneries = Status.Data.Loaded;
|
||||||
this.sceneryData = sceneryData;
|
this.sceneryData = sceneryData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const useMainStore = defineStore('store', {
|
|||||||
isOffline: false,
|
isOffline: false,
|
||||||
|
|
||||||
dispatcherStatsName: '',
|
dispatcherStatsName: '',
|
||||||
dispatcherStatsData: undefined,
|
dispatcherStatsStatus: Status.Data.Initialized,
|
||||||
|
|
||||||
driverStatsName: '',
|
driverStatsName: '',
|
||||||
driverStatsData: undefined,
|
driverStatsData: undefined,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import Station from '../scripts/interfaces/Station';
|
|
||||||
import { API } from '../typings/api';
|
import { API } from '../typings/api';
|
||||||
import { Status } from '../typings/common';
|
import { Status } from '../typings/common';
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
height: 90vh;
|
height: 90vh;
|
||||||
min-height: 550px;
|
min-height: 550px;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
|
||||||
padding-right: 0.2em;
|
padding-right: 0.2em;
|
||||||
}
|
}
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
text-align: end;
|
text-align: end;
|
||||||
|
|
||||||
padding: 0.25em;
|
padding: 0.25em;
|
||||||
margin: 0.5em 0;
|
margin-top: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.journal_warning {
|
.journal_warning {
|
||||||
@@ -53,9 +54,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn--load-data {
|
.btn--load-data {
|
||||||
|
|||||||
@@ -2,24 +2,35 @@
|
|||||||
@import 'responsive.scss';
|
@import 'responsive.scss';
|
||||||
|
|
||||||
.stats-tab {
|
.stats-tab {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
z-index: 99;
|
||||||
|
|
||||||
|
transform: translateY(1em);
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
box-shadow: 0 0 5px 1px $accentCol;
|
box-shadow: 0 0 5px 1px $accentCol;
|
||||||
|
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
margin-bottom: 0.5em;
|
hr.header-separator {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
width: 100%;
|
hr.section-separator {
|
||||||
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-stats {
|
.info-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-badge {
|
.stat-badge {
|
||||||
|
|||||||
@@ -30,7 +30,8 @@
|
|||||||
top: calc(100% + 0.5em);
|
top: calc(100% + 0.5em);
|
||||||
|
|
||||||
background-color: $bgCol;
|
background-color: $bgCol;
|
||||||
box-shadow: 0 5px 10px 2px #0f0f0f;
|
// box-shadow: 0 5px 10px 2px #0f0f0f;
|
||||||
|
box-shadow: 0 0 5px 1px $accentCol;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 550px;
|
max-width: 550px;
|
||||||
|
|||||||
@@ -5,11 +5,6 @@
|
|||||||
.actions-bar {
|
.actions-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters-options {
|
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1.option-title {
|
h1.option-title {
|
||||||
@@ -57,6 +52,7 @@ h1.option-title {
|
|||||||
|
|
||||||
.sort-option[data-selected='true'] {
|
.sort-option[data-selected='true'] {
|
||||||
color: $accentCol;
|
color: $accentCol;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-option {
|
.filter-option {
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Quicksand';
|
||||||
|
src:
|
||||||
|
url('/fonts/Quicksand-Bold.woff2') format('woff2'),
|
||||||
|
url('/fonts/Quicksand-Bold.woff') format('woff');
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Quicksand';
|
||||||
|
src:
|
||||||
|
url('/fonts/Quicksand-SemiBold.woff2') format('woff2'),
|
||||||
|
url('/fonts/Quicksand-SemiBold.woff') format('woff');
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Quicksand';
|
||||||
|
src:
|
||||||
|
url('/fonts/Quicksand-Medium.woff2') format('woff2'),
|
||||||
|
url('/fonts/Quicksand-Medium.woff') format('woff');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Quicksand';
|
||||||
|
src:
|
||||||
|
url('/fonts/Quicksand-Regular.woff2') format('woff2'),
|
||||||
|
url('/fonts/Quicksand-Regular.woff') format('woff');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Quicksand';
|
||||||
|
src:
|
||||||
|
url('/fonts/Quicksand-Light.woff2') format('woff2'),
|
||||||
|
url('/fonts/Quicksand-Light.woff') format('woff');
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@import 'fonts.scss';
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--clr-primary: #ffc014;
|
--clr-primary: #ffc014;
|
||||||
--clr-secondary: #2f2f2f;
|
--clr-secondary: #2f2f2f;
|
||||||
@@ -11,7 +13,7 @@
|
|||||||
--clr-skr: #ff5100;
|
--clr-skr: #ff5100;
|
||||||
--clr-twr: #ffbb00;
|
--clr-twr: #ffbb00;
|
||||||
|
|
||||||
--clr-error: #df3e3e;
|
--clr-error: #fa3636;
|
||||||
--clr-warning: #c59429;
|
--clr-warning: #c59429;
|
||||||
|
|
||||||
--clr-donator: #f7a4ff;
|
--clr-donator: #f7a4ff;
|
||||||
@@ -158,6 +160,10 @@ ul {
|
|||||||
color: #ccc;
|
color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--error {
|
||||||
|
color: var(--clr-error);
|
||||||
|
}
|
||||||
|
|
||||||
&--donator {
|
&--donator {
|
||||||
color: var(--clr-donator);
|
color: var(--clr-donator);
|
||||||
text-shadow: var(--clr-donator) 0 0 10px;
|
text-shadow: var(--clr-donator) 0 0 10px;
|
||||||
@@ -183,7 +189,7 @@ a.a-button {
|
|||||||
&[data-disabled='true'] {
|
&[data-disabled='true'] {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.85;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.btn--filled {
|
&.btn--filled {
|
||||||
|
|||||||
+56
-19
@@ -31,7 +31,11 @@ export namespace API {
|
|||||||
|
|
||||||
export namespace DispatcherStats {
|
export namespace DispatcherStats {
|
||||||
export interface DistanceStat {
|
export interface DistanceStat {
|
||||||
routeDistance: number;
|
routeDistance: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DurationStat {
|
||||||
|
currentDuration: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Count {
|
export interface Count {
|
||||||
@@ -39,11 +43,18 @@ export namespace API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Response {
|
export interface Response {
|
||||||
_sum: DistanceStat;
|
services: {
|
||||||
_max: DistanceStat;
|
count: number;
|
||||||
_min: DistanceStat;
|
durationMax: number;
|
||||||
_avg: DistanceStat;
|
durationAvg: number;
|
||||||
_count: Count;
|
} | null;
|
||||||
|
|
||||||
|
issuedTimetables: {
|
||||||
|
count: number;
|
||||||
|
distanceMax: number;
|
||||||
|
distanceAvg: number;
|
||||||
|
distanceSum: number;
|
||||||
|
} | null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,21 +274,47 @@ export namespace API {
|
|||||||
distanceAvg: number;
|
distanceAvg: number;
|
||||||
maxTimetable: API.TimetableHistory.Data | null;
|
maxTimetable: API.TimetableHistory.Data | null;
|
||||||
|
|
||||||
mostActiveDispatchers: {
|
globalDiff: GlobalDiff;
|
||||||
name: string;
|
globalMax: GlobalMax;
|
||||||
count: number;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
mostActiveDrivers: {
|
mostActiveDispatchers: MostActiveDispatcher[];
|
||||||
name: string;
|
mostActiveDrivers: MostActiveDriver[];
|
||||||
distance: number;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
longestDuties: {
|
longestDuties: LongestDuty[];
|
||||||
name: string;
|
}
|
||||||
duration: number;
|
|
||||||
station: string;
|
export interface MostActiveDispatcher {
|
||||||
}[];
|
name: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MostActiveDriver {
|
||||||
|
name: string;
|
||||||
|
distance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LongestDuty {
|
||||||
|
name: string;
|
||||||
|
duration: number;
|
||||||
|
station: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GlobalDiff {
|
||||||
|
rippedSwitches: number;
|
||||||
|
derailments: number;
|
||||||
|
skippedStopSignals: number;
|
||||||
|
radioStops: number;
|
||||||
|
kills: number;
|
||||||
|
drivenKilometers: number;
|
||||||
|
routedTrains: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GlobalMax {
|
||||||
|
_max: {
|
||||||
|
drivers: number;
|
||||||
|
dispatchers: number;
|
||||||
|
timetables: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export namespace Status {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum Data {
|
export enum Data {
|
||||||
Offline = 2,
|
Offline = -2,
|
||||||
Initialized = -1,
|
Initialized = -1,
|
||||||
Loading = 0,
|
Loading = 0,
|
||||||
Error = 1,
|
Error = 1,
|
||||||
|
|||||||
@@ -3,15 +3,19 @@
|
|||||||
<JournalHeader />
|
<JournalHeader />
|
||||||
|
|
||||||
<div class="journal_wrapper">
|
<div class="journal_wrapper">
|
||||||
<JournalOptions
|
<div class="journal_top-bar">
|
||||||
@on-search-confirm="fetchHistoryData"
|
<JournalOptions
|
||||||
@on-options-reset="resetOptions"
|
@on-search-confirm="fetchHistoryData"
|
||||||
@on-refresh-data="fetchHistoryData(true)"
|
@on-options-reset="resetOptions"
|
||||||
:sorter-option-ids="['timestampFrom', 'duration']"
|
@on-refresh-data="fetchHistoryData(true)"
|
||||||
:data-status="dataStatus"
|
:sorter-option-ids="['timestampFrom', 'duration']"
|
||||||
:current-options-active="currentOptionsActive"
|
:data-status="dataStatus"
|
||||||
optionsType="dispatchers"
|
:current-options-active="currentOptionsActive"
|
||||||
/>
|
optionsType="dispatchers"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<JournalStats :statsButtons="statsButtons" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="journal_refreshed-date" v-if="dataRefreshedAt">
|
<div class="journal_refreshed-date" v-if="dataRefreshedAt">
|
||||||
{{ $t('journal.data-refreshed-at') }}: {{ dataRefreshedAt.toLocaleString($i18n.locale) }}
|
{{ $t('journal.data-refreshed-at') }}: {{ dataRefreshedAt.toLocaleString($i18n.locale) }}
|
||||||
@@ -33,22 +37,33 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, provide, reactive, Ref, ref } from 'vue';
|
import { defineComponent, provide, reactive, Ref, ref } from 'vue';
|
||||||
|
|
||||||
import JournalOptions from '../components/JournalView/JournalOptions.vue';
|
import http from '../http';
|
||||||
import { useMainStore } from '../store/mainStore';
|
import { useMainStore } from '../store/mainStore';
|
||||||
import JournalDispatchersList from '../components/JournalView/JournalDispatchersList.vue';
|
|
||||||
|
|
||||||
import JournalHeader from '../components/JournalView/JournalHeader.vue';
|
|
||||||
import { LocationQuery } from 'vue-router';
|
import { LocationQuery } from 'vue-router';
|
||||||
import { Journal } from '../components/JournalView/typings';
|
import { Journal } from '../components/JournalView/typings';
|
||||||
import { API } from '../typings/api';
|
import { API } from '../typings/api';
|
||||||
import { Status } from '../typings/common';
|
import { Status } from '../typings/common';
|
||||||
import http from '../http';
|
|
||||||
|
import JournalDispatchersList from '../components/JournalView/JournalDispatchers/JournalDispatchersList.vue';
|
||||||
|
import JournalOptions from '../components/JournalView/JournalOptions.vue';
|
||||||
|
import JournalHeader from '../components/JournalView/JournalHeader.vue';
|
||||||
|
import JournalStats from '../components/JournalView/JournalStats.vue';
|
||||||
|
|
||||||
|
const statsButtons: Journal.StatsButton[] = [
|
||||||
|
{
|
||||||
|
tab: Journal.StatsTab.DISPATCHER_STATS,
|
||||||
|
localeKey: 'journal.dispatcher-stats.button',
|
||||||
|
iconName: 'user',
|
||||||
|
disabled: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
JournalOptions,
|
JournalOptions,
|
||||||
JournalDispatchersList,
|
JournalHeader,
|
||||||
JournalHeader
|
JournalStats,
|
||||||
|
JournalDispatchersList
|
||||||
},
|
},
|
||||||
name: 'JournalDispatchers',
|
name: 'JournalDispatchers',
|
||||||
|
|
||||||
@@ -65,6 +80,8 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
statsButtons,
|
||||||
|
|
||||||
currentQuery: '',
|
currentQuery: '',
|
||||||
currentQueryArray: [] as string[],
|
currentQueryArray: [] as string[],
|
||||||
dataRefreshedAt: null as Date | null,
|
dataRefreshedAt: null as Date | null,
|
||||||
@@ -89,7 +106,7 @@ export default defineComponent({
|
|||||||
'search-dispatcher': '',
|
'search-dispatcher': '',
|
||||||
'search-station': '',
|
'search-station': '',
|
||||||
'search-date': ''
|
'search-date': ''
|
||||||
} as Journal.DispatcherSearcher);
|
} as Journal.DispatcherSearchType);
|
||||||
|
|
||||||
const countFromIndex = ref(0);
|
const countFromIndex = ref(0);
|
||||||
const countLimit = 15;
|
const countLimit = 15;
|
||||||
@@ -102,7 +119,7 @@ export default defineComponent({
|
|||||||
const scrollElement: Ref<HTMLElement | null> = ref(null);
|
const scrollElement: Ref<HTMLElement | null> = ref(null);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
store: useMainStore(),
|
mainStore: useMainStore(),
|
||||||
|
|
||||||
sorterActive,
|
sorterActive,
|
||||||
searchersValues,
|
searchersValues,
|
||||||
@@ -120,6 +137,15 @@ export default defineComponent({
|
|||||||
this.currentOptionsActive =
|
this.currentOptionsActive =
|
||||||
q.length > 2 ||
|
q.length > 2 ||
|
||||||
q.some((qv) => qv.startsWith('sortBy=') && qv.split('=')[1] != 'timestampFrom');
|
q.some((qv) => qv.startsWith('sortBy=') && qv.split('=')[1] != 'timestampFrom');
|
||||||
|
},
|
||||||
|
|
||||||
|
'mainStore.dispatcherStatsData'(stats) {
|
||||||
|
this.statsButtons.find((sb) => sb.tab == Journal.StatsTab.DISPATCHER_STATS)!.disabled =
|
||||||
|
stats === undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
async 'mainStore.dispatcherStatsName'() {
|
||||||
|
this.fetchDispatcherStats();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -142,6 +168,16 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
handleRouteParams() {
|
||||||
|
this.$router.push({
|
||||||
|
query: {
|
||||||
|
'search-date': this.searchersValues['search-date'] || undefined,
|
||||||
|
'search-station': this.searchersValues['search-station'] || undefined,
|
||||||
|
'search-dispatcher': this.searchersValues['search-dispatcher'] || undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
handleScroll(e: Event) {
|
handleScroll(e: Event) {
|
||||||
const listElement = e.target as HTMLElement;
|
const listElement = e.target as HTMLElement;
|
||||||
const scrollTop = listElement.scrollTop;
|
const scrollTop = listElement.scrollTop;
|
||||||
@@ -154,24 +190,44 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleQueries(query: LocationQuery) {
|
handleQueries(query: LocationQuery) {
|
||||||
const queryKeys = Object.keys(query);
|
this.setOptions(query as any);
|
||||||
|
|
||||||
if (queryKeys.includes('sceneryName')) this.setSearchers('', `${query.sceneryName}`, '');
|
|
||||||
if (queryKeys.includes('dispatcherName'))
|
|
||||||
this.setSearchers('', '', `${query.dispatcherName}`);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setSearchers(date: string, station: string, dispatcher: string) {
|
async fetchDispatcherStats() {
|
||||||
this.searchersValues['search-date'] = date;
|
if (!this.mainStore.dispatcherStatsName) {
|
||||||
this.searchersValues['search-station'] = station;
|
this.mainStore.dispatcherStatsData = undefined;
|
||||||
this.searchersValues['search-dispatcher'] = dispatcher;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const statsData: API.DispatcherStats.Response = await (
|
||||||
|
await http.get('api/getDispatcherStats', {
|
||||||
|
params: {
|
||||||
|
name: this.mainStore.dispatcherStatsName
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
this.mainStore.dispatcherStatsData = statsData;
|
||||||
|
} catch (error) {
|
||||||
|
this.mainStore.dispatcherStatsData = undefined;
|
||||||
|
|
||||||
|
console.error('Ups! Wystąpił błąd przy próbie pobrania statystyk dyżurnego! :/');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setOptions(options: { [key: string]: string }) {
|
||||||
|
this.searchersValues['search-date'] = options['search-date'] ?? '';
|
||||||
|
this.searchersValues['search-station'] = options['search-station'] ?? '';
|
||||||
|
this.searchersValues['search-dispatcher'] = options['search-dispatcher'] ?? '';
|
||||||
|
|
||||||
|
this.sorterActive.id =
|
||||||
|
(options['sorter-active'] as Journal.DispatcherSorterKey) ?? 'timestampFrom';
|
||||||
},
|
},
|
||||||
|
|
||||||
resetOptions() {
|
resetOptions() {
|
||||||
this.setSearchers('', '', '');
|
this.setOptions({});
|
||||||
this.sorterActive.id = 'timestampFrom';
|
this.sorterActive.id = 'timestampFrom';
|
||||||
|
|
||||||
this.fetchHistoryData();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async addHistoryData() {
|
async addHistoryData() {
|
||||||
@@ -241,7 +297,7 @@ export default defineComponent({
|
|||||||
this.historyList = responseData;
|
this.historyList = responseData;
|
||||||
|
|
||||||
// Stats display
|
// Stats display
|
||||||
this.store.dispatcherStatsName =
|
this.mainStore.dispatcherStatsName =
|
||||||
this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim()
|
this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim()
|
||||||
? this.historyList[0].dispatcherName
|
? this.historyList[0].dispatcherName
|
||||||
: '';
|
: '';
|
||||||
|
|||||||
+135
-72
@@ -3,18 +3,19 @@
|
|||||||
<JournalHeader />
|
<JournalHeader />
|
||||||
|
|
||||||
<div class="journal_wrapper">
|
<div class="journal_wrapper">
|
||||||
<JournalOptions
|
<div class="journal_top-bar">
|
||||||
@on-search-confirm="fetchHistoryData"
|
<JournalOptions
|
||||||
@on-options-reset="resetOptions"
|
@onOptionsReset="resetOptions"
|
||||||
@on-refresh-data="fetchHistoryData"
|
@onRefreshData="fetchHistoryData"
|
||||||
:sorter-option-ids="['timetableId', 'beginDate', 'routeDistance', 'allStopsCount']"
|
:sorter-option-ids="['timetableId', 'beginDate', 'routeDistance', 'allStopsCount']"
|
||||||
:filters="journalTimetableFilters"
|
:filters="journalTimetableFilters"
|
||||||
:currentOptionsActive="currentOptionsActive"
|
:currentOptionsActive="currentOptionsActive"
|
||||||
:data-status="dataStatus"
|
:data-status="dataStatus"
|
||||||
optionsType="timetables"
|
optionsType="timetables"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<JournalStats />
|
<JournalStats :statsButtons="statsButtons" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="journal_refreshed-date" v-if="dataRefreshedAt">
|
<div class="journal_refreshed-date" v-if="dataRefreshedAt">
|
||||||
{{ $t('journal.data-refreshed-at') }}: {{ dataRefreshedAt.toLocaleString($i18n.locale) }}
|
{{ $t('journal.data-refreshed-at') }}: {{ dataRefreshedAt.toLocaleString($i18n.locale) }}
|
||||||
@@ -56,45 +57,58 @@ import http from '../http';
|
|||||||
|
|
||||||
export const journalTimetableFilters: Journal.TimetableFilter[] = [
|
export const journalTimetableFilters: Journal.TimetableFilter[] = [
|
||||||
{
|
{
|
||||||
id: Journal.TimetableFilterId.ALL,
|
id: Journal.TimetableFilterId.ALL_STATUSES,
|
||||||
filterSection: Journal.FilterSection.TIMETABLE_STATUS,
|
filterSection: Journal.FilterSection.TIMETABLE_STATUS,
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
default: true
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: Journal.TimetableFilterId.ACTIVE,
|
id: Journal.TimetableFilterId.ACTIVE,
|
||||||
filterSection: Journal.FilterSection.TIMETABLE_STATUS,
|
filterSection: Journal.FilterSection.TIMETABLE_STATUS,
|
||||||
isActive: false
|
isActive: false,
|
||||||
|
default: false
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: Journal.TimetableFilterId.FULFILLED,
|
id: Journal.TimetableFilterId.FULFILLED,
|
||||||
filterSection: Journal.FilterSection.TIMETABLE_STATUS,
|
filterSection: Journal.FilterSection.TIMETABLE_STATUS,
|
||||||
isActive: false
|
isActive: false,
|
||||||
|
default: false
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: Journal.TimetableFilterId.ABANDONED,
|
id: Journal.TimetableFilterId.ABANDONED,
|
||||||
filterSection: Journal.FilterSection.TIMETABLE_STATUS,
|
filterSection: Journal.FilterSection.TIMETABLE_STATUS,
|
||||||
isActive: false
|
isActive: false,
|
||||||
|
default: false
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: Journal.TimetableFilterId.TWR_SKR,
|
id: Journal.TimetableFilterId.ALL_SPECIALS,
|
||||||
filterSection: Journal.FilterSection.TWRSKR,
|
filterSection: Journal.FilterSection.SPECIAL,
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
default: true
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: Journal.TimetableFilterId.TWR,
|
id: Journal.TimetableFilterId.TWR,
|
||||||
filterSection: Journal.FilterSection.TWRSKR,
|
filterSection: Journal.FilterSection.SPECIAL,
|
||||||
isActive: false
|
isActive: false,
|
||||||
|
default: false
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: Journal.TimetableFilterId.SKR,
|
id: Journal.TimetableFilterId.SKR,
|
||||||
filterSection: Journal.FilterSection.TWRSKR,
|
filterSection: Journal.FilterSection.SPECIAL,
|
||||||
isActive: false
|
isActive: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: Journal.TimetableFilterId.TWR_SKR,
|
||||||
|
filterSection: Journal.FilterSection.SPECIAL,
|
||||||
|
isActive: false,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -104,8 +118,12 @@ interface TimetablesQueryParams {
|
|||||||
timetableId?: string;
|
timetableId?: string;
|
||||||
|
|
||||||
authorName?: string;
|
authorName?: string;
|
||||||
timestampFrom?: number;
|
// timestampFrom?: number;
|
||||||
timestampTo?: number;
|
// timestampTo?: number;
|
||||||
|
|
||||||
|
dateFrom?: string;
|
||||||
|
dateTo?: string;
|
||||||
|
|
||||||
issuedFrom?: string;
|
issuedFrom?: string;
|
||||||
|
|
||||||
countFrom?: number;
|
countFrom?: number;
|
||||||
@@ -138,6 +156,24 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
journalTimetableFilters,
|
||||||
|
mainStore: useMainStore(),
|
||||||
|
|
||||||
|
statsButtons: [
|
||||||
|
{
|
||||||
|
tab: Journal.StatsTab.DAILY_STATS,
|
||||||
|
localeKey: 'journal.daily-stats.button',
|
||||||
|
iconName: 'stats',
|
||||||
|
disabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tab: Journal.StatsTab.DRIVER_STATS,
|
||||||
|
localeKey: 'journal.driver-stats.button',
|
||||||
|
iconName: 'train',
|
||||||
|
disabled: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
currentQueryParams: {} as TimetablesQueryParams,
|
currentQueryParams: {} as TimetablesQueryParams,
|
||||||
dataRefreshedAt: null as Date | null,
|
dataRefreshedAt: null as Date | null,
|
||||||
|
|
||||||
@@ -149,7 +185,6 @@ export default defineComponent({
|
|||||||
currentOptionsActive: false,
|
currentOptionsActive: false,
|
||||||
|
|
||||||
timetableHistory: [] as API.TimetableHistory.Response,
|
timetableHistory: [] as API.TimetableHistory.Response,
|
||||||
journalTimetableFilters,
|
|
||||||
|
|
||||||
dataStatus: Status.Data.Loading,
|
dataStatus: Status.Data.Loading,
|
||||||
dataErrorMessage: ''
|
dataErrorMessage: ''
|
||||||
@@ -157,10 +192,11 @@ export default defineComponent({
|
|||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const sorterActive: Journal.TimetableSorter = reactive({ id: 'timetableId', dir: 'desc' });
|
const sorterActive: Journal.TimetableSorter = reactive({ id: 'timetableId', dir: 'desc' });
|
||||||
// const journalFilterActive = ref(journalTimetableFilters[0]);
|
|
||||||
const initFilters: readonly Journal.TimetableFilter[] = JSON.parse(
|
const initFilters: readonly Journal.TimetableFilter[] = JSON.parse(
|
||||||
JSON.stringify(journalTimetableFilters)
|
JSON.stringify(journalTimetableFilters)
|
||||||
);
|
);
|
||||||
|
|
||||||
const filterList: Journal.TimetableFilter[] = reactive(JSON.parse(JSON.stringify(initFilters)));
|
const filterList: Journal.TimetableFilter[] = reactive(JSON.parse(JSON.stringify(initFilters)));
|
||||||
|
|
||||||
const searchersValues = reactive({
|
const searchersValues = reactive({
|
||||||
@@ -189,15 +225,22 @@ export default defineComponent({
|
|||||||
countFromIndex,
|
countFromIndex,
|
||||||
countLimit,
|
countLimit,
|
||||||
|
|
||||||
scrollElement,
|
scrollElement
|
||||||
|
|
||||||
store: useMainStore()
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
currentQueryParams(q: TimetablesQueryParams) {
|
currentQueryParams(q: TimetablesQueryParams) {
|
||||||
this.currentOptionsActive = Object.values(q).some((v) => v !== undefined);
|
this.currentOptionsActive = Object.values(q).some((v) => v !== undefined);
|
||||||
|
},
|
||||||
|
|
||||||
|
'mainStore.driverStatsData'(driverStats) {
|
||||||
|
this.statsButtons.find((sb) => sb.tab == Journal.StatsTab.DRIVER_STATS)!.disabled =
|
||||||
|
driverStats === undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
async 'mainStore.driverStatsName'() {
|
||||||
|
this.fetchDriverStats();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -225,42 +268,51 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleQueries(query: LocationQuery) {
|
handleQueries(query: LocationQuery) {
|
||||||
const queryKeys = Object.keys(query);
|
this.setOptions(query as any);
|
||||||
|
|
||||||
if (queryKeys.includes('timetableId'))
|
|
||||||
this.setSearchers('', '', `#${query.timetableId}`, '', '');
|
|
||||||
if (queryKeys.includes('issuedFrom'))
|
|
||||||
this.setSearchers('', '', '', '', `${query.issuedFrom}`);
|
|
||||||
if (queryKeys.includes('authorName'))
|
|
||||||
this.setSearchers('', '', '', `${query.authorName}`, '');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setSearchers(
|
async fetchDriverStats() {
|
||||||
date: string,
|
if (!this.mainStore.driverStatsName) {
|
||||||
driver: string,
|
this.mainStore.driverStatsData = undefined;
|
||||||
train: string,
|
this.mainStore.driverStatsStatus = Status.Data.Initialized;
|
||||||
dispatcher: string,
|
return;
|
||||||
issuedFrom: string
|
}
|
||||||
) {
|
|
||||||
this.searchersValues['search-date'] = date;
|
try {
|
||||||
this.searchersValues['search-driver'] = driver;
|
this.mainStore.driverStatsStatus = Status.Data.Loading;
|
||||||
this.searchersValues['search-train'] = train;
|
|
||||||
this.searchersValues['search-dispatcher'] = dispatcher;
|
const statsData: API.DriverStats.Response = await (
|
||||||
this.searchersValues['search-issuedFrom'] = issuedFrom;
|
await http.get(`api/getDriverInfo?name=${this.mainStore.driverStatsName}`)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
this.mainStore.driverStatsData = statsData;
|
||||||
|
this.mainStore.driverStatsStatus = Status.Data.Loaded;
|
||||||
|
} catch (error) {
|
||||||
|
this.mainStore.driverStatsData = undefined;
|
||||||
|
this.mainStore.driverStatsStatus = Status.Data.Error;
|
||||||
|
console.error('Ups! Wystąpił błąd przy próbie pobrania statystyk maszynisty! :/');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setOptions(options: { [key: string]: string }) {
|
||||||
|
this.searchersValues['search-date'] = options['search-date'] ?? '';
|
||||||
|
this.searchersValues['search-driver'] = options['search-driver'] ?? '';
|
||||||
|
this.searchersValues['search-train'] = options['search-train'] ?? '';
|
||||||
|
this.searchersValues['search-dispatcher'] = options['search-dispatcher'] ?? '';
|
||||||
|
this.searchersValues['search-issuedFrom'] = options['search-issuedFrom'] ?? '';
|
||||||
|
|
||||||
|
this.sorterActive.id =
|
||||||
|
(options['sorter-active'] as Journal.TimetableSorterKey) ?? 'timetableId';
|
||||||
|
|
||||||
|
this.filterList.forEach((f) => {
|
||||||
|
f.isActive =
|
||||||
|
options[f.filterSection] === f.id ||
|
||||||
|
(options[f.filterSection] === undefined && f.default);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
resetOptions() {
|
resetOptions() {
|
||||||
this.setSearchers('', '', '', '', '');
|
this.setOptions({});
|
||||||
|
|
||||||
this.sorterActive.id = 'timetableId';
|
|
||||||
|
|
||||||
this.filterList.forEach(
|
|
||||||
(f) =>
|
|
||||||
(f.isActive =
|
|
||||||
this.initFilters.find((initFilter) => initFilter.id == f.id)?.isActive || false)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.fetchHistoryData();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async addHistoryData() {
|
async addHistoryData() {
|
||||||
@@ -289,13 +341,19 @@ export default defineComponent({
|
|||||||
const driverName = this.searchersValues['search-driver'].trim() || undefined;
|
const driverName = this.searchersValues['search-driver'].trim() || undefined;
|
||||||
const trainNo = this.searchersValues['search-train'].trim() || undefined;
|
const trainNo = this.searchersValues['search-train'].trim() || undefined;
|
||||||
const authorName = this.searchersValues['search-dispatcher'].trim() || undefined;
|
const authorName = this.searchersValues['search-dispatcher'].trim() || undefined;
|
||||||
const dateString = this.searchersValues['search-date'].trim() || undefined;
|
const dateFrom = this.searchersValues['search-date'].trim() || undefined;
|
||||||
const issuedFrom = this.searchersValues['search-issuedFrom'].trim() || undefined;
|
const issuedFrom = this.searchersValues['search-issuedFrom'].trim() || undefined;
|
||||||
|
|
||||||
const timestampFrom = dateString
|
let dateTo: string | undefined = undefined;
|
||||||
? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000
|
|
||||||
: undefined;
|
if (dateFrom) {
|
||||||
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
|
const d = new Date(dateFrom);
|
||||||
|
d.setDate(d.getDate() + 1);
|
||||||
|
|
||||||
|
dateTo = d.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
// const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) : undefined;
|
||||||
|
// const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
|
||||||
|
|
||||||
const queryParams: TimetablesQueryParams = {};
|
const queryParams: TimetablesQueryParams = {};
|
||||||
|
|
||||||
@@ -318,23 +376,28 @@ export default defineComponent({
|
|||||||
queryParams['fulfilled'] = 1;
|
queryParams['fulfilled'] = 1;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Journal.TimetableFilterId.ALL:
|
case Journal.TimetableFilterId.ALL_STATUSES:
|
||||||
queryParams['terminated'] = undefined;
|
queryParams['terminated'] = undefined;
|
||||||
queryParams['fulfilled'] = undefined;
|
queryParams['fulfilled'] = undefined;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Journal.TimetableFilterId.TWR_SKR:
|
case Journal.TimetableFilterId.ALL_SPECIALS:
|
||||||
queryParams['twr'] = undefined;
|
queryParams['twr'] = undefined;
|
||||||
queryParams['skr'] = undefined;
|
queryParams['skr'] = undefined;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Journal.TimetableFilterId.TWR:
|
case Journal.TimetableFilterId.TWR:
|
||||||
queryParams['twr'] = 1;
|
queryParams['twr'] = 1;
|
||||||
queryParams['skr'] = undefined;
|
queryParams['skr'] = 0;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Journal.TimetableFilterId.SKR:
|
case Journal.TimetableFilterId.SKR:
|
||||||
queryParams['twr'] = undefined;
|
queryParams['twr'] = 0;
|
||||||
|
queryParams['skr'] = 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Journal.TimetableFilterId.TWR_SKR:
|
||||||
|
queryParams['twr'] = 1;
|
||||||
queryParams['skr'] = 1;
|
queryParams['skr'] = 1;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -349,8 +412,8 @@ export default defineComponent({
|
|||||||
queryParams['countLimit'] = undefined;
|
queryParams['countLimit'] = undefined;
|
||||||
|
|
||||||
queryParams['authorName'] = authorName;
|
queryParams['authorName'] = authorName;
|
||||||
queryParams['timestampFrom'] = timestampFrom;
|
queryParams['dateFrom'] = dateFrom;
|
||||||
queryParams['timestampTo'] = timestampTo;
|
queryParams['dateTo'] = dateTo;
|
||||||
queryParams['issuedFrom'] = issuedFrom;
|
queryParams['issuedFrom'] = issuedFrom;
|
||||||
queryParams['sortBy'] =
|
queryParams['sortBy'] =
|
||||||
this.sorterActive.id != 'timetableId' ? this.sorterActive.id : undefined;
|
this.sorterActive.id != 'timetableId' ? this.sorterActive.id : undefined;
|
||||||
|
|||||||
@@ -1,16 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="scenery-view">
|
<div class="scenery-view">
|
||||||
<!-- <div
|
|
||||||
class="scenery-offline"
|
|
||||||
v-if="!stationInfo && !onlineSceneryInfo && store.dataStatuses.sceneries == 2"
|
|
||||||
>
|
|
||||||
<div>{{ $t('scenery.no-scenery') }}</div>
|
|
||||||
|
|
||||||
<action-button>
|
|
||||||
<router-link to="/">{{ $t('scenery.return-btn') }}</router-link>
|
|
||||||
</action-button>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<div class="scenery-wrapper" ref="card-wrapper">
|
<div class="scenery-wrapper" ref="card-wrapper">
|
||||||
<div class="scenery-left">
|
<div class="scenery-left">
|
||||||
<div class="scenery-actions">
|
<div class="scenery-actions">
|
||||||
@@ -43,7 +32,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
apiStore.dataStatuses.sceneries == Status.Loading ||
|
apiStore.dataStatuses.sceneries == Status.Loading ||
|
||||||
apiStore.dataStatuses.trains == Status.Loading
|
apiStore.dataStatuses.connection == Status.Loading
|
||||||
"
|
"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -6,20 +6,22 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
port: 5001
|
port: 5001
|
||||||
},
|
},
|
||||||
|
publicDir: 'public',
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
VitePWA({
|
VitePWA({
|
||||||
registerType: 'autoUpdate',
|
registerType: 'autoUpdate',
|
||||||
|
includeAssets: ['/images/*.png', '/fonts/*.woff', '/fonts/*.woff2'],
|
||||||
|
|
||||||
workbox: {
|
workbox: {
|
||||||
|
disableDevLogs: true,
|
||||||
globPatterns: ['**/*.{js,css,html,png,svg,jpg}'],
|
globPatterns: ['**/*.{js,css,html,png,svg,jpg}'],
|
||||||
runtimeCaching: [
|
runtimeCaching: [
|
||||||
{
|
{
|
||||||
urlPattern: new RegExp('^https://stacjownik.spythere.pl/api/getSceneries', 'i'),
|
urlPattern: new RegExp('^https://stacjownik.spythere.eu/api/getSceneries', 'i'),
|
||||||
handler: 'NetworkFirst',
|
handler: 'NetworkFirst',
|
||||||
options: {
|
options: {
|
||||||
cacheName: 'sceneries-cache',
|
cacheName: 'sceneries-cache',
|
||||||
|
|
||||||
cacheableResponse: {
|
cacheableResponse: {
|
||||||
statuses: [0, 200]
|
statuses: [0, 200]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user