zmiana wyglądu statystyk dzienników

This commit is contained in:
2023-12-14 18:42:13 +01:00
parent 5a651aedf8
commit e0d3d2585d
15 changed files with 306 additions and 229 deletions
+115 -104
View File
@@ -1,128 +1,138 @@
<template>
<section class="daily-stats">
<span :data-active="statsStatus">
<b v-if="statsStatus == Status.Data.Loading">
{{ $t('app.loading') }}
</b>
<span class="stats-list" v-else>
<span class="stats-list">
<h3>
{{ $t('journal.daily-stats-title') }}
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
</h3>
<hr style="margin-bottom: 0.5em" />
<hr class="header-separator" />
<div v-if="stats.totalTimetables">
&bull;
<i18n-t keypath="journal.timetable-stats-total">
<template #count>
<b class="text--primary">
{{ stats.totalTimetables }}
{{ $t('journal.timetable-count', stats.totalTimetables) }}
</b>
</template>
<b v-if="statsStatus == Status.Data.Loading">
{{ $t('app.loading') }}
</b>
<template #distance>
<b class="text--primary"> {{ stats.distanceSum?.toFixed(2) }} km</b>
</template>
</i18n-t>
</div>
<b class="text--error" v-else-if="statsStatus == Status.Data.Error">
{{ $t('journal.stats-error') }}
</b>
<div v-if="stats.maxTimetable">
&bull;
<i18n-t keypath="journal.timetable-stats-longest">
<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>
<b v-else-if="topDispatchers.length == 0">
{{ $t('journal.daily-stats-info') }}
</b>
<div v-if="topDispatchers.length == 1">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active-dr">
<template #dispatcher>
<router-link :to="`/journal/dispatchers?dispatcherName=${topDispatchers[0].name}`">
<b>{{ topDispatchers[0].name }}</b>
</router-link>
</template>
<template #count>
<b class="text--primary">
{{ topDispatchers[0].count }}
{{ $t('journal.timetable-count', topDispatchers[0].count) }}
</b>
</template>
</i18n-t>
</div>
<div v-else>
<div v-if="stats.totalTimetables">
&bull;
<i18n-t keypath="journal.timetable-stats-total">
<template #count>
<b class="text--primary">
{{ stats.totalTimetables }}
{{ $t('journal.timetable-count', stats.totalTimetables) }}
</b>
</template>
<div v-if="topDispatchers.length > 1">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active-dr-many">
<template #dispatchers>
<span v-for="(disp, i) in topDispatchers" :key="i">
<span v-if="i == topDispatchers.length - 1"> {{ $t('general.and') }} </span>
<template #distance>
<b class="text--primary"> {{ stats.distanceSum?.toFixed(2) }} km</b>
</template>
</i18n-t>
</div>
<router-link :to="`/journal/dispatchers?dispatcherName=${disp.name}`">
<b>{{ disp.name }}</b>
<div v-if="stats.maxTimetable">
&bull;
<i18n-t keypath="journal.timetable-stats-longest">
<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>
<span v-if="i < topDispatchers.length - 2">, </span>
</span>
</template>
<div v-if="topDispatchers.length == 1">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active-dr">
<template #dispatcher>
<router-link :to="`/journal/dispatchers?dispatcherName=${topDispatchers[0].name}`">
<b>{{ topDispatchers[0].name }}</b>
</router-link>
</template>
<template #count>
<b class="text--primary">
{{ topDispatchers[0].count }}
{{ $t('journal.timetable-count', topDispatchers[0].count) }}
</b>
</template>
</i18n-t>
</div>
<template #count>
<b class="text--primary">
{{ topDispatchers[0].count }}
{{ $t('journal.timetable-count', topDispatchers[0].count) }}
</b>
</template>
</i18n-t>
</div>
<div v-if="topDispatchers.length > 1">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active-dr-many">
<template #dispatchers>
<span v-for="(disp, i) in topDispatchers" :key="i">
<span v-if="i == topDispatchers.length - 1"> {{ $t('general.and') }} </span>
<div v-if="stats.longestDuties.length > 0">
&bull;
<i18n-t keypath="journal.timetable-stats-longest-duties">
<template #dispatcher>
<router-link
:to="`/journal/dispatchers?dispatcherName=${stats.longestDuties[0].name}`"
>
<b>{{ stats.longestDuties[0].name }}</b>
</router-link>
</template>
<router-link :to="`/journal/dispatchers?dispatcherName=${disp.name}`">
<b>{{ disp.name }}</b>
</router-link>
<template #station>{{ stats.longestDuties[0].station }}</template>
<span v-if="i < topDispatchers.length - 2">, </span>
</span>
</template>
<template #duration>
{{ calculateDuration(stats.longestDuties[0].duration) }}
</template>
</i18n-t>
</div>
<template #count>
<b class="text--primary">
{{ topDispatchers[0].count }}
{{ $t('journal.timetable-count', topDispatchers[0].count) }}
</b>
</template>
</i18n-t>
</div>
<div v-if="stats.mostActiveDrivers.length > 0">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active-driver">
<template #driver>
<b class="text--primary">{{ stats.mostActiveDrivers[0].name }}</b>
</template>
<template #distance>
<b class="text--primary">{{ stats.mostActiveDrivers[0].distance.toFixed(2) }} km</b>
</template>
</i18n-t>
<div v-if="stats.longestDuties.length > 0">
&bull;
<i18n-t keypath="journal.timetable-stats-longest-duties">
<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>
<template #duration>
{{ calculateDuration(stats.longestDuties[0].duration) }}
</template>
</i18n-t>
</div>
<div v-if="stats.mostActiveDrivers.length > 0">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active-driver">
<template #driver>
<b class="text--primary">{{ stats.mostActiveDrivers[0].name }}</b>
</template>
<template #distance>
<b class="text--primary">{{ stats.mostActiveDrivers[0].distance.toFixed(2) }} km</b>
</template>
</i18n-t>
</div>
</div>
</span>
</span>
@@ -203,6 +213,7 @@ export default defineComponent({
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
@import '../../styles/JournalStats.scss';
.daily-stats {
text-align: left;
@@ -57,6 +57,7 @@ import { API } from '../../typings/api';
import http from '../../http';
export default defineComponent({
name: 'journal-dispatcher-stats',
components: { Loading },
setup() {
@@ -1,11 +1,13 @@
<template>
<div class="journal-stats">
<span v-if="store.driverStatsData">
<div class="journal-stats" v-if="store.driverStatsData">
<span>
<h3>
{{ $t('journal.stats-title') }}
<span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
</h3>
<hr class="header-separator" />
<div class="info-stats">
<span class="stat-badge">
<span>{{ $t('journal.stats-timetables') }}</span>
@@ -43,13 +45,13 @@
</div>
</span>
<b v-else-if="store.driverStatsStatus == Status.Data.Loading">{{
<!-- <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>
<b v-else>{{ $t('journal.driver-stats-info') }}</b> -->
</div>
</template>
+67 -73
View File
@@ -1,12 +1,24 @@
<template>
<div class="journal-stats" v-if="!store.isOffline">
<div class="stats-buttons">
<div
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
v-for="button in data.statsButtons"
:key="button.name"
v-for="button in statsButtons"
:key="button.tab"
class="btn--filled btn--image"
:data-selected="button.name == currentStatsTab"
@click="onTabButtonClick(button.name)"
:data-selected="button.tab == currentStatsTab"
:data-disabled="button.disabled"
:disabled="button.disabled"
@click="onTabButtonClick(button.tab)"
>
<img
v-if="button.iconName"
@@ -17,87 +29,69 @@
</button>
</div>
<div class="stats-tab" v-show="currentStatsTab !== null">
<keep-alive>
<JournalDailyStats v-if="currentStatsTab == 'journal-daily-stats'" />
<JournalDriverStats v-else-if="currentStatsTab == 'journal-driver-stats'" />
</keep-alive>
</div>
<transition name="dropdown-anim">
<div class="dropdown_wrapper" v-if="currentStatsTab !== null">
<keep-alive>
<component :is="currentStatsTab" :key="currentStatsTab"></component>
</keep-alive>
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, Ref, ref, watch } from 'vue';
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { useMainStore } from '../../store/mainStore';
import JournalDailyStats from './JournalDailyStats.vue';
import JournalDriverStats from './JournalDriverStats.vue';
import StorageManager from '../../managers/storageManager';
import { Journal } from './typings';
import JournalDailyStats from './JournalDailyStats.vue';
import JournalDispatcherStats from './JournalDispatcherStats.vue';
import JournalDriverStats from './JournalDriverStats.vue';
export type JournalStatsTab = 'journal-driver-stats' | 'journal-daily-stats';
const store = useMainStore();
const currentStatsTab: Ref<JournalStatsTab | null> = ref(null);
let data = reactive({
statsButtons: [
{
name: 'journal-daily-stats',
localeKey: 'journal.daily-stats-title',
iconName: 'stats'
},
{
name: 'journal-driver-stats',
localeKey: 'journal.driver-stats-title',
iconName: 'user'
export default defineComponent({
components: { JournalDailyStats, JournalDriverStats, JournalDispatcherStats },
props: {
statsButtons: {
type: Array as PropType<Journal.StatsButton[]>,
required: true
}
] as { name: JournalStatsTab; localeKey: string; iconName?: string }[]
});
},
data() {
return {
Journal,
mainStore: useMainStore(),
currentStatsTab: null as Journal.StatsTab | null
};
},
mounted() {
// const storedTab = StorageManager.getStringValue('journalStatsTab');
// if (storedTab && storedTab !== '' && this.statsButtons.some((b) => b.tab == storedTab))
// this.currentStatsTab = storedTab as Journal.StatsTab;
},
// watch: {
// 'mainStore.driverStatsData'(newData, prevData) {
// this.currentStatsTab =
// JSON.stringify(prevData) !== JSON.stringify(newData) && newData !== undefined
// ? Journal.StatsTab.DRIVER_STATS
// : this.currentStatsTab;
// }
// },
methods: {
onTabButtonClick(tab: Journal.StatsTab) {
this.currentStatsTab = tab == this.currentStatsTab ? null : tab;
function onTabButtonClick(tab: JournalStatsTab) {
currentStatsTab.value = tab == currentStatsTab.value ? null : tab;
StorageManager.setStringValue('journalStatsTab', currentStatsTab.value ?? '');
}
watch(
computed(() => store.driverStatsData),
(newData, prevData) => {
currentStatsTab.value =
JSON.stringify(prevData) !== JSON.stringify(newData) && newData !== undefined
? 'journal-driver-stats'
: currentStatsTab.value;
StorageManager.setStringValue('journalStatsTab', this.currentStatsTab ?? '');
}
}
);
onMounted(() => {
const storedTab = StorageManager.getStringValue('journalStatsTab');
if (storedTab && storedTab !== '') currentStatsTab.value = storedTab as JournalStatsTab;
});
</script>
<style lang="scss" scoped>
@import '../../styles/JournalStats.scss';
@import '../../styles/dropdown.scss';
@import '../../styles/dropdown_filters.scss';
@import '../../styles/variables.scss';
.stats-buttons {
position: relative;
display: flex;
gap: 0.5em;
margin-bottom: 0.5em;
button {
font-weight: bold;
padding: 0.5em 0.75em;
&[data-inactive='true'] {
color: gray;
}
&[data-selected='true'] {
color: $accentCol;
}
}
.dropdown_wrapper {
max-width: 100%;
}
</style>
+13
View File
@@ -46,4 +46,17 @@ export namespace Journal {
id: TimetableSorterKey;
dir: 'asc' | 'desc';
}
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;
}
}
+3
View File
@@ -371,6 +371,9 @@
"driver-stats-title": "DRIVER STATS",
"driver-stats-info": "Enter a proper nickname into filters [F] to see user's driving statistics!",
"dispatcher-stats-title": "DISPATCHER STATS",
"dispatcher-stats-info": "Enter a proper nickname into filters [F] to see user's dispatcher statistics!",
"stats-loading": "Fetching statistics...",
"stats-error": "Oops! An unexpected error occurred while trying to fetch statistics! :/",
+4 -1
View File
@@ -349,9 +349,12 @@
"daily-stats-title": "STATYSTYKI DNIA",
"daily-stats-info": "Dzisiejsze statystyki nie są jeszcze dostępne!",
"driver-stats-title": "STATYSTYKI GRACZA",
"driver-stats-title": "STAT. MASZYNISTY",
"driver-stats-info": "Wpisz nazwę użytkownika w filtrach [F], aby zobaczyć jego statystyki maszynisty!",
"dispatcher-stats-title": "STATYSTYKI DYŻURNEGO",
"dispatcher-stats-info": "Wpisz nazwę użytkownika w filtrach [F], aby zobaczyć jego statystyki dyżurnego!",
"stats-loading": "Pobieranie statystyk...",
"stats-error": "Ups! Wystąpił błąd podczas próby pobrania statystyk!",
+1
View File
@@ -19,6 +19,7 @@ export const useMainStore = defineStore('store', {
dispatcherStatsName: '',
dispatcherStatsData: undefined,
dispatcherStatsStatus: Status.Data.Initialized,
driverStatsName: '',
driverStatsData: undefined,
-2
View File
@@ -53,9 +53,7 @@
justify-content: space-between;
align-items: center;
gap: 0.5em;
position: relative;
margin-bottom: 0.5em;
}
.btn--load-data {
+11 -5
View File
@@ -2,24 +2,30 @@
@import 'responsive.scss';
.stats-tab {
position: absolute;
right: 0;
z-index: 99;
transform: translateY(1em);
width: 100%;
background-color: #1a1a1a;
box-shadow: 0 0 5px 1px $accentCol;
padding: 1em;
display: flex;
align-items: flex-end;
}
margin-bottom: 0.5em;
width: 100%;
hr.header-separator {
margin-bottom: 1em;
}
.info-stats {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 1em;
}
.stat-badge {
+2 -1
View File
@@ -30,7 +30,8 @@
top: calc(100% + 0.5em);
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%;
max-width: 550px;
-5
View File
@@ -5,11 +5,6 @@
.actions-bar {
display: flex;
gap: 0.5em;
margin-bottom: 0.5em;
}
.filters-options {
position: relative;
}
h1.option-title {
+6 -2
View File
@@ -11,7 +11,7 @@
--clr-skr: #ff5100;
--clr-twr: #ffbb00;
--clr-error: #df3e3e;
--clr-error: #fa3636;
--clr-warning: #c59429;
--clr-donator: #f7a4ff;
@@ -158,6 +158,10 @@ ul {
color: #ccc;
}
&--error {
color: var(--clr-error);
}
&--donator {
color: var(--clr-donator);
text-shadow: var(--clr-donator) 0 0 10px;
@@ -183,7 +187,7 @@ a.a-button {
&[data-disabled='true'] {
user-select: none;
pointer-events: none;
opacity: 0.85;
opacity: 0.7;
}
&.btn--filled {
+40 -17
View File
@@ -3,15 +3,19 @@
<JournalHeader />
<div class="journal_wrapper">
<JournalOptions
@on-search-confirm="fetchHistoryData"
@on-options-reset="resetOptions"
@on-refresh-data="fetchHistoryData(true)"
:sorter-option-ids="['timestampFrom', 'duration']"
:data-status="dataStatus"
:current-options-active="currentOptionsActive"
optionsType="dispatchers"
/>
<div class="journal_top-bar">
<JournalOptions
@on-search-confirm="fetchHistoryData"
@on-options-reset="resetOptions"
@on-refresh-data="fetchHistoryData(true)"
:sorter-option-ids="['timestampFrom', 'duration']"
:data-status="dataStatus"
:current-options-active="currentOptionsActive"
optionsType="dispatchers"
/>
<JournalStats :statsButtons="statsButtons" />
</div>
<div class="journal_refreshed-date" v-if="dataRefreshedAt">
{{ $t('journal.data-refreshed-at') }}: {{ dataRefreshedAt.toLocaleString($i18n.locale) }}
@@ -33,22 +37,33 @@
<script lang="ts">
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 JournalDispatchersList from '../components/JournalView/JournalDispatchersList.vue';
import JournalHeader from '../components/JournalView/JournalHeader.vue';
import { LocationQuery } from 'vue-router';
import { Journal } from '../components/JournalView/typings';
import { API } from '../typings/api';
import { Status } from '../typings/common';
import http from '../http';
import JournalOptions from '../components/JournalView/JournalOptions.vue';
import JournalDispatchersList from '../components/JournalView/JournalDispatchersList.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-title',
iconName: 'user',
disabled: false
}
];
export default defineComponent({
components: {
JournalOptions,
JournalDispatchersList,
JournalHeader
JournalHeader,
JournalStats
},
name: 'JournalDispatchers',
@@ -65,6 +80,8 @@ export default defineComponent({
},
data: () => ({
statsButtons,
currentQuery: '',
currentQueryArray: [] as string[],
dataRefreshedAt: null as Date | null,
@@ -102,7 +119,7 @@ export default defineComponent({
const scrollElement: Ref<HTMLElement | null> = ref(null);
return {
store: useMainStore(),
mainStore: useMainStore(),
sorterActive,
searchersValues,
@@ -120,6 +137,12 @@ export default defineComponent({
this.currentOptionsActive =
q.length > 2 ||
q.some((qv) => qv.startsWith('sortBy=') && qv.split('=')[1] != 'timestampFrom');
},
'mainStore.dispatcherStatsData'(stats) {
// console.log(stats);
// this.statsButtons.find((sb) => sb.tab == Journal.StatsTab.DRIVER_STATS)!.disabled =
// driverStats === undefined;
}
},
@@ -241,7 +264,7 @@ export default defineComponent({
this.historyList = responseData;
// Stats display
this.store.dispatcherStatsName =
this.mainStore.dispatcherStatsName =
this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim()
? this.historyList[0].dispatcherName
: '';
+37 -15
View File
@@ -3,18 +3,20 @@
<JournalHeader />
<div class="journal_wrapper">
<JournalOptions
@on-search-confirm="fetchHistoryData"
@on-options-reset="resetOptions"
@on-refresh-data="fetchHistoryData"
:sorter-option-ids="['timetableId', 'beginDate', 'routeDistance', 'allStopsCount']"
:filters="journalTimetableFilters"
:currentOptionsActive="currentOptionsActive"
:data-status="dataStatus"
optionsType="timetables"
/>
<div class="journal_top-bar">
<JournalOptions
@on-search-confirm="fetchHistoryData"
@on-options-reset="resetOptions"
@on-refresh-data="fetchHistoryData"
:sorter-option-ids="['timetableId', 'beginDate', 'routeDistance', 'allStopsCount']"
:filters="journalTimetableFilters"
:currentOptionsActive="currentOptionsActive"
:data-status="dataStatus"
optionsType="timetables"
/>
<JournalStats />
<JournalStats :statsButtons="statsButtons" />
</div>
<div class="journal_refreshed-date" v-if="dataRefreshedAt">
{{ $t('journal.data-refreshed-at') }}: {{ dataRefreshedAt.toLocaleString($i18n.locale) }}
@@ -138,6 +140,24 @@ export default defineComponent({
},
data: () => ({
journalTimetableFilters,
mainStore: useMainStore(),
statsButtons: [
{
tab: Journal.StatsTab.DAILY_STATS,
localeKey: 'journal.daily-stats-title',
iconName: 'stats',
disabled: false
},
{
tab: Journal.StatsTab.DRIVER_STATS,
localeKey: 'journal.driver-stats-title',
iconName: 'user',
disabled: true
}
],
currentQueryParams: {} as TimetablesQueryParams,
dataRefreshedAt: null as Date | null,
@@ -149,7 +169,6 @@ export default defineComponent({
currentOptionsActive: false,
timetableHistory: [] as API.TimetableHistory.Response,
journalTimetableFilters,
dataStatus: Status.Data.Loading,
dataErrorMessage: ''
@@ -189,15 +208,18 @@ export default defineComponent({
countFromIndex,
countLimit,
scrollElement,
store: useMainStore()
scrollElement
};
},
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;
}
},