Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3be964ec99 | |||
| caf6137ca3 | |||
| 0f2e5e084b | |||
| fc7a9be9dd | |||
| 3c3a114a38 | |||
| fe6972c1f8 | |||
| 08b9b72dcd | |||
| c90be042e7 | |||
| 430a05ab38 |
@@ -1,17 +0,0 @@
|
|||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
github-releases-to-discord:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Github Releases To Discord
|
|
||||||
uses: SethCohen/github-releases-to-discord@v1.13.1
|
|
||||||
with:
|
|
||||||
webhook_url: ${{ secrets.WEBHOOK_URL }}
|
|
||||||
color: "15844367"
|
|
||||||
footer_title: "Changelog - Stacjownik"
|
|
||||||
footer_timestamp: true
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
name: Build & Deploy to VPS
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
env:
|
|
||||||
PROJECT_NAME: stacjownik-td2
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build_and_deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Build the app
|
|
||||||
run: yarn && yarn build
|
|
||||||
- name: Setup SSH key for connection with the server
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
echo "${{ secrets.VPS_SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa && chmod 600 ~/.ssh/id_rsa
|
|
||||||
- name: Send new files
|
|
||||||
run: rsync -avP -e "ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa -p 2022" ./dist/ ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }}:/var/www/$PROJECT_NAME --delete
|
|
||||||
@@ -15,8 +15,8 @@ app.get('/api/getSceneries', (_, res) => {
|
|||||||
res.sendFile(path.join(cwd(), 'endpoints', 'getSceneries.json'));
|
res.sendFile(path.join(cwd(), 'endpoints', 'getSceneries.json'));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/getVehiclesData', (_, res) => {
|
app.get('/api/getVehicles', (_, res) => {
|
||||||
res.sendFile(path.join(cwd(), 'endpoints', 'getVehiclesData.json'));
|
res.sendFile(path.join(cwd(), 'endpoints', 'getVehicles.json'));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/getDonators', (_, res) => {
|
app.get('/api/getDonators', (_, res) => {
|
||||||
|
|||||||
@@ -62,24 +62,24 @@
|
|||||||
crossorigin
|
crossorigin
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<link rel="preload" as="image" href="/images/icon-pl.svg" />
|
||||||
<link rel="preload" as="image" href="/images/stacjownik-header-logo.svg" />
|
<link rel="preload" as="image" href="/images/stacjownik-header-logo.svg" />
|
||||||
<link rel="preload" as="image" href="/images/icon-dispatcher.svg" />
|
<link rel="preload" as="image" href="/images/icon-dispatcher.svg" />
|
||||||
<link rel="preload" as="image" href="/images/icon-train.svg" />
|
<link rel="preload" as="image" href="/images/icon-train.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-arrow-asc.svg" />
|
||||||
<link rel="preload" as="image" href="/images/icon-arrow-desc.svg" />
|
<link rel="preload" as="image" href="/images/icon-arrow-desc.svg" />
|
||||||
<link rel="preload" as="image" href="/images/icon-pojazdownik.svg" />
|
|
||||||
<link rel="preload" as="image" href="/images/icon-stats.svg" />
|
|
||||||
<link rel="preload" as="image" href="/images/icon-filter2.svg" />
|
<link rel="preload" as="image" href="/images/icon-filter2.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-stats.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-gnr.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-pojazdownik.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-diamond.svg" />
|
||||||
<link rel="preload" as="image" href="/images/icon-user.svg" />
|
<link rel="preload" as="image" href="/images/icon-user.svg" />
|
||||||
<link rel="preload" as="image" href="/images/icon-like.svg" />
|
<link rel="preload" as="image" href="/images/icon-like.svg" />
|
||||||
<link rel="preload" as="image" href="/images/icon-gnr.svg" />
|
|
||||||
<link rel="preload" as="image" href="/images/icon-spawn.svg" />
|
<link rel="preload" as="image" href="/images/icon-spawn.svg" />
|
||||||
<link rel="preload" as="image" href="/images/icon-timetableAll.svg" />
|
<link rel="preload" as="image" href="/images/icon-timetableAll.svg" />
|
||||||
<link rel="preload" as="image" href="/images/icon-timetableUnconfirmed.svg" />
|
<link rel="preload" as="image" href="/images/icon-timetableUnconfirmed.svg" />
|
||||||
<link rel="preload" as="image" href="/images/icon-timetableConfirmed.svg" />
|
<link rel="preload" as="image" href="/images/icon-timetableConfirmed.svg" />
|
||||||
<link rel="preload" as="image" href="/images/icon-discord.png" />
|
<link rel="preload" as="image" href="/images/icon-discord.png" />
|
||||||
|
|
||||||
<link rel="prefetch" as="image" href="/images/icon-arrow-asc.svg" />
|
|
||||||
<link rel="prefetch" as="image" href="/images/icon-diamond.svg" />
|
|
||||||
|
|
||||||
<!-- Static OpenGraph meta -->
|
<!-- Static OpenGraph meta -->
|
||||||
<meta name="description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
|
<meta name="description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "stacjownik",
|
"name": "stacjownik",
|
||||||
"version": "1.31.1",
|
"version": "1.30.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -1,5 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-cz" viewBox="0 0 640 480">
|
|
||||||
<path fill="#fff" d="M0 0h640v240H0z"/>
|
|
||||||
<path fill="#d7141a" d="M0 240h640v240H0z"/>
|
|
||||||
<path fill="#11457e" d="M360 240 0 0v480z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 225 B |
@@ -1,5 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-de" viewBox="0 0 640 480">
|
|
||||||
<path fill="#fc0" d="M0 320h640v160H0z"/>
|
|
||||||
<path fill="#000001" d="M0 0h640v160H0z"/>
|
|
||||||
<path fill="red" d="M0 160h640v160H0z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 221 B |
@@ -1,7 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-it" viewBox="0 0 640 480">
|
|
||||||
<g fill-rule="evenodd" stroke-width="1pt">
|
|
||||||
<path fill="#fff" d="M0 0h640v480H0z"/>
|
|
||||||
<path fill="#009246" d="M0 0h213.3v480H0z"/>
|
|
||||||
<path fill="#ce2b37" d="M426.7 0H640v480H426.7z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 289 B |
@@ -1,5 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-ru" viewBox="0 0 640 480">
|
|
||||||
<path fill="#fff" d="M0 0h640v160H0z"/>
|
|
||||||
<path fill="#0039a6" d="M0 160h640v160H0z"/>
|
|
||||||
<path fill="#d52b1e" d="M0 320h640v160H0z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 225 B |
@@ -1,4 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-se" viewBox="0 0 640 480">
|
|
||||||
<path fill="#005293" d="M0 0h640v480H0z"/>
|
|
||||||
<path fill="#fecb00" d="M176 0v192H0v96h176v192h96V288h368v-96H272V0z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 209 B |
@@ -1,9 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-sk" viewBox="0 0 640 480">
|
|
||||||
<path fill="#ee1c25" d="M0 0h640v480H0z"/>
|
|
||||||
<path fill="#0b4ea2" d="M0 0h640v320H0z"/>
|
|
||||||
<path fill="#fff" d="M0 0h640v160H0z"/>
|
|
||||||
<path fill="#fff" d="M233 370.8c-43-20.7-104.6-61.9-104.6-143.2 0-81.4 4-118.4 4-118.4h201.3s3.9 37 3.9 118.4S276 350 233 370.8"/>
|
|
||||||
<path fill="#ee1c25" d="M233 360c-39.5-19-96-56.8-96-131.4s3.6-108.6 3.6-108.6h184.8s3.5 34 3.5 108.6C329 303.3 272.5 341 233 360"/>
|
|
||||||
<path fill="#fff" d="M241.4 209c10.7.2 31.6.6 50.1-5.6 0 0-.4 6.7-.4 14.4s.5 14.4.5 14.4c-17-5.7-38.1-5.8-50.2-5.7v41.2h-16.8v-41.2c-12-.1-33.1 0-50.1 5.7 0 0 .5-6.7.5-14.4s-.5-14.4-.5-14.4c18.5 6.2 39.4 5.8 50 5.6v-25.9c-9.7 0-23.7.4-39.6 5.7 0 0 .5-6.6.5-14.4 0-7.7-.5-14.4-.5-14.4 15.9 5.3 29.9 5.8 39.6 5.7-.5-16.4-5.3-37-5.3-37s9.9.7 13.8.7 13.8-.7 13.8-.7-4.8 20.6-5.3 37c9.7.1 23.7-.4 39.6-5.7 0 0-.5 6.7-.5 14.4s.5 14.4.5 14.4a119 119 0 0 0-39.7-5.7v26z"/>
|
|
||||||
<path fill="#0b4ea2" d="M233 263.3c-19.9 0-30.5 27.5-30.5 27.5s-6-13-22.2-13c-11 0-19 9.7-24.2 18.8 20 31.7 51.9 51.3 76.9 63.4 25-12 57-31.7 76.9-63.4-5.2-9-13.2-18.8-24.2-18.8-16.2 0-22.2 13-22.2 13S253 263.3 233 263.3"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,6 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-ua" viewBox="0 0 640 480">
|
|
||||||
<g fill-rule="evenodd" stroke-width="1pt">
|
|
||||||
<path fill="gold" d="M0 0h640v480H0z"/>
|
|
||||||
<path fill="#0057b8" d="M0 0h640v240H0z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 232 B |
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 504 B After Width: | Height: | Size: 504 B |
|
Before Width: | Height: | Size: 219 B After Width: | Height: | Size: 219 B |
@@ -1,211 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app_container">
|
<div class="app_container">
|
||||||
<UpdateCard
|
<AppNewDomainInfo />
|
||||||
:is-update-card-open="isUpdateCardOpen"
|
|
||||||
@toggle-card="() => (isUpdateCardOpen = false)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AppWelcomeCard :is-card-open="isWelcomeCardOpen" @toggle-card="closeWelcomeCard" />
|
|
||||||
|
|
||||||
<Tooltip />
|
|
||||||
|
|
||||||
<AppHeader />
|
|
||||||
|
|
||||||
<main class="app_main">
|
|
||||||
<router-view v-slot="{ Component }">
|
|
||||||
<keep-alive>
|
|
||||||
<component :is="Component" :key="$route.name" />
|
|
||||||
</keep-alive>
|
|
||||||
</router-view>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<AppFooter
|
|
||||||
:version="VERSION"
|
|
||||||
:is-on-production-host="isOnProductionHost"
|
|
||||||
:is-update-card-open="isUpdateCardOpen"
|
|
||||||
@open-update-card="() => (isUpdateCardOpen = true)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import AppNewDomainInfo from './components/App/AppNewDomainInfo.vue';
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
import { version } from '../package.json';
|
|
||||||
import { Status } from './typings/common';
|
|
||||||
import { useMainStore } from './store/mainStore';
|
|
||||||
import { useApiStore } from './store/apiStore';
|
|
||||||
import { useTooltipStore } from './store/tooltipStore';
|
|
||||||
|
|
||||||
import Clock from './components/App/Clock.vue';
|
|
||||||
import StatusIndicator from './components/App/StatusIndicator.vue';
|
|
||||||
import AppHeader from './components/App/AppHeader.vue';
|
|
||||||
import Tooltip from './components/Tooltip/Tooltip.vue';
|
|
||||||
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';
|
|
||||||
|
|
||||||
const STORAGE_VERSION_KEY = 'app_version';
|
|
||||||
const WELCOME_CARD_SEEN_KEY = 'welcome_card_seen';
|
|
||||||
const MIGRATE_INFO_CARD_SEEN_KEY = 'migrate_info_card_seen';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: {
|
|
||||||
Clock,
|
|
||||||
StatusIndicator,
|
|
||||||
AppHeader,
|
|
||||||
AppFooter,
|
|
||||||
UpdateCard,
|
|
||||||
AppWelcomeCard,
|
|
||||||
Tooltip
|
|
||||||
},
|
|
||||||
|
|
||||||
data: () => ({
|
|
||||||
VERSION: version,
|
|
||||||
store: useMainStore(),
|
|
||||||
apiStore: useApiStore(),
|
|
||||||
tooltipStore: useTooltipStore(),
|
|
||||||
|
|
||||||
isUpdateCardOpen: false,
|
|
||||||
isWelcomeCardOpen: false,
|
|
||||||
|
|
||||||
isOnProductionHost: /(stacjownik-td2)(\.web\.app|\.spythere\.eu)/.test(location.hostname)
|
|
||||||
}),
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.init();
|
|
||||||
},
|
|
||||||
|
|
||||||
async mounted() {
|
|
||||||
window.addEventListener('mousemove', (e: MouseEvent) => this.tooltipStore.handle(e));
|
|
||||||
window.addEventListener('mousedown', () => this.tooltipStore.hide());
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
init() {
|
|
||||||
if (!this.isOnProductionHost) document.title = 'Stacjownik Dev';
|
|
||||||
|
|
||||||
this.loadLang();
|
|
||||||
this.setupOfflineHandling();
|
|
||||||
this.checkAppVersion();
|
|
||||||
this.handleQueries();
|
|
||||||
this.handleMigrateInfo();
|
|
||||||
|
|
||||||
this.apiStore.setupAPIData();
|
|
||||||
},
|
|
||||||
|
|
||||||
handleQueries() {
|
|
||||||
const query = new URLSearchParams(window.location.search);
|
|
||||||
|
|
||||||
if (query.get('welcomeCard') == '1') {
|
|
||||||
this.isWelcomeCardOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.get('migrateCard') == '1') {
|
|
||||||
this.store.isMigrateInfoCardOpen = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async checkAppVersion() {
|
|
||||||
const isWelcomeCardSeen = StorageManager.getBooleanValue(WELCOME_CARD_SEEN_KEY);
|
|
||||||
const storageVersion = StorageManager.getStringValue(STORAGE_VERSION_KEY);
|
|
||||||
|
|
||||||
if (isWelcomeCardSeen == false && storageVersion == '') {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.isWelcomeCardOpen = true;
|
|
||||||
}, 1500);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const releaseData = await (
|
|
||||||
await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest')
|
|
||||||
).data;
|
|
||||||
|
|
||||||
if (!releaseData) return;
|
|
||||||
|
|
||||||
this.store.appUpdate = {
|
|
||||||
version,
|
|
||||||
changelog: releaseData.body,
|
|
||||||
releaseURL: releaseData.html_url
|
|
||||||
};
|
|
||||||
|
|
||||||
this.isUpdateCardOpen =
|
|
||||||
(storageVersion != '' && storageVersion != version && this.isOnProductionHost) ||
|
|
||||||
import.meta.env.VITE_UPDATE_TEST === 'test';
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
StorageManager.setStringValue(STORAGE_VERSION_KEY, version);
|
|
||||||
},
|
|
||||||
|
|
||||||
setupOfflineHandling() {
|
|
||||||
this.store.isOffline = !window.navigator.onLine;
|
|
||||||
|
|
||||||
if (this.store.isOffline) this.handleOfflineMode();
|
|
||||||
|
|
||||||
window.addEventListener('offline', this.handleOfflineMode);
|
|
||||||
window.addEventListener('online', this.handleOnlineMode);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleOfflineMode() {
|
|
||||||
this.store.isOffline = true;
|
|
||||||
|
|
||||||
this.apiStore.activeData = undefined;
|
|
||||||
this.apiStore.dataStatuses.connection = Status.Data.Offline;
|
|
||||||
},
|
|
||||||
|
|
||||||
handleOnlineMode() {
|
|
||||||
this.store.isOffline = false;
|
|
||||||
this.apiStore.dataStatuses.connection = Status.Data.Loading;
|
|
||||||
|
|
||||||
this.apiStore.connectToAPI();
|
|
||||||
},
|
|
||||||
|
|
||||||
handleMigrateInfo() {
|
|
||||||
if (location.hostname != 'stacjownik-td2.web.app') return;
|
|
||||||
if (StorageManager.getBooleanValue(MIGRATE_INFO_CARD_SEEN_KEY) === true) return;
|
|
||||||
|
|
||||||
this.store.isMigrateInfoCardOpen = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
loadLang() {
|
|
||||||
const storageLang = StorageManager.getStringValue('lang');
|
|
||||||
|
|
||||||
if (storageLang) {
|
|
||||||
this.store.changeLocale(storageLang);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!window.navigator.language) return;
|
|
||||||
|
|
||||||
const naviLanguage = window.navigator.language.toString();
|
|
||||||
|
|
||||||
if (!naviLanguage.startsWith('pl')) {
|
|
||||||
this.store.changeLocale('en');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
closeWelcomeCard() {
|
|
||||||
this.isWelcomeCardOpen = false;
|
|
||||||
StorageManager.setBooleanValue(WELCOME_CARD_SEEN_KEY, true);
|
|
||||||
},
|
|
||||||
|
|
||||||
closeMigrateInfoCard() {
|
|
||||||
this.store.isMigrateInfoCardOpen = false;
|
|
||||||
StorageManager.setBooleanValue(MIGRATE_INFO_CARD_SEEN_KEY, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use './styles/animations';
|
@use './styles/animations';
|
||||||
@use './styles/global';
|
|
||||||
|
|
||||||
// APP
|
// APP
|
||||||
#app {
|
#app {
|
||||||
@@ -228,38 +32,4 @@ export default defineComponent({
|
|||||||
.app_main {
|
.app_main {
|
||||||
padding: 0 0.5em;
|
padding: 0 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning {
|
|
||||||
background-color: firebrick;
|
|
||||||
text-align: center;
|
|
||||||
padding: 0.5em 0.4em;
|
|
||||||
max-width: 1100px;
|
|
||||||
margin: 0 auto;
|
|
||||||
|
|
||||||
border-radius: 0 0 1em 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FOOTER
|
|
||||||
.app_footer {
|
|
||||||
max-width: 100%;
|
|
||||||
padding: 0.5em;
|
|
||||||
|
|
||||||
button {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 1.1em;
|
|
||||||
vertical-align: text-bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-index: 10;
|
|
||||||
|
|
||||||
background: #111;
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,6 +7,13 @@
|
|||||||
v{{ version }}{{ isOnProductionHost ? '' : 'dev' }}
|
v{{ version }}{{ isOnProductionHost ? '' : 'dev' }}
|
||||||
</button>
|
</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>
|
<div style="display: none">∫ ukryta taktyczna całka do programowania w HTMLu</div>
|
||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-domain-info">
|
||||||
|
<div>
|
||||||
|
<img src="/images/icon-loading.svg" alt="loading" height="200" />
|
||||||
|
<h1><span class="text--primary">Aplikacja</span> została przeniesiona na nową domenę!</h1>
|
||||||
|
<h1><span class="text--primary">This app</span> has been moved to a new domain!</h1>
|
||||||
|
|
||||||
|
<div style="margin-top: 1em">
|
||||||
|
<a :href="newLink">Nowy link dla obecnego adresu / New link to the current address</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const newLink = computed(() => {
|
||||||
|
return 'https://stacjownik-td2.spythere.eu' + location.pathname + location.search;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.app-domain-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-size: 1.35em;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,11 +5,11 @@
|
|||||||
|
|
||||||
<div class="language-select">
|
<div class="language-select">
|
||||||
<button :data-active="$i18n.locale == 'pl'" @click="store.changeLocale('pl')">
|
<button :data-active="$i18n.locale == 'pl'" @click="store.changeLocale('pl')">
|
||||||
<FlagIcon :language-id="0" width="2.5em" />
|
<img src="/images/icon-pl.svg" alt="" width="45" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button :data-active="$i18n.locale == 'en'" @click="store.changeLocale('en')">
|
<button :data-active="$i18n.locale == 'en'" @click="store.changeLocale('en')">
|
||||||
<FlagIcon :language-id="1" width="2.5em" />
|
<img src="/images/icon-en.svg" alt="" width="45" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -116,7 +116,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Card from '../Global/Card.vue';
|
import Card from '../Global/Card.vue';
|
||||||
import { useMainStore } from '../../store/mainStore';
|
import { useMainStore } from '../../store/mainStore';
|
||||||
import FlagIcon from '../Global/FlagIcon.vue';
|
|
||||||
|
|
||||||
const store = useMainStore();
|
const store = useMainStore();
|
||||||
|
|
||||||
@@ -158,7 +157,7 @@ a.link {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
|
|
||||||
button[data-active='false'] ::v-deep(img) {
|
button[data-active='false'] img {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<Card :is-open="isUpdateCardOpen" @toggle-card="toggleCard(false)">
|
<Card :is-open="isUpdateCardOpen" @toggle-card="toggleCard(false)">
|
||||||
<div class="content" tabindex="0" ref="content">
|
<div class="content">
|
||||||
<h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1>
|
<h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1>
|
||||||
|
|
||||||
<div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div>
|
<div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div>
|
||||||
@@ -13,14 +13,7 @@
|
|||||||
<p class="bottom-info">
|
<p class="bottom-info">
|
||||||
{{ $t('update.info-1') }}
|
{{ $t('update.info-1') }}
|
||||||
<br />
|
<br />
|
||||||
|
<span v-html="$t('update.info-2')"></span>
|
||||||
<i18n-t keypath="update.info-2">
|
|
||||||
<template v-slot:link>
|
|
||||||
<a href="https://github.com/Spythere/stacjownik" target="_blank">{{
|
|
||||||
$t('update.info-2-link-text')
|
|
||||||
}}</a>
|
|
||||||
</template>
|
|
||||||
</i18n-t>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -58,7 +51,7 @@ export default defineComponent({
|
|||||||
watch: {
|
watch: {
|
||||||
isUpdateCardOpen(val: boolean) {
|
isUpdateCardOpen(val: boolean) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (val) (this.$refs['content'] as HTMLElement).focus();
|
if (val) (this.$refs['confirm-btn'] as HTMLElement).focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -86,18 +79,13 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
::v-deep(h2) {
|
::v-deep(h2) {
|
||||||
margin-top: 1em;
|
padding: 0.25em 0;
|
||||||
padding: 0.5em 0;
|
|
||||||
border-bottom: 1px solid #aaa;
|
border-bottom: 1px solid #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep(h3) {
|
|
||||||
padding: 0.5em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep(ul) {
|
::v-deep(ul) {
|
||||||
list-style: disc;
|
list-style: initial;
|
||||||
padding: 0 1.5em;
|
padding: 1em;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +105,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
margin: 0.5em auto;
|
margin: 0 auto;
|
||||||
padding: 0.5em 0.75em;
|
padding: 0.5em 0.75em;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
@@ -129,6 +117,5 @@ p.bottom-info {
|
|||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,41 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="driver-top-actions">
|
<div class="driver-top-actions">
|
||||||
<div class="actions-container">
|
<div class="actions-container">
|
||||||
<div class="actions actions-left">
|
<div class="actions actions-left">
|
||||||
<button class="a-button btn--filled btn--image" @click="routerReturn">
|
<button class="a-button btn--filled btn--image" @click="routerReturn">
|
||||||
<img src="/images/icon-back.svg" alt="train icon" />
|
<img src="/images/icon-back.svg" alt="train icon" />
|
||||||
<span>
|
<span>
|
||||||
{{ t('trains.driver-return-link') }}
|
{{ t('trains.driver-return-link') }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions actions-right">
|
<div class="actions actions-right">
|
||||||
<a
|
<a class="a-button btn--filled btn--image" :href="`https://srjp-td2.web.app/?id=${chosenTrain.id}`"
|
||||||
class="a-button btn--filled btn--image"
|
target="_blank">
|
||||||
:href="`https://srjp-td2.web.app/?id=${chosenTrain.id}`"
|
<span class="hidable">
|
||||||
target="_blank"
|
{{ t('trains.driver-srjp-link') }}
|
||||||
>
|
</span>
|
||||||
<span class="hidable">
|
|
||||||
{{ t('trains.driver-srjp-link') }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<img src="/images/icon-srjp.svg" alt="srjp icon" />
|
<img src="/images/icon-srjp.svg" alt="srjp icon" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<router-link
|
<router-link :to="`/journal/timetables?search-driver=${chosenTrain.driverName}`"
|
||||||
:to="`/profile?playerId=${chosenTrain.driverId}`"
|
class="a-button btn--filled btn--image">
|
||||||
class="a-button btn--filled btn--image"
|
<span class="hidable">
|
||||||
>
|
{{ t('trains.driver-journal-link') }}
|
||||||
<span class="hidable">
|
</span>
|
||||||
{{ t('trains.driver-profile-link') }}
|
|
||||||
</span>
|
<img src="/images/icon-train.svg" alt="train icon" />
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<img src="/images/icon-user.svg" alt="user icon" />
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -48,40 +44,42 @@ const router = useRouter();
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
chosenTrain: {
|
chosenTrain: {
|
||||||
type: Object as PropType<Train>,
|
type: Object as PropType<Train>,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function routerReturn() {
|
function routerReturn() {
|
||||||
router.back();
|
router.back();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use '../../styles/responsive';
|
@use '../../styles/responsive';
|
||||||
|
|
||||||
|
|
||||||
.actions-container {
|
.actions-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions-container > .actions > .a-button {
|
.actions-container>.actions>.a-button {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border-radius: 0.5em 0.5em 0 0;
|
border-radius: 0.5em 0.5em 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen {
|
@include responsive.smallScreen {
|
||||||
span.hidable {
|
span.hidable {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -205,7 +205,7 @@ const availableCategories = computed(() => {
|
|||||||
for (const stockName of stockList) {
|
for (const stockName of stockList) {
|
||||||
const [vehicleName, ...cargoList] = stockName.split(':');
|
const [vehicleName, ...cargoList] = stockName.split(':');
|
||||||
|
|
||||||
const vehicleData = apiStore.vehiclesData?.vehicles.find((v) => v.name == vehicleName);
|
const vehicleData = apiStore.vehiclesData?.find((v) => v.name == vehicleName);
|
||||||
|
|
||||||
if (!vehicleData) continue;
|
if (!vehicleData) continue;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<button class="action-btn btn--filled">
|
||||||
|
<div class="button_content">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '../../styles/responsive';
|
||||||
|
|
||||||
|
.button_content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flag-icon">
|
|
||||||
<img
|
|
||||||
:src="languageFlagSrc"
|
|
||||||
alt="language flag"
|
|
||||||
:style="{
|
|
||||||
width: width
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { getLanguageNameById } from '../../utils/languageUtils';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
languageId: {
|
|
||||||
type: Number,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
|
|
||||||
width: {
|
|
||||||
type: String
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const languageFlagSrc = computed(
|
|
||||||
() => `/images/flags/${getLanguageNameById(props.languageId)}.svg`
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.flag-icon {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flag-icon img {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -160,7 +160,7 @@ ul.options {
|
|||||||
|
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
z-index: 150;
|
z-index: 100;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="daily-stats">
|
<section class="daily-stats">
|
||||||
<span :data-active="apiStore.dataStatuses.dailyStatsData">
|
<span :data-active="statsStatus">
|
||||||
<h3>
|
<h3>
|
||||||
{{ $t('journal.daily-stats.title') }}
|
{{ $t('journal.daily-stats.title') }}
|
||||||
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
|
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
|
||||||
@@ -8,11 +8,11 @@
|
|||||||
|
|
||||||
<hr class="header-separator" />
|
<hr class="header-separator" />
|
||||||
|
|
||||||
<b v-if="apiStore.dataStatuses.dailyStatsData == Status.Data.Loading">
|
<b v-if="statsStatus == Status.Data.Loading">
|
||||||
{{ $t('app.loading') }}
|
{{ $t('app.loading') }}
|
||||||
</b>
|
</b>
|
||||||
|
|
||||||
<b class="text--error" v-else-if="apiStore.dataStatuses.dailyStatsData == Status.Data.Error">
|
<b class="text--error" v-else-if="statsStatus == Status.Data.Error">
|
||||||
{{ $t('journal.stats-error') }}
|
{{ $t('journal.stats-error') }}
|
||||||
</b>
|
</b>
|
||||||
|
|
||||||
@@ -20,48 +20,42 @@
|
|||||||
{{ $t('journal.daily-stats.info') }}
|
{{ $t('journal.daily-stats.info') }}
|
||||||
</b>
|
</b>
|
||||||
|
|
||||||
<div v-else-if="apiStore.dailyStatsData">
|
<div v-else>
|
||||||
<ul class="stats-list">
|
<ul class="stats-list">
|
||||||
<li v-if="apiStore.dailyStatsData.totalTimetables">
|
<li v-if="stats.totalTimetables">
|
||||||
<i18n-t keypath="journal.daily-stats.total">
|
<i18n-t keypath="journal.daily-stats.total">
|
||||||
<template #count>
|
<template #count>
|
||||||
<b class="text--primary">
|
<b class="text--primary">
|
||||||
{{ apiStore.dailyStatsData.totalTimetables }}
|
{{ stats.totalTimetables }}
|
||||||
{{ $t('journal.daily-stats.count', apiStore.dailyStatsData.totalTimetables) }}
|
{{ $t('journal.daily-stats.count', stats.totalTimetables) }}
|
||||||
</b>
|
</b>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #distance>
|
<template #distance>
|
||||||
<b class="text--primary">
|
<b class="text--primary"> {{ stats.distanceSum?.toFixed(2) }} km</b>
|
||||||
{{ apiStore.dailyStatsData.distanceSum?.toFixed(2) }} km</b
|
|
||||||
>
|
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li v-if="apiStore.dailyStatsData.maxTimetable">
|
<li v-if="stats.maxTimetable">
|
||||||
<i18n-t keypath="journal.daily-stats.longest">
|
<i18n-t keypath="journal.daily-stats.longest">
|
||||||
<template #id>
|
<template #id>
|
||||||
<router-link
|
<router-link :to="`/journal/timetables?search-train=%23${stats.maxTimetable.id}`">
|
||||||
:to="`/journal/timetables?search-train=%23${apiStore.dailyStatsData.maxTimetable.id}`"
|
<b>{{ stats.maxTimetable.id }}</b>
|
||||||
>
|
|
||||||
<b>{{ apiStore.dailyStatsData.maxTimetable.id }}</b>
|
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
<template #author>
|
<template #author>
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/journal/timetables?search-dispatcher=${apiStore.dailyStatsData.maxTimetable.authorName}`"
|
:to="`/journal/timetables?search-dispatcher=${stats.maxTimetable.authorName}`"
|
||||||
>
|
>
|
||||||
<b>{{ apiStore.dailyStatsData.maxTimetable.authorName }}</b>
|
<b>{{ stats.maxTimetable.authorName }}</b>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
<template #driver>
|
<template #driver>
|
||||||
<b class="text--primary">{{ apiStore.dailyStatsData.maxTimetable.driverName }}</b>
|
<b class="text--primary">{{ stats.maxTimetable.driverName }}</b>
|
||||||
</template>
|
</template>
|
||||||
<template #distance>
|
<template #distance>
|
||||||
<b class="text--primary"
|
<b class="text--primary">{{ stats.maxTimetable.routeDistance }} km</b>
|
||||||
>{{ apiStore.dailyStatsData.maxTimetable.routeDistance }} km</b
|
|
||||||
>
|
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</li>
|
</li>
|
||||||
@@ -107,37 +101,35 @@
|
|||||||
</i18n-t>
|
</i18n-t>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li v-if="apiStore.dailyStatsData.longestDuties.length > 0">
|
<li v-if="stats.longestDuties.length > 0">
|
||||||
<i18n-t keypath="journal.daily-stats.longest-duties">
|
<i18n-t keypath="journal.daily-stats.longest-duties">
|
||||||
<template #dispatcher>
|
<template #dispatcher>
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/journal/dispatchers?search-dispatcher=${apiStore.dailyStatsData.longestDuties[0].name}`"
|
:to="`/journal/dispatchers?search-dispatcher=${stats.longestDuties[0].name}`"
|
||||||
>
|
>
|
||||||
<b>{{ apiStore.dailyStatsData.longestDuties[0].name }}</b>
|
<b>{{ stats.longestDuties[0].name }}</b>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #station>{{ apiStore.dailyStatsData.longestDuties[0].station }}</template>
|
<template #station>{{ stats.longestDuties[0].station }}</template>
|
||||||
|
|
||||||
<template #duration>
|
<template #duration>
|
||||||
{{ humanizeDuration(apiStore.dailyStatsData.longestDuties[0].duration) }}
|
{{ calculateDuration(stats.longestDuties[0].duration) }}
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li v-if="apiStore.dailyStatsData.mostActiveDrivers.length > 0">
|
<li v-if="stats.mostActiveDrivers.length > 0">
|
||||||
<i18n-t keypath="journal.daily-stats.most-active-driver">
|
<i18n-t keypath="journal.daily-stats.most-active-driver">
|
||||||
<template #driver>
|
<template #driver>
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/journal/timetables?search-driver=${apiStore.dailyStatsData.mostActiveDrivers[0].name}`"
|
:to="`/journal/timetables?search-driver=${stats.mostActiveDrivers[0].name}`"
|
||||||
>
|
>
|
||||||
<b>{{ apiStore.dailyStatsData.mostActiveDrivers[0].name }}</b>
|
<b>{{ stats.mostActiveDrivers[0].name }}</b>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
<template #distance>
|
<template #distance>
|
||||||
<b class="text--primary"
|
<b class="text--primary">{{ stats.mostActiveDrivers[0].distance.toFixed(2) }} km</b>
|
||||||
>{{ apiStore.dailyStatsData.mostActiveDrivers[0].distance.toFixed(2) }} km</b
|
|
||||||
>
|
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</li>
|
</li>
|
||||||
@@ -159,11 +151,7 @@
|
|||||||
>
|
>
|
||||||
<span>{{ $t(`journal.daily-stats.${key}`) }}</span>
|
<span>{{ $t(`journal.daily-stats.${key}`) }}</span>
|
||||||
<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>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,25 +160,76 @@
|
|||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts">
|
||||||
import { computed, onMounted } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { useApiStore } from '../../store/apiStore';
|
import dateMixin from '../../mixins/dateMixin';
|
||||||
|
|
||||||
|
import { API } from '../../typings/api';
|
||||||
import { Status } from '../../typings/common';
|
import { Status } from '../../typings/common';
|
||||||
import { humanizeDuration } from '../../composables/time';
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
|
||||||
onMounted(() => {
|
export default defineComponent({
|
||||||
apiStore.fetchDailyStats();
|
name: 'journal-daily-stats',
|
||||||
});
|
|
||||||
|
|
||||||
const apiStore = useApiStore();
|
mixins: [dateMixin],
|
||||||
|
|
||||||
const topDispatchers = computed(() => {
|
data() {
|
||||||
if (!apiStore.dailyStatsData || apiStore.dailyStatsData.mostActiveDispatchers.length == 0)
|
return {
|
||||||
return [];
|
Status,
|
||||||
|
statsStatus: Status.Data.Loading,
|
||||||
|
intervalId: -1,
|
||||||
|
|
||||||
const maxCount = apiStore.dailyStatsData.mostActiveDispatchers[0].count;
|
stats: {} as API.DailyStats.Response,
|
||||||
|
apiStore: useApiStore()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
return apiStore.dailyStatsData.mostActiveDispatchers.filter((disp) => disp.count === maxCount);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -226,7 +265,7 @@ ul.stats-list {
|
|||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen {
|
@include responsive.smallScreen{
|
||||||
h3 {
|
h3 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<li class="dispatcher-history-entry">
|
<li class="dispatcher-history-entry">
|
||||||
<div class="entry-info">
|
<div class="entry-info">
|
||||||
<span class="entry-info-left">
|
<span>
|
||||||
<div class="station-info">
|
<span>
|
||||||
<router-link :to="`/journal/dispatchers?search-station=${entry.stationName}`">
|
<router-link :to="`/journal/dispatchers?search-station=${entry.stationName}`">
|
||||||
<b>{{ entry.stationName }}</b>
|
<b>{{ entry.stationName }}</b>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<b class="text--grayed"> #{{ entry.stationHash }}</b>
|
<b class="text--grayed"> #{{ entry.stationHash }}</b>
|
||||||
•
|
</span>
|
||||||
<b
|
•
|
||||||
v-if="entry.dispatcherLevel !== null"
|
<b
|
||||||
class="level-badge dispatcher"
|
v-if="entry.dispatcherLevel !== null"
|
||||||
:style="calculateExpStyle(entry.dispatcherLevel, entry.dispatcherIsSupporter)"
|
class="level-badge dispatcher"
|
||||||
>
|
:style="calculateExpStyle(entry.dispatcherLevel, entry.dispatcherIsSupporter)"
|
||||||
{{ entry.dispatcherLevel >= 2 ? entry.dispatcherLevel : 'L' }}
|
>
|
||||||
</b>
|
{{ entry.dispatcherLevel >= 2 ? entry.dispatcherLevel : 'L' }}
|
||||||
|
</b>
|
||||||
|
<b style="margin-left: 5px">
|
||||||
<span
|
<span
|
||||||
v-if="apiStore.donatorsData.includes(entry.dispatcherName)"
|
v-if="apiStore.donatorsData.includes(entry.dispatcherName)"
|
||||||
data-tooltip-type="DonatorTooltip"
|
data-tooltip-type="DonatorTooltip"
|
||||||
@@ -36,11 +37,7 @@
|
|||||||
>
|
>
|
||||||
{{ entry.dispatcherName }}
|
{{ entry.dispatcherName }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
</b>
|
||||||
<span class="dispatcher-language" v-if="entry.dispatcherLanguageId != null">
|
|
||||||
<FlagIcon :language-id="entry.dispatcherLanguageId" width="1.75em" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span v-if="entry.timestampTo">
|
<span v-if="entry.timestampTo">
|
||||||
@@ -121,7 +118,6 @@ import dateMixin from '../../../mixins/dateMixin';
|
|||||||
import styleMixin from '../../../mixins/styleMixin';
|
import styleMixin from '../../../mixins/styleMixin';
|
||||||
import { useApiStore } from '../../../store/apiStore';
|
import { useApiStore } from '../../../store/apiStore';
|
||||||
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
|
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
|
||||||
import FlagIcon from '../../Global/FlagIcon.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
@@ -129,7 +125,7 @@ export default defineComponent({
|
|||||||
showExtraInfo: { type: Boolean, required: true }
|
showExtraInfo: { type: Boolean, required: true }
|
||||||
},
|
},
|
||||||
|
|
||||||
components: { StationStatusBadge, FlagIcon },
|
components: { StationStatusBadge },
|
||||||
mixins: [dateMixin, styleMixin],
|
mixins: [dateMixin, styleMixin],
|
||||||
emits: ['toggleShowExtraInfo'],
|
emits: ['toggleShowExtraInfo'],
|
||||||
|
|
||||||
@@ -168,11 +164,6 @@ export default defineComponent({
|
|||||||
padding: 1em;
|
padding: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dispatcher-language {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-info {
|
.entry-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -194,15 +185,6 @@ export default defineComponent({
|
|||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.station-info {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
text-align: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25em;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-list {
|
.status-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -216,15 +198,11 @@ export default defineComponent({
|
|||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen {
|
@include responsive.smallScreen{
|
||||||
.entry-info {
|
.entry-info {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.station-info {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<div class="journal-stats dispatcher" v-if="dispatcherName && stats">
|
||||||
|
<span class="loading" v-if="!stats.issuedTimetables && !stats.services">
|
||||||
|
{{ $t('journal.dispatcher-stats.empty') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else>
|
||||||
|
<h3>
|
||||||
|
<i18n-t keypath="journal.dispatcher-stats.title">
|
||||||
|
<template #name>
|
||||||
|
<span class="text--primary">{{ dispatcherName.toUpperCase() }}</span>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<hr class="header-separator" />
|
||||||
|
|
||||||
|
<div class="info-stats">
|
||||||
|
<span class="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>
|
<template>
|
||||||
<div class="dropdown filters-options" @keydown.esc="showOptions = false">
|
<div class="filters-options dropdown" @keydown.esc="showOptions = false">
|
||||||
<div class="dropdown_background" v-if="showOptions" @click="showOptions = false"></div>
|
<div class="dropdown_background" v-if="showOptions" @click="showOptions = false"></div>
|
||||||
|
|
||||||
<div class="actions-bar">
|
<div class="actions-bar">
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
<label v-if="propName == 'search-date-from'" for="search-date">{{
|
<label v-if="propName == 'search-date-from'" for="search-date">{{
|
||||||
$t(`options.search-${optionsType}-date`)
|
$t(`options.search-${optionsType}-date`)
|
||||||
}}</label>
|
}}</label>
|
||||||
|
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<input
|
<input
|
||||||
class="search-input"
|
class="search-input"
|
||||||
@@ -330,9 +330,4 @@ export default defineComponent({
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use '../../styles/dropdown';
|
@use '../../styles/dropdown';
|
||||||
@use '../../styles/dropdown-filters';
|
@use '../../styles/dropdown-filters';
|
||||||
|
|
||||||
.filters-options > .dropdown_wrapper {
|
|
||||||
height: calc(100vh - 19em);
|
|
||||||
min-height: 500px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,70 +2,87 @@
|
|||||||
<div
|
<div
|
||||||
class="journal-stats dropdown"
|
class="journal-stats dropdown"
|
||||||
v-if="!mainStore.isOffline"
|
v-if="!mainStore.isOffline"
|
||||||
@keydown.esc="isDropdownOpen = false"
|
@keydown.esc="currentStatsTab = null"
|
||||||
>
|
>
|
||||||
<div class="dropdown_background" v-if="isDropdownOpen" @click="isDropdownOpen = false"></div>
|
<div
|
||||||
|
class="dropdown_background"
|
||||||
|
v-if="currentStatsTab !== null"
|
||||||
|
@click="currentStatsTab = null"
|
||||||
|
></div>
|
||||||
|
|
||||||
<div class="actions-bar">
|
<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
|
<button
|
||||||
|
v-for="button in statsButtons"
|
||||||
|
:key="button.tab"
|
||||||
class="btn--filled btn--image"
|
class="btn--filled btn--image"
|
||||||
:data-disabled="chosenPlayerId == -1"
|
:data-selected="button.tab == currentStatsTab"
|
||||||
@click="navigateToProfile"
|
:data-disabled="button.disabled"
|
||||||
|
:disabled="button.disabled"
|
||||||
|
@click="onTabButtonClick(button.tab)"
|
||||||
>
|
>
|
||||||
<img :src="`/images/icon-user.svg`" alt="user icon" />
|
<img
|
||||||
{{ $t('profile.journal-button') }}
|
v-if="button.iconName"
|
||||||
|
:src="`/images/icon-${button.iconName}.svg`"
|
||||||
|
:alt="button.iconName"
|
||||||
|
/>
|
||||||
|
{{ $t(button.localeKey) }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<transition name="dropdown-anim">
|
<transition name="dropdown-anim">
|
||||||
<div class="dropdown_wrapper" v-if="isDropdownOpen">
|
<div
|
||||||
|
class="dropdown_wrapper"
|
||||||
|
:class="{ 'dropdown-align-right': true }"
|
||||||
|
v-if="currentStatsTab !== null"
|
||||||
|
>
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<JournalDailyStats />
|
<component :is="currentStatsTab" :key="currentStatsTab"></component>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts">
|
||||||
import { ref } from 'vue';
|
import { defineComponent, PropType } from 'vue';
|
||||||
import { useMainStore } from '../../store/mainStore';
|
import { useMainStore } from '../../store/mainStore';
|
||||||
|
import StorageManager from '../../managers/storageManager';
|
||||||
|
import { Journal } from './typings';
|
||||||
import JournalDailyStats from './JournalDailyStats.vue';
|
import JournalDailyStats from './JournalDailyStats.vue';
|
||||||
import { useRouter } from 'vue-router';
|
import JournalDispatcherStats from '../JournalView/JournalDispatchers/JournalDispatcherStats.vue';
|
||||||
|
import JournalDriverStats from '../JournalView/JournalTimetables/JournalDriverStats.vue';
|
||||||
|
|
||||||
const router = useRouter();
|
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 props = defineProps({
|
methods: {
|
||||||
chosenPlayerId: {
|
onTabButtonClick(tab: Journal.StatsTab) {
|
||||||
type: Number,
|
this.currentStatsTab = tab == this.currentStatsTab ? null : tab;
|
||||||
required: true
|
|
||||||
|
StorageManager.setStringValue('journalStatsTab', this.currentStatsTab ?? '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use '../../styles/dropdown';
|
@use '../../styles/dropdown';
|
||||||
@use '../../styles/dropdown-filters';
|
@use '../../styles/dropdown-filters';
|
||||||
|
|
||||||
.dropdown_wrapper {
|
.dropdown_wrapper.dropdown-align-right {
|
||||||
left: auto;
|
left: auto;
|
||||||
right: 0;
|
right: 0;
|
||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
|
|||||||
@@ -19,238 +19,209 @@
|
|||||||
<div class="details-body" v-if="showExtraInfo">
|
<div class="details-body" v-if="showExtraInfo">
|
||||||
<div class="g-separator"></div>
|
<div class="g-separator"></div>
|
||||||
|
|
||||||
<div v-if="timetableDetails">
|
<EntryStops :timetable="timetable" />
|
||||||
<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>
|
<div class="g-separator"></div>
|
||||||
|
|
||||||
<div class="timetable-specs">
|
<b>{{ $t('journal.stock-dangers') }}:</b>
|
||||||
<span class="badge specs-badge" v-if="timetableDetails.authorName">
|
|
||||||
<span>{{ $t('journal.dispatcher-name') }}</span>
|
<ul>
|
||||||
<span>{{ timetableDetails.authorName }}</span>
|
<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>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="badge specs-badge" v-if="timetableDetails.trainMaxSpeed">
|
<span class="badge specs-badge" v-if="timetable.stockMass">
|
||||||
<span>{{ $t('journal.stock-timetable-speed') }}</span>
|
<span>{{ $t('journal.stock-mass') }}</span>
|
||||||
<span> {{ timetableDetails.trainMaxSpeed }}km/h </span>
|
<span>
|
||||||
</span>
|
{{
|
||||||
|
Math.floor(
|
||||||
<span class="badge specs-badge" v-if="timetableDetails.maxSpeed">
|
(currentHistoryIndex == 0
|
||||||
<span>{{ $t('journal.stock-max-speed') }}</span>
|
? timetable.stockMass
|
||||||
<span>{{ timetableDetails.maxSpeed }}km/h</span>
|
: stockHistory[currentHistoryIndex].stockMass || timetable.stockMass) / 1000
|
||||||
|
)
|
||||||
|
}}t
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stock-dangers" v-if="timetableDetails.warningNotes">
|
<div class="stock-history">
|
||||||
<div class="g-separator"></div>
|
<button class="btn btn--action" @click="copyStockToClipboard()">
|
||||||
|
<i class="fa-regular fa-copy"></i> {{ $t('journal.stock-copy') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
<b>{{ $t('journal.stock-dangers') }}:</b>
|
<button
|
||||||
|
v-for="(sh, i) in stockHistory"
|
||||||
<ul>
|
:key="i"
|
||||||
<li v-if="timetableDetails.twr">
|
class="btn--action"
|
||||||
<b class="text--primary">{{ $t('warnings.TWR') }} (TWR)</b>
|
:data-checked="i == currentHistoryIndex"
|
||||||
</li>
|
@click.stop="currentHistoryIndex = i"
|
||||||
|
>
|
||||||
<li v-if="timetableDetails.skr">
|
{{ sh.updatedAt }}
|
||||||
<b class="text--primary">{{ $t('warnings.SKR') }}</b>
|
</button>
|
||||||
</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>
|
||||||
|
|
||||||
<!-- Historia zmian w składzie -->
|
<div v-if="timetable.stockString" style="margin-top: 1em">
|
||||||
<div v-if="timetableDetails.stockString || stockHistory.length != 0">
|
<StockList
|
||||||
<div class="g-separator"></div>
|
:trainStockList="
|
||||||
|
(currentHistoryIndex == 0
|
||||||
<b>{{ $t('journal.stock-preview') }}:</b>
|
? timetable.stockString
|
||||||
|
: stockHistory[currentHistoryIndex].stockString
|
||||||
<div class="stock-specs" style="margin-top: 0.5em">
|
).split(';')
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts">
|
||||||
import { computed, PropType, ref } from 'vue';
|
import { PropType, defineComponent } from 'vue';
|
||||||
|
import StockList from '../../Global/StockList.vue';
|
||||||
|
import { API } from '../../../typings/api';
|
||||||
import { RouteLocationRaw } from 'vue-router';
|
import { RouteLocationRaw } from 'vue-router';
|
||||||
|
import EntryStops from './EntryStops.vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import StockList from '../../Global/StockList.vue';
|
export default defineComponent({
|
||||||
import EntryStops from './EntryStops.vue';
|
components: { StockList, EntryStops },
|
||||||
import { API } from '../../../typings/api';
|
|
||||||
import { useApiStore } from '../../../store/apiStore';
|
|
||||||
|
|
||||||
const i18n = useI18n();
|
emits: ['toggleExtraInfo'],
|
||||||
const apiStore = useApiStore();
|
|
||||||
|
|
||||||
const props = defineProps({
|
props: {
|
||||||
showExtraInfo: {
|
showExtraInfo: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
timetable: {
|
||||||
|
type: Object as PropType<API.TimetableHistory.Data>,
|
||||||
|
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
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
timetableEntry: {
|
driverRouteLocation(): RouteLocationRaw | null {
|
||||||
type: Object as PropType<API.TimetableHistory.DataShort>,
|
if (this.timetable.terminated) return null;
|
||||||
required: true
|
return {
|
||||||
}
|
name: 'DriverView',
|
||||||
});
|
query: {
|
||||||
|
trainId: `${this.timetable.driverId}|${this.timetable.trainNo}|eu`
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onImageError(e: Event) {
|
||||||
|
const imageEl = e.target as HTMLImageElement;
|
||||||
|
imageEl.src = '/images/icon-unknown.png';
|
||||||
|
},
|
||||||
|
|
||||||
timetableDetails.value = responseData.data[0];
|
toggleExtraInfo() {
|
||||||
} catch (error) {
|
this.$emit('toggleExtraInfo', this.timetable.id);
|
||||||
// this.dataStatus = Status.Data.Error;
|
},
|
||||||
console.error(error);
|
|
||||||
|
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'));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -328,7 +299,7 @@ hr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen {
|
@include responsive.smallScreen{
|
||||||
.timetable-specs {
|
.timetable-specs {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,10 +71,6 @@
|
|||||||
<router-link v-else :to="`/journal/timetables?search-driver=${timetable.driverName}`">
|
<router-link v-else :to="`/journal/timetables?search-driver=${timetable.driverName}`">
|
||||||
<strong>{{ timetable.driverName }}</strong>
|
<strong>{{ timetable.driverName }}</strong>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<div v-if="timetable.driverLanguageId != null">
|
|
||||||
<FlagIcon :language-id="timetable.driverLanguageId" width="1.75em" />
|
|
||||||
</div>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="general-time">
|
<span class="general-time">
|
||||||
@@ -87,7 +83,7 @@
|
|||||||
</b>
|
</b>
|
||||||
|
|
||||||
<b
|
<b
|
||||||
class="timetable-status-badge"
|
class="info-badge"
|
||||||
:class="{
|
:class="{
|
||||||
fulfilled: timetable.fulfilled,
|
fulfilled: timetable.fulfilled,
|
||||||
terminated: timetable.terminated && !timetable.fulfilled,
|
terminated: timetable.terminated && !timetable.fulfilled,
|
||||||
@@ -114,10 +110,8 @@ import dateMixin from '../../../mixins/dateMixin';
|
|||||||
import styleMixin from '../../../mixins/styleMixin';
|
import styleMixin from '../../../mixins/styleMixin';
|
||||||
import { useApiStore } from '../../../store/apiStore';
|
import { useApiStore } from '../../../store/apiStore';
|
||||||
import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
|
import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
|
||||||
import FlagIcon from '../../Global/FlagIcon.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { FlagIcon },
|
|
||||||
mixins: [dateMixin, styleMixin, trainCategoryMixin],
|
mixins: [dateMixin, styleMixin, trainCategoryMixin],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -128,7 +122,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
props: {
|
props: {
|
||||||
timetable: {
|
timetable: {
|
||||||
type: Object as PropType<API.TimetableHistory.DataShort>,
|
type: Object as PropType<API.TimetableHistory.Data>,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,6 +165,23 @@ export default defineComponent({
|
|||||||
gap: 0.25em;
|
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 {
|
.btn-timetable {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0.2em 0.5em;
|
padding: 0.2em 0.5em;
|
||||||
@@ -180,7 +191,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen {
|
@include responsive.smallScreen{
|
||||||
.item-general {
|
.item-general {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default defineComponent({
|
|||||||
components: { ProgressBar },
|
components: { ProgressBar },
|
||||||
props: {
|
props: {
|
||||||
timetable: {
|
timetable: {
|
||||||
type: Object as PropType<API.TimetableHistory.DataShort>,
|
type: Object as PropType<API.TimetableHistory.Data>,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<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 />
|
<hr />
|
||||||
|
|
||||||
<div style="cursor: pointer">
|
<div @click="toggleExtraInfo" style="cursor: pointer">
|
||||||
<!-- Status -->
|
<!-- Status -->
|
||||||
<EntryStatus :timetable="timetableEntry" />
|
<EntryStatus :timetable="timetableEntry" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Extra -->
|
<!-- Extra -->
|
||||||
<EntryDetails
|
<EntryDetails
|
||||||
:timetableEntry="timetableEntry"
|
:timetable="timetableEntry"
|
||||||
:show-extra-info="showExtraInfo"
|
:show-extra-info="showExtraInfo"
|
||||||
@toggle-extra-info="toggleExtraInfo"
|
@toggle-extra-info="toggleExtraInfo"
|
||||||
/>
|
/>
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
import { defineComponent, PropType } from 'vue';
|
import { defineComponent, PropType } from 'vue';
|
||||||
import { API } from '../../../typings/api';
|
import { API } from '../../../typings/api';
|
||||||
import { useApiStore } from '../../../store/apiStore';
|
import { useApiStore } from '../../../store/apiStore';
|
||||||
|
import { Journal } from '../typings';
|
||||||
|
|
||||||
import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
|
import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
|
||||||
import dateMixin from '../../../mixins/dateMixin';
|
import dateMixin from '../../../mixins/dateMixin';
|
||||||
@@ -40,7 +41,7 @@ import EntryDetails from './EntryDetails.vue';
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
timetableEntry: {
|
timetableEntry: {
|
||||||
type: Object as PropType<API.TimetableHistory.DataShort>,
|
type: Object as PropType<API.TimetableHistory.Data>,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
showExtraInfo: {
|
showExtraInfo: {
|
||||||
@@ -59,9 +60,74 @@ 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: {
|
methods: {
|
||||||
toggleExtraInfo(data: API.TimetableHistory.Data | null) {
|
toggleExtraInfo() {
|
||||||
this.$emit('toggleShowExtraInfo', data);
|
this.$emit('toggleShowExtraInfo');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -79,7 +145,7 @@ export default defineComponent({
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen {
|
@include responsive.smallScreen{
|
||||||
.entry-route {
|
.entry-route {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
v-for="(timetableEntry, i) in timetableHistory"
|
v-for="(timetableEntry, i) in timetableHistory"
|
||||||
:key="timetableEntry.id"
|
:key="timetableEntry.id"
|
||||||
:timetableEntry="timetableEntry"
|
:timetableEntry="timetableEntry"
|
||||||
:onToggleShowExtraInfo="toggleExtraInfo"
|
:onToggleShowExtraInfo="() => toggleExtraInfo(timetableEntry.id)"
|
||||||
:showExtraInfo="extraInfoIndexes.includes(timetableEntry.id)"
|
:showExtraInfo="extraInfoIndexes.includes(timetableEntry.id)"
|
||||||
/>
|
/>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
@@ -59,11 +59,9 @@ export default defineComponent({
|
|||||||
JournalTimetableEntry
|
JournalTimetableEntry
|
||||||
},
|
},
|
||||||
|
|
||||||
emits: ['toggleExtraInfo'],
|
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
timetableHistory: {
|
timetableHistory: {
|
||||||
type: Array as PropType<API.TimetableHistory.ResponseShort>,
|
type: Array as PropType<API.TimetableHistory.Response>,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
scrollNoMoreData: {
|
scrollNoMoreData: {
|
||||||
@@ -77,23 +75,32 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
dataStatus: {
|
dataStatus: {
|
||||||
type: Number as PropType<Status.Data>
|
type: Number as PropType<Status.Data>
|
||||||
},
|
|
||||||
extraInfoIndexes: {
|
|
||||||
type: Object as PropType<number[]>,
|
|
||||||
required: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
Status,
|
Status,
|
||||||
store: useMainStore()
|
store: useMainStore(),
|
||||||
|
extraInfoIndexes: [] as number[]
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'$route.query': {
|
||||||
|
deep: true,
|
||||||
|
handler() {
|
||||||
|
this.extraInfoIndexes.length = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
toggleExtraInfo(data: API.TimetableHistory.Data | null) {
|
toggleExtraInfo(id: number) {
|
||||||
this.$emit('toggleExtraInfo', data);
|
const existingIdx = this.extraInfoIndexes.indexOf(id);
|
||||||
|
|
||||||
|
if (existingIdx != -1) this.extraInfoIndexes.splice(existingIdx, 1);
|
||||||
|
else this.extraInfoIndexes.push(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -104,7 +111,7 @@ export default defineComponent({
|
|||||||
@use '../../../styles/journal-section';
|
@use '../../../styles/journal-section';
|
||||||
@use '../../../styles/responsive';
|
@use '../../../styles/responsive';
|
||||||
|
|
||||||
@include responsive.smallScreen {
|
@include responsive.smallScreen{
|
||||||
.journal_item-info {
|
.journal_item-info {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export namespace Journal {
|
export namespace Journal {
|
||||||
export type DispatcherSearchKey =
|
export type DispatcherSearchKey =
|
||||||
| 'search-duty-id'
|
|
||||||
| 'search-dispatcher'
|
| 'search-dispatcher'
|
||||||
| 'search-station'
|
| 'search-station'
|
||||||
| 'search-date-from'
|
| 'search-date-from'
|
||||||
@@ -11,7 +10,6 @@ export namespace Journal {
|
|||||||
| 'search-train'
|
| 'search-train'
|
||||||
| 'search-date-from'
|
| 'search-date-from'
|
||||||
| 'search-dispatcher'
|
| 'search-dispatcher'
|
||||||
| 'search-includesScenery'
|
|
||||||
| 'search-issuedFrom'
|
| 'search-issuedFrom'
|
||||||
| 'search-terminatingAt'
|
| 'search-terminatingAt'
|
||||||
| 'search-via'
|
| 'search-via'
|
||||||
@@ -63,6 +61,19 @@ export namespace Journal {
|
|||||||
default: boolean;
|
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 {
|
export interface TimetableStopDetails {
|
||||||
stopName: string;
|
stopName: string;
|
||||||
arrivalTimestamp: number;
|
arrivalTimestamp: number;
|
||||||
|
|||||||
@@ -1,327 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,418 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -151,7 +151,6 @@ export default defineComponent({
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use '../../styles/responsive';
|
@use '../../styles/responsive';
|
||||||
@use '../../styles/scenery-history-table';
|
@use '../../styles/scenery-history-table';
|
||||||
@use '../../styles/badge';
|
|
||||||
|
|
||||||
.scenery-dispatchers-history {
|
.scenery-dispatchers-history {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -57,8 +57,20 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss">
|
||||||
@use '../../styles/responsive';
|
@use '../../styles/responsive';
|
||||||
|
@use '../../styles/badge';
|
||||||
|
|
||||||
|
h3.section-header {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
padding: 0.3em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
.info-lists {
|
.info-lists {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -8,7 +8,10 @@
|
|||||||
{{ onlineScenery.dispatcherExp > 1 ? onlineScenery.dispatcherExp : 'L' }}
|
{{ onlineScenery.dispatcherExp > 1 ? onlineScenery.dispatcherExp : 'L' }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<router-link class="dispatcher-name" :to="`/profile?playerId=${onlineScenery.dispatcherId}`">
|
<router-link
|
||||||
|
class="dispatcher-name"
|
||||||
|
:to="`/journal/dispatchers?search-dispatcher=${onlineScenery.dispatcherName}`"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
class="text--donator"
|
class="text--donator"
|
||||||
v-if="apiStore.donatorsData.includes(onlineScenery.dispatcherName)"
|
v-if="apiStore.donatorsData.includes(onlineScenery.dispatcherName)"
|
||||||
@@ -18,8 +21,6 @@
|
|||||||
</span>
|
</span>
|
||||||
<span v-else>{{ onlineScenery.dispatcherName }}</span>
|
<span v-else>{{ onlineScenery.dispatcherName }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<FlagIcon :languageId="onlineScenery.dispatcherLanguageId" width="1.25em" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-bottom">
|
<div class="info-bottom">
|
||||||
@@ -50,11 +51,9 @@ import styleMixin from '../../../mixins/styleMixin';
|
|||||||
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
|
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
|
||||||
import { ActiveScenery } from '../../../typings/common';
|
import { ActiveScenery } from '../../../typings/common';
|
||||||
import { useApiStore } from '../../../store/apiStore';
|
import { useApiStore } from '../../../store/apiStore';
|
||||||
import FlagIcon from '../../Global/FlagIcon.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
mixins: [styleMixin, dateMixin, routerMixin],
|
mixins: [styleMixin, dateMixin, routerMixin],
|
||||||
components: { StationStatusBadge, FlagIcon },
|
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -67,7 +66,8 @@ export default defineComponent({
|
|||||||
type: Object as PropType<ActiveScenery>,
|
type: Object as PropType<ActiveScenery>,
|
||||||
required: false
|
required: false
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
components: { StationStatusBadge }
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="info-spawn-list">
|
<section class="info-spawn-list">
|
||||||
<h3 class="spawn-header">
|
<h3 class="spawn-header section-header">
|
||||||
<img src="/images/icon-spawn.svg" alt="Open spawns icon" />
|
<img src="/images/icon-spawn.svg" alt="Open spawns icon" />
|
||||||
{{ $t('scenery.spawns') }}
|
{{ $t('scenery.spawns') }}
|
||||||
<span class="text--primary">{{ onlineScenery?.spawns.length || '0' }}</span>
|
<span class="text--primary">{{ onlineScenery?.spawns.length || '0' }}</span>
|
||||||
@@ -53,23 +53,10 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use '../../../styles/badge';
|
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3.spawn-header {
|
|
||||||
margin: 0.5em 0;
|
|
||||||
padding: 0.3em;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spawns-anim {
|
.spawns-anim {
|
||||||
&-move,
|
&-move,
|
||||||
&-enter-active,
|
&-enter-active,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="info-user-list">
|
<section class="info-user-list">
|
||||||
<h3 class="user-header">
|
<h3 class="user-header section-header">
|
||||||
<img src="/images/icon-user.svg" alt="Users icon" />
|
<img src="/images/icon-user.svg" alt="Users icon" />
|
||||||
{{ $t('scenery.users') }}
|
{{ $t('scenery.users') }}
|
||||||
<span class="text--primary">{{ onlineScenery?.stationTrains?.length || 0 }}</span
|
<span class="text--primary">{{ onlineScenery?.stationTrains?.length || 0 }}</span
|
||||||
@@ -111,8 +111,6 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use '../../../styles/badge';
|
|
||||||
|
|
||||||
$no-timetable: #aaa;
|
$no-timetable: #aaa;
|
||||||
$departed: springgreen;
|
$departed: springgreen;
|
||||||
$stopped: #ffa600;
|
$stopped: #ffa600;
|
||||||
@@ -120,17 +118,6 @@ $online: gold;
|
|||||||
$terminated: salmon;
|
$terminated: salmon;
|
||||||
$disconnected: slategray;
|
$disconnected: slategray;
|
||||||
|
|
||||||
h3.user-header {
|
|
||||||
margin: 0.5em 0;
|
|
||||||
padding: 0.3em;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-user-list {
|
.info-user-list {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
historyList: [] as API.TimetableHistory.ResponseShort,
|
historyList: [] as API.TimetableHistory.Response,
|
||||||
historyModeList,
|
historyModeList,
|
||||||
|
|
||||||
apiStore: useApiStore(),
|
apiStore: useApiStore(),
|
||||||
@@ -149,7 +149,7 @@ export default defineComponent({
|
|||||||
requestFilters['returnType'] = 'short';
|
requestFilters['returnType'] = 'short';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response: API.TimetableHistory.ResponseShort = await (
|
const response: API.TimetableHistory.Response = await (
|
||||||
await this.apiStore.client!.get('api/getTimetables', {
|
await this.apiStore.client!.get('api/getTimetables', {
|
||||||
params: requestFilters
|
params: requestFilters
|
||||||
})
|
})
|
||||||
@@ -178,7 +178,7 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
parseCreatedDate(timetable: API.TimetableHistory.DataShort, locale: string) {
|
parseCreatedDate(timetable: API.TimetableHistory.Data, locale: string) {
|
||||||
const createdDate =
|
const createdDate =
|
||||||
timetable.createdAt > timetable.beginDate
|
timetable.createdAt > timetable.beginDate
|
||||||
? new Date(timetable.beginDate)
|
? new Date(timetable.beginDate)
|
||||||
|
|||||||
@@ -35,7 +35,6 @@
|
|||||||
id="scenery-search"
|
id="scenery-search"
|
||||||
list="sceneries"
|
list="sceneries"
|
||||||
:placeholder="$t('filters.sceneries-placeholder')"
|
:placeholder="$t('filters.sceneries-placeholder')"
|
||||||
@change="handleSceneriesInput"
|
|
||||||
@focus="preventKeyDown = true"
|
@focus="preventKeyDown = true"
|
||||||
@blur="preventKeyDown = false"
|
@blur="preventKeyDown = false"
|
||||||
/>
|
/>
|
||||||
@@ -45,40 +44,42 @@
|
|||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card_input-search">
|
<section class="card_input-search authors">
|
||||||
|
<datalist id="authors" name="authors">
|
||||||
|
<option v-for="(author, i) in authorsOptions" :key="i" :value="author"></option>
|
||||||
|
</datalist>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
v-model="filters['lines']"
|
type="text"
|
||||||
id="line-numbers-search"
|
id="author"
|
||||||
:placeholder="$t('filters.line-numbers-placeholder')"
|
list="authors"
|
||||||
|
name="authors"
|
||||||
|
v-model="filters['authors']"
|
||||||
|
:placeholder="$t('filters.authors-placeholder')"
|
||||||
@focus="preventKeyDown = true"
|
@focus="preventKeyDown = true"
|
||||||
@blur="preventKeyDown = false"
|
@blur="preventKeyDown = false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button class="btn--action btn--image" @click="resetLineNumbersInput">
|
|
||||||
<img src="/images/icon-exit.svg" alt="reset line numbers search" />
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="card_input-search">
|
|
||||||
<select id="author" name="authors" v-model="filters['authors']">
|
|
||||||
<option value="">{{ $t('filters.authors-placeholder') }}</option>
|
|
||||||
<option v-for="(author, i) in authorsOptions" :key="i" :value="author">
|
|
||||||
{{ author }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<button class="btn--action btn--image" @click="resetAuthorsInput">
|
<button class="btn--action btn--image" @click="resetAuthorsInput">
|
||||||
<img src="/images/icon-exit.svg" alt="reset authors search" />
|
<img src="/images/icon-exit.svg" alt="reset authors search" />
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card_input-search">
|
<section class="card_input-search">
|
||||||
<select id="projects" name="projects" v-model="filters['projects']">
|
<datalist id="projects" name="projects">
|
||||||
<option value="">{{ $t('filters.projects-placeholder') }}</option>
|
<option v-for="(project, i) in projectsOptions" :key="i" :value="project"></option>
|
||||||
<option v-for="(project, i) in projectsOptions" :key="i" :value="project">
|
</datalist>
|
||||||
{{ project }}
|
|
||||||
</option>
|
<input
|
||||||
</select>
|
type="text"
|
||||||
|
id="projects"
|
||||||
|
list="projects"
|
||||||
|
name="projects"
|
||||||
|
v-model="filters['projects']"
|
||||||
|
:placeholder="$t('filters.projects-placeholder')"
|
||||||
|
@focus="preventKeyDown = true"
|
||||||
|
@blur="preventKeyDown = false"
|
||||||
|
/>
|
||||||
|
|
||||||
<button class="btn--action btn--image" @click="resetProjectsInput">
|
<button class="btn--action btn--image" @click="resetProjectsInput">
|
||||||
<img src="/images/icon-exit.svg" alt="reset projects search" />
|
<img src="/images/icon-exit.svg" alt="reset projects search" />
|
||||||
@@ -91,7 +92,7 @@
|
|||||||
v-for="(sectionFilters, sectionKey) in filtersSections"
|
v-for="(sectionFilters, sectionKey) in filtersSections"
|
||||||
:key="sectionKey"
|
:key="sectionKey"
|
||||||
>
|
>
|
||||||
<h3 class="section-header">
|
<h3 class="text--primary">
|
||||||
<span class="active-indicator" v-if="!areSectionFiltersDefault(sectionKey)"></span>
|
<span class="active-indicator" v-if="!areSectionFiltersDefault(sectionKey)"></span>
|
||||||
{{ $t(`filters.sections.${sectionKey}`) }}
|
{{ $t(`filters.sections.${sectionKey}`) }}
|
||||||
<button @click="resetSectionFilters(sectionKey)">RESET</button>
|
<button @click="resetSectionFilters(sectionKey)">RESET</button>
|
||||||
@@ -121,7 +122,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card_timestamp">
|
<section class="card_timestamp">
|
||||||
<h3 class="hours-section-header">{{ $t('filters.minimum-hours-title') }}</h3>
|
<h3 class="section-header">{{ $t('filters.minimum-hours-title') }}</h3>
|
||||||
|
|
||||||
<span class="clock">
|
<span class="clock">
|
||||||
<button class="btn--action" @click="subHour">-</button>
|
<button class="btn--action" @click="subHour">-</button>
|
||||||
@@ -216,6 +217,8 @@ export default defineComponent({
|
|||||||
sliderStates,
|
sliderStates,
|
||||||
|
|
||||||
minimumHours: 0,
|
minimumHours: 0,
|
||||||
|
authorSearchFilter: '',
|
||||||
|
projectSearchFilter: '',
|
||||||
|
|
||||||
currentRegion: { id: '', value: '' },
|
currentRegion: { id: '', value: '' },
|
||||||
|
|
||||||
@@ -273,8 +276,6 @@ export default defineComponent({
|
|||||||
authorsOptions() {
|
authorsOptions() {
|
||||||
return this.store.stationList
|
return this.store.stationList
|
||||||
.reduce((acc, station) => {
|
.reduce((acc, station) => {
|
||||||
if (station.generalInfo?.hidden === true) return acc;
|
|
||||||
|
|
||||||
station.generalInfo?.authors?.forEach((author) => {
|
station.generalInfo?.authors?.forEach((author) => {
|
||||||
if (author.trim() != '' && !acc.includes(author.toLocaleLowerCase()))
|
if (author.trim() != '' && !acc.includes(author.toLocaleLowerCase()))
|
||||||
acc.push(author.toLocaleLowerCase());
|
acc.push(author.toLocaleLowerCase());
|
||||||
@@ -288,10 +289,8 @@ export default defineComponent({
|
|||||||
projectsOptions() {
|
projectsOptions() {
|
||||||
return this.store.stationList
|
return this.store.stationList
|
||||||
.reduce((acc, station) => {
|
.reduce((acc, station) => {
|
||||||
if (!station.generalInfo || !station.generalInfo.project || station.generalInfo.hidden)
|
if (!station.generalInfo || !station.generalInfo.project || station.generalInfo.hidden) return acc;
|
||||||
return acc;
|
if (!acc.includes(station.generalInfo.project.trim())) acc.push(station.generalInfo.project.trim());
|
||||||
if (!acc.includes(station.generalInfo.project.trim()))
|
|
||||||
acc.push(station.generalInfo.project.trim());
|
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, [] as string[])
|
}, [] as string[])
|
||||||
@@ -321,15 +320,11 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
resetAuthorsInput() {
|
resetAuthorsInput() {
|
||||||
this.filters['authors'] = '';
|
this.filters['authors'] = this.authorSearchFilter;
|
||||||
},
|
},
|
||||||
|
|
||||||
resetProjectsInput() {
|
resetProjectsInput() {
|
||||||
this.filters['projects'] = '';
|
this.filters['projects'] = this.projectSearchFilter;
|
||||||
},
|
|
||||||
|
|
||||||
resetLineNumbersInput() {
|
|
||||||
this.filters['lines'] = '';
|
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSceneriesInput() {
|
handleSceneriesInput() {
|
||||||
@@ -374,6 +369,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
// Reset local model values
|
// Reset local model values
|
||||||
this.minimumHours = 0;
|
this.minimumHours = 0;
|
||||||
|
this.authorSearchFilter = '';
|
||||||
|
|
||||||
// Reset global filters
|
// Reset global filters
|
||||||
Object.keys(this.filters).forEach((filterKey) => {
|
Object.keys(this.filters).forEach((filterKey) => {
|
||||||
@@ -417,14 +413,6 @@ export default defineComponent({
|
|||||||
@use '../../styles/animations';
|
@use '../../styles/animations';
|
||||||
|
|
||||||
h3.section-header {
|
h3.section-header {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 0.25em;
|
|
||||||
gap: 0.5em;
|
|
||||||
color: var(--clr-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3.hours-section-header {
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
}
|
}
|
||||||
@@ -506,12 +494,15 @@ h3.hours-section-header {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
input {
|
||||||
select {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border: 1px solid #aaa;
|
border: 1px solid #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.authors {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-filters {
|
.section-filters {
|
||||||
@@ -582,6 +573,12 @@ h3.hours-section-header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.option-section h3 {
|
.option-section h3 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.25em;
|
||||||
|
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 0.15em;
|
padding: 0.15em;
|
||||||
color: coral;
|
color: coral;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
class="header-text"
|
class="header-text"
|
||||||
:class="headerName"
|
:class="headerName"
|
||||||
>
|
>
|
||||||
<div class="header_wrapper">
|
<span class="header_wrapper">
|
||||||
<div v-html="$t(`sceneries.headers.${headerName}`)"></div>
|
<div v-html="$t(`sceneries.headers.${headerName}`)"></div>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
:src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
|
:src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
|
||||||
alt="sort icon"
|
alt="sort icon"
|
||||||
/>
|
/>
|
||||||
</div>
|
</span>
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th
|
<th
|
||||||
@@ -52,14 +52,14 @@
|
|||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<router-link
|
||||||
v-for="station in filteredStationList"
|
v-for="station in filteredStationList"
|
||||||
class="a-row"
|
class="a-row"
|
||||||
tabindex="0"
|
role="row"
|
||||||
:key="station.name"
|
:key="station.name"
|
||||||
@click.right.prevent="openForumSite($event, station.generalInfo?.url)"
|
@click.right.prevent="openForumSite($event, station.generalInfo?.url)"
|
||||||
@click="getSceneryRoute(station)"
|
@keydown.space.prevent="openForumSite($event, station.generalInfo?.url)"
|
||||||
@keydown.enter="getSceneryRoute(station)"
|
:to="getSceneryRoute(station)"
|
||||||
>
|
>
|
||||||
<td class="station-name" :class="station.generalInfo?.availability">
|
<td class="station-name" :class="station.generalInfo?.availability">
|
||||||
<b v-if="station.generalInfo?.project" style="color: salmon">{{
|
<b v-if="station.generalInfo?.project" style="color: salmon">{{
|
||||||
@@ -132,6 +132,7 @@
|
|||||||
<span v-if="station.onlineInfo?.dispatcherName">
|
<span v-if="station.onlineInfo?.dispatcherName">
|
||||||
<b
|
<b
|
||||||
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
|
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
|
||||||
|
@click.prevent="openDonationCard"
|
||||||
data-tooltip-type="DonatorTooltip"
|
data-tooltip-type="DonatorTooltip"
|
||||||
:data-tooltip-content="$t('donations.dispatcher-message')"
|
:data-tooltip-content="$t('donations.dispatcher-message')"
|
||||||
>
|
>
|
||||||
@@ -145,14 +146,6 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="station-dispatcher-lang">
|
|
||||||
<FlagIcon
|
|
||||||
v-if="station.onlineInfo && station.onlineInfo.dispatcherLanguageId != -1"
|
|
||||||
:language-id="station.onlineInfo.dispatcherLanguageId"
|
|
||||||
width="2.25em"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="station-dispatcher-exp">
|
<td class="station-dispatcher-exp">
|
||||||
<span
|
<span
|
||||||
v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1"
|
v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1"
|
||||||
@@ -321,7 +314,7 @@
|
|||||||
>
|
>
|
||||||
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }}
|
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</router-link>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -351,13 +344,11 @@ import { useTooltipStore } from '../../store/tooltipStore';
|
|||||||
import { getChangedFilters } from '../../managers/stationFilterManager';
|
import { getChangedFilters } from '../../managers/stationFilterManager';
|
||||||
import { ActiveSorter, HeadIdsType, headIconsIds, headIds } from './typings';
|
import { ActiveSorter, HeadIdsType, headIconsIds, headIds } from './typings';
|
||||||
import { filterStations, sortStations } from './utils';
|
import { filterStations, sortStations } from './utils';
|
||||||
import { getLanguageNameById } from '../../utils/languageUtils';
|
|
||||||
import FlagIcon from '../Global/FlagIcon.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
emits: ['toggleDonationCard'],
|
emits: ['toggleDonationCard'],
|
||||||
|
|
||||||
components: { Loading, StationStatusBadge, FlagIcon },
|
components: { Loading, StationStatusBadge },
|
||||||
mixins: [styleMixin, dateMixin],
|
mixins: [styleMixin, dateMixin],
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
@@ -393,13 +384,15 @@ export default defineComponent({
|
|||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
getSceneryRoute(station: Station) {
|
getSceneryRoute(station: Station) {
|
||||||
this.$router.push({
|
// TODO: Hide tooltips when navigating away
|
||||||
|
|
||||||
|
return {
|
||||||
name: 'SceneryView',
|
name: 'SceneryView',
|
||||||
query: {
|
query: {
|
||||||
station: station.name,
|
station: station.name,
|
||||||
region: this.$route.query.region || undefined
|
region: this.$route.query.region || undefined
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
openDonationCard(e: Event) {
|
openDonationCard(e: Event) {
|
||||||
@@ -445,7 +438,7 @@ export default defineComponent({
|
|||||||
$rowCol: #424242;
|
$rowCol: #424242;
|
||||||
|
|
||||||
.station_table {
|
.station_table {
|
||||||
height: calc(100vh - 17em);
|
height: calc(100vh - 11em);
|
||||||
max-height: 2000px;
|
max-height: 2000px;
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -466,82 +459,78 @@ table {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 1250px;
|
min-width: 1250px;
|
||||||
white-space: wrap;
|
white-space: wrap;
|
||||||
}
|
|
||||||
|
|
||||||
thead {
|
thead {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead tr {
|
|
||||||
background-color: var(--clr-bg3);
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th {
|
|
||||||
background-color: var(--clr-bg3);
|
|
||||||
white-space: pre-wrap;
|
|
||||||
padding: 0.5em 0.25em;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
|
|
||||||
&.station {
|
|
||||||
width: 12em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.min-lvl {
|
thead tr {
|
||||||
width: 4em;
|
background-color: var(--clr-bg3);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.status {
|
thead th {
|
||||||
width: 10em;
|
&.station {
|
||||||
}
|
width: 12em;
|
||||||
|
}
|
||||||
|
|
||||||
&.dispatcher {
|
&.min-lvl {
|
||||||
width: 12em;
|
width: 4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dispatcher-lang {
|
&.status {
|
||||||
width: 6em;
|
width: 10em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dispatcher-lvl {
|
&.dispatcher {
|
||||||
width: 6em;
|
width: 12em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.routes-double,
|
&.dispatcher-lvl {
|
||||||
&.routes-single {
|
width: 6em;
|
||||||
width: 7em;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.general {
|
&.routes-double,
|
||||||
width: 11em;
|
&.routes-single {
|
||||||
}
|
width: 7em;
|
||||||
|
}
|
||||||
|
|
||||||
&.header-image {
|
&.general {
|
||||||
width: 3.5em;
|
width: 11em;
|
||||||
|
}
|
||||||
|
|
||||||
&.user {
|
&.header-image {
|
||||||
width: 5em;
|
width: 3.5em;
|
||||||
|
|
||||||
|
&.user {
|
||||||
|
width: 5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
padding: 0.5em 0.25em;
|
||||||
|
background-color: var(--clr-bg3);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 1.5em;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
thead th .header_wrapper {
|
tr,
|
||||||
display: flex;
|
.a-row {
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 1.5em;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr {
|
|
||||||
background-color: $rowCol;
|
background-color: $rowCol;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
||||||
@@ -561,7 +550,6 @@ tbody tr {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
height: 2.5em;
|
|
||||||
|
|
||||||
&.inactive {
|
&.inactive {
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export const headIds = [
|
|||||||
'min-lvl',
|
'min-lvl',
|
||||||
'status',
|
'status',
|
||||||
'dispatcher',
|
'dispatcher',
|
||||||
'dispatcher-lang',
|
|
||||||
'dispatcher-lvl',
|
'dispatcher-lvl',
|
||||||
'routes-single',
|
'routes-single',
|
||||||
'routes-double',
|
'routes-double',
|
||||||
|
|||||||
@@ -145,33 +145,13 @@ function filterSliderValues(filters: Record<string, any>, generalInfo: StationGe
|
|||||||
}
|
}
|
||||||
|
|
||||||
function filterInputValues(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
|
function filterInputValues(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
|
||||||
if (
|
return (
|
||||||
filters['authors'].length > 3 &&
|
(filters['authors'].length > 3 &&
|
||||||
generalInfo.authors &&
|
!generalInfo.authors
|
||||||
!generalInfo.authors.some(
|
?.map((a) => a.toLocaleLowerCase())
|
||||||
(a) => a.toLocaleLowerCase() == filters['authors'].toLocaleLowerCase()
|
.includes(filters['authors'].toLocaleLowerCase())) ||
|
||||||
)
|
(filters['projects'].length > 0 && generalInfo.project != filters['projects'])
|
||||||
)
|
);
|
||||||
return true;
|
|
||||||
|
|
||||||
if (filters['projects'].length > 0 && generalInfo.project != filters['projects']) return true;
|
|
||||||
|
|
||||||
if (filters['lines'].length > 0) {
|
|
||||||
const linesNumbers = (filters['lines'] as string)
|
|
||||||
.split(',')
|
|
||||||
.map((l) => Number(l))
|
|
||||||
.filter((l) => !isNaN(l) && l != 0);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!generalInfo.lines
|
|
||||||
?.split(',')
|
|
||||||
.map((l) => Number(l))
|
|
||||||
.some((l) => linesNumbers.includes(l))
|
|
||||||
)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sortStations = (a: Station, b: Station, sorter: ActiveSorter) => {
|
export const sortStations = (a: Station, b: Station, sorter: ActiveSorter) => {
|
||||||
@@ -210,11 +190,6 @@ export const sortStations = (a: Station, b: Station, sorter: ActiveSorter) => {
|
|||||||
diff = (a.onlineInfo?.dispatcherExp || 0) - (b.onlineInfo?.dispatcherExp || 0);
|
diff = (a.onlineInfo?.dispatcherExp || 0) - (b.onlineInfo?.dispatcherExp || 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'dispatcher-lang':
|
|
||||||
diff =
|
|
||||||
(a.onlineInfo?.dispatcherLanguageId ?? -1) - (b.onlineInfo?.dispatcherLanguageId ?? -1);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'routes-single':
|
case 'routes-single':
|
||||||
diff =
|
diff =
|
||||||
(a.generalInfo?.routes.single.filter((r) => !r.hidden && !r.isInternal).length ?? -1) -
|
(a.generalInfo?.routes.single.filter((r) => !r.hidden && !r.isInternal).length ?? -1) -
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
<span v-if="vehicleCargo">({{ vehicleCargo.id }})</span>
|
<span v-if="vehicleCargo">({{ vehicleCargo.id }})</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="vehicle-props" v-if="vehicleGroup">
|
<div class="vehicle-props" v-if="vehicleData">
|
||||||
{{ vehicleGroup.speed }}km/h • {{ vehicleGroup.length }}m •
|
{{ vehicleData.group.speed }}km/h • {{ vehicleData.group.length }}m •
|
||||||
{{ (vehicleGroup.weight / 1000).toFixed(1) }}t
|
{{ (vehicleData.group.weight / 1000).toFixed(1) }}t
|
||||||
<span v-if="vehicleCargo">(+{{ (vehicleCargo.weight / 1000).toFixed(1) }}t)</span>
|
<span v-if="vehicleCargo">(+{{ (vehicleCargo.weight / 1000).toFixed(1) }}t)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,18 +73,12 @@ export default defineComponent({
|
|||||||
return this.tooltipStore.content.split(':')[0];
|
return this.tooltipStore.content.split(':')[0];
|
||||||
},
|
},
|
||||||
|
|
||||||
vehicleGroup() {
|
vehicleData() {
|
||||||
if (!this.apiStore.vehiclesData) return null;
|
return this.apiStore.vehiclesData?.find((v) => v.name == this.vehicleName);
|
||||||
|
|
||||||
const vehicle = this.apiStore.vehiclesData.vehicles.find((v) => v.name == this.vehicleName);
|
|
||||||
|
|
||||||
if (!vehicle) return null;
|
|
||||||
|
|
||||||
return this.apiStore.vehiclesData.vehicleGroups.find((g) => g.id == vehicle?.vehicleGroupsId);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
vehicleCargo() {
|
vehicleCargo() {
|
||||||
const x = this.vehicleGroup?.cargoTypes?.find(
|
const x = this.vehicleData?.group.cargoTypes?.find(
|
||||||
(c) => c.id == this.tooltipStore.content.split(':')[1]
|
(c) => c.id == this.tooltipStore.content.split(':')[1]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -66,10 +66,6 @@
|
|||||||
|
|
||||||
<span v-else>{{ train.driverName }}</span>
|
<span v-else>{{ train.driverName }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="train-language-flag">
|
|
||||||
<FlagIcon :language-id="train.driverLanguageId" width="1.75em" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -203,11 +199,10 @@ import trainInfoMixin from '../../mixins/trainInfoMixin';
|
|||||||
import trainCategoryMixin from '../../mixins/trainCategoryMixin';
|
import trainCategoryMixin from '../../mixins/trainCategoryMixin';
|
||||||
import ProgressBar from '../Global/ProgressBar.vue';
|
import ProgressBar from '../Global/ProgressBar.vue';
|
||||||
import StockList from '../Global/StockList.vue';
|
import StockList from '../Global/StockList.vue';
|
||||||
import FlagIcon from '../Global/FlagIcon.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
mixins: [trainInfoMixin, styleMixin, trainCategoryMixin],
|
mixins: [trainInfoMixin, styleMixin, trainCategoryMixin],
|
||||||
components: { ProgressBar, StockList, FlagIcon },
|
components: { ProgressBar, StockList },
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
train: {
|
train: {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export default defineComponent({
|
|||||||
@use '../../styles/animations';
|
@use '../../styles/animations';
|
||||||
|
|
||||||
.train-table {
|
.train-table {
|
||||||
height: calc(100vh - 17em);
|
height: calc(100vh - 11em);
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
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 };
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@@ -69,8 +69,7 @@
|
|||||||
"confirm": "ROGER THAT!",
|
"confirm": "ROGER THAT!",
|
||||||
"no-data": "No data about the latest app update has been found",
|
"no-data": "No data about the latest app update has been found",
|
||||||
"info-1": "This changelog will be available to see once again after clicking the version number in the footer",
|
"info-1": "This changelog will be available to see once again after clicking the version number in the footer",
|
||||||
"info-2": "The full app changelog available on {link}",
|
"info-2": "The full app changelog available on <a href='https://github.com/Spythere/stacjownik' target='_blank'>the project's GitHub</a>"
|
||||||
"info-2-link-text": "the project's GitHub page"
|
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"sceneries": "SCENERIES",
|
"sceneries": "SCENERIES",
|
||||||
@@ -87,8 +86,10 @@
|
|||||||
"tooltip-scenery-offline": "Scenery is offline",
|
"tooltip-scenery-offline": "Scenery is offline",
|
||||||
"pojazdownik-link-content": "POJAZDOWNIK",
|
"pojazdownik-link-content": "POJAZDOWNIK",
|
||||||
"language-tooltip-content": "JĘZYK / LANGUAGE",
|
"language-tooltip-content": "JĘZYK / LANGUAGE",
|
||||||
"gnr-link-content": "TRAIN ORDERS <br> GENERATOR",
|
"gnr-link-content": "TRAIN ORDERS <br> GENERATOR"
|
||||||
"discord-link-content": "STACJOWNIK <br> DISCORD SERVER"
|
},
|
||||||
|
"footer": {
|
||||||
|
"discord": "Stacjownik Discord server"
|
||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"EI": "domestic express",
|
"EI": "domestic express",
|
||||||
@@ -195,11 +196,9 @@
|
|||||||
"search-train": "Train no. / #",
|
"search-train": "Train no. / #",
|
||||||
"select-driver": "Choose a driver...",
|
"select-driver": "Choose a driver...",
|
||||||
"search-driver": "Driver name",
|
"search-driver": "Driver name",
|
||||||
"search-duty-id": "Duty ID",
|
|
||||||
"search-dispatcher": "Dispatcher name",
|
"search-dispatcher": "Dispatcher name",
|
||||||
"search-station": "Scenery name / #",
|
"search-station": "Scenery name / #",
|
||||||
"search-author": "Timetable author name",
|
"search-author": "Timetable author name",
|
||||||
"search-includesScenery": "Includes scenery name",
|
|
||||||
"search-issuedFrom": "Issuing scenery name",
|
"search-issuedFrom": "Issuing scenery name",
|
||||||
"search-via": "Via scenery name",
|
"search-via": "Via scenery name",
|
||||||
"search-terminatingAt": "Terminating scenery name",
|
"search-terminatingAt": "Terminating scenery name",
|
||||||
@@ -317,9 +316,8 @@
|
|||||||
"minTwoWayInt": "MIN. INTERNAL OTHER DOUBLE TRACK ROUTES"
|
"minTwoWayInt": "MIN. INTERNAL OTHER DOUBLE TRACK ROUTES"
|
||||||
},
|
},
|
||||||
"sceneries-placeholder": "Search for scenery",
|
"sceneries-placeholder": "Search for scenery",
|
||||||
"line-numbers-placeholder": "Line numbers (separated by commas)",
|
"authors-placeholder": "Scenery author (other filters apply)",
|
||||||
"authors-placeholder": "Scenery author",
|
"projects-placeholder": "Scenery project (other filters apply)",
|
||||||
"projects-placeholder": "Scenery project",
|
|
||||||
"search-button-title": "SEARCH",
|
"search-button-title": "SEARCH",
|
||||||
"minimum-hours-title": "SHOW ONLY SCENERIES UNTIL:",
|
"minimum-hours-title": "SHOW ONLY SCENERIES UNTIL:",
|
||||||
"now": "NOW",
|
"now": "NOW",
|
||||||
@@ -336,7 +334,6 @@
|
|||||||
"min-lvl": "Scenery\nlevel",
|
"min-lvl": "Scenery\nlevel",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"dispatcher": "Dispatcher",
|
"dispatcher": "Dispatcher",
|
||||||
"dispatcher-lang": "Language",
|
|
||||||
"dispatcher-lvl": "Dispatcher\nlevel",
|
"dispatcher-lvl": "Dispatcher\nlevel",
|
||||||
"routes-single": "1-track\nroutes",
|
"routes-single": "1-track\nroutes",
|
||||||
"routes-double": "2-track\nroutes",
|
"routes-double": "2-track\nroutes",
|
||||||
@@ -427,7 +424,7 @@
|
|||||||
"last-seen-ago": "since {minutes} minutes",
|
"last-seen-ago": "since {minutes} minutes",
|
||||||
"scenery-offline": "Offline ride",
|
"scenery-offline": "Offline ride",
|
||||||
"timeout": "An error occured while trying to refresh SWDR timetable data!",
|
"timeout": "An error occured while trying to refresh SWDR timetable data!",
|
||||||
"driver-profile-link": "PLAYER'S PROFILE",
|
"driver-journal-link": "DRIVER JOURNAL",
|
||||||
"driver-srjp-link": "SRJP",
|
"driver-srjp-link": "SRJP",
|
||||||
"driver-return-link": "RETURN",
|
"driver-return-link": "RETURN",
|
||||||
"driver-not-found-header": "Train not found! :/",
|
"driver-not-found-header": "Train not found! :/",
|
||||||
@@ -618,54 +615,9 @@
|
|||||||
"desc-end": "The train terminates here",
|
"desc-end": "The train terminates here",
|
||||||
"desc-terminated": "The train has been terminated"
|
"desc-terminated": "The train has been terminated"
|
||||||
},
|
},
|
||||||
"profile": {
|
"history": {
|
||||||
"journal-button": "PLAYER'S PROFILE",
|
"title": "TIMETABLE JOURNAL",
|
||||||
"no-player-found": "Player not found! :/",
|
"search-train": "Train no.",
|
||||||
"return-to-main": "Return to the main site",
|
"search-driver": "Driver name"
|
||||||
|
|
||||||
"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 :("
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,8 +68,7 @@
|
|||||||
"confirm": "PRZYJĄŁEM!",
|
"confirm": "PRZYJĄŁEM!",
|
||||||
"no-data": "Nie znaleziono informacji o ostatnich zmianach w aplikacji",
|
"no-data": "Nie znaleziono informacji o ostatnich zmianach w aplikacji",
|
||||||
"info-1": "Ten changelog będzie zawsze dostępny po kliknięciu numeru wersji w stopce strony",
|
"info-1": "Ten changelog będzie zawsze dostępny po kliknięciu numeru wersji w stopce strony",
|
||||||
"info-2": "Pełny changelog dostępny na {link}",
|
"info-2": "Pełny changelog dostępny na <a href='https://github.com/Spythere/stacjownik' target='_blank'>GitHubie projektu</a>"
|
||||||
"info-2-link-text": "GitHubie projektu"
|
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"sceneries": "SCENERIE",
|
"sceneries": "SCENERIE",
|
||||||
@@ -83,8 +82,10 @@
|
|||||||
"tooltip-scenery-offline": "Sceneria offline",
|
"tooltip-scenery-offline": "Sceneria offline",
|
||||||
"pojazdownik-link-content": "POJAZDOWNIK",
|
"pojazdownik-link-content": "POJAZDOWNIK",
|
||||||
"language-tooltip-content": "JĘZYK / LANGUAGE",
|
"language-tooltip-content": "JĘZYK / LANGUAGE",
|
||||||
"gnr-link-content": "GENERATOR <br> ROZKAZÓW PISEMNYCH",
|
"gnr-link-content": "GENERATOR <br> ROZKAZÓW PISEMNYCH"
|
||||||
"discord-link-content": "SERWER DISCORD <br> STACJOWNIKA"
|
},
|
||||||
|
"footer": {
|
||||||
|
"discord": "Serwer Discord Stacjownika"
|
||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"EI": "ekspres krajowy",
|
"EI": "ekspres krajowy",
|
||||||
@@ -191,11 +192,9 @@
|
|||||||
"search-train": "Nr pociągu / #",
|
"search-train": "Nr pociągu / #",
|
||||||
"search-driver": "Nick maszynisty",
|
"search-driver": "Nick maszynisty",
|
||||||
"select-driver": "Wybierz maszynistę...",
|
"select-driver": "Wybierz maszynistę...",
|
||||||
"search-duty-id": "ID służby",
|
|
||||||
"search-dispatcher": "Nick dyżurnego",
|
"search-dispatcher": "Nick dyżurnego",
|
||||||
"search-station": "Nazwa scenerii / #",
|
"search-station": "Nazwa scenerii / #",
|
||||||
"search-author": "Nick autora rozkładu jazdy",
|
"search-author": "Nick autora rozkładu jazdy",
|
||||||
"search-includesScenery": "Zawiera scenerię",
|
|
||||||
"search-issuedFrom": "Sceneria początkowa",
|
"search-issuedFrom": "Sceneria początkowa",
|
||||||
"search-via": "Przez scenerię",
|
"search-via": "Przez scenerię",
|
||||||
"search-terminatingAt": "Sceneria końcowa",
|
"search-terminatingAt": "Sceneria końcowa",
|
||||||
@@ -314,9 +313,8 @@
|
|||||||
"minTwoWayInt": "SZLAKI DWUTOROWE NIEZELEKTR. WEWNĘTRZNE (MINIMUM)"
|
"minTwoWayInt": "SZLAKI DWUTOROWE NIEZELEKTR. WEWNĘTRZNE (MINIMUM)"
|
||||||
},
|
},
|
||||||
"sceneries-placeholder": "Wyszukaj scenerię",
|
"sceneries-placeholder": "Wyszukaj scenerię",
|
||||||
"line-numbers-placeholder": "Numery linii (oddzielone przecinkami)",
|
"authors-placeholder": "Autor scenerii (uwzględnia inne filtry)",
|
||||||
"authors-placeholder": "Autor scenerii",
|
"projects-placeholder": "Projekt scenerii (uwzględnia inne filtry)",
|
||||||
"projects-placeholder": "Projekt scenerii",
|
|
||||||
"search-button-title": "SZUKAJ",
|
"search-button-title": "SZUKAJ",
|
||||||
"minimum-hours-title": "POKAŻ TYLKO SCENERIE DOSTĘPNE MINIMUM DO:",
|
"minimum-hours-title": "POKAŻ TYLKO SCENERIE DOSTĘPNE MINIMUM DO:",
|
||||||
"now": "TERAZ",
|
"now": "TERAZ",
|
||||||
@@ -333,7 +331,6 @@
|
|||||||
"min-lvl": "Poziom\nscenerii",
|
"min-lvl": "Poziom\nscenerii",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"dispatcher": "Dyżurny",
|
"dispatcher": "Dyżurny",
|
||||||
"dispatcher-lang": "Język",
|
|
||||||
"dispatcher-lvl": "Poziom\ndyżurnego",
|
"dispatcher-lvl": "Poziom\ndyżurnego",
|
||||||
"routes-single": "Szlaki\n1-torowe",
|
"routes-single": "Szlaki\n1-torowe",
|
||||||
"routes-double": "Szlaki\n2-torowe",
|
"routes-double": "Szlaki\n2-torowe",
|
||||||
@@ -413,7 +410,7 @@
|
|||||||
"last-seen-ago": "od {minutes} minut",
|
"last-seen-ago": "od {minutes} minut",
|
||||||
"scenery-offline": "Przejazd offline",
|
"scenery-offline": "Przejazd offline",
|
||||||
"timeout": "Wystąpił problem z aktualizacją rozkładów jazdy z SWDR",
|
"timeout": "Wystąpił problem z aktualizacją rozkładów jazdy z SWDR",
|
||||||
"driver-profile-link": "PROFIL GRACZA",
|
"driver-journal-link": "DZIENNIK MASZYNISTY",
|
||||||
"driver-srjp-link": "SRJP",
|
"driver-srjp-link": "SRJP",
|
||||||
"driver-return-link": "POWRÓT",
|
"driver-return-link": "POWRÓT",
|
||||||
"driver-not-found-header": "Nie znaleziono pociągu! :/",
|
"driver-not-found-header": "Nie znaleziono pociągu! :/",
|
||||||
@@ -603,54 +600,7 @@
|
|||||||
"desc-end": "Pociąg kończy bieg",
|
"desc-end": "Pociąg kończy bieg",
|
||||||
"desc-terminated": "Pociąg zakończył bieg"
|
"desc-terminated": "Pociąg zakończył bieg"
|
||||||
},
|
},
|
||||||
"profile": {
|
"history": {
|
||||||
"journal-button": "PROFIL GRACZA",
|
"title": "DZIENNIK ROZKŁADÓW JAZDY"
|
||||||
"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 :("
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,8 +69,7 @@ export const initFilters = {
|
|||||||
minTwoWayInt: 0,
|
minTwoWayInt: 0,
|
||||||
minTwoWayCatenaryInt: 0,
|
minTwoWayCatenaryInt: 0,
|
||||||
authors: '',
|
authors: '',
|
||||||
projects: '',
|
projects: ''
|
||||||
lines: ''
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sliderStates = [
|
export const sliderStates = [
|
||||||
|
|||||||
@@ -122,27 +122,19 @@ export default defineComponent({
|
|||||||
|
|
||||||
// Check the whole consist speed limit
|
// Check the whole consist speed limit
|
||||||
const vehicleMaxSpeed = stockList.reduce((acc, stockName, i) => {
|
const vehicleMaxSpeed = stockList.reduce((acc, stockName, i) => {
|
||||||
if (!this.apiStore.vehiclesData) return acc;
|
|
||||||
|
|
||||||
const [vehicleName, vehicleCargo] = stockName.split(':');
|
const [vehicleName, vehicleCargo] = stockName.split(':');
|
||||||
|
|
||||||
const vehicle = this.apiStore.vehiclesData.vehicles.find((v) => v.name == vehicleName);
|
const vehicleData = this.apiStore.vehiclesData?.find((v) => v.name == vehicleName);
|
||||||
|
|
||||||
if (!vehicle) return acc;
|
if (!vehicleData) return acc;
|
||||||
|
|
||||||
const vehicleGroup = this.apiStore.vehiclesData.vehicleGroups.find(
|
let vehicleSpeed = vehicleData.group.speed;
|
||||||
(g) => g.id == vehicle.vehicleGroupsId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!vehicleGroup) return acc;
|
if (vehicleData.type == 'wagon-freight') {
|
||||||
|
|
||||||
let vehicleSpeed = vehicleGroup.speed;
|
|
||||||
|
|
||||||
if (vehicle.type == 'wagon-freight') {
|
|
||||||
isPassenger = false;
|
isPassenger = false;
|
||||||
|
|
||||||
if (vehicleCargo !== undefined && vehicleGroup.speedLoaded) {
|
if (vehicleCargo !== undefined && vehicleData.group.speedLoaded) {
|
||||||
vehicleSpeed = vehicleGroup.speedLoaded;
|
vehicleSpeed = vehicleData.group.speedLoaded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,23 +143,14 @@ export default defineComponent({
|
|||||||
|
|
||||||
// Check the head vehicle speed limit
|
// Check the head vehicle speed limit
|
||||||
const headLocoName = stockList[0];
|
const headLocoName = stockList[0];
|
||||||
|
const headLocoVehicleData = this.apiStore.vehiclesData?.find((v) => v.name == headLocoName);
|
||||||
const headLocoVehicle = this.apiStore.vehiclesData!.vehicles.find(
|
|
||||||
(v) => v.name == headLocoName
|
|
||||||
);
|
|
||||||
|
|
||||||
const headLocoVehicleGroup = this.apiStore.vehiclesData!.vehicleGroups.find(
|
|
||||||
(g) => g.id == headLocoVehicle?.vehicleGroupsId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!headLocoVehicleGroup) return vehicleMaxSpeed;
|
|
||||||
|
|
||||||
// Omit speed check for head vehicle if there's no data for it
|
// Omit speed check for head vehicle if there's no data for it
|
||||||
if (!headLocoName || !headLocoVehicle || !headLocoVehicleGroup.massSpeeds)
|
if (!headLocoName || !headLocoVehicleData || !headLocoVehicleData.group.massSpeeds)
|
||||||
return vehicleMaxSpeed;
|
return vehicleMaxSpeed;
|
||||||
|
|
||||||
const massSpeeds =
|
const massSpeeds =
|
||||||
headLocoVehicleGroup.massSpeeds[
|
headLocoVehicleData.group.massSpeeds[
|
||||||
stockList.length == 1 ? 'none' : isPassenger ? 'passenger' : 'cargo'
|
stockList.length == 1 ? 'none' : isPassenger ? 'passenger' : 'cargo'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -61,11 +61,6 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
region: route.query.region
|
region: route.query.region
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/profile',
|
|
||||||
name: 'PlayerProfileView',
|
|
||||||
component: () => import('../views/PlayerProfileView.vue')
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/:catchAll(.*)',
|
path: '/:catchAll(.*)',
|
||||||
redirect: '/'
|
redirect: '/'
|
||||||
@@ -74,14 +69,13 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
scrollBehavior(to, from, savedPosition) {
|
scrollBehavior(to, from, savedPosition) {
|
||||||
console.log(to.name);
|
|
||||||
if (
|
if (
|
||||||
(to.name == 'SceneryView' || to.name == 'DriverView' || to.name == 'PlayerProfileView') &&
|
(to.name == 'SceneryView' || to.name == 'DriverView') &&
|
||||||
from.name !== to.name &&
|
from.name !== to.name &&
|
||||||
from.query['view'] === undefined &&
|
from.query['view'] === undefined &&
|
||||||
!savedPosition
|
!savedPosition
|
||||||
)
|
)
|
||||||
return { el: `.app_main`, behavior: 'smooth', top: 0 };
|
return { el: `.app_main`, behavior: 'instant', top: -13 };
|
||||||
|
|
||||||
if (savedPosition) return savedPosition;
|
if (savedPosition) return savedPosition;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,18 +9,15 @@ export const useApiStore = defineStore('apiStore', {
|
|||||||
dataStatuses: {
|
dataStatuses: {
|
||||||
connection: Status.Data.Loading,
|
connection: Status.Data.Loading,
|
||||||
sceneries: 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,
|
activeData: undefined as API.ActiveData.Response | undefined,
|
||||||
vehiclesData: undefined as API.VehiclesData.Response | undefined,
|
vehiclesData: undefined as API.Vehicles.Response | undefined,
|
||||||
|
|
||||||
donatorsData: [] as API.Donators.Response,
|
donatorsData: [] as API.Donators.Response,
|
||||||
sceneryData: [] as StationJSONData[],
|
sceneryData: [] as StationJSONData[],
|
||||||
|
|
||||||
dailyStatsData: null as API.DailyStats.Response | null,
|
|
||||||
|
|
||||||
nextUpdateTime: 0,
|
nextUpdateTime: 0,
|
||||||
nextDataCheckTime: 0,
|
nextDataCheckTime: 0,
|
||||||
|
|
||||||
@@ -114,7 +111,7 @@ export const useApiStore = defineStore('apiStore', {
|
|||||||
|
|
||||||
async fetchVehiclesInfo() {
|
async fetchVehiclesInfo() {
|
||||||
try {
|
try {
|
||||||
const response = await this.client!.get<API.VehiclesData.Response>('api/getVehiclesData');
|
const response = await this.client!.get<API.Vehicles.Response>('api/getVehicles');
|
||||||
|
|
||||||
this.vehiclesData = response.data;
|
this.vehiclesData = response.data;
|
||||||
this.dataStatuses.vehicles = response.data ? Status.Data.Loaded : Status.Data.Warning;
|
this.dataStatuses.vehicles = response.data ? Status.Data.Loaded : Status.Data.Warning;
|
||||||
@@ -122,21 +119,6 @@ export const useApiStore = defineStore('apiStore', {
|
|||||||
this.dataStatuses.vehicles = Status.Data.Error;
|
this.dataStatuses.vehicles = Status.Data.Error;
|
||||||
console.error('Ups! Wystąpił błąd podczas pobierania informacji o pojazdach:', 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,6 +26,13 @@ export const useMainStore = defineStore('mainStore', {
|
|||||||
isOffline: false,
|
isOffline: false,
|
||||||
appUpdate: null,
|
appUpdate: null,
|
||||||
|
|
||||||
|
dispatcherStatsName: '',
|
||||||
|
dispatcherStatsStatus: Status.Data.Initialized,
|
||||||
|
|
||||||
|
driverStatsName: '',
|
||||||
|
driverStatsData: undefined,
|
||||||
|
driverStatsStatus: Status.Data.Initialized,
|
||||||
|
|
||||||
chosenModalTrainId: undefined,
|
chosenModalTrainId: undefined,
|
||||||
|
|
||||||
modalLastClickedTarget: null,
|
modalLastClickedTarget: null,
|
||||||
@@ -79,7 +86,6 @@ export const useMainStore = defineStore('mainStore', {
|
|||||||
online: Boolean(train.online),
|
online: Boolean(train.online),
|
||||||
driverId: train.driverId,
|
driverId: train.driverId,
|
||||||
driverName: train.driverName,
|
driverName: train.driverName,
|
||||||
driverLanguageId: train.driverLanguageId,
|
|
||||||
currentStationName: train.currentStationName,
|
currentStationName: train.currentStationName,
|
||||||
currentStationHash: train.currentStationHash,
|
currentStationHash: train.currentStationHash,
|
||||||
connectedTrack: train.connectedTrack,
|
connectedTrack: train.connectedTrack,
|
||||||
@@ -251,7 +257,6 @@ export const useMainStore = defineStore('mainStore', {
|
|||||||
dispatcherIsSupporter: false,
|
dispatcherIsSupporter: false,
|
||||||
dispatcherStatus: Status.ActiveDispatcher.FREE,
|
dispatcherStatus: Status.ActiveDispatcher.FREE,
|
||||||
dispatcherTimestamp: -1,
|
dispatcherTimestamp: -1,
|
||||||
dispatcherLanguageId: -1,
|
|
||||||
|
|
||||||
isOnline: false,
|
isOnline: false,
|
||||||
|
|
||||||
@@ -298,7 +303,6 @@ export const useMainStore = defineStore('mainStore', {
|
|||||||
dispatcherIsSupporter: scenery.dispatcherIsSupporter,
|
dispatcherIsSupporter: scenery.dispatcherIsSupporter,
|
||||||
dispatcherStatus: scenery.dispatcherStatus,
|
dispatcherStatus: scenery.dispatcherStatus,
|
||||||
dispatcherTimestamp: dispatcherTimestamp,
|
dispatcherTimestamp: dispatcherTimestamp,
|
||||||
dispatcherLanguageId: scenery.dispatcherLanguageId,
|
|
||||||
|
|
||||||
isOnline: scenery.isOnline == 1,
|
isOnline: scenery.isOnline == 1,
|
||||||
|
|
||||||
@@ -427,6 +431,7 @@ export const useMainStore = defineStore('mainStore', {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
name: scenery.name,
|
name: scenery.name,
|
||||||
|
|
||||||
generalInfo: {
|
generalInfo: {
|
||||||
...scenery,
|
...scenery,
|
||||||
authors: scenery.authors?.split(',').map((a) => a.trim()),
|
authors: scenery.authors?.split(',').map((a) => a.trim()),
|
||||||
@@ -441,7 +446,7 @@ export const useMainStore = defineStore('mainStore', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
allStationInfo(): Station[] {
|
allStationInfo(): Station[] {
|
||||||
const onlineUnsavedStations: Station[] = this.activeSceneryList
|
const onlineUnsavedStations = this.activeSceneryList
|
||||||
.filter(
|
.filter(
|
||||||
(scenery) =>
|
(scenery) =>
|
||||||
this.stationList.findIndex((st) => st.name == scenery.name) == -1 &&
|
this.stationList.findIndex((st) => st.name == scenery.name) == -1 &&
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ export interface MainStoreState {
|
|||||||
region: { id: string; value: string; name: string };
|
region: { id: string; value: string; name: string };
|
||||||
isOffline: boolean;
|
isOffline: boolean;
|
||||||
appUpdate: { version: string; changelog: string; releaseURL: string } | null;
|
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;
|
chosenModalTrainId?: string;
|
||||||
modalLastClickedTarget: EventTarget | null;
|
modalLastClickedTarget: EventTarget | null;
|
||||||
currentLocale: string;
|
currentLocale: string;
|
||||||
|
|||||||
@@ -135,20 +135,3 @@
|
|||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.timetable-status-badge {
|
|
||||||
padding: 0.05em 0.35em;
|
|
||||||
color: black;
|
|
||||||
|
|
||||||
&.terminated {
|
|
||||||
background-color: salmon;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.fulfilled {
|
|
||||||
background-color: lightgreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: lightblue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,9 +9,6 @@
|
|||||||
--clr-bg2: #1b1b1b;
|
--clr-bg2: #1b1b1b;
|
||||||
--clr-bg3: #1d1d1d;
|
--clr-bg3: #1d1d1d;
|
||||||
--clr-view-bg: #1a1a1a;
|
--clr-view-bg: #1a1a1a;
|
||||||
--clr-bg-light: #2b2b2b;
|
|
||||||
|
|
||||||
--clr-tile: #181818;
|
|
||||||
|
|
||||||
--clr-accent: #1085b3;
|
--clr-accent: #1085b3;
|
||||||
--clr-accent2: #ff3d5d;
|
--clr-accent2: #ff3d5d;
|
||||||
@@ -26,8 +23,6 @@
|
|||||||
|
|
||||||
--clr-donator: #f7a4ff;
|
--clr-donator: #f7a4ff;
|
||||||
|
|
||||||
--clr-success: springgreen;
|
|
||||||
|
|
||||||
--no-scroll-padding: 17px;
|
--no-scroll-padding: 17px;
|
||||||
--max-container-width: 1700px;
|
--max-container-width: 1700px;
|
||||||
|
|
||||||
@@ -35,8 +30,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
// width: var(--no-scroll-padding);
|
width: var(--no-scroll-padding);
|
||||||
// height: var(--no-scroll-padding);
|
height: var(--no-scroll-padding);
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
&-track {
|
&-track {
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
@@ -53,7 +49,6 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--clr-bg);
|
background: var(--clr-bg);
|
||||||
color-scheme: dark;
|
|
||||||
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -94,8 +89,7 @@ select {
|
|||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
input {
|
||||||
select {
|
|
||||||
background: none;
|
background: none;
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
@@ -336,6 +330,19 @@ a.a-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen {
|
@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]:hover::after,
|
||||||
[data-tooltip]:focus::after {
|
[data-tooltip]:focus::after {
|
||||||
transform: translate(-50%, 2em);
|
transform: translate(-50%, 2em);
|
||||||
@@ -351,22 +358,3 @@ a.a-button {
|
|||||||
background-color: #aaa;
|
background-color: #aaa;
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.g-checkbox {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
input {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
|
|
||||||
.list_wrapper {
|
.list_wrapper {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
height: calc(100vh - 21em);
|
height: calc(100vh - 12.5em);
|
||||||
min-height: 500px;
|
min-height: 700px;
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
|||||||
@@ -61,4 +61,8 @@
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
padding-right: 0.5em;
|
padding-right: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select.search-input {
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Journal } from '../components/JournalView/typings';
|
import { Status, VehicleData } from './common';
|
||||||
import { Status, Vehicle, VehicleGroup } from './common';
|
|
||||||
|
|
||||||
export enum APIDataStatus {
|
export enum APIDataStatus {
|
||||||
OK = 'OK',
|
OK = 'OK',
|
||||||
@@ -28,29 +27,17 @@ 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 namespace DispatcherHistory {
|
||||||
export type Response = Data[];
|
export type Response = Data[];
|
||||||
|
|
||||||
export interface Data {
|
export interface Data {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
currentDuration: number;
|
currentDuration: number;
|
||||||
dispatcherId: number;
|
dispatcherId: number;
|
||||||
dispatcherName: string;
|
dispatcherName: string;
|
||||||
dispatcherLevel: number | null;
|
dispatcherLevel: number | null;
|
||||||
dispatcherRate: number;
|
dispatcherRate: number;
|
||||||
dispatcherIsSupporter: boolean;
|
dispatcherIsSupporter: boolean;
|
||||||
dispatcherLanguageId: number | null;
|
|
||||||
dispatcherStatus?: number;
|
dispatcherStatus?: number;
|
||||||
isOnline: boolean;
|
isOnline: boolean;
|
||||||
lastOnlineTimestamp: number;
|
lastOnlineTimestamp: number;
|
||||||
@@ -64,64 +51,61 @@ export namespace API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export namespace DispatcherStats {
|
export namespace DispatcherStats {
|
||||||
export interface Services {
|
export interface DistanceStat {
|
||||||
count: number;
|
routeDistance: number | null;
|
||||||
durationMax: number;
|
|
||||||
durationAvg: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IssuedTimetables {
|
export interface DurationStat {
|
||||||
count: number;
|
currentDuration: number | null;
|
||||||
distanceMax: number;
|
|
||||||
distanceAvg: number;
|
|
||||||
distanceSum: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Data {
|
export interface Count {
|
||||||
dispatcherId: number | null;
|
_all: number;
|
||||||
dispatcherName: string | null;
|
|
||||||
dispatcherLevel: number | null;
|
|
||||||
services: Services | null;
|
|
||||||
issuedTimetables: IssuedTimetables | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Response = Data;
|
export interface Response {
|
||||||
|
services: {
|
||||||
|
count: number;
|
||||||
|
durationMax: number;
|
||||||
|
durationAvg: number;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
issuedTimetables: {
|
||||||
|
count: number;
|
||||||
|
distanceMax: number;
|
||||||
|
distanceAvg: number;
|
||||||
|
distanceSum: number;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace DriverStats {
|
export namespace DriverStats {
|
||||||
export interface Data {
|
export interface SumStats {
|
||||||
driverName: string | null;
|
routeDistance: number;
|
||||||
driverId: number | null;
|
confirmedStopsCount: number;
|
||||||
driverLevel: number | null;
|
allStopsCount: number;
|
||||||
countAll: number;
|
currentDistance: number;
|
||||||
countTerminated: number;
|
|
||||||
countFulfilled: number;
|
|
||||||
routeDistanceTotal: number | null;
|
|
||||||
routeDistanceAvg: number | null;
|
|
||||||
routeDistanceMax: number | null;
|
|
||||||
currentDistanceTotal: number | null;
|
|
||||||
confirmedStopsTotal: number | null;
|
|
||||||
allStopsTotal: number | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Response = Data;
|
export interface CountStats {
|
||||||
}
|
fulfilled: number;
|
||||||
|
terminated: number;
|
||||||
export namespace PlayerInfo {
|
_all: number;
|
||||||
export interface Data {
|
|
||||||
currentActivity: PlayerActivity.Data;
|
|
||||||
dispatcherStats: DispatcherStats.Data;
|
|
||||||
dispatcherStatsLastMonth: DispatcherStats.Data;
|
|
||||||
driverStats: DriverStats.Data;
|
|
||||||
driverStatsLastMonth: DriverStats.Data;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export namespace PlayerJournal {
|
export interface MaxStats {
|
||||||
export interface Data {
|
routeDistance: number;
|
||||||
timetables: TimetableHistory.DataShort[];
|
}
|
||||||
issuedTimetables: TimetableHistory.DataShort[];
|
|
||||||
duties: DispatcherHistory.Data[];
|
export interface AvdStats {
|
||||||
|
routeDistance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
_sum: SumStats;
|
||||||
|
_count: CountStats;
|
||||||
|
_max: MaxStats;
|
||||||
|
_avg: AvdStats;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +114,6 @@ export namespace API {
|
|||||||
dispatcherId: number;
|
dispatcherId: number;
|
||||||
dispatcherName: string;
|
dispatcherName: string;
|
||||||
dispatcherIsSupporter: boolean;
|
dispatcherIsSupporter: boolean;
|
||||||
dispatcherLanguageId: number;
|
|
||||||
stationName: string;
|
stationName: string;
|
||||||
stationHash: string;
|
stationHash: string;
|
||||||
region: string;
|
region: string;
|
||||||
@@ -169,7 +152,6 @@ export namespace API {
|
|||||||
driverId: number;
|
driverId: number;
|
||||||
driverIsSupporter: boolean;
|
driverIsSupporter: boolean;
|
||||||
driverLevel?: number;
|
driverLevel?: number;
|
||||||
driverLanguageId: number;
|
|
||||||
|
|
||||||
currentStationName: string;
|
currentStationName: string;
|
||||||
currentStationHash?: string;
|
currentStationHash?: string;
|
||||||
@@ -226,58 +208,24 @@ export namespace API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export namespace TimetableHistory {
|
export namespace TimetableHistory {
|
||||||
export interface QueryParams {
|
export interface Data {
|
||||||
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;
|
id: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
|
||||||
|
timetableId: number;
|
||||||
trainNo: number;
|
trainNo: number;
|
||||||
trainCategoryCode: string;
|
trainCategoryCode: string;
|
||||||
timetableId: number;
|
|
||||||
|
|
||||||
driverId: number;
|
driverId: number;
|
||||||
driverName: string;
|
driverName: string;
|
||||||
driverLevel: number | null;
|
driverLevel: number | null;
|
||||||
driverIsSupporter: boolean;
|
driverIsSupporter: boolean;
|
||||||
driverLanguageId: number | null;
|
|
||||||
|
|
||||||
route: string;
|
route: string;
|
||||||
twr: number;
|
twr: number;
|
||||||
skr: number;
|
skr: number;
|
||||||
|
sceneriesString: string;
|
||||||
currentLocation: string[];
|
currentLocation: string[];
|
||||||
|
|
||||||
routeDistance: number;
|
routeDistance: number;
|
||||||
@@ -288,6 +236,7 @@ export namespace API {
|
|||||||
|
|
||||||
beginDate: string;
|
beginDate: string;
|
||||||
endDate: string;
|
endDate: string;
|
||||||
|
|
||||||
scheduledBeginDate: string;
|
scheduledBeginDate: string;
|
||||||
scheduledEndDate: string;
|
scheduledEndDate: string;
|
||||||
|
|
||||||
@@ -297,25 +246,15 @@ export namespace API {
|
|||||||
authorName?: string;
|
authorName?: string;
|
||||||
authorId?: number;
|
authorId?: number;
|
||||||
|
|
||||||
currentSceneryName?: string;
|
|
||||||
currentSceneryHash?: string;
|
|
||||||
hasDangerousCargo: boolean;
|
|
||||||
hasExtraDeliveries: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DataDetailsOnly {
|
|
||||||
id: number;
|
|
||||||
timetableId: number;
|
|
||||||
|
|
||||||
sceneriesString: string;
|
|
||||||
stockString?: string;
|
stockString?: string;
|
||||||
stockHistory: string[];
|
stockHistory: string[];
|
||||||
|
|
||||||
stockMass?: number;
|
stockMass?: number;
|
||||||
stockLength?: number;
|
stockLength?: number;
|
||||||
maxSpeed?: number;
|
maxSpeed?: number;
|
||||||
trainMaxSpeed?: number;
|
|
||||||
|
|
||||||
|
currentSceneryName?: string;
|
||||||
|
currentSceneryHash?: string;
|
||||||
routeSceneries: string;
|
routeSceneries: string;
|
||||||
checkpointArrivals: string[];
|
checkpointArrivals: string[];
|
||||||
checkpointDepartures: string[];
|
checkpointDepartures: string[];
|
||||||
@@ -325,20 +264,14 @@ export namespace API {
|
|||||||
checkpointComments: string[];
|
checkpointComments: string[];
|
||||||
visitedSceneries: string[];
|
visitedSceneries: string[];
|
||||||
sceneryNames: string[];
|
sceneryNames: string[];
|
||||||
|
|
||||||
path: string;
|
path: string;
|
||||||
warningNotes: string | null;
|
warningNotes: string | null;
|
||||||
|
hasDangerousCargo: boolean;
|
||||||
authorId?: number;
|
hasExtraDeliveries: boolean;
|
||||||
authorName?: string;
|
trainMaxSpeed?: number;
|
||||||
driverId: number;
|
|
||||||
driverName: string;
|
|
||||||
driverLanguageId: number | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Response = Data[];
|
export type Response = Data[];
|
||||||
export type ResponseShort = DataShort[];
|
|
||||||
export type ResponseDetailsOnly = DataDetailsOnly[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace DailyStats {
|
export namespace DailyStats {
|
||||||
@@ -396,51 +329,8 @@ export namespace API {
|
|||||||
export type Response = string[];
|
export type Response = string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace VehiclesData {
|
export namespace Vehicles {
|
||||||
export interface VehicleObject {
|
export type Response = VehicleData[];
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
cabinName: string | null;
|
|
||||||
restrictions: Record<string, any> | null;
|
|
||||||
vehicleGroupsId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VehicleGroupObject {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
speed: number;
|
|
||||||
speedLoaded?: number;
|
|
||||||
speedLoco?: number;
|
|
||||||
length: number;
|
|
||||||
weight: number;
|
|
||||||
cargoTypes: VehicleCargo[] | null;
|
|
||||||
|
|
||||||
locoProps: {
|
|
||||||
coldStart: boolean;
|
|
||||||
doubleManned: boolean;
|
|
||||||
} | null;
|
|
||||||
|
|
||||||
massSpeeds: VehicleGroupMassSpeeds | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VehicleGroupMassSpeeds {
|
|
||||||
passenger: Record<string, number> | null;
|
|
||||||
cargo: Record<string, number> | null;
|
|
||||||
none: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VehicleCargo {
|
|
||||||
id: string;
|
|
||||||
weight: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Data {
|
|
||||||
vehicles: VehicleObject[];
|
|
||||||
vehicleGroups: VehicleGroupObject[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Response = Data;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,62 +380,6 @@ 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 namespace Websocket {
|
||||||
export interface Payload {
|
export interface Payload {
|
||||||
activeSceneries: API.ActiveSceneries.Response;
|
activeSceneries: API.ActiveSceneries.Response;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { RouteLocationRaw } from 'vue-router';
|
import { RouteLocationRaw } from 'vue-router';
|
||||||
import { StationJSONData } from '../store/typings';
|
import { StationJSONData } from '../store/typings';
|
||||||
import { API } from './api';
|
|
||||||
|
|
||||||
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
|
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
|
||||||
export type ScenerySpawnType = 'passenger' | 'freight' | 'loco' | 'all';
|
export type ScenerySpawnType = 'passenger' | 'freight' | 'loco' | 'all';
|
||||||
@@ -60,7 +59,6 @@ export interface Train {
|
|||||||
distance: number;
|
distance: number;
|
||||||
connectedTrack: string;
|
connectedTrack: string;
|
||||||
driverId: number;
|
driverId: number;
|
||||||
driverLanguageId: number;
|
|
||||||
trainNo: number;
|
trainNo: number;
|
||||||
driverName: string;
|
driverName: string;
|
||||||
driverLevel: number;
|
driverLevel: number;
|
||||||
@@ -97,7 +95,9 @@ export interface TrainTimetableData {
|
|||||||
|
|
||||||
export interface Station {
|
export interface Station {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
generalInfo?: StationGeneralInfo;
|
generalInfo?: StationGeneralInfo;
|
||||||
|
|
||||||
onlineInfo?: ActiveScenery;
|
onlineInfo?: ActiveScenery;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ export interface StationGeneralInfo {
|
|||||||
abbr: string;
|
abbr: string;
|
||||||
hash?: string;
|
hash?: string;
|
||||||
reqLevel: number;
|
reqLevel: number;
|
||||||
lines?: string;
|
lines: string;
|
||||||
project: string;
|
project: string;
|
||||||
projectUrl?: string;
|
projectUrl?: string;
|
||||||
signalType: string;
|
signalType: string;
|
||||||
@@ -163,7 +163,6 @@ export interface ActiveScenery {
|
|||||||
dispatcherIsSupporter: boolean;
|
dispatcherIsSupporter: boolean;
|
||||||
dispatcherStatus: Status.ActiveDispatcher | number;
|
dispatcherStatus: Status.ActiveDispatcher | number;
|
||||||
dispatcherTimestamp: number | null;
|
dispatcherTimestamp: number | null;
|
||||||
dispatcherLanguageId: number;
|
|
||||||
isOnline: boolean;
|
isOnline: boolean;
|
||||||
stationTrains: Train[];
|
stationTrains: Train[];
|
||||||
scheduledTrains: CheckpointTrain[];
|
scheduledTrains: CheckpointTrain[];
|
||||||
@@ -172,7 +171,7 @@ export interface ActiveScenery {
|
|||||||
confirmed: number;
|
confirmed: number;
|
||||||
unconfirmed: number;
|
unconfirmed: number;
|
||||||
};
|
};
|
||||||
missingCheckpoints: string[];
|
missingCheckpoints: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScenerySpawn {
|
export interface ScenerySpawn {
|
||||||
@@ -217,10 +216,46 @@ export interface CheckpointTrain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Vehicles Data
|
// Vehicles Data
|
||||||
export type Vehicle = API.VehiclesData.VehicleObject;
|
|
||||||
export type VehicleGroup = API.VehiclesData.VehicleGroupObject;
|
|
||||||
|
|
||||||
// Train Tooltip Info
|
export interface VehicleData {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
cabinName: string | null;
|
||||||
|
restrictions: Record<string, any> | null;
|
||||||
|
vehicleGroupsId: number;
|
||||||
|
group: VehiclesGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VehiclesGroup {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
speed: number;
|
||||||
|
speedLoaded?: number;
|
||||||
|
speedLoco?: number;
|
||||||
|
length: number;
|
||||||
|
weight: number;
|
||||||
|
cargoTypes: VehicleCargo[] | null;
|
||||||
|
|
||||||
|
locoProps: {
|
||||||
|
coldStart: boolean;
|
||||||
|
doubleManned: boolean;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
massSpeeds: VehicleGroupMassSpeeds | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VehicleGroupMassSpeeds {
|
||||||
|
passenger: Record<string, number> | null;
|
||||||
|
cargo: Record<string, number> | null;
|
||||||
|
none: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VehicleCargo {
|
||||||
|
id: string;
|
||||||
|
weight: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TooltipUserTrain {
|
export interface TooltipUserTrain {
|
||||||
driverName: string;
|
driverName: string;
|
||||||
trainNo: number;
|
trainNo: number;
|
||||||
@@ -241,4 +276,4 @@ export interface TooltipTrainInfo {
|
|||||||
headVehicleName: string;
|
headVehicleName: string;
|
||||||
stockCount: number;
|
stockCount: number;
|
||||||
trainTimetableCategory?: string;
|
trainTimetableCategory?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
export function getCountPercentage(partCount: number, allCount: number, fixedDigits: number) {
|
|
||||||
if (allCount == 0) return 0;
|
|
||||||
|
|
||||||
return ((partCount / allCount) * 100).toFixed(fixedDigits);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export const languageFlagNames = ['pl', 'en', 'de', 'cz', 'sk', 'ru', 'se', 'ua', 'it'];
|
|
||||||
|
|
||||||
export function getLanguageNameById(languageId: number) {
|
|
||||||
return languageFlagNames[languageId] ?? 'pl';
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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;
|
margin: 0 auto;
|
||||||
padding: 1em 0;
|
padding: 1em 0;
|
||||||
max-width: var(--max-container-width);
|
max-width: var(--max-container-width);
|
||||||
min-height: 100vh;
|
min-height: calc(100vh - 7em);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
optionsType="dispatchers"
|
optionsType="dispatchers"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<JournalStats :chosen-player-id="chosenPlayerId" />
|
<JournalStats :statsButtons="statsButtons" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="journal_refreshed-date">
|
<div class="journal_refreshed-date">
|
||||||
@@ -50,8 +50,16 @@ import JournalHeader from '../components/JournalView/JournalHeader.vue';
|
|||||||
import JournalStats from '../components/JournalView/JournalStats.vue';
|
import JournalStats from '../components/JournalView/JournalStats.vue';
|
||||||
import { useApiStore } from '../store/apiStore';
|
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 {
|
interface DispatchersQueryParams {
|
||||||
dutyId?: number;
|
|
||||||
dispatcherName?: string;
|
dispatcherName?: string;
|
||||||
stationName?: string;
|
stationName?: string;
|
||||||
stationHash?: string;
|
stationHash?: string;
|
||||||
@@ -97,15 +105,18 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
statsButtons,
|
||||||
|
|
||||||
dataRefreshedAt: null as Date | null,
|
dataRefreshedAt: null as Date | null,
|
||||||
currentQueryParams: {} as DispatchersQueryParams,
|
currentQueryParams: {} as DispatchersQueryParams,
|
||||||
|
|
||||||
scrollDataLoaded: true,
|
scrollDataLoaded: true,
|
||||||
scrollNoMoreData: false,
|
scrollNoMoreData: false,
|
||||||
|
|
||||||
chosenPlayerId: -1,
|
showReturnButton: false,
|
||||||
|
statsCardOpen: false,
|
||||||
currentOptionsActive: false,
|
currentOptionsActive: false,
|
||||||
|
|
||||||
dataStatus: Status.Data.Loading,
|
dataStatus: Status.Data.Loading,
|
||||||
|
|
||||||
historyList: [] as API.DispatcherHistory.Response
|
historyList: [] as API.DispatcherHistory.Response
|
||||||
@@ -115,13 +126,12 @@ export default defineComponent({
|
|||||||
const sorterActive: Journal.DispatcherSorter = reactive({ id: 'timestampFrom', dir: -1 });
|
const sorterActive: Journal.DispatcherSorter = reactive({ id: 'timestampFrom', dir: -1 });
|
||||||
const journalFilterActive = ref({});
|
const journalFilterActive = ref({});
|
||||||
|
|
||||||
const searchersValues = reactive<Record<Journal.DispatcherSearchKey, string>>({
|
const searchersValues = reactive({
|
||||||
'search-duty-id': '',
|
|
||||||
'search-dispatcher': '',
|
'search-dispatcher': '',
|
||||||
'search-station': '',
|
'search-station': '',
|
||||||
'search-date-from': '',
|
'search-date-from': '',
|
||||||
'search-date-to': ''
|
'search-date-to': ''
|
||||||
});
|
} as Journal.DispatcherSearchType);
|
||||||
|
|
||||||
provide('sorterActive', sorterActive);
|
provide('sorterActive', sorterActive);
|
||||||
provide('journalFilterActive', journalFilterActive);
|
provide('journalFilterActive', journalFilterActive);
|
||||||
@@ -148,6 +158,15 @@ export default defineComponent({
|
|||||||
queryParams[k as keyof DispatchersQueryParams] !=
|
queryParams[k as keyof DispatchersQueryParams] !=
|
||||||
defaultQueryParams[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();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -173,7 +192,6 @@ export default defineComponent({
|
|||||||
handleRouteParams() {
|
handleRouteParams() {
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
query: {
|
query: {
|
||||||
'search-duty-id': this.searchersValues['search-duty-id'] || undefined,
|
|
||||||
'search-date-from': this.searchersValues['search-date-from'] || undefined,
|
'search-date-from': this.searchersValues['search-date-from'] || undefined,
|
||||||
'search-date-to': this.searchersValues['search-date-to'] || undefined,
|
'search-date-to': this.searchersValues['search-date-to'] || undefined,
|
||||||
'search-station': this.searchersValues['search-station'] || undefined,
|
'search-station': this.searchersValues['search-station'] || undefined,
|
||||||
@@ -197,8 +215,30 @@ export default defineComponent({
|
|||||||
this.setOptions(query as any);
|
this.setOptions(query as any);
|
||||||
},
|
},
|
||||||
|
|
||||||
setOptions(options: Record<string, string>) {
|
async fetchDispatcherStats() {
|
||||||
this.searchersValues['search-duty-id'] = options['search-duty-id'] ?? '';
|
if (!this.mainStore.dispatcherStatsName) {
|
||||||
|
this.mainStore.dispatcherStatsData = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const statsData: API.DispatcherStats.Response = await (
|
||||||
|
await this.apiStore.client!.get('api/getDispatcherStats', {
|
||||||
|
params: {
|
||||||
|
name: this.mainStore.dispatcherStatsName
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
this.mainStore.dispatcherStatsData = statsData;
|
||||||
|
} catch (error) {
|
||||||
|
this.mainStore.dispatcherStatsData = undefined;
|
||||||
|
|
||||||
|
console.error('Ups! Wystąpił błąd przy próbie pobrania statystyk dyżurnego! :/');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setOptions(options: { [key: string]: string }) {
|
||||||
this.searchersValues['search-date-from'] = options['search-date-from'] ?? '';
|
this.searchersValues['search-date-from'] = options['search-date-from'] ?? '';
|
||||||
this.searchersValues['search-date-to'] = options['search-date-to'] ?? '';
|
this.searchersValues['search-date-to'] = options['search-date-to'] ?? '';
|
||||||
this.searchersValues['search-station'] = options['search-station'] ?? '';
|
this.searchersValues['search-station'] = options['search-station'] ?? '';
|
||||||
@@ -232,35 +272,17 @@ export default defineComponent({
|
|||||||
this.scrollDataLoaded = true;
|
this.scrollDataLoaded = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetchHistoryData() {
|
async fetchHistoryData() {
|
||||||
const queryParams: DispatchersQueryParams = {};
|
const queryParams: DispatchersQueryParams = {};
|
||||||
|
|
||||||
const dutyId = this.searchersValues['search-duty-id'].trim() || undefined;
|
|
||||||
const dispatcherName = this.searchersValues['search-dispatcher'].trim() || undefined;
|
const dispatcherName = this.searchersValues['search-dispatcher'].trim() || undefined;
|
||||||
const stationName = this.searchersValues['search-station'].trim() || undefined;
|
const stationName = this.searchersValues['search-station'].trim() || undefined;
|
||||||
const dateFromString = this.searchersValues['search-date-from'].trim() || undefined;
|
const dateFromString = this.searchersValues['search-date-from'].trim() || undefined;
|
||||||
const dateToString = this.searchersValues['search-date-to'].trim() || undefined;
|
const dateToString = this.searchersValues['search-date-to'].trim() || undefined;
|
||||||
|
|
||||||
let dateFromISO: string | undefined = undefined;
|
|
||||||
let dateToISO: string | undefined = undefined;
|
|
||||||
|
|
||||||
if (dateFromString) {
|
|
||||||
let dateFrom = new Date(dateFromString);
|
|
||||||
dateFrom.setMinutes(dateFrom.getMinutes() + dateFrom.getTimezoneOffset());
|
|
||||||
dateFromISO = dateFrom.toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dateToString) {
|
|
||||||
let dateTo = new Date(dateToString);
|
|
||||||
dateTo.setMinutes(dateTo.getMinutes() + dateTo.getTimezoneOffset());
|
|
||||||
dateToISO = dateTo.toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
queryParams['dutyId'] = Number(dutyId) || undefined;
|
|
||||||
queryParams['dispatcherName'] = dispatcherName;
|
queryParams['dispatcherName'] = dispatcherName;
|
||||||
|
queryParams['dateFrom'] = dateFromString;
|
||||||
queryParams['dateFrom'] = dateFromISO;
|
queryParams['dateTo'] = dateToString ? `${dateToString}T23:00:00` : undefined;
|
||||||
queryParams['dateTo'] = dateToISO;
|
|
||||||
|
|
||||||
queryParams['countLimit'] = 30;
|
queryParams['countLimit'] = 30;
|
||||||
|
|
||||||
@@ -282,24 +304,24 @@ export default defineComponent({
|
|||||||
|
|
||||||
if (!responseData) {
|
if (!responseData) {
|
||||||
this.dataStatus = Status.Data.Error;
|
this.dataStatus = Status.Data.Error;
|
||||||
this.chosenPlayerId = -1;
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!responseData) return;
|
||||||
|
|
||||||
// Response data exists
|
// Response data exists
|
||||||
this.historyList = responseData;
|
this.historyList = responseData;
|
||||||
|
|
||||||
this.chosenPlayerId =
|
// Stats display
|
||||||
this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim() != ''
|
this.mainStore.dispatcherStatsName =
|
||||||
? this.historyList[0].dispatcherId
|
this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim()
|
||||||
: -1;
|
? this.historyList[0].dispatcherName
|
||||||
|
: '';
|
||||||
|
|
||||||
this.dataRefreshedAt = new Date();
|
this.dataRefreshedAt = new Date();
|
||||||
this.dataStatus = Status.Data.Loaded;
|
this.dataStatus = Status.Data.Loaded;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.dataStatus = Status.Data.Error;
|
this.dataStatus = Status.Data.Error;
|
||||||
this.chosenPlayerId = -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scrollNoMoreData = false;
|
this.scrollNoMoreData = false;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
optionsType="timetables"
|
optionsType="timetables"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<JournalStats :chosen-player-id="chosenPlayerId" />
|
<JournalStats :statsButtons="statsButtons" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="journal_refreshed-date">
|
<div class="journal_refreshed-date">
|
||||||
@@ -29,8 +29,6 @@
|
|||||||
:dataStatus="dataStatus"
|
:dataStatus="dataStatus"
|
||||||
:scrollDataLoaded="scrollDataLoaded"
|
:scrollDataLoaded="scrollDataLoaded"
|
||||||
:scrollNoMoreData="scrollNoMoreData"
|
:scrollNoMoreData="scrollNoMoreData"
|
||||||
:extraInfoIndexes="extraInfoIndexes"
|
|
||||||
@toggleExtraInfo="toggleExtraInfo"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,6 +118,35 @@ 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;
|
||||||
|
|
||||||
|
countFrom?: number;
|
||||||
|
countLimit?: number;
|
||||||
|
|
||||||
|
fulfilled?: number;
|
||||||
|
terminated?: number;
|
||||||
|
|
||||||
|
twr?: number;
|
||||||
|
skr?: number;
|
||||||
|
pn?: number;
|
||||||
|
tn?: number;
|
||||||
|
|
||||||
|
sortBy?: Journal.TimetableSorter['id'];
|
||||||
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
JournalOptions,
|
JournalOptions,
|
||||||
@@ -142,18 +169,35 @@ export default defineComponent({
|
|||||||
mainStore: useMainStore(),
|
mainStore: useMainStore(),
|
||||||
apiStore: useApiStore(),
|
apiStore: useApiStore(),
|
||||||
|
|
||||||
currentQueryParams: {} as API.TimetableHistory.QueryParams,
|
statsButtons: [
|
||||||
|
{
|
||||||
|
tab: Journal.StatsTab.DAILY_STATS,
|
||||||
|
localeKey: 'journal.daily-stats.button',
|
||||||
|
iconName: 'stats',
|
||||||
|
disabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tab: Journal.StatsTab.DRIVER_STATS,
|
||||||
|
localeKey: 'journal.driver-stats.button',
|
||||||
|
iconName: 'train',
|
||||||
|
disabled: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
currentQueryParams: {} as TimetablesQueryParams,
|
||||||
dataRefreshedAt: null as Date | null,
|
dataRefreshedAt: null as Date | null,
|
||||||
|
|
||||||
scrollDataLoaded: true,
|
scrollDataLoaded: true,
|
||||||
scrollNoMoreData: false,
|
scrollNoMoreData: false,
|
||||||
extraInfoIndexes: [] as number[],
|
|
||||||
|
|
||||||
chosenPlayerId: -1,
|
showReturnButton: false,
|
||||||
|
statsCardOpen: false,
|
||||||
|
currentOptionsActive: false,
|
||||||
|
|
||||||
timetableHistory: [] as API.TimetableHistory.ResponseShort,
|
timetableHistory: [] as API.TimetableHistory.Response,
|
||||||
|
|
||||||
dataStatus: Status.Data.Loading
|
dataStatus: Status.Data.Loading,
|
||||||
|
dataErrorMessage: ''
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
@@ -169,7 +213,6 @@ export default defineComponent({
|
|||||||
'search-train': '',
|
'search-train': '',
|
||||||
'search-driver': '',
|
'search-driver': '',
|
||||||
'search-dispatcher': '',
|
'search-dispatcher': '',
|
||||||
'search-includesScenery': '',
|
|
||||||
'search-issuedFrom': '',
|
'search-issuedFrom': '',
|
||||||
'search-via': '',
|
'search-via': '',
|
||||||
'search-terminatingAt': '',
|
'search-terminatingAt': '',
|
||||||
@@ -200,11 +243,18 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
watch: {
|
||||||
currentOptionsActive() {
|
currentQueryParams(q: TimetablesQueryParams) {
|
||||||
return Object.keys(this.currentQueryParams)
|
this.currentOptionsActive = Object.values(q).some((v) => v !== undefined);
|
||||||
.filter((k) => k != 'countFrom' && k != 'returnType')
|
},
|
||||||
.some((k) => (this.currentQueryParams as any)[k] !== undefined);
|
|
||||||
|
'mainStore.driverStatsData'(driverStats) {
|
||||||
|
this.statsButtons.find((sb) => sb.tab == Journal.StatsTab.DRIVER_STATS)!.disabled =
|
||||||
|
driverStats === undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
async 'mainStore.driverStatsName'() {
|
||||||
|
this.fetchDriverStats();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -235,21 +285,28 @@ export default defineComponent({
|
|||||||
this.setOptions(query as any);
|
this.setOptions(query as any);
|
||||||
},
|
},
|
||||||
|
|
||||||
async toggleExtraInfo(timetableDetails: API.TimetableHistory.Data | null) {
|
async fetchDriverStats() {
|
||||||
if (!timetableDetails) return;
|
if (!this.mainStore.driverStatsName) {
|
||||||
|
this.mainStore.driverStatsData = undefined;
|
||||||
|
this.mainStore.driverStatsStatus = Status.Data.Initialized;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const existingIdx = this.extraInfoIndexes.indexOf(timetableDetails.id);
|
try {
|
||||||
|
this.mainStore.driverStatsStatus = Status.Data.Loading;
|
||||||
|
|
||||||
if (existingIdx == -1) {
|
const statsData: API.DriverStats.Response = await (
|
||||||
this.extraInfoIndexes.push(timetableDetails.id);
|
await this.apiStore.client!.get(
|
||||||
|
`api/getDriverInfo?name=${this.mainStore.driverStatsName}`
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
const synchedTimetable = this.timetableHistory.find((t) => t.id == timetableDetails.id);
|
this.mainStore.driverStatsData = statsData;
|
||||||
|
this.mainStore.driverStatsStatus = Status.Data.Loaded;
|
||||||
if (synchedTimetable) {
|
} catch (error) {
|
||||||
Object.assign(synchedTimetable, timetableDetails);
|
this.mainStore.driverStatsData = undefined;
|
||||||
}
|
this.mainStore.driverStatsStatus = Status.Data.Error;
|
||||||
} else {
|
console.error('Ups! Wystąpił błąd przy próbie pobrania statystyk maszynisty! :/');
|
||||||
this.extraInfoIndexes.splice(existingIdx, 1);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -295,33 +352,25 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
async fetchHistoryData() {
|
async fetchHistoryData() {
|
||||||
this.extraInfoIndexes.length = 0;
|
|
||||||
|
|
||||||
const driverName = this.searchersValues['search-driver'].trim() || undefined;
|
const driverName = this.searchersValues['search-driver'].trim() || undefined;
|
||||||
const trainNo = this.searchersValues['search-train'].trim() || undefined;
|
const trainNo = this.searchersValues['search-train'].trim() || undefined;
|
||||||
const authorName = this.searchersValues['search-dispatcher'].trim() || undefined;
|
const authorName = this.searchersValues['search-dispatcher'].trim() || undefined;
|
||||||
const dateFromString = this.searchersValues['search-date-from'].trim() || undefined;
|
const dateFrom = this.searchersValues['search-date-from'].trim() || undefined;
|
||||||
const includesScenery = this.searchersValues['search-includesScenery'].trim() || undefined;
|
|
||||||
const issuedFrom = this.searchersValues['search-issuedFrom'].trim() || undefined;
|
const issuedFrom = this.searchersValues['search-issuedFrom'].trim() || undefined;
|
||||||
const via = this.searchersValues['search-via'].trim() || undefined;
|
const via = this.searchersValues['search-via'].trim() || undefined;
|
||||||
const terminatingAt = this.searchersValues['search-terminatingAt'].trim() || undefined;
|
const terminatingAt = this.searchersValues['search-terminatingAt'].trim() || undefined;
|
||||||
const categoryCode = this.searchersValues['select-categoryCode'].trim() || undefined;
|
const categoryCode = this.searchersValues['select-categoryCode'].trim() || undefined;
|
||||||
|
|
||||||
let dateFromISO: string | undefined = undefined;
|
let dateTo: string | undefined = undefined;
|
||||||
let dateToISO: string | undefined = undefined;
|
|
||||||
|
|
||||||
if (dateFromString) {
|
if (dateFrom) {
|
||||||
let dateFrom = new Date(dateFromString);
|
const d = new Date(dateFrom);
|
||||||
dateFrom.setMinutes(dateFrom.getMinutes() + dateFrom.getTimezoneOffset());
|
d.setDate(d.getDate() + 1);
|
||||||
|
|
||||||
let dateTo = new Date(dateFrom);
|
dateTo = d.toISOString().split('T')[0];
|
||||||
dateTo.setDate(dateTo.getDate() + 1);
|
|
||||||
|
|
||||||
dateFromISO = dateFrom.toISOString();
|
|
||||||
dateToISO = dateTo.toISOString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryParams: API.TimetableHistory.QueryParams = {};
|
const queryParams: TimetablesQueryParams = {};
|
||||||
|
|
||||||
this.filterList
|
this.filterList
|
||||||
.filter((f) => f.isActive)
|
.filter((f) => f.isActive)
|
||||||
@@ -381,14 +430,12 @@ export default defineComponent({
|
|||||||
queryParams['countLimit'] = undefined;
|
queryParams['countLimit'] = undefined;
|
||||||
|
|
||||||
queryParams['authorName'] = authorName;
|
queryParams['authorName'] = authorName;
|
||||||
queryParams['dateFrom'] = dateFromISO;
|
queryParams['dateFrom'] = dateFrom;
|
||||||
queryParams['dateTo'] = dateToISO;
|
queryParams['dateTo'] = dateTo;
|
||||||
queryParams['includesScenery'] = includesScenery;
|
|
||||||
queryParams['issuedFrom'] = issuedFrom;
|
queryParams['issuedFrom'] = issuedFrom;
|
||||||
queryParams['terminatingAt'] = terminatingAt;
|
queryParams['terminatingAt'] = terminatingAt;
|
||||||
queryParams['via'] = via;
|
queryParams['via'] = via;
|
||||||
queryParams['categoryCode'] = categoryCode;
|
queryParams['categoryCode'] = categoryCode;
|
||||||
queryParams['returnType'] = 'short';
|
|
||||||
|
|
||||||
queryParams['issuedFrom'] = issuedFrom;
|
queryParams['issuedFrom'] = issuedFrom;
|
||||||
queryParams['sortBy'] =
|
queryParams['sortBy'] =
|
||||||
@@ -400,7 +447,7 @@ export default defineComponent({
|
|||||||
this.currentQueryParams = queryParams;
|
this.currentQueryParams = queryParams;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const responseData: API.TimetableHistory.ResponseShort = await (
|
const responseData: API.TimetableHistory.Response = await (
|
||||||
await this.apiStore.client!.get('api/getTimetables', {
|
await this.apiStore.client!.get('api/getTimetables', {
|
||||||
params: this.currentQueryParams
|
params: this.currentQueryParams
|
||||||
})
|
})
|
||||||
@@ -408,23 +455,26 @@ export default defineComponent({
|
|||||||
|
|
||||||
if (!responseData) {
|
if (!responseData) {
|
||||||
this.dataStatus = Status.Data.Error;
|
this.dataStatus = Status.Data.Error;
|
||||||
this.chosenPlayerId = -1;
|
this.dataErrorMessage = 'Brak danych!';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!responseData) return;
|
||||||
|
|
||||||
// Response data exists
|
// Response data exists
|
||||||
this.timetableHistory = responseData;
|
this.timetableHistory = responseData;
|
||||||
|
|
||||||
this.chosenPlayerId =
|
// Stats display
|
||||||
this.timetableHistory.length > 0 && this.searchersValues['search-driver'].trim() != ''
|
this.mainStore.driverStatsName =
|
||||||
? this.timetableHistory[0].driverId
|
this.timetableHistory.length > 0 && this.searchersValues['search-driver'].trim()
|
||||||
: -1;
|
? this.timetableHistory[0].driverName
|
||||||
|
: '';
|
||||||
|
|
||||||
this.dataStatus = Status.Data.Loaded;
|
this.dataStatus = Status.Data.Loaded;
|
||||||
this.dataRefreshedAt = new Date();
|
this.dataRefreshedAt = new Date();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.dataStatus = Status.Data.Error;
|
this.dataStatus = Status.Data.Error;
|
||||||
this.chosenPlayerId = -1;
|
this.dataErrorMessage = 'Ups! Coś poszło nie tak!';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scrollNoMoreData = false;
|
this.scrollNoMoreData = false;
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
<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,10 +135,6 @@ function setViewMode(componentName: string) {
|
|||||||
&-view {
|
&-view {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
height: 100vh;
|
|
||||||
min-height: 500px;
|
|
||||||
max-height: 2000px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-offline {
|
&-offline {
|
||||||
@@ -185,6 +181,10 @@ function setViewMode(componentName: string) {
|
|||||||
background-color: #181818;
|
background-color: #181818;
|
||||||
border-radius: 0.5em;
|
border-radius: 0.5em;
|
||||||
padding: 1em 0.5em;
|
padding: 1em 0.5em;
|
||||||
|
|
||||||
|
height: calc(100vh - 0.5em);
|
||||||
|
min-height: 500px;
|
||||||
|
max-height: 2000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scenery-left {
|
.scenery-left {
|
||||||
@@ -236,10 +236,6 @@ function setViewMode(componentName: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.midScreen {
|
@include responsive.midScreen {
|
||||||
.scenery-view {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scenery-wrapper {
|
.scenery-wrapper {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
|
|||||||
@@ -29,19 +29,12 @@
|
|||||||
data-tooltip-type="HtmlTooltip"
|
data-tooltip-type="HtmlTooltip"
|
||||||
:data-tooltip-content="`<b>${$t('app.language-tooltip-content')}</b>`"
|
:data-tooltip-content="`<b>${$t('app.language-tooltip-content')}</b>`"
|
||||||
>
|
>
|
||||||
<FlagIcon :language-id="mainStore.currentLocale == 'pl' ? 0 : 1" />
|
<img
|
||||||
|
:src="`/images/icon-${mainStore.currentLocale}.svg`"
|
||||||
|
alt="change language flag icon"
|
||||||
|
/>
|
||||||
</button>
|
</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
|
<a
|
||||||
class="a-button btn--image gnr-link"
|
class="a-button btn--image gnr-link"
|
||||||
href="https://generator-td2.web.app/"
|
href="https://generator-td2.web.app/"
|
||||||
@@ -92,7 +85,6 @@ import { reactive } from 'vue';
|
|||||||
import { provide } from 'vue';
|
import { provide } from 'vue';
|
||||||
import { ActiveSorter } from '../components/StationsView/typings';
|
import { ActiveSorter } from '../components/StationsView/typings';
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import FlagIcon from '../components/Global/FlagIcon.vue';
|
|
||||||
|
|
||||||
const filterInitStates = { ...initFilters };
|
const filterInitStates = { ...initFilters };
|
||||||
|
|
||||||
@@ -101,8 +93,7 @@ export default defineComponent({
|
|||||||
StationTable,
|
StationTable,
|
||||||
StationFilterCard,
|
StationFilterCard,
|
||||||
StationStats,
|
StationStats,
|
||||||
DonationCard,
|
DonationCard
|
||||||
FlagIcon
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
@@ -213,12 +204,11 @@ a.pojazdownik-link {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a.gnr-link,
|
a.gnr-link {
|
||||||
a.discord-link {
|
|
||||||
background-color: #141414;
|
background-color: #141414;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #333;
|
background-color: #222222;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { VitePWA } from 'vite-plugin-pwa';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
server: { port: 5123, open: false },
|
server: { port: 5123, open: true },
|
||||||
preview: { port: 4001, open: false },
|
preview: { port: 4001, open: false },
|
||||||
publicDir: 'public',
|
publicDir: 'public',
|
||||||
css: {
|
css: {
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
scss: { silenceDeprecations: ['legacy-js-api'] }
|
scss: { additionalData: `@use '@/styles/global';`, silenceDeprecations: ['legacy-js-api'] }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
@@ -28,7 +28,7 @@ export default defineConfig({
|
|||||||
runtimeCaching: [
|
runtimeCaching: [
|
||||||
{
|
{
|
||||||
urlPattern:
|
urlPattern:
|
||||||
/^https:\/\/stacjownik.spythere.eu\/api\/(getVehiclesData|getDonators|getSceneries)/i,
|
/^https:\/\/stacjownik.spythere.eu\/api\/(getVehicles|getDonators|getSceneries)/i,
|
||||||
handler: 'NetworkFirst',
|
handler: 'NetworkFirst',
|
||||||
options: {
|
options: {
|
||||||
cacheName: 'stacjownik-api-cache',
|
cacheName: 'stacjownik-api-cache',
|
||||||
|
|||||||