refactor(journal): removed seperate driver & dispatcher stats dropdowns; added button leading to player profile view

This commit is contained in:
2026-02-08 22:00:15 +01:00
parent 1365140802
commit 0f8932b53c
9 changed files with 57 additions and 386 deletions
@@ -1,85 +0,0 @@
<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="badge stat-badge" v-if="stats.services">
<span>{{ $t('journal.dispatcher-stats.services-count') }}</span>
<span>{{ stats.services.count }}</span>
</span>
<span class="badge stat-badge" v-if="stats.services">
<span>{{ $t('journal.dispatcher-stats.service-max') }}</span>
<span>{{ calculateDuration(stats.services.durationMax) }}</span>
</span>
<span class="badge 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" v-if="stats.issuedTimetables" />
<div class="info-stats" v-if="stats.issuedTimetables">
<span class="badge stat-badge">
<span>{{ $t('journal.dispatcher-stats.timetables-count') }}</span>
<span>{{ stats.issuedTimetables.count }}</span>
</span>
<span class="badge stat-badge">
<span>{{ $t('journal.dispatcher-stats.timetables-sum') }}</span>
<span>{{ stats.issuedTimetables.distanceSum.toFixed(2) }}km</span>
</span>
<span class="badge stat-badge">
<span>{{ $t('journal.dispatcher-stats.timetables-max') }}</span>
<span>{{ stats.issuedTimetables.distanceMax.toFixed(2) }}km</span>
</span>
<span class="badge stat-badge">
<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>
@use '../../../styles/journal-stats';
</style>
+35 -52
View File
@@ -2,87 +2,70 @@
<div
class="journal-stats dropdown"
v-if="!mainStore.isOffline"
@keydown.esc="currentStatsTab = null"
@keydown.esc="isDropdownOpen = false"
>
<div
class="dropdown_background"
v-if="currentStatsTab !== null"
@click="currentStatsTab = null"
></div>
<div class="dropdown_background" v-if="isDropdownOpen" @click="isDropdownOpen = false"></div>
<div class="actions-bar">
<button class="btn--filled btn--image" @click="toggleDropdown">
<img :src="`/images/icon-stats.svg`" alt="stats icon" />
{{ $t('journal.daily-stats.button') }}
</button>
<button
v-for="button in statsButtons"
:key="button.tab"
class="btn--filled btn--image"
:data-selected="button.tab == currentStatsTab"
:data-disabled="button.disabled"
:disabled="button.disabled"
@click="onTabButtonClick(button.tab)"
:data-disabled="chosenPlayerId == -1"
@click="navigateToProfile"
>
<img
v-if="button.iconName"
:src="`/images/icon-${button.iconName}.svg`"
:alt="button.iconName"
/>
{{ $t(button.localeKey) }}
<img :src="`/images/icon-user.svg`" alt="user icon" />
{{ $t('profile.journal-button') }}
</button>
</div>
<transition name="dropdown-anim">
<div
class="dropdown_wrapper"
:class="{ 'dropdown-align-right': true }"
v-if="currentStatsTab !== null"
>
<div class="dropdown_wrapper" v-if="isDropdownOpen">
<keep-alive>
<component :is="currentStatsTab" :key="currentStatsTab"></component>
<JournalDailyStats />
</keep-alive>
</div>
</transition>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
<script lang="ts" setup>
import { ref } from 'vue';
import { useMainStore } from '../../store/mainStore';
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';
import { useRouter } from 'vue-router';
export default defineComponent({
components: { JournalDailyStats, JournalDriverStats, JournalDispatcherStats },
props: {
statsButtons: {
type: Array as PropType<Journal.StatsButton[]>,
required: true
}
},
data() {
return {
Journal,
mainStore: useMainStore(),
currentStatsTab: null as Journal.StatsTab | null
};
},
const router = useRouter();
methods: {
onTabButtonClick(tab: Journal.StatsTab) {
this.currentStatsTab = tab == this.currentStatsTab ? null : tab;
StorageManager.setStringValue('journalStatsTab', this.currentStatsTab ?? '');
}
const props = defineProps({
chosenPlayerId: {
type: Number,
required: true
}
});
const mainStore = useMainStore();
const isDropdownOpen = ref(false);
function toggleDropdown() {
isDropdownOpen.value = !isDropdownOpen.value;
}
function navigateToProfile() {
if (props.chosenPlayerId == -1) return;
router.push(`/profile?playerId=${props.chosenPlayerId}`);
}
</script>
<style lang="scss" scoped>
@use '../../styles/dropdown';
@use '../../styles/dropdown-filters';
.dropdown_wrapper.dropdown-align-right {
.dropdown_wrapper {
left: auto;
right: 0;
max-width: 700px;
@@ -1,105 +0,0 @@
<template>
<div class="journal-stats driver" v-if="store.driverStatsData">
<span>
<h3>
<i18n-t keypath="journal.driver-stats.title">
<template #name>
<span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
</template>
</i18n-t>
</h3>
<hr class="header-separator" />
<div class="info-stats">
<span class="badge stat-badge">
<span>{{ $t('journal.driver-stats.longest-timetable') }}</span>
<span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span>
</span>
<span class="badge stat-badge">
<span>{{ $t('journal.driver-stats.avg-timetable') }}</span>
<span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
</span>
</div>
<hr class="section-separator" />
<div class="info-stats">
<span class="badge stat-badge">
<span>{{ $t('journal.driver-stats.timetables') }}</span>
<span>
{{ store.driverStatsData._count.fulfilled }} /
{{ store.driverStatsData._count._all }}
<template v-if="store.driverStatsData._count._all > 0">
({{
(
(store.driverStatsData._count.fulfilled / store.driverStatsData._count._all) *
100
).toFixed(2)
}}%)
</template>
</span>
</span>
<span class="badge stat-badge">
<span>{{ $t('journal.driver-stats.distance') }}</span>
<span>
{{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km
<template v-if="store.driverStatsData._sum.routeDistance > 0">
({{
(
(store.driverStatsData._sum.currentDistance /
store.driverStatsData._sum.routeDistance) *
100
).toFixed(2)
}}%)
</template>
</span>
</span>
<span class="badge stat-badge">
<span>{{ $t('journal.driver-stats.stations') }}</span>
<span>
{{ store.driverStatsData._sum.confirmedStopsCount }} /
{{ store.driverStatsData._sum.allStopsCount }}
<template v-if="store.driverStatsData._sum.allStopsCount > 0">
({{
(
(store.driverStatsData._sum.confirmedStopsCount /
store.driverStatsData._sum.allStopsCount) *
100
).toFixed(2)
}}%)
</template>
</span>
</span>
</div>
</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../../../store/mainStore';
import { Status } from '../../../typings/common';
export default defineComponent({
name: 'journal-driver-stats',
data() {
return {
store: useMainStore(),
Status: Status
};
}
});
</script>
<style lang="scss" scoped>
@use '../../../styles/journal-stats';
</style>
-13
View File
@@ -62,19 +62,6 @@ export namespace Journal {
default: boolean;
}
export enum StatsTab {
DRIVER_STATS = 'journal-driver-stats',
DISPATCHER_STATS = 'journal-dispatcher-stats',
DAILY_STATS = 'journal-daily-stats'
}
export interface StatsButton {
tab: StatsTab;
localeKey: string;
iconName: string;
disabled: boolean;
}
export interface TimetableStopDetails {
stopName: string;
arrivalTimestamp: number;
+1
View File
@@ -605,6 +605,7 @@
"desc-terminated": "Pociąg zakończył bieg"
},
"profile": {
"journal-button": "PROFIL GRACZA",
"filters": {
"Timetable": "ROZKŁADY JAZDY",
"Dispatcher": "SŁUŻBY DYŻURNEGO",
+1 -9
View File
@@ -26,20 +26,12 @@ export const useMainStore = defineStore('mainStore', {
isOffline: false,
appUpdate: null,
dispatcherStatsName: '',
dispatcherStatsStatus: Status.Data.Initialized,
driverStatsName: '',
driverStatsData: undefined,
driverStatsStatus: Status.Data.Initialized,
chosenModalTrainId: undefined,
modalLastClickedTarget: null,
currentLocale: 'pl',
isMigrateInfoCardOpen: false,
pinnedStationNames: []
isMigrateInfoCardOpen: false
}) as MainStoreState,
actions: {
-5
View File
@@ -5,11 +5,6 @@ export interface MainStoreState {
region: { id: string; value: string; name: string };
isOffline: boolean;
appUpdate: { version: string; changelog: string; releaseURL: string } | null;
dispatcherStatsName: string;
dispatcherStatsData?: API.DispatcherStats.Response;
driverStatsName: string;
driverStatsData?: API.DriverStats.Response;
driverStatsStatus: Status.Data;
chosenModalTrainId?: string;
modalLastClickedTarget: EventTarget | null;
currentLocale: string;
+10 -54
View File
@@ -14,7 +14,7 @@
optionsType="dispatchers"
/>
<JournalStats :statsButtons="statsButtons" />
<JournalStats :chosen-player-id="chosenPlayerId" />
</div>
<div class="journal_refreshed-date">
@@ -50,15 +50,6 @@ import JournalHeader from '../components/JournalView/JournalHeader.vue';
import JournalStats from '../components/JournalView/JournalStats.vue';
import { useApiStore } from '../store/apiStore';
const statsButtons: Journal.StatsButton[] = [
{
tab: Journal.StatsTab.DISPATCHER_STATS,
localeKey: 'journal.dispatcher-stats.button',
iconName: 'user',
disabled: true
}
];
interface DispatchersQueryParams {
dispatcherName?: string;
stationName?: string;
@@ -105,18 +96,15 @@ export default defineComponent({
},
data: () => ({
statsButtons,
dataRefreshedAt: null as Date | null,
currentQueryParams: {} as DispatchersQueryParams,
scrollDataLoaded: true,
scrollNoMoreData: false,
showReturnButton: false,
statsCardOpen: false,
currentOptionsActive: false,
chosenPlayerId: -1,
currentOptionsActive: false,
dataStatus: Status.Data.Loading,
historyList: [] as API.DispatcherHistory.Response
@@ -158,15 +146,6 @@ export default defineComponent({
queryParams[k as keyof DispatchersQueryParams] !=
defaultQueryParams[k as keyof DispatchersQueryParams]
);
},
'mainStore.dispatcherStatsData'(stats) {
this.statsButtons.find((sb) => sb.tab == Journal.StatsTab.DISPATCHER_STATS)!.disabled =
stats === undefined;
},
async 'mainStore.dispatcherStatsName'() {
this.fetchDispatcherStats();
}
},
@@ -215,29 +194,6 @@ export default defineComponent({
this.setOptions(query as any);
},
async fetchDispatcherStats() {
if (!this.mainStore.dispatcherStatsName) {
this.mainStore.dispatcherStatsData = undefined;
return;
}
try {
const statsData: API.DispatcherStats.Response = await (
await this.apiStore.client!.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-from'] = options['search-date-from'] ?? '';
this.searchersValues['search-date-to'] = options['search-date-to'] ?? '';
@@ -320,24 +276,24 @@ export default defineComponent({
if (!responseData) {
this.dataStatus = Status.Data.Error;
this.chosenPlayerId = -1;
return;
}
if (!responseData) return;
// Response data exists
this.historyList = responseData;
// Stats display
this.mainStore.dispatcherStatsName =
this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim()
? this.historyList[0].dispatcherName
: '';
this.chosenPlayerId =
this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim() != ''
? this.historyList[0].dispatcherId
: -1;
this.dataRefreshedAt = new Date();
this.dataStatus = Status.Data.Loaded;
} catch (error) {
this.dataStatus = Status.Data.Error;
this.chosenPlayerId = -1;
}
this.scrollNoMoreData = false;
+10 -63
View File
@@ -14,7 +14,7 @@
optionsType="timetables"
/>
<JournalStats :statsButtons="statsButtons" />
<JournalStats :chosen-player-id="chosenPlayerId" />
</div>
<div class="journal_refreshed-date">
@@ -170,35 +170,19 @@ export default defineComponent({
mainStore: useMainStore(),
apiStore: useApiStore(),
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,
dataRefreshedAt: null as Date | null,
scrollDataLoaded: true,
scrollNoMoreData: false,
showReturnButton: false,
statsCardOpen: false,
chosenPlayerId: -1,
currentOptionsActive: false,
timetableHistory: [] as API.TimetableHistory.Response,
dataStatus: Status.Data.Loading,
dataErrorMessage: ''
dataStatus: Status.Data.Loading
}),
setup() {
@@ -248,15 +232,6 @@ export default defineComponent({
watch: {
currentQueryParams(q: TimetablesQueryParams) {
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();
}
},
@@ -287,31 +262,6 @@ export default defineComponent({
this.setOptions(query as any);
},
async fetchDriverStats() {
if (!this.mainStore.driverStatsName) {
this.mainStore.driverStatsData = undefined;
this.mainStore.driverStatsStatus = Status.Data.Initialized;
return;
}
try {
this.mainStore.driverStatsStatus = Status.Data.Loading;
const statsData: API.DriverStats.Response = await (
await this.apiStore.client!.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 }) {
Object.keys(this.searchersValues).forEach((v) => {
this.searchersValues[v as Journal.TimetableSearchKey] = options[v] ?? '';
@@ -464,26 +414,23 @@ export default defineComponent({
if (!responseData) {
this.dataStatus = Status.Data.Error;
this.dataErrorMessage = 'Brak danych!';
this.chosenPlayerId = -1;
return;
}
if (!responseData) return;
// Response data exists
this.timetableHistory = responseData;
// Stats display
this.mainStore.driverStatsName =
this.timetableHistory.length > 0 && this.searchersValues['search-driver'].trim()
? this.timetableHistory[0].driverName
: '';
this.chosenPlayerId =
this.timetableHistory.length > 0 && this.searchersValues['search-driver'].trim() != ''
? this.timetableHistory[0].driverId
: -1;
this.dataStatus = Status.Data.Loaded;
this.dataRefreshedAt = new Date();
} catch (error) {
this.dataStatus = Status.Data.Error;
this.dataErrorMessage = 'Ups! Coś poszło nie tak!';
this.chosenPlayerId = -1;
}
this.scrollNoMoreData = false;