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
@@ -1,18 +1,27 @@
<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" />
<b v-if="statsStatus == Status.Data.Loading">
{{ $t('app.loading') }}
</b>
<b class="text--error" v-else-if="statsStatus == Status.Data.Error">
{{ $t('journal.stats-error') }}
</b>
<b v-else-if="topDispatchers.length == 0">
{{ $t('journal.daily-stats-info') }}
</b>
<div v-else>
<div v-if="stats.totalTimetables"> <div v-if="stats.totalTimetables">
&bull; &bull;
<i18n-t keypath="journal.timetable-stats-total"> <i18n-t keypath="journal.timetable-stats-total">
@@ -124,6 +133,7 @@
</template> </template>
</i18n-t> </i18n-t>
</div> </div>
</div>
</span> </span>
</span> </span>
</section> </section>
@@ -203,6 +213,7 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/JournalStats.scss';
.daily-stats { .daily-stats {
text-align: left; text-align: left;
@@ -57,6 +57,7 @@ import { API } from '../../typings/api';
import http from '../../http'; import http from '../../http';
export default defineComponent({ export default defineComponent({
name: 'journal-dispatcher-stats',
components: { Loading }, components: { Loading },
setup() { setup() {
@@ -1,11 +1,13 @@
<template> <template>
<div class="journal-stats"> <div class="journal-stats" v-if="store.driverStatsData">
<span v-if="store.driverStatsData"> <span>
<h3> <h3>
{{ $t('journal.stats-title') }} {{ $t('journal.stats-title') }}
<span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span> <span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
</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.stats-timetables') }}</span>
@@ -43,13 +45,13 @@
</div> </div>
</span> </span>
<b v-else-if="store.driverStatsStatus == Status.Data.Loading">{{ <!-- <b v-else-if="store.driverStatsStatus == Status.Data.Loading">{{
$t('journal.stats-loading') $t('journal.stats-loading')
}}</b> }}</b>
<b v-else-if="store.driverStatsStatus == Status.Data.Error"> <b v-else-if="store.driverStatsStatus == Status.Data.Error">
{{ $t('journal.stats-error ') }} {{ $t('journal.stats-error ') }}
</b> </b>
<b v-else>{{ $t('journal.driver-stats-info') }}</b> <b v-else>{{ $t('journal.driver-stats-info') }}</b> -->
</div> </div>
</template> </template>
+64 -70
View File
@@ -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,69 @@
</button> </button>
</div> </div>
<div class="stats-tab" v-show="currentStatsTab !== null"> <transition name="dropdown-anim">
<div class="dropdown_wrapper" v-if="currentStatsTab !== null">
<keep-alive> <keep-alive>
<JournalDailyStats v-if="currentStatsTab == 'journal-daily-stats'" /> <component :is="currentStatsTab" :key="currentStatsTab"></component>
<JournalDriverStats v-else-if="currentStatsTab == 'journal-driver-stats'" />
</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 './JournalDispatcherStats.vue';
import JournalDriverStats from './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'
}, },
{ data() {
name: 'journal-driver-stats', return {
localeKey: 'journal.driver-stats-title', Journal,
iconName: 'user' 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;
StorageManager.setStringValue('journalStatsTab', this.currentStatsTab ?? '');
} }
] as { name: JournalStatsTab; localeKey: string; iconName?: string }[]
});
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;
}
);
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>
+13
View File
@@ -46,4 +46,17 @@ export namespace Journal {
id: TimetableSorterKey; id: TimetableSorterKey;
dir: 'asc' | 'desc'; 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-title": "DRIVER STATS",
"driver-stats-info": "Enter a proper nickname into filters [F] to see user's driving statistics!", "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-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! :/",
+4 -1
View File
@@ -349,9 +349,12 @@
"daily-stats-title": "STATYSTYKI DNIA", "daily-stats-title": "STATYSTYKI DNIA",
"daily-stats-info": "Dzisiejsze statystyki nie są jeszcze dostępne!", "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!", "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-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!",
+1
View File
@@ -19,6 +19,7 @@ export const useMainStore = defineStore('store', {
dispatcherStatsName: '', dispatcherStatsName: '',
dispatcherStatsData: undefined, dispatcherStatsData: undefined,
dispatcherStatsStatus: Status.Data.Initialized,
driverStatsName: '', driverStatsName: '',
driverStatsData: undefined, driverStatsData: undefined,
-2
View File
@@ -53,9 +53,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 {
+11 -5
View File
@@ -2,24 +2,30 @@
@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%;
} }
.info-stats { .info-stats {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5em; gap: 0.5em;
margin-top: 1em;
} }
.stat-badge { .stat-badge {
+2 -1
View File
@@ -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
View File
@@ -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 {
+6 -2
View File
@@ -11,7 +11,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 +158,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 +187,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 {
+31 -8
View File
@@ -3,6 +3,7 @@
<JournalHeader /> <JournalHeader />
<div class="journal_wrapper"> <div class="journal_wrapper">
<div class="journal_top-bar">
<JournalOptions <JournalOptions
@on-search-confirm="fetchHistoryData" @on-search-confirm="fetchHistoryData"
@on-options-reset="resetOptions" @on-options-reset="resetOptions"
@@ -13,6 +14,9 @@
optionsType="dispatchers" 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) }}
</div> </div>
@@ -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 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({ export default defineComponent({
components: { components: {
JournalOptions, JournalOptions,
JournalDispatchersList, JournalDispatchersList,
JournalHeader JournalHeader,
JournalStats
}, },
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,
@@ -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,12 @@ 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) {
// 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; 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
: ''; : '';
+27 -5
View File
@@ -3,6 +3,7 @@
<JournalHeader /> <JournalHeader />
<div class="journal_wrapper"> <div class="journal_wrapper">
<div class="journal_top-bar">
<JournalOptions <JournalOptions
@on-search-confirm="fetchHistoryData" @on-search-confirm="fetchHistoryData"
@on-options-reset="resetOptions" @on-options-reset="resetOptions"
@@ -14,7 +15,8 @@
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) }}
@@ -138,6 +140,24 @@ export default defineComponent({
}, },
data: () => ({ 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, currentQueryParams: {} as TimetablesQueryParams,
dataRefreshedAt: null as Date | null, dataRefreshedAt: null as Date | null,
@@ -149,7 +169,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: ''
@@ -189,15 +208,18 @@ 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;
} }
}, },