mirror of
https://github.com/Spythere/stacjownik.git
synced 2026-05-03 13:28:11 +00:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65c1ab809f | |||
| e7c8ba62d7 | |||
| 38a9f1987f | |||
| f90dfd3cc8 | |||
| 9b765c7fdd | |||
| 0f7e3e8820 | |||
| 1735444176 | |||
| 1d95b26e9c | |||
| 86fbaa2510 | |||
| b7db3edd9b | |||
| 72fa9523e8 | |||
| 7b07a43715 | |||
| 448c6e387e | |||
| 527c929b53 | |||
| b622df19f6 | |||
| 03e69b315c | |||
| f2c11bf2cf | |||
| 92c73b9ed9 | |||
| acc15619a9 | |||
| 3705325a9a | |||
| 1655aa2c94 | |||
| f38ad8fa81 | |||
| 1a7801259f | |||
| abd1c8b684 | |||
| 7f315b549e | |||
| 329c85b858 | |||
| dcef8cdac8 | |||
| 298f8a5f23 | |||
| 51d952ffee | |||
| 83b22e5978 | |||
| 87ad7b8ede | |||
| 440e11bdd9 | |||
| 84ecd3c175 | |||
| 72b3aef045 | |||
| 36ae24fdaf | |||
| 41e3d018e6 | |||
| d9faa486d2 | |||
| 89dc265e1b | |||
| 200e994ae6 | |||
| 150b7749ae | |||
| 0f8932b53c | |||
| 1365140802 | |||
| ce8bbe4c67 | |||
| 1d49de1c6b | |||
| b8574f9ea1 | |||
| ecced14cca | |||
| 212a87126d | |||
| 41e50b8207 | |||
| 565b0dfd8c | |||
| 40a0b47984 | |||
| ccca1c8752 | |||
| cf51045343 | |||
| 23a8b9e8d4 |
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
+1
-7
@@ -7,11 +7,6 @@
|
||||
|
||||
<AppWelcomeCard :is-card-open="isWelcomeCardOpen" @toggle-card="closeWelcomeCard" />
|
||||
|
||||
<MigrateInfoCard
|
||||
:is-open="store.isMigrateInfoCardOpen"
|
||||
@toggle-card="closeMigrateInfoCard"
|
||||
></MigrateInfoCard>
|
||||
|
||||
<Tooltip />
|
||||
|
||||
<AppHeader />
|
||||
@@ -52,7 +47,6 @@ import UpdateCard from './components/App/UpdateCard.vue';
|
||||
import StorageManager from './managers/storageManager';
|
||||
import AppFooter from './components/App/AppFooter.vue';
|
||||
import AppWelcomeCard from './components/App/AppWelcomeCard.vue';
|
||||
import MigrateInfoCard from './components/App/MigrateInfoCard.vue';
|
||||
|
||||
const STORAGE_VERSION_KEY = 'app_version';
|
||||
const WELCOME_CARD_SEEN_KEY = 'welcome_card_seen';
|
||||
@@ -66,7 +60,6 @@ export default defineComponent({
|
||||
AppFooter,
|
||||
UpdateCard,
|
||||
AppWelcomeCard,
|
||||
MigrateInfoCard,
|
||||
Tooltip
|
||||
},
|
||||
|
||||
@@ -212,6 +205,7 @@ export default defineComponent({
|
||||
|
||||
<style lang="scss">
|
||||
@use './styles/animations';
|
||||
@use './styles/global';
|
||||
|
||||
// APP
|
||||
#app {
|
||||
|
||||
@@ -7,13 +7,6 @@
|
||||
v{{ version }}{{ isOnProductionHost ? '' : 'dev' }}
|
||||
</button>
|
||||
|
||||
<br />
|
||||
<a href="https://discord.gg/x2mpNN3svk">
|
||||
<img src="/images/icon-discord.png" alt="discord logo icon" /> <b class="text--discord">
|
||||
{{ $t('footer.discord') }}
|
||||
</b>
|
||||
</a>
|
||||
|
||||
<div style="display: none">∫ ukryta taktyczna całka do programowania w HTMLu</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
@@ -1,37 +1,41 @@
|
||||
<template>
|
||||
<div class="driver-top-actions">
|
||||
<div class="actions-container">
|
||||
<div class="actions actions-left">
|
||||
<button class="a-button btn--filled btn--image" @click="routerReturn">
|
||||
<img src="/images/icon-back.svg" alt="train icon" />
|
||||
<span>
|
||||
{{ t('trains.driver-return-link') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="driver-top-actions">
|
||||
<div class="actions-container">
|
||||
<div class="actions actions-left">
|
||||
<button class="a-button btn--filled btn--image" @click="routerReturn">
|
||||
<img src="/images/icon-back.svg" alt="train icon" />
|
||||
<span>
|
||||
{{ t('trains.driver-return-link') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="actions actions-right">
|
||||
<a class="a-button btn--filled btn--image" :href="`https://srjp-td2.web.app/?id=${chosenTrain.id}`"
|
||||
target="_blank">
|
||||
<span class="hidable">
|
||||
{{ t('trains.driver-srjp-link') }}
|
||||
</span>
|
||||
<div class="actions actions-right">
|
||||
<a
|
||||
class="a-button btn--filled btn--image"
|
||||
:href="`https://srjp-td2.web.app/?id=${chosenTrain.id}`"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="hidable">
|
||||
{{ t('trains.driver-srjp-link') }}
|
||||
</span>
|
||||
|
||||
<img src="/images/icon-srjp.svg" alt="srjp icon" />
|
||||
</a>
|
||||
<img src="/images/icon-srjp.svg" alt="srjp icon" />
|
||||
</a>
|
||||
|
||||
<router-link :to="`/journal/timetables?search-driver=${chosenTrain.driverName}`"
|
||||
class="a-button btn--filled btn--image">
|
||||
<span class="hidable">
|
||||
{{ t('trains.driver-journal-link') }}
|
||||
</span>
|
||||
|
||||
<img src="/images/icon-train.svg" alt="train icon" />
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<router-link
|
||||
:to="`/profile?playerId=${chosenTrain.driverId}`"
|
||||
class="a-button btn--filled btn--image"
|
||||
>
|
||||
<span class="hidable">
|
||||
{{ t('trains.driver-profile-link') }}
|
||||
</span>
|
||||
|
||||
<img src="/images/icon-user.svg" alt="user icon" />
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -44,42 +48,40 @@ const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
defineProps({
|
||||
chosenTrain: {
|
||||
type: Object as PropType<Train>,
|
||||
required: true
|
||||
}
|
||||
chosenTrain: {
|
||||
type: Object as PropType<Train>,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
function routerReturn() {
|
||||
router.back();
|
||||
router.back();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '../../styles/responsive';
|
||||
|
||||
|
||||
.actions-container {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 0.5em;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.actions-container>.actions>.a-button {
|
||||
padding: 0.5em;
|
||||
border-radius: 0.5em 0.5em 0 0;
|
||||
.actions-container > .actions > .a-button {
|
||||
padding: 0.5em;
|
||||
border-radius: 0.5em 0.5em 0 0;
|
||||
}
|
||||
|
||||
@include responsive.smallScreen {
|
||||
span.hidable {
|
||||
display: none;
|
||||
}
|
||||
span.hidable {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -160,7 +160,7 @@ ul.options {
|
||||
|
||||
height: auto;
|
||||
|
||||
z-index: 100;
|
||||
z-index: 150;
|
||||
width: 100%;
|
||||
|
||||
font-size: 0.9em;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<section class="daily-stats">
|
||||
<span :data-active="statsStatus">
|
||||
<span :data-active="apiStore.dataStatuses.dailyStatsData">
|
||||
<h3>
|
||||
{{ $t('journal.daily-stats.title') }}
|
||||
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
|
||||
@@ -8,11 +8,11 @@
|
||||
|
||||
<hr class="header-separator" />
|
||||
|
||||
<b v-if="statsStatus == Status.Data.Loading">
|
||||
<b v-if="apiStore.dataStatuses.dailyStatsData == Status.Data.Loading">
|
||||
{{ $t('app.loading') }}
|
||||
</b>
|
||||
|
||||
<b class="text--error" v-else-if="statsStatus == Status.Data.Error">
|
||||
<b class="text--error" v-else-if="apiStore.dataStatuses.dailyStatsData == Status.Data.Error">
|
||||
{{ $t('journal.stats-error') }}
|
||||
</b>
|
||||
|
||||
@@ -20,42 +20,48 @@
|
||||
{{ $t('journal.daily-stats.info') }}
|
||||
</b>
|
||||
|
||||
<div v-else>
|
||||
<div v-else-if="apiStore.dailyStatsData">
|
||||
<ul class="stats-list">
|
||||
<li v-if="stats.totalTimetables">
|
||||
<li v-if="apiStore.dailyStatsData.totalTimetables">
|
||||
<i18n-t keypath="journal.daily-stats.total">
|
||||
<template #count>
|
||||
<b class="text--primary">
|
||||
{{ stats.totalTimetables }}
|
||||
{{ $t('journal.daily-stats.count', stats.totalTimetables) }}
|
||||
{{ apiStore.dailyStatsData.totalTimetables }}
|
||||
{{ $t('journal.daily-stats.count', apiStore.dailyStatsData.totalTimetables) }}
|
||||
</b>
|
||||
</template>
|
||||
|
||||
<template #distance>
|
||||
<b class="text--primary"> {{ stats.distanceSum?.toFixed(2) }} km</b>
|
||||
<b class="text--primary">
|
||||
{{ apiStore.dailyStatsData.distanceSum?.toFixed(2) }} km</b
|
||||
>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</li>
|
||||
|
||||
<li v-if="stats.maxTimetable">
|
||||
<li v-if="apiStore.dailyStatsData.maxTimetable">
|
||||
<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
|
||||
:to="`/journal/timetables?search-train=%23${apiStore.dailyStatsData.maxTimetable.id}`"
|
||||
>
|
||||
<b>{{ apiStore.dailyStatsData.maxTimetable.id }}</b>
|
||||
</router-link>
|
||||
</template>
|
||||
<template #author>
|
||||
<router-link
|
||||
:to="`/journal/timetables?search-dispatcher=${stats.maxTimetable.authorName}`"
|
||||
:to="`/journal/timetables?search-dispatcher=${apiStore.dailyStatsData.maxTimetable.authorName}`"
|
||||
>
|
||||
<b>{{ stats.maxTimetable.authorName }}</b>
|
||||
<b>{{ apiStore.dailyStatsData.maxTimetable.authorName }}</b>
|
||||
</router-link>
|
||||
</template>
|
||||
<template #driver>
|
||||
<b class="text--primary">{{ stats.maxTimetable.driverName }}</b>
|
||||
<b class="text--primary">{{ apiStore.dailyStatsData.maxTimetable.driverName }}</b>
|
||||
</template>
|
||||
<template #distance>
|
||||
<b class="text--primary">{{ stats.maxTimetable.routeDistance }} km</b>
|
||||
<b class="text--primary"
|
||||
>{{ apiStore.dailyStatsData.maxTimetable.routeDistance }} km</b
|
||||
>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</li>
|
||||
@@ -101,35 +107,37 @@
|
||||
</i18n-t>
|
||||
</li>
|
||||
|
||||
<li v-if="stats.longestDuties.length > 0">
|
||||
<li v-if="apiStore.dailyStatsData.longestDuties.length > 0">
|
||||
<i18n-t keypath="journal.daily-stats.longest-duties">
|
||||
<template #dispatcher>
|
||||
<router-link
|
||||
:to="`/journal/dispatchers?search-dispatcher=${stats.longestDuties[0].name}`"
|
||||
:to="`/journal/dispatchers?search-dispatcher=${apiStore.dailyStatsData.longestDuties[0].name}`"
|
||||
>
|
||||
<b>{{ stats.longestDuties[0].name }}</b>
|
||||
<b>{{ apiStore.dailyStatsData.longestDuties[0].name }}</b>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<template #station>{{ stats.longestDuties[0].station }}</template>
|
||||
<template #station>{{ apiStore.dailyStatsData.longestDuties[0].station }}</template>
|
||||
|
||||
<template #duration>
|
||||
{{ calculateDuration(stats.longestDuties[0].duration) }}
|
||||
{{ humanizeDuration(apiStore.dailyStatsData.longestDuties[0].duration) }}
|
||||
</template>
|
||||
</i18n-t>
|
||||
</li>
|
||||
|
||||
<li v-if="stats.mostActiveDrivers.length > 0">
|
||||
<li v-if="apiStore.dailyStatsData.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}`"
|
||||
:to="`/journal/timetables?search-driver=${apiStore.dailyStatsData.mostActiveDrivers[0].name}`"
|
||||
>
|
||||
<b>{{ stats.mostActiveDrivers[0].name }}</b>
|
||||
<b>{{ apiStore.dailyStatsData.mostActiveDrivers[0].name }}</b>
|
||||
</router-link>
|
||||
</template>
|
||||
<template #distance>
|
||||
<b class="text--primary">{{ stats.mostActiveDrivers[0].distance.toFixed(2) }} km</b>
|
||||
<b class="text--primary"
|
||||
>{{ apiStore.dailyStatsData.mostActiveDrivers[0].distance.toFixed(2) }} km</b
|
||||
>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</li>
|
||||
@@ -151,7 +159,11 @@
|
||||
>
|
||||
<span>{{ $t(`journal.daily-stats.${key}`) }}</span>
|
||||
<span>
|
||||
{{ Object.entries(stats.globalDiff).find(([k, v]) => k == key)?.[1] || '--' }}
|
||||
{{
|
||||
Object.entries(apiStore.dailyStatsData.globalDiff).find(
|
||||
([k, v]) => k == key
|
||||
)?.[1] || '--'
|
||||
}}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -160,76 +172,25 @@
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import dateMixin from '../../mixins/dateMixin';
|
||||
|
||||
import { API } from '../../typings/api';
|
||||
import { Status } from '../../typings/common';
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { useApiStore } from '../../store/apiStore';
|
||||
import { Status } from '../../typings/common';
|
||||
import { humanizeDuration } from '../../composables/time';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'journal-daily-stats',
|
||||
onMounted(() => {
|
||||
apiStore.fetchDailyStats();
|
||||
});
|
||||
|
||||
mixins: [dateMixin],
|
||||
const apiStore = useApiStore();
|
||||
|
||||
data() {
|
||||
return {
|
||||
Status,
|
||||
statsStatus: Status.Data.Loading,
|
||||
intervalId: -1,
|
||||
const topDispatchers = computed(() => {
|
||||
if (!apiStore.dailyStatsData || apiStore.dailyStatsData.mostActiveDispatchers.length == 0)
|
||||
return [];
|
||||
|
||||
stats: {} as API.DailyStats.Response,
|
||||
apiStore: useApiStore()
|
||||
};
|
||||
},
|
||||
const maxCount = apiStore.dailyStatsData.mostActiveDispatchers[0].count;
|
||||
|
||||
activated() {
|
||||
this.startFetchingDailyStats();
|
||||
},
|
||||
|
||||
deactivated() {
|
||||
this.stopFetchingDailyStats();
|
||||
},
|
||||
|
||||
computed: {
|
||||
topDispatchers() {
|
||||
if (this.stats.mostActiveDispatchers.length == 0) return [];
|
||||
const maxCount = this.stats.mostActiveDispatchers[0].count;
|
||||
|
||||
return this.stats.mostActiveDispatchers.filter((disp) => disp.count === maxCount);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchDailyTimetableStats() {
|
||||
try {
|
||||
const res: API.DailyStats.Response = await (
|
||||
await this.apiStore.client!.get('api/getDailyStats')
|
||||
).data;
|
||||
|
||||
this.stats = res;
|
||||
|
||||
this.statsStatus = Status.Data.Loaded;
|
||||
} catch (error) {
|
||||
console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...');
|
||||
this.statsStatus = Status.Data.Error;
|
||||
}
|
||||
},
|
||||
|
||||
startFetchingDailyStats() {
|
||||
this.fetchDailyTimetableStats();
|
||||
|
||||
if (this.intervalId != -1) return;
|
||||
|
||||
this.intervalId = window.setInterval(this.fetchDailyTimetableStats, 60000);
|
||||
},
|
||||
|
||||
stopFetchingDailyStats() {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = -1;
|
||||
}
|
||||
}
|
||||
return apiStore.dailyStatsData.mostActiveDispatchers.filter((disp) => disp.count === maxCount);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -265,7 +226,7 @@ ul.stats-list {
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
@include responsive.smallScreen{
|
||||
@include responsive.smallScreen {
|
||||
h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="filters-options dropdown" @keydown.esc="showOptions = false">
|
||||
<div class="dropdown filters-options" @keydown.esc="showOptions = false">
|
||||
<div class="dropdown_background" v-if="showOptions" @click="showOptions = false"></div>
|
||||
|
||||
<div class="actions-bar">
|
||||
@@ -57,7 +57,7 @@
|
||||
<label v-if="propName == 'search-date-from'" for="search-date">{{
|
||||
$t(`options.search-${optionsType}-date`)
|
||||
}}</label>
|
||||
|
||||
|
||||
<div class="search-box">
|
||||
<input
|
||||
class="search-input"
|
||||
@@ -330,4 +330,9 @@ export default defineComponent({
|
||||
<style lang="scss" scoped>
|
||||
@use '../../styles/dropdown';
|
||||
@use '../../styles/dropdown-filters';
|
||||
|
||||
.filters-options > .dropdown_wrapper {
|
||||
height: calc(100vh - 19em);
|
||||
min-height: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -19,209 +19,238 @@
|
||||
<div class="details-body" v-if="showExtraInfo">
|
||||
<div class="g-separator"></div>
|
||||
|
||||
<EntryStops :timetable="timetable" />
|
||||
<div v-if="timetableDetails">
|
||||
<EntryStops :timetable="timetableDetails" />
|
||||
|
||||
<div class="g-separator"></div>
|
||||
|
||||
<div class="timetable-specs">
|
||||
<span class="badge specs-badge" v-if="timetable.authorName">
|
||||
<span>{{ $t('journal.dispatcher-name') }}</span>
|
||||
<span>{{ timetable.authorName }}</span>
|
||||
</span>
|
||||
|
||||
<span class="badge specs-badge" v-if="timetable.trainMaxSpeed">
|
||||
<span>{{ $t('journal.stock-timetable-speed') }}</span>
|
||||
<span> {{ timetable.trainMaxSpeed }}km/h </span>
|
||||
</span>
|
||||
|
||||
<span class="badge specs-badge" v-if="timetable.maxSpeed">
|
||||
<span>{{ $t('journal.stock-max-speed') }}</span>
|
||||
<span>{{ timetable.maxSpeed }}km/h</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="stock-dangers" v-if="timetable.warningNotes">
|
||||
<div class="g-separator"></div>
|
||||
|
||||
<b>{{ $t('journal.stock-dangers') }}:</b>
|
||||
|
||||
<ul>
|
||||
<li v-if="timetable.twr">
|
||||
<b class="text--primary">{{ $t('warnings.TWR') }} (TWR)</b>
|
||||
</li>
|
||||
|
||||
<li v-if="timetable.skr">
|
||||
<b class="text--primary">{{ $t('warnings.SKR') }}</b>
|
||||
</li>
|
||||
|
||||
<li v-if="timetable.hasDangerousCargo">
|
||||
<b class="text--primary">{{ $t('warnings.TN') }}</b>
|
||||
</li>
|
||||
|
||||
<li v-if="timetable.hasExtraDeliveries">
|
||||
<b class="text--primary">{{ $t('warnings.PN') }}</b>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="dangers-notes" v-if="timetable.warningNotes">
|
||||
<h4>{{ $t('warnings.header-title') }}</h4>
|
||||
<p>
|
||||
<i>{{ timetable.warningNotes }}</i>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Historia zmian w składzie -->
|
||||
<div v-if="timetable.stockString || stockHistory.length != 0">
|
||||
<div class="g-separator"></div>
|
||||
|
||||
<b>{{ $t('journal.stock-preview') }}:</b>
|
||||
|
||||
<div class="stock-specs" style="margin-top: 0.5em">
|
||||
<span class="badge specs-badge" v-if="timetable.stockLength">
|
||||
<span>{{ $t('journal.stock-length') }}</span>
|
||||
<span>
|
||||
{{
|
||||
currentHistoryIndex == 0
|
||||
? timetable.stockLength
|
||||
: stockHistory[currentHistoryIndex].stockLength || timetable.stockLength
|
||||
}}m
|
||||
</span>
|
||||
<div class="timetable-specs">
|
||||
<span class="badge specs-badge" v-if="timetableDetails.authorName">
|
||||
<span>{{ $t('journal.dispatcher-name') }}</span>
|
||||
<span>{{ timetableDetails.authorName }}</span>
|
||||
</span>
|
||||
|
||||
<span class="badge specs-badge" v-if="timetable.stockMass">
|
||||
<span>{{ $t('journal.stock-mass') }}</span>
|
||||
<span>
|
||||
{{
|
||||
Math.floor(
|
||||
(currentHistoryIndex == 0
|
||||
? timetable.stockMass
|
||||
: stockHistory[currentHistoryIndex].stockMass || timetable.stockMass) / 1000
|
||||
)
|
||||
}}t
|
||||
</span>
|
||||
<span class="badge specs-badge" v-if="timetableDetails.trainMaxSpeed">
|
||||
<span>{{ $t('journal.stock-timetable-speed') }}</span>
|
||||
<span> {{ timetableDetails.trainMaxSpeed }}km/h </span>
|
||||
</span>
|
||||
|
||||
<span class="badge specs-badge" v-if="timetableDetails.maxSpeed">
|
||||
<span>{{ $t('journal.stock-max-speed') }}</span>
|
||||
<span>{{ timetableDetails.maxSpeed }}km/h</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="stock-history">
|
||||
<button class="btn btn--action" @click="copyStockToClipboard()">
|
||||
<i class="fa-regular fa-copy"></i> {{ $t('journal.stock-copy') }}
|
||||
</button>
|
||||
<div class="stock-dangers" v-if="timetableDetails.warningNotes">
|
||||
<div class="g-separator"></div>
|
||||
|
||||
<button
|
||||
v-for="(sh, i) in stockHistory"
|
||||
:key="i"
|
||||
class="btn--action"
|
||||
:data-checked="i == currentHistoryIndex"
|
||||
@click.stop="currentHistoryIndex = i"
|
||||
>
|
||||
{{ sh.updatedAt }}
|
||||
</button>
|
||||
<b>{{ $t('journal.stock-dangers') }}:</b>
|
||||
|
||||
<ul>
|
||||
<li v-if="timetableDetails.twr">
|
||||
<b class="text--primary">{{ $t('warnings.TWR') }} (TWR)</b>
|
||||
</li>
|
||||
|
||||
<li v-if="timetableDetails.skr">
|
||||
<b class="text--primary">{{ $t('warnings.SKR') }}</b>
|
||||
</li>
|
||||
|
||||
<li v-if="timetableDetails.hasDangerousCargo">
|
||||
<b class="text--primary">{{ $t('warnings.TN') }}</b>
|
||||
</li>
|
||||
|
||||
<li v-if="timetableDetails.hasExtraDeliveries">
|
||||
<b class="text--primary">{{ $t('warnings.PN') }}</b>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="dangers-notes" v-if="timetableDetails.warningNotes">
|
||||
<h4>{{ $t('warnings.header-title') }}</h4>
|
||||
<p>
|
||||
<i>{{ timetableDetails.warningNotes }}</i>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="timetable.stockString" style="margin-top: 1em">
|
||||
<StockList
|
||||
:trainStockList="
|
||||
(currentHistoryIndex == 0
|
||||
? timetable.stockString
|
||||
: stockHistory[currentHistoryIndex].stockString
|
||||
).split(';')
|
||||
"
|
||||
/>
|
||||
<!-- Historia zmian w składzie -->
|
||||
<div v-if="timetableDetails.stockString || stockHistory.length != 0">
|
||||
<div class="g-separator"></div>
|
||||
|
||||
<b>{{ $t('journal.stock-preview') }}:</b>
|
||||
|
||||
<div class="stock-specs" style="margin-top: 0.5em">
|
||||
<span class="badge specs-badge" v-if="timetableDetails.stockLength">
|
||||
<span>{{ $t('journal.stock-length') }}</span>
|
||||
<span>
|
||||
{{
|
||||
currentHistoryIndex == 0
|
||||
? timetableDetails.stockLength
|
||||
: stockHistory[currentHistoryIndex].stockLength || timetableDetails.stockLength
|
||||
}}m
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="badge specs-badge" v-if="timetableDetails.stockMass">
|
||||
<span>{{ $t('journal.stock-mass') }}</span>
|
||||
<span>
|
||||
{{
|
||||
Math.floor(
|
||||
(currentHistoryIndex == 0
|
||||
? timetableDetails.stockMass
|
||||
: stockHistory[currentHistoryIndex].stockMass || timetableDetails.stockMass) /
|
||||
1000
|
||||
)
|
||||
}}t
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="stock-history">
|
||||
<button class="btn btn--action" @click="copyStockToClipboard()">
|
||||
<i class="fa-regular fa-copy"></i> {{ $t('journal.stock-copy') }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-for="(sh, i) in stockHistory"
|
||||
:key="i"
|
||||
class="btn--action"
|
||||
:data-checked="i == currentHistoryIndex"
|
||||
@click.stop="currentHistoryIndex = i"
|
||||
>
|
||||
{{ sh.updatedAt }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="timetableDetails.stockString" style="margin-top: 1em">
|
||||
<StockList
|
||||
:trainStockList="
|
||||
(currentHistoryIndex == 0
|
||||
? timetableDetails.stockString
|
||||
: stockHistory[currentHistoryIndex].stockString
|
||||
).split(';')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { PropType, defineComponent } from 'vue';
|
||||
import StockList from '../../Global/StockList.vue';
|
||||
import { API } from '../../../typings/api';
|
||||
<script lang="ts" setup>
|
||||
import { computed, PropType, ref } from 'vue';
|
||||
import { RouteLocationRaw } from 'vue-router';
|
||||
import EntryStops from './EntryStops.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: { StockList, EntryStops },
|
||||
import StockList from '../../Global/StockList.vue';
|
||||
import EntryStops from './EntryStops.vue';
|
||||
import { API } from '../../../typings/api';
|
||||
import { useApiStore } from '../../../store/apiStore';
|
||||
|
||||
emits: ['toggleExtraInfo'],
|
||||
const i18n = useI18n();
|
||||
const apiStore = useApiStore();
|
||||
|
||||
props: {
|
||||
showExtraInfo: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
timetable: {
|
||||
type: Object as PropType<API.TimetableHistory.Data>,
|
||||
required: true
|
||||
}
|
||||
const props = defineProps({
|
||||
showExtraInfo: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentHistoryIndex: 0,
|
||||
i18n: useI18n()
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
stockHistory() {
|
||||
return this.timetable.stockHistory
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((h) => {
|
||||
const historyData = h.split('@');
|
||||
return {
|
||||
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}),
|
||||
stockString: historyData[1],
|
||||
stockMass: Number(historyData[2]) || undefined,
|
||||
stockLength: Number(historyData[3]) || undefined
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
driverRouteLocation(): RouteLocationRaw | null {
|
||||
if (this.timetable.terminated) return null;
|
||||
return {
|
||||
name: 'DriverView',
|
||||
query: {
|
||||
trainId: `${this.timetable.driverId}|${this.timetable.trainNo}|eu`
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onImageError(e: Event) {
|
||||
const imageEl = e.target as HTMLImageElement;
|
||||
imageEl.src = '/images/icon-unknown.png';
|
||||
},
|
||||
|
||||
toggleExtraInfo() {
|
||||
this.$emit('toggleExtraInfo', this.timetable.id);
|
||||
},
|
||||
|
||||
copyStockToClipboard() {
|
||||
const currentStockString =
|
||||
this.stockHistory[this.currentHistoryIndex]?.stockString ?? this.timetable.stockString;
|
||||
|
||||
if (!currentStockString) {
|
||||
alert(this.i18n.t('journal.stock-clipboard-failure'));
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(currentStockString)
|
||||
.then(() => {
|
||||
prompt(this.i18n.t('journal.stock-clipboard-success'), currentStockString);
|
||||
})
|
||||
.catch(() => {
|
||||
alert(this.i18n.t('journal.stock-clipboard-failure'));
|
||||
});
|
||||
}
|
||||
timetableEntry: {
|
||||
type: Object as PropType<API.TimetableHistory.DataShort>,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(['toggleExtraInfo']);
|
||||
const currentHistoryIndex = ref(0);
|
||||
|
||||
const timetableDetails = ref<API.TimetableHistory.Data | null>(null);
|
||||
|
||||
const stockHistory = computed(() => {
|
||||
return (
|
||||
timetableDetails.value?.stockHistory
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((h) => {
|
||||
const historyData = h.split('@');
|
||||
return {
|
||||
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(i18n.locale.value, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}),
|
||||
stockString: historyData[1],
|
||||
stockMass: Number(historyData[2]) || undefined,
|
||||
stockLength: Number(historyData[3]) || undefined
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
});
|
||||
|
||||
const driverRouteLocation = computed<RouteLocationRaw | null>(() => {
|
||||
if (props.timetableEntry.terminated) return null;
|
||||
|
||||
return {
|
||||
name: 'DriverView',
|
||||
query: {
|
||||
trainId: `${props.timetableEntry.driverId}|${props.timetableEntry.trainNo}|eu`
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
async function fetchTimetableDetails() {
|
||||
try {
|
||||
const responseData = await apiStore.client!.get<API.TimetableHistory.Response>(
|
||||
'api/getTimetables',
|
||||
{
|
||||
params: {
|
||||
timetableId: props.timetableEntry.id,
|
||||
returnType: 'detailed'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!responseData || responseData.data.length != 1) {
|
||||
timetableDetails.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
timetableDetails.value = responseData.data[0];
|
||||
} catch (error) {
|
||||
// this.dataStatus = Status.Data.Error;
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleExtraInfo() {
|
||||
if (props.showExtraInfo == false) {
|
||||
await fetchTimetableDetails();
|
||||
}
|
||||
|
||||
emits('toggleExtraInfo', timetableDetails.value);
|
||||
}
|
||||
|
||||
function copyStockToClipboard() {
|
||||
if (!timetableDetails.value) return;
|
||||
|
||||
const currentStockString =
|
||||
stockHistory.value[currentHistoryIndex.value]?.stockString ??
|
||||
timetableDetails.value.stockString;
|
||||
|
||||
if (!currentStockString) {
|
||||
alert(i18n.t('journal.stock-clipboard-failure'));
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(currentStockString)
|
||||
.then(() => {
|
||||
prompt(i18n.t('journal.stock-clipboard-success'), currentStockString);
|
||||
})
|
||||
.catch(() => {
|
||||
alert(i18n.t('journal.stock-clipboard-failure'));
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -299,7 +328,7 @@ hr {
|
||||
}
|
||||
}
|
||||
|
||||
@include responsive.smallScreen{
|
||||
@include responsive.smallScreen {
|
||||
.timetable-specs {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
</b>
|
||||
|
||||
<b
|
||||
class="info-badge"
|
||||
class="timetable-status-badge"
|
||||
:class="{
|
||||
fulfilled: timetable.fulfilled,
|
||||
terminated: timetable.terminated && !timetable.fulfilled,
|
||||
@@ -128,7 +128,7 @@ export default defineComponent({
|
||||
|
||||
props: {
|
||||
timetable: {
|
||||
type: Object as PropType<API.TimetableHistory.Data>,
|
||||
type: Object as PropType<API.TimetableHistory.DataShort>,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
@@ -171,23 +171,6 @@ export default defineComponent({
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.info-badge {
|
||||
padding: 0.05em 0.35em;
|
||||
color: black;
|
||||
|
||||
&.terminated {
|
||||
background-color: salmon;
|
||||
}
|
||||
|
||||
&.fulfilled {
|
||||
background-color: lightgreen;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: lightblue;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-timetable {
|
||||
display: flex;
|
||||
padding: 0.2em 0.5em;
|
||||
|
||||
@@ -51,7 +51,7 @@ export default defineComponent({
|
||||
components: { ProgressBar },
|
||||
props: {
|
||||
timetable: {
|
||||
type: Object as PropType<API.TimetableHistory.Data>,
|
||||
type: Object as PropType<API.TimetableHistory.DataShort>,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -10,14 +10,14 @@
|
||||
|
||||
<hr />
|
||||
|
||||
<div @click="toggleExtraInfo" style="cursor: pointer">
|
||||
<div style="cursor: pointer">
|
||||
<!-- Status -->
|
||||
<EntryStatus :timetable="timetableEntry" />
|
||||
</div>
|
||||
|
||||
<!-- Extra -->
|
||||
<EntryDetails
|
||||
:timetable="timetableEntry"
|
||||
:timetableEntry="timetableEntry"
|
||||
:show-extra-info="showExtraInfo"
|
||||
@toggle-extra-info="toggleExtraInfo"
|
||||
/>
|
||||
@@ -28,7 +28,6 @@
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import { API } from '../../../typings/api';
|
||||
import { useApiStore } from '../../../store/apiStore';
|
||||
import { Journal } from '../typings';
|
||||
|
||||
import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
|
||||
import dateMixin from '../../../mixins/dateMixin';
|
||||
@@ -41,7 +40,7 @@ import EntryDetails from './EntryDetails.vue';
|
||||
export default defineComponent({
|
||||
props: {
|
||||
timetableEntry: {
|
||||
type: Object as PropType<API.TimetableHistory.Data>,
|
||||
type: Object as PropType<API.TimetableHistory.DataShort>,
|
||||
required: true
|
||||
},
|
||||
showExtraInfo: {
|
||||
@@ -60,74 +59,9 @@ export default defineComponent({
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
timetablePathDetails() {
|
||||
if (!this.timetableEntry.path || this.timetableEntry.path == '') return null;
|
||||
|
||||
return this.timetableEntry.path.split(';').map((pathEl, i) => {
|
||||
const [arrival, name, departure] = pathEl.split(',');
|
||||
const sceneryName = name.split(' ').slice(0, -1).join(' ');
|
||||
const sceneryHash = name.split(' ').pop()?.replace('.sc', '') ?? '';
|
||||
|
||||
return {
|
||||
arrival,
|
||||
sceneryName,
|
||||
sceneryHash,
|
||||
departure,
|
||||
isVisited: this.timetableEntry.visitedSceneries?.includes(sceneryHash) ?? false
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
timetableStops(): Journal.TimetableStopDetails[] {
|
||||
const timetableEntry = this.timetableEntry;
|
||||
|
||||
const stopNames = timetableEntry.sceneriesString.split('%');
|
||||
|
||||
return stopNames.reduce<Journal.TimetableStopDetails[]>((acc, stopName, i, arr) => {
|
||||
const arrivalDate =
|
||||
i == arr.length - 1
|
||||
? (timetableEntry.checkpointArrivals.at(i) ?? timetableEntry.endDate)
|
||||
: timetableEntry.checkpointArrivals.at(i);
|
||||
|
||||
const scheduledArrivalDate =
|
||||
i == arr.length - 1
|
||||
? (timetableEntry.checkpointArrivalsScheduled.at(i) ?? timetableEntry.scheduledEndDate)
|
||||
: timetableEntry.checkpointArrivalsScheduled.at(i);
|
||||
|
||||
const departureDate =
|
||||
i == 0
|
||||
? (timetableEntry.checkpointDepartures.at(i) ?? timetableEntry.beginDate)
|
||||
: timetableEntry.checkpointDepartures.at(i);
|
||||
|
||||
const scheduledDepartureDate =
|
||||
i == 0
|
||||
? (timetableEntry.checkpointDeparturesScheduled.at(i) ??
|
||||
timetableEntry.scheduledBeginDate)
|
||||
: timetableEntry.checkpointDeparturesScheduled.at(i);
|
||||
|
||||
const stopTime = Number(timetableEntry.checkpointStopTypes.at(i)?.split(',')[0]) || 0;
|
||||
const stopType = timetableEntry.checkpointStopTypes.at(i)?.split(',')[1] || '';
|
||||
|
||||
acc.push({
|
||||
stopName,
|
||||
arrivalTimestamp: this.dateStringToTimestamp(arrivalDate),
|
||||
scheduledArrivalTimestamp: this.dateStringToTimestamp(scheduledArrivalDate),
|
||||
departureTimestamp: this.dateStringToTimestamp(departureDate),
|
||||
scheduledDepartureTimestamp: this.dateStringToTimestamp(scheduledDepartureDate),
|
||||
stopTime,
|
||||
stopType,
|
||||
isConfirmed: i < timetableEntry.confirmedStopsCount
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleExtraInfo() {
|
||||
this.$emit('toggleShowExtraInfo');
|
||||
toggleExtraInfo(data: API.TimetableHistory.Data | null) {
|
||||
this.$emit('toggleShowExtraInfo', data);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -145,7 +79,7 @@ export default defineComponent({
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@include responsive.smallScreen{
|
||||
@include responsive.smallScreen {
|
||||
.entry-route {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
v-for="(timetableEntry, i) in timetableHistory"
|
||||
:key="timetableEntry.id"
|
||||
:timetableEntry="timetableEntry"
|
||||
:onToggleShowExtraInfo="() => toggleExtraInfo(timetableEntry.id)"
|
||||
:onToggleShowExtraInfo="toggleExtraInfo"
|
||||
:showExtraInfo="extraInfoIndexes.includes(timetableEntry.id)"
|
||||
/>
|
||||
</transition-group>
|
||||
@@ -59,9 +59,11 @@ export default defineComponent({
|
||||
JournalTimetableEntry
|
||||
},
|
||||
|
||||
emits: ['toggleExtraInfo'],
|
||||
|
||||
props: {
|
||||
timetableHistory: {
|
||||
type: Array as PropType<API.TimetableHistory.Response>,
|
||||
type: Array as PropType<API.TimetableHistory.ResponseShort>,
|
||||
required: true
|
||||
},
|
||||
scrollNoMoreData: {
|
||||
@@ -75,32 +77,23 @@ export default defineComponent({
|
||||
},
|
||||
dataStatus: {
|
||||
type: Number as PropType<Status.Data>
|
||||
},
|
||||
extraInfoIndexes: {
|
||||
type: Object as PropType<number[]>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
Status,
|
||||
store: useMainStore(),
|
||||
extraInfoIndexes: [] as number[]
|
||||
store: useMainStore()
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$route.query': {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.extraInfoIndexes.length = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleExtraInfo(id: number) {
|
||||
const existingIdx = this.extraInfoIndexes.indexOf(id);
|
||||
|
||||
if (existingIdx != -1) this.extraInfoIndexes.splice(existingIdx, 1);
|
||||
else this.extraInfoIndexes.push(id);
|
||||
toggleExtraInfo(data: API.TimetableHistory.Data | null) {
|
||||
this.$emit('toggleExtraInfo', data);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -111,7 +104,7 @@ export default defineComponent({
|
||||
@use '../../../styles/journal-section';
|
||||
@use '../../../styles/responsive';
|
||||
|
||||
@include responsive.smallScreen{
|
||||
@include responsive.smallScreen {
|
||||
.journal_item-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export namespace Journal {
|
||||
export type DispatcherSearchKey =
|
||||
| 'search-duty-id'
|
||||
| 'search-dispatcher'
|
||||
| 'search-station'
|
||||
| 'search-date-from'
|
||||
@@ -62,19 +63,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;
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
<template>
|
||||
<section class="profile-history-list">
|
||||
<div class="list-header">
|
||||
<div class="history-menu">
|
||||
<button
|
||||
v-for="(filterState, filterKey) in activeFilterTypes"
|
||||
class="menu-btn btn--option"
|
||||
:data-active="filterState"
|
||||
@click="toggleFilter(filterKey)"
|
||||
>
|
||||
{{ t(`profile.filters.${filterKey}`) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="history-list-box">
|
||||
<Loading v-if="journalDataStatus == Status.Data.Loading" />
|
||||
|
||||
<div v-else-if="combinedJournal.length == 0" class="no-recent-history">
|
||||
{{ t('profile.list.no-recent-history') }}
|
||||
</div>
|
||||
|
||||
<router-link
|
||||
v-else
|
||||
v-for="entry in combinedJournal"
|
||||
:to="
|
||||
'trainNo' in entry.value
|
||||
? `/journal/timetables?search-train=%23${entry.value.id}`
|
||||
: `/journal/dispatchers?search-duty-id=${entry.value.id}`
|
||||
"
|
||||
>
|
||||
<!-- Date -->
|
||||
<div class="entry-top-date">
|
||||
<img
|
||||
v-if="entry.type == 'Dispatcher'"
|
||||
src="/images/icon-user.svg"
|
||||
width="25"
|
||||
alt="user icon"
|
||||
/>
|
||||
|
||||
<img
|
||||
v-else-if="entry.type == 'Timetable'"
|
||||
src="/images/icon-train.svg"
|
||||
width="25"
|
||||
alt="train icon"
|
||||
/>
|
||||
|
||||
<img v-else src="/images/icon-timetable.svg" width="25" alt="timetable icon" />
|
||||
|
||||
<b
|
||||
class="timestamp-indicator"
|
||||
:data-online="
|
||||
'isOnline' in entry.value
|
||||
? entry.value.isOnline
|
||||
: !entry.value.terminated && entry.type != 'IssuedTimetable'
|
||||
"
|
||||
>
|
||||
{{ dateToLocaleString(entry.date, { dateStyle: 'long', timeStyle: 'short' }) }}
|
||||
<span v-if="'timestampTo' in entry.value && entry.value.timestampTo">
|
||||
-
|
||||
<span v-if="new Date(entry.value.timestampTo).getDay() == entry.date.getDay()">{{
|
||||
dateToLocaleString(new Date(entry.value.timestampTo), {
|
||||
timeStyle: 'short'
|
||||
})
|
||||
}}</span>
|
||||
<span v-else>{{
|
||||
dateToLocaleString(new Date(entry.value.timestampTo), {
|
||||
dateStyle: 'long',
|
||||
timeStyle: 'short'
|
||||
})
|
||||
}}</span>
|
||||
</span>
|
||||
</b>
|
||||
</div>
|
||||
|
||||
<!-- Timetables -->
|
||||
<div v-if="'trainNo' in entry.value">
|
||||
<b class="text--primary">
|
||||
{{ entry.value.trainCategoryCode }}
|
||||
</b>
|
||||
{{ ' ' }}
|
||||
<b>{{ entry.value.trainNo }}</b>
|
||||
<b class="text--grayed" v-if="entry.type == 'IssuedTimetable'">
|
||||
{{ ' ' }} {{ t('profile.list.for') }}: {{ entry.value.driverName }}
|
||||
</b>
|
||||
{{ ' ' }}
|
||||
<b>{{ entry.value.route.replace('|', ' > ') }}</b>
|
||||
{{ ' ' }}
|
||||
<b class="text--primary">{{ entry.value.currentDistance }} km</b>
|
||||
<b> / {{ entry.value.routeDistance }} km</b>
|
||||
</div>
|
||||
|
||||
<!-- Dispatchers -->
|
||||
<div v-else>
|
||||
<b class="text--primary">{{ entry.value.stationName }}</b>
|
||||
{{ ' - ' }}
|
||||
<b class="timestamp-indicator" :data-online="entry.value.isOnline">
|
||||
<span v-if="entry.value.isOnline">{{ t('profile.list.online-since') }}: </span>
|
||||
<span>{{
|
||||
humanizeDuration((entry.value.timestampTo || Date.now()) - entry.value.timestampFrom)
|
||||
}}</span>
|
||||
</b>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onActivated, onDeactivated, onMounted, reactive, ref } from 'vue';
|
||||
import { dateToLocaleString, humanizeDuration } from '../../composables/time';
|
||||
import { API } from '../../typings/api';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useApiStore } from '../../store/apiStore';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { Status } from '../../typings/common';
|
||||
import Loading from '../Global/Loading.vue';
|
||||
|
||||
type JournalEntryType = 'Timetable' | 'Dispatcher' | 'IssuedTimetable';
|
||||
|
||||
interface JournalEntry {
|
||||
type: JournalEntryType;
|
||||
date: Date;
|
||||
value: API.TimetableHistory.DataShort | API.DispatcherHistory.Data;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
playerName: {
|
||||
type: String
|
||||
}
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const apiStore = useApiStore();
|
||||
const route = useRoute();
|
||||
|
||||
const playerId = ref(-1);
|
||||
const playerJournal = ref<API.PlayerJournal.Data | null>(null);
|
||||
const journalDataStatus = ref(Status.Data.Initialized);
|
||||
|
||||
const intervalId = ref(-1);
|
||||
|
||||
const activeFilterTypes = reactive<Record<JournalEntryType, boolean>>({
|
||||
Timetable: true,
|
||||
Dispatcher: true,
|
||||
IssuedTimetable: true
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
fetchPlayerJournal();
|
||||
intervalId.value = setInterval(fetchPlayerJournal, 30000);
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
clearInterval(intervalId.value);
|
||||
intervalId.value = -1;
|
||||
});
|
||||
|
||||
const combinedJournal = computed<JournalEntry[]>(() => {
|
||||
if (!playerJournal.value || !props.playerName) return [];
|
||||
|
||||
const list = [
|
||||
...playerJournal.value.timetables,
|
||||
...playerJournal.value.duties,
|
||||
...playerJournal.value.issuedTimetables
|
||||
]
|
||||
.reduce<JournalEntry[]>((acc, v) => {
|
||||
// Timetable or dispatcher type
|
||||
if ('trainNo' in v) {
|
||||
const isIssued = v.authorName == props.playerName;
|
||||
|
||||
if (!isIssued && !activeFilterTypes['Timetable']) return acc;
|
||||
if (isIssued && !activeFilterTypes['IssuedTimetable']) return acc;
|
||||
|
||||
acc.push({
|
||||
date: new Date(v.createdAt),
|
||||
type: isIssued ? 'IssuedTimetable' : 'Timetable',
|
||||
value: v
|
||||
});
|
||||
} else {
|
||||
if (!activeFilterTypes['Dispatcher']) return acc;
|
||||
|
||||
acc.push({
|
||||
date: new Date(v.timestampFrom),
|
||||
type: 'Dispatcher',
|
||||
value: v
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [])
|
||||
.sort((a, b) => {
|
||||
return a.date.getTime() - b.date.getTime() > 0 ? -1 : 1;
|
||||
});
|
||||
|
||||
return list;
|
||||
});
|
||||
|
||||
function toggleFilter(filterType: JournalEntryType) {
|
||||
const toggledState = !activeFilterTypes[filterType];
|
||||
|
||||
// Prevent switching off all filters at the same time (at least one must be active)
|
||||
if (
|
||||
toggledState === false &&
|
||||
Object.values(activeFilterTypes).filter((v) => v === false).length ==
|
||||
Object.values(activeFilterTypes).length - 1
|
||||
)
|
||||
return;
|
||||
|
||||
activeFilterTypes[filterType] = toggledState;
|
||||
}
|
||||
|
||||
async function fetchPlayerJournal() {
|
||||
const queryPlayerId = Number(route.query.playerId) || -1;
|
||||
|
||||
if (!apiStore.client || !queryPlayerId) return;
|
||||
|
||||
if (queryPlayerId != playerId.value) {
|
||||
journalDataStatus.value = Status.Data.Loading;
|
||||
}
|
||||
|
||||
playerId.value = queryPlayerId;
|
||||
|
||||
try {
|
||||
const response = await apiStore.client.get<API.PlayerJournal.Data>('api/getPlayerJournal', {
|
||||
params: {
|
||||
playerId: queryPlayerId,
|
||||
dateScope: '30d'
|
||||
}
|
||||
});
|
||||
|
||||
playerJournal.value = response.data;
|
||||
journalDataStatus.value = Status.Data.Loaded;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
journalDataStatus.value = Status.Data.Error;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '../../styles/responsive';
|
||||
|
||||
.profile-history-list {
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
|
||||
& > h3 {
|
||||
padding: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.history-menu {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1em;
|
||||
background-color: var(--clr-tile);
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.menu-btn {
|
||||
padding: 0.5em;
|
||||
font-weight: bold;
|
||||
color: #aaa;
|
||||
|
||||
&[data-active='true'] {
|
||||
color: var(--clr-success);
|
||||
}
|
||||
}
|
||||
|
||||
.history-list-box {
|
||||
padding: 0 0.5em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.history-list-box > a {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25em;
|
||||
|
||||
background-color: var(--clr-bg-light);
|
||||
padding: 0.5em;
|
||||
|
||||
margin-bottom: 0.5em;
|
||||
text-align: initial;
|
||||
|
||||
&:hover {
|
||||
background-color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.no-recent-history {
|
||||
padding: 1em;
|
||||
font-size: 1.25em;
|
||||
font-weight: bold;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.entry-top-date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.timestamp-indicator {
|
||||
color: #ccc;
|
||||
|
||||
&[data-online='true'] {
|
||||
color: var(--clr-success);
|
||||
}
|
||||
}
|
||||
|
||||
@include responsive.midScreen {
|
||||
.profile-history-list {
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="player-avatar">
|
||||
<img
|
||||
v-if="avatarId"
|
||||
class="player-avatar-image"
|
||||
ref="avatarImageRef"
|
||||
:src="`https://td2.info.pl/index.php?action=dlattach;attach=${avatarId};type=avatar`"
|
||||
alt="player image"
|
||||
@load="onAvatarLoadSuccess"
|
||||
@error="onAvatarLoadError"
|
||||
/>
|
||||
|
||||
<img
|
||||
v-if="avatarLoadingStatus == Status.Data.Error || avatarId == 0"
|
||||
class="img-placeholder"
|
||||
height="100"
|
||||
src="/images/default-avatar.jpg"
|
||||
/>
|
||||
|
||||
<Loading v-else-if="avatarLoadingStatus == Status.Data.Loading || avatarId === undefined" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { Status } from '../../typings/common';
|
||||
import Loading from '../Global/Loading.vue';
|
||||
|
||||
defineProps({
|
||||
avatarId: {
|
||||
type: Number
|
||||
}
|
||||
});
|
||||
|
||||
const avatarImageRef = ref<HTMLImageElement | null>(null);
|
||||
const avatarLoadingStatus = ref<Status.Data>(Status.Data.Loading);
|
||||
|
||||
function onAvatarLoadSuccess() {
|
||||
if (!avatarImageRef.value) return;
|
||||
|
||||
avatarLoadingStatus.value = Status.Data.Loaded;
|
||||
avatarImageRef.value.style.opacity = '1';
|
||||
}
|
||||
|
||||
function onAvatarLoadError() {
|
||||
if (!avatarImageRef.value) return;
|
||||
|
||||
avatarLoadingStatus.value = Status.Data.Error;
|
||||
avatarImageRef.value.src = '/images/default-avatar.jpg';
|
||||
avatarImageRef.value.style.opacity = '1';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.player-avatar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
position: relative;
|
||||
min-height: 110px;
|
||||
|
||||
.loading {
|
||||
top: 50%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
img.player-avatar-image {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<section class="profile-recent-stats">
|
||||
<h2 class="stats-header">
|
||||
<img src="/images/icon-stats.svg" width="30" alt="stats icon" />
|
||||
{{ t('profile.recent-stats.header') }}
|
||||
</h2>
|
||||
|
||||
<div class="month-stats-box">
|
||||
<div class="month-stat">
|
||||
<div><img src="/images/icon-train.svg" width="30" alt="train icon" /></div>
|
||||
<div>
|
||||
<h3 class="text--primary">{{ playerInfo.driverStatsLastMonth.countAll }}</h3>
|
||||
</div>
|
||||
<div>{{ t('profile.recent-stats.timetables') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="month-stat">
|
||||
<div><img src="/images/icon-spawn.svg" width="30" alt="spawn icon" /></div>
|
||||
<div>
|
||||
<h3 class="text--primary">
|
||||
{{ playerInfo.driverStatsLastMonth.currentDistanceTotal?.toFixed(2) || 0 }}
|
||||
</h3>
|
||||
</div>
|
||||
<div>{{ t('profile.recent-stats.distance') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="month-stat">
|
||||
<div><img src="/images/icon-user.svg" width="30" alt="user icon" /></div>
|
||||
<div>
|
||||
<h3 class="text--primary">
|
||||
{{ playerInfo.dispatcherStatsLastMonth.services?.count || 0 }}
|
||||
</h3>
|
||||
</div>
|
||||
<div>{{ t('profile.recent-stats.duties') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="month-stat">
|
||||
<div><img src="/images/icon-timetable.svg" width="30" alt="timetable icon" /></div>
|
||||
<div>
|
||||
<h3 class="text--primary">
|
||||
{{ playerInfo.dispatcherStatsLastMonth.issuedTimetables?.count || 0 }}
|
||||
</h3>
|
||||
</div>
|
||||
<div>{{ t('profile.recent-stats.created-timetables') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue';
|
||||
import { API } from '../../typings/api';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
defineProps({
|
||||
playerInfo: {
|
||||
type: Object as PropType<API.PlayerInfo.Data>,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '../../styles/responsive';
|
||||
|
||||
.profile-recent-stats {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stats-header {
|
||||
padding: 1em;
|
||||
|
||||
img {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.month-stats-box {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.5em;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.month-stat {
|
||||
background-color: var(--clr-bg-light);
|
||||
border-radius: 0.5em;
|
||||
padding: 0.5em;
|
||||
|
||||
h3 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
div:nth-child(3) {
|
||||
margin-top: 0.5em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
@include responsive.smallScreen {
|
||||
.month-stats-box {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,418 @@
|
||||
<template>
|
||||
<section class="profile-summary">
|
||||
<div class="player-info">
|
||||
<div class="info-main">
|
||||
<ProfilePlayerAvatar :avatarId="playerTD2Info?.avatar" />
|
||||
|
||||
<div>
|
||||
<h2 class="player-name-header" :class="{ 'text--donator': isPlayerDonator }">
|
||||
<a :href="`https://td2.info.pl/profile/?u=${route.query.playerId}`" target="_blank">
|
||||
<img
|
||||
v-if="isPlayerDonator"
|
||||
src="/images/icon-diamond.svg"
|
||||
width="25"
|
||||
alt="diamond icon"
|
||||
/>
|
||||
{{ playerName }}
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<div class="player-badges">
|
||||
<div class="badge-container" v-if="playerInfo.driverStats.driverLevel != null">
|
||||
<span
|
||||
class="level-badge driver"
|
||||
:style="calculateExpStyles(playerInfo.driverStats.driverLevel)"
|
||||
>
|
||||
{{
|
||||
playerInfo.driverStats.driverLevel > 1 ? playerInfo.driverStats.driverLevel : 'L'
|
||||
}}
|
||||
</span>
|
||||
{{ t('profile.stats.driver') }}
|
||||
</div>
|
||||
|
||||
<div class="badge-container" v-if="playerInfo.dispatcherStats.dispatcherLevel != null">
|
||||
<span
|
||||
class="level-badge dispatcher"
|
||||
:style="calculateExpStyles(playerInfo.dispatcherStats.dispatcherLevel)"
|
||||
>
|
||||
{{
|
||||
playerInfo.dispatcherStats.dispatcherLevel > 1
|
||||
? playerInfo.dispatcherStats.dispatcherLevel
|
||||
: 'L'
|
||||
}}
|
||||
</span>
|
||||
{{ t('profile.stats.dispatcher') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="player-journal-links">
|
||||
<router-link
|
||||
class="a-button btn--action"
|
||||
:to="`/journal/timetables?search-driver=${playerInfo.driverStats.driverName}`"
|
||||
>
|
||||
{{ t('profile.stats.timetables-journal') }}
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
class="a-button btn--action"
|
||||
:to="`/journal/dispatchers?search-dispatcher=${playerInfo.dispatcherStats.dispatcherName}`"
|
||||
>
|
||||
{{ t('profile.stats.dispatchers-journal') }}
|
||||
</router-link>
|
||||
|
||||
<a
|
||||
class="a-button btn--action"
|
||||
:href="`https://td2.info.pl/profile/?u=${route.query.playerId}`"
|
||||
target="_blank"
|
||||
>
|
||||
{{ t('profile.stats.forum-profile') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Current activity -->
|
||||
<div
|
||||
class="player-activity"
|
||||
v-if="activeDispatches.length > 0 || activeTrains.length > 0"
|
||||
>
|
||||
<div class="info-activity" v-if="activeDispatches.length > 0">
|
||||
<router-link
|
||||
v-for="d in activeDispatches"
|
||||
class="dispatcher-badge"
|
||||
:to="`/scenery?station=${d.stationName}`"
|
||||
>
|
||||
<img src="/images/icon-user.svg" width="25" alt="user icon" />
|
||||
<b>{{ d.stationName }} ({{ getRegionNameById(d.region) }})</b>
|
||||
<StationStatusBadge :isOnline="true" :dispatcherStatus="d.dispatcherStatus" />
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="info-activity" v-if="activeTrains.length > 0">
|
||||
<router-link
|
||||
v-for="d in activeTrains"
|
||||
:to="`/driver?trainId=${d.id}`"
|
||||
class="driver-badge"
|
||||
>
|
||||
<img src="/images/icon-train.svg" width="25" alt="train icon" />
|
||||
<span v-if="d.timetable" class="text--primary">{{ d.timetable.category }}</span>
|
||||
<span>{{ d.trainNo }}</span>
|
||||
•
|
||||
<span>{{ d.currentStationName }} ({{ getRegionNameById(d.region) }})</span>
|
||||
•
|
||||
<span class="text--grayed">{{ d.stockString.split(';')[0] }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="player-stats">
|
||||
<div class="stats-driver">
|
||||
<h3 class="stats-header">
|
||||
<img src="/images/icon-train.svg" width="30" alt="train icon" />
|
||||
{{ t('profile.stats.header-driver') }}
|
||||
</h3>
|
||||
<hr />
|
||||
|
||||
<div v-if="playerInfo.driverStats.countAll > 0">
|
||||
<div>
|
||||
<b class="text--primary">
|
||||
{{ playerInfo.driverStats.countFulfilled }} /
|
||||
{{ playerInfo.driverStats.countAll }} ({{
|
||||
getCountPercentage(
|
||||
playerInfo.driverStats.countFulfilled,
|
||||
playerInfo.driverStats.countAll,
|
||||
2
|
||||
)
|
||||
}}%)
|
||||
</b>
|
||||
- {{ t('profile.stats.fulfilled-timetables') }}
|
||||
</div>
|
||||
<div>
|
||||
<b class="text--primary">
|
||||
{{ playerInfo.driverStats.currentDistanceTotal?.toFixed(2) }} /
|
||||
{{ playerInfo.driverStats.routeDistanceTotal?.toFixed(2) }} ({{
|
||||
getCountPercentage(
|
||||
playerInfo.driverStats.currentDistanceTotal || 0,
|
||||
playerInfo.driverStats.routeDistanceTotal || 0,
|
||||
2
|
||||
)
|
||||
}}%)
|
||||
</b>
|
||||
- {{ t('profile.stats.route-distance') }}
|
||||
</div>
|
||||
<div>
|
||||
<b class="text--primary">
|
||||
{{ playerInfo.driverStats.confirmedStopsTotal }} /
|
||||
{{ playerInfo.driverStats.allStopsTotal }} ({{
|
||||
getCountPercentage(
|
||||
playerInfo.driverStats.confirmedStopsTotal || 0,
|
||||
playerInfo.driverStats.allStopsTotal || 0,
|
||||
2
|
||||
)
|
||||
}}%)
|
||||
</b>
|
||||
- {{ t('profile.stats.confirmed-stops') }}
|
||||
</div>
|
||||
<div>
|
||||
<b class="text--primary">{{ playerInfo.driverStats.routeDistanceMax || 0 }}km</b> -
|
||||
{{ t('profile.stats.longest-timetable') }}
|
||||
</div>
|
||||
<div>
|
||||
<b class="text--primary">
|
||||
{{ playerInfo.driverStats.routeDistanceAvg?.toFixed(2) || 0 }}km
|
||||
</b>
|
||||
- {{ t('profile.stats.avg-timetable-length') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text--grayed" v-else>
|
||||
{{ t('profile.stats.no-timetable-stats') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="stats-dispatcher"
|
||||
v-if="playerInfo.dispatcherStats && playerInfo.dispatcherStats.services?.count"
|
||||
>
|
||||
<h3 class="stats-header">
|
||||
<img src="/images/icon-user.svg" width="30" alt="user icon" />
|
||||
{{ t('profile.stats.header-dispatcher') }}
|
||||
</h3>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<b class="text--primary">{{ playerInfo.dispatcherStats.services.count }}</b> -
|
||||
{{ t('profile.stats.duties-count') }}
|
||||
</div>
|
||||
<div>
|
||||
<b class="text--primary">{{
|
||||
humanizeDuration(playerInfo.dispatcherStats.services.durationMax)
|
||||
}}</b>
|
||||
- {{ t('profile.stats.longest-duty') }}
|
||||
</div>
|
||||
|
||||
<div v-if="playerInfo.dispatcherStats.issuedTimetables">
|
||||
<div>
|
||||
<b class="text--primary">{{ playerInfo.dispatcherStats.issuedTimetables.count }}</b>
|
||||
- {{ t('profile.stats.created-timetables-count') }}
|
||||
</div>
|
||||
<div>
|
||||
<b class="text--primary">
|
||||
{{ playerInfo.dispatcherStats.issuedTimetables.distanceMax }}km
|
||||
</b>
|
||||
- {{ t('profile.stats.longest-created-timetable') }}
|
||||
</div>
|
||||
<div>
|
||||
<b class="text--primary">
|
||||
{{ playerInfo.dispatcherStats.issuedTimetables.distanceSum.toFixed(2) }}km
|
||||
</b>
|
||||
- {{ t('profile.stats.created-timetables-length-sum') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text--grayed" v-else>
|
||||
{{ t('profile.stats.no-dispatcher-stats') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, PropType, ref } from 'vue';
|
||||
import { API, Td2API } from '../../typings/api';
|
||||
import { calculateExpStyles } from '../../composables/badge';
|
||||
import { getCountPercentage } from '../../utils/calcUtils';
|
||||
import { humanizeDuration } from '../../composables/time';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useApiStore } from '../../store/apiStore';
|
||||
import StationStatusBadge from '../Global/StationStatusBadge.vue';
|
||||
import axios from 'axios';
|
||||
import ProfilePlayerAvatar from './ProfilePlayerAvatar.vue';
|
||||
import { getRegionNameById } from '../../utils/regionUtils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const route = useRoute();
|
||||
const apiStore = useApiStore();
|
||||
|
||||
const props = defineProps({
|
||||
playerInfo: {
|
||||
type: Object as PropType<API.PlayerInfo.Data>,
|
||||
required: true
|
||||
},
|
||||
|
||||
playerName: {
|
||||
type: String
|
||||
}
|
||||
});
|
||||
|
||||
const playerTD2Info = ref<Td2API.UsersInfoByName.UserInfo | null>(null);
|
||||
|
||||
const isPlayerDonator = computed(() =>
|
||||
props.playerName ? apiStore.donatorsData.includes(props.playerName) : false
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
fetchTD2Data();
|
||||
});
|
||||
|
||||
const activeDispatches = computed(() => {
|
||||
if (!props.playerName) return [];
|
||||
if (!apiStore.activeData || !apiStore.activeData.activeSceneries) return [];
|
||||
|
||||
return apiStore.activeData.activeSceneries.filter(
|
||||
(sc) =>
|
||||
sc.dispatcherName == props.playerName && (sc.lastSeen >= Date.now() - 60000 || sc.isOnline)
|
||||
);
|
||||
});
|
||||
|
||||
const activeTrains = computed(() => {
|
||||
if (!props.playerName) return [];
|
||||
if (!apiStore.activeData || !apiStore.activeData.trains) return [];
|
||||
|
||||
return apiStore.activeData.trains.filter(
|
||||
(t) => t.driverName == props.playerName && (t.lastSeen >= Date.now() - 60000 || t.online)
|
||||
);
|
||||
});
|
||||
|
||||
async function fetchTD2Data() {
|
||||
if (!props.playerName) return;
|
||||
|
||||
try {
|
||||
const response = await axios.get<Td2API.UsersInfoByName.Response>('https://api.td2.info.pl', {
|
||||
params: {
|
||||
method: 'getUsersInfoByName',
|
||||
name: props.playerName
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.success && response.data.message.length == 1) {
|
||||
playerTD2Info.value = response.data.message[0];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '../../styles/badge';
|
||||
@use '../../styles/responsive';
|
||||
|
||||
.profile-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.player-name-header {
|
||||
margin: 0.5em 0;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
.player-badges {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.badge-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
|
||||
font-weight: bold;
|
||||
|
||||
& > .level-badge {
|
||||
font-size: 1.15em;
|
||||
}
|
||||
}
|
||||
|
||||
.player-journal-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.info-activity {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1em;
|
||||
margin-top: 1em;
|
||||
|
||||
.dispatcher-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.driver-badge {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
gap: 0.25em;
|
||||
font-weight: bold;
|
||||
|
||||
padding: 0.25em 0.5em;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.player-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
|
||||
hr {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
}
|
||||
|
||||
.player-info,
|
||||
.player-stats > div {
|
||||
background-color: var(--clr-tile);
|
||||
border-radius: 0.5em;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.stats-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
@include responsive.midScreen {
|
||||
.player-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@include responsive.smallScreen {
|
||||
.player-stats {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -8,10 +8,7 @@
|
||||
{{ onlineScenery.dispatcherExp > 1 ? onlineScenery.dispatcherExp : 'L' }}
|
||||
</span>
|
||||
|
||||
<router-link
|
||||
class="dispatcher-name"
|
||||
:to="`/journal/dispatchers?search-dispatcher=${onlineScenery.dispatcherName}`"
|
||||
>
|
||||
<router-link class="dispatcher-name" :to="`/profile?playerId=${onlineScenery.dispatcherId}`">
|
||||
<span
|
||||
class="text--donator"
|
||||
v-if="apiStore.donatorsData.includes(onlineScenery.dispatcherName)"
|
||||
|
||||
@@ -115,7 +115,7 @@ export default defineComponent({
|
||||
|
||||
data() {
|
||||
return {
|
||||
historyList: [] as API.TimetableHistory.Response,
|
||||
historyList: [] as API.TimetableHistory.ResponseShort,
|
||||
historyModeList,
|
||||
|
||||
apiStore: useApiStore(),
|
||||
@@ -149,7 +149,7 @@ export default defineComponent({
|
||||
requestFilters['returnType'] = 'short';
|
||||
|
||||
try {
|
||||
const response: API.TimetableHistory.Response = await (
|
||||
const response: API.TimetableHistory.ResponseShort = await (
|
||||
await this.apiStore.client!.get('api/getTimetables', {
|
||||
params: requestFilters
|
||||
})
|
||||
@@ -178,7 +178,7 @@ export default defineComponent({
|
||||
});
|
||||
},
|
||||
|
||||
parseCreatedDate(timetable: API.TimetableHistory.Data, locale: string) {
|
||||
parseCreatedDate(timetable: API.TimetableHistory.DataShort, locale: string) {
|
||||
const createdDate =
|
||||
timetable.createdAt > timetable.beginDate
|
||||
? new Date(timetable.beginDate)
|
||||
|
||||
@@ -132,7 +132,6 @@
|
||||
<span v-if="station.onlineInfo?.dispatcherName">
|
||||
<b
|
||||
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
|
||||
@click.prevent="openDonationCard"
|
||||
data-tooltip-type="DonatorTooltip"
|
||||
:data-tooltip-content="$t('donations.dispatcher-message')"
|
||||
>
|
||||
@@ -446,7 +445,7 @@ export default defineComponent({
|
||||
$rowCol: #424242;
|
||||
|
||||
.station_table {
|
||||
height: calc(100vh - 11em);
|
||||
height: calc(100vh - 17em);
|
||||
max-height: 2000px;
|
||||
min-height: 500px;
|
||||
overflow: auto;
|
||||
|
||||
@@ -97,7 +97,7 @@ export default defineComponent({
|
||||
@use '../../styles/animations';
|
||||
|
||||
.train-table {
|
||||
height: calc(100vh - 11em);
|
||||
height: calc(100vh - 17em);
|
||||
min-height: 500px;
|
||||
|
||||
position: relative;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export function calculateExpStyles(exp: number, isSupporter = false) {
|
||||
const bgColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 85%, 50%)`) : '#666';
|
||||
|
||||
const fontColor = exp > 14 || exp == -1 ? 'white' : 'black';
|
||||
const boxShadow = isSupporter ? `0 0 6px 2px ${bgColor};` : '';
|
||||
|
||||
return { 'background-color': bgColor, color: fontColor, 'box-shadow': boxShadow };
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export function calculateDuration(timestampMs: number) {
|
||||
const secondsTotal = Math.floor(timestampMs / 1000);
|
||||
const minsTotal = Math.round(timestampMs / 60000);
|
||||
const hoursTotal = Math.floor(minsTotal / 60);
|
||||
const minsInHour = minsTotal % 60;
|
||||
|
||||
return {
|
||||
secondsTotal,
|
||||
minsTotal,
|
||||
hoursTotal,
|
||||
minsInHour
|
||||
};
|
||||
}
|
||||
|
||||
export function humanizeDuration(timestampMs: number, showSeconds = false) {
|
||||
const { t } = useI18n();
|
||||
|
||||
const duration = calculateDuration(timestampMs);
|
||||
|
||||
return duration.minsTotal >= 60
|
||||
? `${t('journal.hours', { value: duration.hoursTotal }, duration.hoursTotal)} ${t(
|
||||
'journal.minutes',
|
||||
{ value: duration.minsInHour },
|
||||
duration.minsInHour
|
||||
)}`
|
||||
: showSeconds && duration.secondsTotal <= 60
|
||||
? t('journal.seconds', { value: duration.secondsTotal }, duration.secondsTotal)
|
||||
: t('journal.minutes', { value: duration.minsTotal }, duration.minsTotal);
|
||||
}
|
||||
|
||||
export function dateToLocaleString(date: Date, dateOptions: Intl.DateTimeFormatOptions) {
|
||||
const { locale } = useI18n();
|
||||
|
||||
return date.toLocaleString(locale.value == 'pl' ? 'pl-PL' : 'en-GB', dateOptions);
|
||||
}
|
||||
+53
-9
@@ -87,10 +87,8 @@
|
||||
"tooltip-scenery-offline": "Scenery is offline",
|
||||
"pojazdownik-link-content": "POJAZDOWNIK",
|
||||
"language-tooltip-content": "JĘZYK / LANGUAGE",
|
||||
"gnr-link-content": "TRAIN ORDERS <br> GENERATOR"
|
||||
},
|
||||
"footer": {
|
||||
"discord": "Stacjownik Discord server"
|
||||
"gnr-link-content": "TRAIN ORDERS <br> GENERATOR",
|
||||
"discord-link-content": "STACJOWNIK <br> DISCORD SERVER"
|
||||
},
|
||||
"categories": {
|
||||
"EI": "domestic express",
|
||||
@@ -197,6 +195,7 @@
|
||||
"search-train": "Train no. / #",
|
||||
"select-driver": "Choose a driver...",
|
||||
"search-driver": "Driver name",
|
||||
"search-duty-id": "Duty ID",
|
||||
"search-dispatcher": "Dispatcher name",
|
||||
"search-station": "Scenery name / #",
|
||||
"search-author": "Timetable author name",
|
||||
@@ -428,7 +427,7 @@
|
||||
"last-seen-ago": "since {minutes} minutes",
|
||||
"scenery-offline": "Offline ride",
|
||||
"timeout": "An error occured while trying to refresh SWDR timetable data!",
|
||||
"driver-journal-link": "DRIVER JOURNAL",
|
||||
"driver-profile-link": "PLAYER'S PROFILE",
|
||||
"driver-srjp-link": "SRJP",
|
||||
"driver-return-link": "RETURN",
|
||||
"driver-not-found-header": "Train not found! :/",
|
||||
@@ -619,9 +618,54 @@
|
||||
"desc-end": "The train terminates here",
|
||||
"desc-terminated": "The train has been terminated"
|
||||
},
|
||||
"history": {
|
||||
"title": "TIMETABLE JOURNAL",
|
||||
"search-train": "Train no.",
|
||||
"search-driver": "Driver name"
|
||||
"profile": {
|
||||
"journal-button": "PLAYER'S PROFILE",
|
||||
"no-player-found": "Player not found! :/",
|
||||
"return-to-main": "Return to the main site",
|
||||
|
||||
"filters": {
|
||||
"Timetable": "TIMETABLES",
|
||||
"Dispatcher": "DISPATCHER DUTIES",
|
||||
"IssuedTimetable": "ISSUED TIMETABLES"
|
||||
},
|
||||
|
||||
"stats": {
|
||||
"timetables-journal": "TIMETABLE JOURNAL",
|
||||
"dispatchers-journal": "DISPATCHER JOURNAL",
|
||||
"forum-profile": "FORUM PROFILE",
|
||||
|
||||
"driver": "DRIVER",
|
||||
"dispatcher": "DISPATCHER",
|
||||
|
||||
"header-driver": "DRIVER'S STATS",
|
||||
"fulfilled-timetables": "fulfilled timetables",
|
||||
"route-distance": "confirmed timetables distance",
|
||||
"confirmed-stops": "confirmed stations in timetables",
|
||||
"longest-timetable": "longest timetable",
|
||||
"avg-timetable-length": "average distance of all timetables",
|
||||
"no-timetable-stats": "This player does not have any registered timetables in Stacjownik!",
|
||||
|
||||
"header-dispatcher": "DISPATCHER'S STATS",
|
||||
"duties-count": "duties as dispatcher",
|
||||
"longest-duty": "longest duty",
|
||||
"created-timetables-count": "issued timetables as dispatcher",
|
||||
"longest-created-timetable": "longest issued timetable",
|
||||
"created-timetables-length-sum": "distance sum of issued timetables",
|
||||
"no-dispatcher-stats": "No registered dispatcher duties in Stacjownik!"
|
||||
},
|
||||
|
||||
"recent-stats": {
|
||||
"header": "ACTIVITY STATISTICS (30 LAST DAYS)",
|
||||
"timetables": "TIMETABLES",
|
||||
"distance": "MADE KILOMETERS",
|
||||
"duties": "DISPATCHER DUTIES",
|
||||
"created-timetables": "ISSUED TIMETABLES"
|
||||
},
|
||||
|
||||
"list": {
|
||||
"for": "for",
|
||||
"online-since": "online since",
|
||||
"no-recent-history": "No recent activity in the simulator :("
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+53
-7
@@ -83,10 +83,8 @@
|
||||
"tooltip-scenery-offline": "Sceneria offline",
|
||||
"pojazdownik-link-content": "POJAZDOWNIK",
|
||||
"language-tooltip-content": "JĘZYK / LANGUAGE",
|
||||
"gnr-link-content": "GENERATOR <br> ROZKAZÓW PISEMNYCH"
|
||||
},
|
||||
"footer": {
|
||||
"discord": "Serwer Discord Stacjownika"
|
||||
"gnr-link-content": "GENERATOR <br> ROZKAZÓW PISEMNYCH",
|
||||
"discord-link-content": "SERWER DISCORD <br> STACJOWNIKA"
|
||||
},
|
||||
"categories": {
|
||||
"EI": "ekspres krajowy",
|
||||
@@ -193,6 +191,7 @@
|
||||
"search-train": "Nr pociągu / #",
|
||||
"search-driver": "Nick maszynisty",
|
||||
"select-driver": "Wybierz maszynistę...",
|
||||
"search-duty-id": "ID służby",
|
||||
"search-dispatcher": "Nick dyżurnego",
|
||||
"search-station": "Nazwa scenerii / #",
|
||||
"search-author": "Nick autora rozkładu jazdy",
|
||||
@@ -414,7 +413,7 @@
|
||||
"last-seen-ago": "od {minutes} minut",
|
||||
"scenery-offline": "Przejazd offline",
|
||||
"timeout": "Wystąpił problem z aktualizacją rozkładów jazdy z SWDR",
|
||||
"driver-journal-link": "DZIENNIK MASZYNISTY",
|
||||
"driver-profile-link": "PROFIL GRACZA",
|
||||
"driver-srjp-link": "SRJP",
|
||||
"driver-return-link": "POWRÓT",
|
||||
"driver-not-found-header": "Nie znaleziono pociągu! :/",
|
||||
@@ -604,7 +603,54 @@
|
||||
"desc-end": "Pociąg kończy bieg",
|
||||
"desc-terminated": "Pociąg zakończył bieg"
|
||||
},
|
||||
"history": {
|
||||
"title": "DZIENNIK ROZKŁADÓW JAZDY"
|
||||
"profile": {
|
||||
"journal-button": "PROFIL GRACZA",
|
||||
"no-player-found": "Nie znaleziono gracza! :/",
|
||||
"return-to-main": "Powrót do strony głównej",
|
||||
|
||||
"filters": {
|
||||
"Timetable": "ROZKŁADY JAZDY",
|
||||
"Dispatcher": "SŁUŻBY DYŻURNEGO",
|
||||
"IssuedTimetable": "WYSTAWIONE RJ"
|
||||
},
|
||||
|
||||
"stats": {
|
||||
"timetables-journal": "DZIENNIK RJ",
|
||||
"dispatchers-journal": "DZIENNIK DR",
|
||||
"forum-profile": "PROFIL FORUM",
|
||||
|
||||
"driver": "MASZYNISTA",
|
||||
"dispatcher": "DYŻURNY RUCHU",
|
||||
|
||||
"header-driver": "STATYSTYKI MASZYNISTY",
|
||||
"fulfilled-timetables": "wypełnione rozkłady jazdy",
|
||||
"route-distance": "zatwierdzony kilometraż w RJ",
|
||||
"confirmed-stops": "potwierdzonych stacji w RJ",
|
||||
"longest-timetable": "najdłuższy rozkład jazdy",
|
||||
"avg-timetable-length": "średnia długość wszystkich rozkładów",
|
||||
"no-timetable-stats": "Ten użytkownik nie posiada statystyk maszynisty zarejestrowanych przez Stacjownik!",
|
||||
|
||||
"header-dispatcher": "STATYSTYKI DYŻURNEGO RUCHU",
|
||||
"duties-count": "służby jako dyżurny ruchu",
|
||||
"longest-duty": "najdłuższa służba",
|
||||
"created-timetables-count": "wystawione RJ jako dyżurny ruchu",
|
||||
"longest-created-timetable": "najdłuższy wystawiony RJ",
|
||||
"created-timetables-length-sum": "suma długości wystawionych RJ",
|
||||
"no-dispatcher-stats": "Ten użytkownik nie posiada statystyk dyżurnego zarejestrowanych przez Stacjownik!"
|
||||
},
|
||||
|
||||
"recent-stats": {
|
||||
"header": "STATYSTYKI AKTYWNOŚCI (30 DNI)",
|
||||
"timetables": "ROZKŁADÓW JAZDY",
|
||||
"distance": "POKONANYCH KILOMETRÓW",
|
||||
"duties": "SŁUŻB DYŻURNEGO",
|
||||
"created-timetables": "WYSTAWIONYCH ROZKŁADÓW"
|
||||
},
|
||||
|
||||
"list": {
|
||||
"for": "dla",
|
||||
"online-since": "online od",
|
||||
"no-recent-history": "Brak ostatniej aktywności w symulatorze :("
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-2
@@ -61,6 +61,11 @@ const routes: Array<RouteRecordRaw> = [
|
||||
region: route.query.region
|
||||
})
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'PlayerProfileView',
|
||||
component: () => import('../views/PlayerProfileView.vue')
|
||||
},
|
||||
{
|
||||
path: '/:catchAll(.*)',
|
||||
redirect: '/'
|
||||
@@ -69,13 +74,14 @@ const routes: Array<RouteRecordRaw> = [
|
||||
|
||||
const router = createRouter({
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
console.log(to.name);
|
||||
if (
|
||||
(to.name == 'SceneryView' || to.name == 'DriverView') &&
|
||||
(to.name == 'SceneryView' || to.name == 'DriverView' || to.name == 'PlayerProfileView') &&
|
||||
from.name !== to.name &&
|
||||
from.query['view'] === undefined &&
|
||||
!savedPosition
|
||||
)
|
||||
return { el: `.app_main`, behavior: 'instant', top: -13 };
|
||||
return { el: `.app_main`, behavior: 'smooth', top: 0 };
|
||||
|
||||
if (savedPosition) return savedPosition;
|
||||
},
|
||||
|
||||
+19
-1
@@ -9,7 +9,8 @@ export const useApiStore = defineStore('apiStore', {
|
||||
dataStatuses: {
|
||||
connection: Status.Data.Loading,
|
||||
sceneries: Status.Data.Loading,
|
||||
vehicles: Status.Data.Loading
|
||||
vehicles: Status.Data.Loading,
|
||||
dailyStatsData: Status.Data.Loading
|
||||
},
|
||||
|
||||
activeData: undefined as API.ActiveData.Response | undefined,
|
||||
@@ -18,6 +19,8 @@ export const useApiStore = defineStore('apiStore', {
|
||||
donatorsData: [] as API.Donators.Response,
|
||||
sceneryData: [] as StationJSONData[],
|
||||
|
||||
dailyStatsData: null as API.DailyStats.Response | null,
|
||||
|
||||
nextUpdateTime: 0,
|
||||
nextDataCheckTime: 0,
|
||||
|
||||
@@ -119,6 +122,21 @@ export const useApiStore = defineStore('apiStore', {
|
||||
this.dataStatuses.vehicles = Status.Data.Error;
|
||||
console.error('Ups! Wystąpił błąd podczas pobierania informacji o pojazdach:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async fetchDailyStats() {
|
||||
try {
|
||||
const res: API.DailyStats.Response = await (
|
||||
await this.client!.get('api/getDailyStats')
|
||||
).data;
|
||||
|
||||
this.dailyStatsData = res;
|
||||
|
||||
this.dataStatuses.dailyStatsData = Status.Data.Loaded;
|
||||
} catch (error) {
|
||||
console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...');
|
||||
this.dataStatuses.dailyStatsData = Status.Data.Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -135,3 +135,20 @@
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
.timetable-status-badge {
|
||||
padding: 0.05em 0.35em;
|
||||
color: black;
|
||||
|
||||
&.terminated {
|
||||
background-color: salmon;
|
||||
}
|
||||
|
||||
&.fulfilled {
|
||||
background-color: lightgreen;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: lightblue;
|
||||
}
|
||||
}
|
||||
+8
-16
@@ -9,6 +9,9 @@
|
||||
--clr-bg2: #1b1b1b;
|
||||
--clr-bg3: #1d1d1d;
|
||||
--clr-view-bg: #1a1a1a;
|
||||
--clr-bg-light: #2b2b2b;
|
||||
|
||||
--clr-tile: #181818;
|
||||
|
||||
--clr-accent: #1085b3;
|
||||
--clr-accent2: #ff3d5d;
|
||||
@@ -23,6 +26,8 @@
|
||||
|
||||
--clr-donator: #f7a4ff;
|
||||
|
||||
--clr-success: springgreen;
|
||||
|
||||
--no-scroll-padding: 17px;
|
||||
--max-container-width: 1700px;
|
||||
|
||||
@@ -30,9 +35,8 @@
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: var(--no-scroll-padding);
|
||||
height: var(--no-scroll-padding);
|
||||
background-color: transparent;
|
||||
// width: var(--no-scroll-padding);
|
||||
// height: var(--no-scroll-padding);
|
||||
|
||||
&-track {
|
||||
background-color: #333;
|
||||
@@ -49,6 +53,7 @@
|
||||
|
||||
body {
|
||||
background: var(--clr-bg);
|
||||
color-scheme: dark;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -331,19 +336,6 @@ a.a-button {
|
||||
}
|
||||
|
||||
@include responsive.smallScreen {
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5em;
|
||||
height: 0.5em;
|
||||
|
||||
&-track {
|
||||
background-color: #222;
|
||||
}
|
||||
|
||||
&-thumb {
|
||||
background-color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
[data-tooltip]:hover::after,
|
||||
[data-tooltip]:focus::after {
|
||||
transform: translate(-50%, 2em);
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
|
||||
.list_wrapper {
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - 12.5em);
|
||||
min-height: 700px;
|
||||
height: calc(100vh - 21em);
|
||||
min-height: 500px;
|
||||
margin-top: 0.5em;
|
||||
position: relative;
|
||||
|
||||
|
||||
+171
-52
@@ -1,3 +1,4 @@
|
||||
import { Journal } from '../components/JournalView/typings';
|
||||
import { Status, Vehicle, VehicleGroup } from './common';
|
||||
|
||||
export enum APIDataStatus {
|
||||
@@ -27,11 +28,22 @@ export namespace API {
|
||||
}
|
||||
}
|
||||
|
||||
export namespace PlayerActivity {
|
||||
export interface Data {
|
||||
dispatcher: API.ActiveSceneries.Data[];
|
||||
driver: API.ActiveTrains.Data | null;
|
||||
}
|
||||
|
||||
export type Response = Data;
|
||||
}
|
||||
|
||||
export namespace DispatcherHistory {
|
||||
export type Response = Data[];
|
||||
|
||||
export interface Data {
|
||||
id: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
currentDuration: number;
|
||||
dispatcherId: number;
|
||||
dispatcherName: string;
|
||||
@@ -52,61 +64,64 @@ export namespace API {
|
||||
}
|
||||
|
||||
export namespace DispatcherStats {
|
||||
export interface DistanceStat {
|
||||
routeDistance: number | null;
|
||||
export interface Services {
|
||||
count: number;
|
||||
durationMax: number;
|
||||
durationAvg: number;
|
||||
}
|
||||
|
||||
export interface DurationStat {
|
||||
currentDuration: number | null;
|
||||
export interface IssuedTimetables {
|
||||
count: number;
|
||||
distanceMax: number;
|
||||
distanceAvg: number;
|
||||
distanceSum: number;
|
||||
}
|
||||
|
||||
export interface Count {
|
||||
_all: number;
|
||||
export interface Data {
|
||||
dispatcherId: number | null;
|
||||
dispatcherName: string | null;
|
||||
dispatcherLevel: number | null;
|
||||
services: Services | null;
|
||||
issuedTimetables: IssuedTimetables | null;
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
services: {
|
||||
count: number;
|
||||
durationMax: number;
|
||||
durationAvg: number;
|
||||
} | null;
|
||||
|
||||
issuedTimetables: {
|
||||
count: number;
|
||||
distanceMax: number;
|
||||
distanceAvg: number;
|
||||
distanceSum: number;
|
||||
} | null;
|
||||
}
|
||||
export type Response = Data;
|
||||
}
|
||||
|
||||
export namespace DriverStats {
|
||||
export interface SumStats {
|
||||
routeDistance: number;
|
||||
confirmedStopsCount: number;
|
||||
allStopsCount: number;
|
||||
currentDistance: number;
|
||||
export interface Data {
|
||||
driverName: string | null;
|
||||
driverId: number | null;
|
||||
driverLevel: number | null;
|
||||
countAll: number;
|
||||
countTerminated: number;
|
||||
countFulfilled: number;
|
||||
routeDistanceTotal: number | null;
|
||||
routeDistanceAvg: number | null;
|
||||
routeDistanceMax: number | null;
|
||||
currentDistanceTotal: number | null;
|
||||
confirmedStopsTotal: number | null;
|
||||
allStopsTotal: number | null;
|
||||
}
|
||||
|
||||
export interface CountStats {
|
||||
fulfilled: number;
|
||||
terminated: number;
|
||||
_all: number;
|
||||
}
|
||||
export type Response = Data;
|
||||
}
|
||||
|
||||
export interface MaxStats {
|
||||
routeDistance: number;
|
||||
export namespace PlayerInfo {
|
||||
export interface Data {
|
||||
currentActivity: PlayerActivity.Data;
|
||||
dispatcherStats: DispatcherStats.Data;
|
||||
dispatcherStatsLastMonth: DispatcherStats.Data;
|
||||
driverStats: DriverStats.Data;
|
||||
driverStatsLastMonth: DriverStats.Data;
|
||||
}
|
||||
}
|
||||
|
||||
export interface AvdStats {
|
||||
routeDistance: number;
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
_sum: SumStats;
|
||||
_count: CountStats;
|
||||
_max: MaxStats;
|
||||
_avg: AvdStats;
|
||||
export namespace PlayerJournal {
|
||||
export interface Data {
|
||||
timetables: TimetableHistory.DataShort[];
|
||||
issuedTimetables: TimetableHistory.DataShort[];
|
||||
duties: DispatcherHistory.Data[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,14 +226,48 @@ export namespace API {
|
||||
}
|
||||
|
||||
export namespace TimetableHistory {
|
||||
export interface Data {
|
||||
export interface QueryParams {
|
||||
driverName?: string;
|
||||
trainNo?: string;
|
||||
timetableId?: string;
|
||||
categoryCode?: string;
|
||||
|
||||
authorName?: string;
|
||||
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
|
||||
issuedFrom?: string;
|
||||
terminatingAt?: string;
|
||||
via?: string;
|
||||
includesScenery?: string;
|
||||
|
||||
countFrom?: number;
|
||||
countLimit?: number;
|
||||
|
||||
fulfilled?: number;
|
||||
terminated?: number;
|
||||
|
||||
twr?: number;
|
||||
skr?: number;
|
||||
pn?: number;
|
||||
tn?: number;
|
||||
|
||||
returnType?: 'all' | 'short' | 'detailed';
|
||||
|
||||
sortBy?: Journal.TimetableSorter['id'];
|
||||
}
|
||||
|
||||
export interface Data extends DataShort, DataDetailsOnly {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface DataShort {
|
||||
id: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
||||
timetableId: number;
|
||||
trainNo: number;
|
||||
trainCategoryCode: string;
|
||||
timetableId: number;
|
||||
|
||||
driverId: number;
|
||||
driverName: string;
|
||||
@@ -229,7 +278,6 @@ export namespace API {
|
||||
route: string;
|
||||
twr: number;
|
||||
skr: number;
|
||||
sceneriesString: string;
|
||||
currentLocation: string[];
|
||||
|
||||
routeDistance: number;
|
||||
@@ -240,7 +288,6 @@ export namespace API {
|
||||
|
||||
beginDate: string;
|
||||
endDate: string;
|
||||
|
||||
scheduledBeginDate: string;
|
||||
scheduledEndDate: string;
|
||||
|
||||
@@ -250,15 +297,25 @@ export namespace API {
|
||||
authorName?: string;
|
||||
authorId?: number;
|
||||
|
||||
currentSceneryName?: string;
|
||||
currentSceneryHash?: string;
|
||||
hasDangerousCargo: boolean;
|
||||
hasExtraDeliveries: boolean;
|
||||
}
|
||||
|
||||
export interface DataDetailsOnly {
|
||||
id: number;
|
||||
timetableId: number;
|
||||
|
||||
sceneriesString: string;
|
||||
stockString?: string;
|
||||
stockHistory: string[];
|
||||
|
||||
stockMass?: number;
|
||||
stockLength?: number;
|
||||
maxSpeed?: number;
|
||||
trainMaxSpeed?: number;
|
||||
|
||||
currentSceneryName?: string;
|
||||
currentSceneryHash?: string;
|
||||
routeSceneries: string;
|
||||
checkpointArrivals: string[];
|
||||
checkpointDepartures: string[];
|
||||
@@ -268,14 +325,20 @@ export namespace API {
|
||||
checkpointComments: string[];
|
||||
visitedSceneries: string[];
|
||||
sceneryNames: string[];
|
||||
|
||||
path: string;
|
||||
warningNotes: string | null;
|
||||
hasDangerousCargo: boolean;
|
||||
hasExtraDeliveries: boolean;
|
||||
trainMaxSpeed?: number;
|
||||
|
||||
authorId?: number;
|
||||
authorName?: string;
|
||||
driverId: number;
|
||||
driverName: string;
|
||||
driverLanguageId: number | null;
|
||||
}
|
||||
|
||||
export type Response = Data[];
|
||||
export type ResponseShort = DataShort[];
|
||||
export type ResponseDetailsOnly = DataDetailsOnly[];
|
||||
}
|
||||
|
||||
export namespace DailyStats {
|
||||
@@ -427,6 +490,62 @@ export namespace GithubAPI {
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Td2API {
|
||||
export namespace UsersInfoByName {
|
||||
export interface UserStat {
|
||||
variable: string;
|
||||
value: number;
|
||||
position: number;
|
||||
server_total: number;
|
||||
server_max: number;
|
||||
server_min: number;
|
||||
server_avg: number;
|
||||
}
|
||||
|
||||
export interface Levels {
|
||||
driver: number;
|
||||
dispatcher: number;
|
||||
}
|
||||
|
||||
export interface UserGroup {
|
||||
id_group: number;
|
||||
group_name: string;
|
||||
description: string;
|
||||
online_color: string;
|
||||
min_posts: number;
|
||||
max_messages: number;
|
||||
stars: string;
|
||||
group_type: number;
|
||||
hidden: number;
|
||||
id_parent: number;
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
id_member: number;
|
||||
id_group: number;
|
||||
additional_groups: string;
|
||||
member_name: string;
|
||||
karma_bad: number;
|
||||
karma_good: number;
|
||||
date_registered: number;
|
||||
last_login: number;
|
||||
avatar: number;
|
||||
lngfile: string;
|
||||
user_stats: UserStat[];
|
||||
levels: Levels;
|
||||
user_groups: UserGroup[];
|
||||
}
|
||||
|
||||
export type Message = UserInfo[];
|
||||
|
||||
export interface Response {
|
||||
success: boolean;
|
||||
respCode: number;
|
||||
message: Message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Websocket {
|
||||
export interface Payload {
|
||||
activeSceneries: API.ActiveSceneries.Response;
|
||||
|
||||
@@ -220,6 +220,7 @@ export interface CheckpointTrain {
|
||||
export type Vehicle = API.VehiclesData.VehicleObject;
|
||||
export type VehicleGroup = API.VehiclesData.VehicleGroupObject;
|
||||
|
||||
// Train Tooltip Info
|
||||
export interface TooltipUserTrain {
|
||||
driverName: string;
|
||||
trainNo: number;
|
||||
@@ -240,4 +241,4 @@ export interface TooltipTrainInfo {
|
||||
headVehicleName: string;
|
||||
stockCount: number;
|
||||
trainTimetableCategory?: string;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export function getCountPercentage(partCount: number, allCount: number, fixedDigits: number) {
|
||||
if (allCount == 0) return 0;
|
||||
|
||||
return ((partCount / allCount) * 100).toFixed(fixedDigits);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export enum ServerRegion {
|
||||
'eu' = 'PL1',
|
||||
'cae' = 'PL2',
|
||||
'usw' = 'DE',
|
||||
'us' = 'CZE',
|
||||
'ru' = 'ENG'
|
||||
}
|
||||
|
||||
export const regions: Record<string, string> = {
|
||||
eu: 'PL1',
|
||||
cae: 'PL2',
|
||||
usw: 'DE',
|
||||
us: 'CZE',
|
||||
ru: 'ENG'
|
||||
};
|
||||
|
||||
export function getRegionNameById(id: string) {
|
||||
return regions[id] ?? 'PL1';
|
||||
}
|
||||
@@ -47,6 +47,6 @@ const chosenTrain = computed(() =>
|
||||
margin: 0 auto;
|
||||
padding: 1em 0;
|
||||
max-width: var(--max-container-width);
|
||||
min-height: calc(100vh - 7em);
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
optionsType="dispatchers"
|
||||
/>
|
||||
|
||||
<JournalStats :statsButtons="statsButtons" />
|
||||
<JournalStats :chosen-player-id="chosenPlayerId" />
|
||||
</div>
|
||||
|
||||
<div class="journal_refreshed-date">
|
||||
@@ -50,16 +50,8 @@ 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 {
|
||||
dutyId?: number;
|
||||
dispatcherName?: string;
|
||||
stationName?: string;
|
||||
stationHash?: string;
|
||||
@@ -105,18 +97,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
|
||||
@@ -126,12 +115,13 @@ export default defineComponent({
|
||||
const sorterActive: Journal.DispatcherSorter = reactive({ id: 'timestampFrom', dir: -1 });
|
||||
const journalFilterActive = ref({});
|
||||
|
||||
const searchersValues = reactive({
|
||||
const searchersValues = reactive<Record<Journal.DispatcherSearchKey, string>>({
|
||||
'search-duty-id': '',
|
||||
'search-dispatcher': '',
|
||||
'search-station': '',
|
||||
'search-date-from': '',
|
||||
'search-date-to': ''
|
||||
} as Journal.DispatcherSearchType);
|
||||
});
|
||||
|
||||
provide('sorterActive', sorterActive);
|
||||
provide('journalFilterActive', journalFilterActive);
|
||||
@@ -158,15 +148,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();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -192,6 +173,7 @@ export default defineComponent({
|
||||
handleRouteParams() {
|
||||
this.$router.push({
|
||||
query: {
|
||||
'search-duty-id': this.searchersValues['search-duty-id'] || undefined,
|
||||
'search-date-from': this.searchersValues['search-date-from'] || undefined,
|
||||
'search-date-to': this.searchersValues['search-date-to'] || undefined,
|
||||
'search-station': this.searchersValues['search-station'] || undefined,
|
||||
@@ -215,30 +197,8 @@ 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 }) {
|
||||
setOptions(options: Record<string, string>) {
|
||||
this.searchersValues['search-duty-id'] = options['search-duty-id'] ?? '';
|
||||
this.searchersValues['search-date-from'] = options['search-date-from'] ?? '';
|
||||
this.searchersValues['search-date-to'] = options['search-date-to'] ?? '';
|
||||
this.searchersValues['search-station'] = options['search-station'] ?? '';
|
||||
@@ -275,6 +235,7 @@ export default defineComponent({
|
||||
async fetchHistoryData() {
|
||||
const queryParams: DispatchersQueryParams = {};
|
||||
|
||||
const dutyId = this.searchersValues['search-duty-id'].trim() || undefined;
|
||||
const dispatcherName = this.searchersValues['search-dispatcher'].trim() || undefined;
|
||||
const stationName = this.searchersValues['search-station'].trim() || undefined;
|
||||
const dateFromString = this.searchersValues['search-date-from'].trim() || undefined;
|
||||
@@ -295,6 +256,7 @@ export default defineComponent({
|
||||
dateToISO = dateTo.toISOString();
|
||||
}
|
||||
|
||||
queryParams['dutyId'] = Number(dutyId) || undefined;
|
||||
queryParams['dispatcherName'] = dispatcherName;
|
||||
|
||||
queryParams['dateFrom'] = dateFromISO;
|
||||
@@ -320,24 +282,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;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
optionsType="timetables"
|
||||
/>
|
||||
|
||||
<JournalStats :statsButtons="statsButtons" />
|
||||
<JournalStats :chosen-player-id="chosenPlayerId" />
|
||||
</div>
|
||||
|
||||
<div class="journal_refreshed-date">
|
||||
@@ -29,6 +29,8 @@
|
||||
:dataStatus="dataStatus"
|
||||
:scrollDataLoaded="scrollDataLoaded"
|
||||
:scrollNoMoreData="scrollNoMoreData"
|
||||
:extraInfoIndexes="extraInfoIndexes"
|
||||
@toggleExtraInfo="toggleExtraInfo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,36 +120,6 @@ export const journalTimetableFilters: Journal.TimetableFilter[] = [
|
||||
}
|
||||
];
|
||||
|
||||
interface TimetablesQueryParams {
|
||||
driverName?: string;
|
||||
trainNo?: string;
|
||||
timetableId?: string;
|
||||
categoryCode?: string;
|
||||
|
||||
authorName?: string;
|
||||
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
|
||||
issuedFrom?: string;
|
||||
terminatingAt?: string;
|
||||
via?: string;
|
||||
includesScenery?: string;
|
||||
|
||||
countFrom?: number;
|
||||
countLimit?: number;
|
||||
|
||||
fulfilled?: number;
|
||||
terminated?: number;
|
||||
|
||||
twr?: number;
|
||||
skr?: number;
|
||||
pn?: number;
|
||||
tn?: number;
|
||||
|
||||
sortBy?: Journal.TimetableSorter['id'];
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
JournalOptions,
|
||||
@@ -170,35 +142,18 @@ 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,
|
||||
currentQueryParams: {} as API.TimetableHistory.QueryParams,
|
||||
dataRefreshedAt: null as Date | null,
|
||||
|
||||
scrollDataLoaded: true,
|
||||
scrollNoMoreData: false,
|
||||
extraInfoIndexes: [] as number[],
|
||||
|
||||
showReturnButton: false,
|
||||
statsCardOpen: false,
|
||||
currentOptionsActive: false,
|
||||
chosenPlayerId: -1,
|
||||
|
||||
timetableHistory: [] as API.TimetableHistory.Response,
|
||||
timetableHistory: [] as API.TimetableHistory.ResponseShort,
|
||||
|
||||
dataStatus: Status.Data.Loading,
|
||||
dataErrorMessage: ''
|
||||
dataStatus: Status.Data.Loading
|
||||
}),
|
||||
|
||||
setup() {
|
||||
@@ -245,18 +200,11 @@ 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();
|
||||
computed: {
|
||||
currentOptionsActive() {
|
||||
return Object.keys(this.currentQueryParams)
|
||||
.filter((k) => k != 'countFrom' && k != 'returnType')
|
||||
.some((k) => (this.currentQueryParams as any)[k] !== undefined);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -287,28 +235,21 @@ 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;
|
||||
}
|
||||
async toggleExtraInfo(timetableDetails: API.TimetableHistory.Data | null) {
|
||||
if (!timetableDetails) return;
|
||||
|
||||
try {
|
||||
this.mainStore.driverStatsStatus = Status.Data.Loading;
|
||||
const existingIdx = this.extraInfoIndexes.indexOf(timetableDetails.id);
|
||||
|
||||
const statsData: API.DriverStats.Response = await (
|
||||
await this.apiStore.client!.get(
|
||||
`api/getDriverInfo?name=${this.mainStore.driverStatsName}`
|
||||
)
|
||||
).data;
|
||||
if (existingIdx == -1) {
|
||||
this.extraInfoIndexes.push(timetableDetails.id);
|
||||
|
||||
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! :/');
|
||||
const synchedTimetable = this.timetableHistory.find((t) => t.id == timetableDetails.id);
|
||||
|
||||
if (synchedTimetable) {
|
||||
Object.assign(synchedTimetable, timetableDetails);
|
||||
}
|
||||
} else {
|
||||
this.extraInfoIndexes.splice(existingIdx, 1);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -354,6 +295,8 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
async fetchHistoryData() {
|
||||
this.extraInfoIndexes.length = 0;
|
||||
|
||||
const driverName = this.searchersValues['search-driver'].trim() || undefined;
|
||||
const trainNo = this.searchersValues['search-train'].trim() || undefined;
|
||||
const authorName = this.searchersValues['search-dispatcher'].trim() || undefined;
|
||||
@@ -378,7 +321,7 @@ export default defineComponent({
|
||||
dateToISO = dateTo.toISOString();
|
||||
}
|
||||
|
||||
const queryParams: TimetablesQueryParams = {};
|
||||
const queryParams: API.TimetableHistory.QueryParams = {};
|
||||
|
||||
this.filterList
|
||||
.filter((f) => f.isActive)
|
||||
@@ -445,6 +388,7 @@ export default defineComponent({
|
||||
queryParams['terminatingAt'] = terminatingAt;
|
||||
queryParams['via'] = via;
|
||||
queryParams['categoryCode'] = categoryCode;
|
||||
queryParams['returnType'] = 'short';
|
||||
|
||||
queryParams['issuedFrom'] = issuedFrom;
|
||||
queryParams['sortBy'] =
|
||||
@@ -456,7 +400,7 @@ export default defineComponent({
|
||||
this.currentQueryParams = queryParams;
|
||||
|
||||
try {
|
||||
const responseData: API.TimetableHistory.Response = await (
|
||||
const responseData: API.TimetableHistory.ResponseShort = await (
|
||||
await this.apiStore.client!.get('api/getTimetables', {
|
||||
params: this.currentQueryParams
|
||||
})
|
||||
@@ -464,26 +408,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;
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div class="profile-view">
|
||||
<div class="profile-wrapper" v-if="playerInfo && playerDataStatus == Status.Data.Loaded">
|
||||
<ProfileSummary :playerInfo="playerInfo" :playerName="playerName" />
|
||||
|
||||
<div class="profile-side">
|
||||
<ProfileRecentStats :playerInfo="playerInfo" />
|
||||
<ProfileHistoryList :playerName="playerName" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Loading v-else-if="playerDataStatus == Status.Data.Loading" />
|
||||
|
||||
<div class="no-data-found" v-else>
|
||||
<div>
|
||||
<h3>{{ t('profile.no-player-found') }}</h3>
|
||||
<router-link to="/" class="btn btn--text"> {{ t('profile.return-to-main') }}</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onActivated, onDeactivated, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useApiStore } from '../store/apiStore';
|
||||
import { API } from '../typings/api';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Status } from '../typings/common';
|
||||
|
||||
import Loading from '../components/Global/Loading.vue';
|
||||
import ProfileSummary from '../components/PlayerProfileView/ProfileSummary.vue';
|
||||
import ProfileRecentStats from '../components/PlayerProfileView/ProfileRecentStats.vue';
|
||||
import ProfileHistoryList from '../components/PlayerProfileView/ProfileHistoryList.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const apiStore = useApiStore();
|
||||
const route = useRoute();
|
||||
|
||||
const playerId = ref(-1);
|
||||
const playerName = ref('');
|
||||
|
||||
const playerInfo = ref<API.PlayerInfo.Data | null>(null);
|
||||
const playerDataStatus = ref(Status.Data.Initialized);
|
||||
|
||||
const intervalId = ref(-1);
|
||||
|
||||
onActivated(() => {
|
||||
fetchPlayerData();
|
||||
|
||||
intervalId.value = setInterval(fetchPlayerData, 30000);
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
clearInterval(intervalId.value);
|
||||
intervalId.value = -1;
|
||||
});
|
||||
|
||||
async function fetchPlayerData() {
|
||||
const queryPlayerId = Number(route.query.playerId) || -1;
|
||||
|
||||
if (!apiStore.client || !queryPlayerId) return;
|
||||
|
||||
if (queryPlayerId != playerId.value) {
|
||||
playerDataStatus.value = Status.Data.Loading;
|
||||
}
|
||||
|
||||
playerId.value = queryPlayerId;
|
||||
|
||||
try {
|
||||
const response = await apiStore.client.get<API.PlayerInfo.Data>('api/getPlayerInfo', {
|
||||
params: {
|
||||
playerId: queryPlayerId
|
||||
}
|
||||
});
|
||||
|
||||
playerName.value =
|
||||
response.data.driverStats.driverName || response.data.dispatcherStats.dispatcherName || '';
|
||||
|
||||
playerInfo.value = response.data || null;
|
||||
playerDataStatus.value = Status.Data.Loaded;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
playerDataStatus.value = Status.Data.Error;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '../styles/responsive';
|
||||
|
||||
.profile-view {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
height: 100vh;
|
||||
min-height: 500px;
|
||||
max-height: 2000px;
|
||||
}
|
||||
|
||||
.no-data-found {
|
||||
text-align: center;
|
||||
|
||||
max-width: var(--max-container-width);
|
||||
width: 100%;
|
||||
background-color: var(--clr-tile);
|
||||
padding: 1em;
|
||||
margin: 1em;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
text-decoration: underline;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 500px 1fr;
|
||||
|
||||
gap: 1em;
|
||||
position: relative;
|
||||
|
||||
max-width: var(--max-container-width);
|
||||
width: 100%;
|
||||
|
||||
padding: 1rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profile-side {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
overflow: auto;
|
||||
background-color: var(--clr-tile);
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
@include responsive.midScreen {
|
||||
.profile-view {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.profile-wrapper {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 1000px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -135,6 +135,10 @@ function setViewMode(componentName: string) {
|
||||
&-view {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
height: 100vh;
|
||||
min-height: 500px;
|
||||
max-height: 2000px;
|
||||
}
|
||||
|
||||
&-offline {
|
||||
@@ -181,10 +185,6 @@ function setViewMode(componentName: string) {
|
||||
background-color: #181818;
|
||||
border-radius: 0.5em;
|
||||
padding: 1em 0.5em;
|
||||
|
||||
height: calc(100vh - 0.5em);
|
||||
min-height: 500px;
|
||||
max-height: 2000px;
|
||||
}
|
||||
|
||||
.scenery-left {
|
||||
@@ -236,6 +236,10 @@ function setViewMode(componentName: string) {
|
||||
}
|
||||
|
||||
@include responsive.midScreen {
|
||||
.scenery-view {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.scenery-wrapper {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0;
|
||||
|
||||
@@ -32,6 +32,16 @@
|
||||
<FlagIcon :language-id="mainStore.currentLocale == 'pl' ? 0 : 1" />
|
||||
</button>
|
||||
|
||||
<a
|
||||
class="a-button btn--image discord-link"
|
||||
href="https://discord.gg/x2mpNN3svk"
|
||||
target="_blank"
|
||||
data-tooltip-type="HtmlTooltip"
|
||||
:data-tooltip-content="`<b>${$t('app.discord-link-content')}</b>`"
|
||||
>
|
||||
<img src="/images/icon-discord.png" alt="discord logo icon" />
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="a-button btn--image gnr-link"
|
||||
href="https://generator-td2.web.app/"
|
||||
@@ -203,11 +213,12 @@ a.pojazdownik-link {
|
||||
}
|
||||
}
|
||||
|
||||
a.gnr-link {
|
||||
a.gnr-link,
|
||||
a.discord-link {
|
||||
background-color: #141414;
|
||||
|
||||
&:hover {
|
||||
background-color: #222222;
|
||||
background-color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ export default defineConfig({
|
||||
publicDir: 'public',
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: { additionalData: `@use '@/styles/global';`, silenceDeprecations: ['legacy-js-api'] }
|
||||
scss: { silenceDeprecations: ['legacy-js-api'] }
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
|
||||
Reference in New Issue
Block a user