mirror of
https://github.com/Spythere/stacjownik.git
synced 2026-05-03 13:28:11 +00:00
Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f53f3a18fe | |||
| fac8fced3e | |||
| a3d9e68c8a | |||
| b09761de58 | |||
| 8ac2c68660 | |||
| 4177c6e5f4 | |||
| b8f135a454 | |||
| f0863b2459 | |||
| 55b4732992 | |||
| 7b3dcea89e | |||
| f4b0c39185 | |||
| 275d602f97 | |||
| c93514fdf0 | |||
| 0861d92e4b | |||
| bdfd73f4be | |||
| df86364c51 | |||
| 631bb20c61 | |||
| bed79ed2d0 | |||
| 2a07471e12 | |||
| cfe188d0dc | |||
| d9865be83e | |||
| 9155fd9f8d | |||
| 4674bf886e | |||
| 289fd310df | |||
| b04797052f | |||
| 7079f20791 | |||
| c244275aee | |||
| fbc9785341 | |||
| 9fd02c2336 | |||
| c031dd55c1 | |||
| b0870699a4 | |||
| a8da634b0e | |||
| 8920b1e5e8 | |||
| 4fa1c05831 | |||
| ecef2d5ee4 | |||
| 1749871d08 | |||
| 5545616706 | |||
| b35bb03868 | |||
| b9521918cb | |||
| 7ad17fc2c5 | |||
| a80144cb1c | |||
| 1227cdb94a | |||
| b33594fd6f | |||
| 9f8656e590 | |||
| 81cd165fe7 | |||
| 41e4b45599 | |||
| 462dd7dd7a | |||
| 9837ae97e1 | |||
| a818cd980b | |||
| 573ebc233b | |||
| aae47c6abd | |||
| 24c9b62162 | |||
| 481d43b6d8 | |||
| 4969a433cc | |||
| 8a2b453dc6 | |||
| 86d178ef56 | |||
| 7769477508 | |||
| 551b60c733 | |||
| 80a5b56785 | |||
| 6bd62f13a1 | |||
| 42591f6e76 | |||
| 4ca0f09e75 | |||
| 02c3629c00 | |||
| 9c4c806f0e | |||
| 58d6a97762 | |||
| cd71c78eb4 | |||
| 300e70dcfe | |||
| 09b31f7914 | |||
| c29c3c6abe | |||
| 916f6070ac | |||
| a74ab6eb2a | |||
| 985c699ced | |||
| 7e0e9146a5 | |||
| 30a0f05922 | |||
| a30e04ca96 | |||
| 1852d3e234 | |||
| a17bf6c03f | |||
| 766b08bc15 | |||
| cd1a4fa734 | |||
| 619ce97b52 | |||
| acbe761068 | |||
| 47d35f335f | |||
| 8fda8fa0df | |||
| 71d697eda5 | |||
| f2b1fc5369 | |||
| 4a9b142e16 | |||
| 08d8bf3c57 | |||
| 0ee90357aa | |||
| c8964dc20f | |||
| 6a62276d95 | |||
| b8550eed9a | |||
| 27b23ccc95 | |||
| b49517aded | |||
| ed2b8be4dc | |||
| 54c1dbbf15 | |||
| 44f6cf4232 |
@@ -1,6 +1,3 @@
|
||||
# This file was auto-generated by the Firebase CLI
|
||||
# https://github.com/firebase/firebase-tools
|
||||
|
||||
name: Deploy to Firebase Hosting on merge
|
||||
'on':
|
||||
push:
|
||||
@@ -11,10 +8,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: npm ci && npm run build
|
||||
- run: yarn && yarn build
|
||||
- uses: FirebaseExtended/action-hosting-deploy@v0
|
||||
with:
|
||||
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
||||
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STACJOWNIK_TD2 }}'
|
||||
channelId: live
|
||||
projectId: stacjownik-td2
|
||||
projectId: stacjownik-td2
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: npm ci && npm run build
|
||||
- run: yarn && yarn build
|
||||
- uses: FirebaseExtended/action-hosting-deploy@v0
|
||||
with:
|
||||
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#222222" />
|
||||
|
||||
<link rel="icon" href="favicon.ico" />
|
||||
|
||||
|
||||
Generated
+457
-1761
File diff suppressed because it is too large
Load Diff
+13
-16
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"name": "stacjownik",
|
||||
"version": "1.25.2",
|
||||
"version": "1.28.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"deploy": "yarn build && firebase deploy --only hosting",
|
||||
"deploy:prod": "yarn build && firebase deploy --only hosting",
|
||||
"deploy:dev": "yarn build && firebase hosting:channel:deploy dev --expires 7d",
|
||||
"preview": "yarn build && vite preview",
|
||||
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
@@ -19,25 +21,20 @@
|
||||
"showdown": "^2.1.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.4.1",
|
||||
"vue-router": "^4.2.4"
|
||||
"vue-router": "^4.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@types/node": "^20.6.2",
|
||||
"@types/node": "^20.14.12",
|
||||
"@types/showdown": "^2.0.6",
|
||||
"@vite-pwa/assets-generator": "^0.2.4",
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"axios": "^1.5.0",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"prettier": "^3.0.3",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.9",
|
||||
"@vitejs/plugin-vue": "^5.1.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"axios": "^1.7.2",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.3.4",
|
||||
"vite-plugin-pwa": "^0.20.0",
|
||||
"vue-tsc": "^1.8.11"
|
||||
"vue-tsc": "^2.0.28"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffc014",
|
||||
"theme_color": "#4d4d4d",
|
||||
"background_color": "#4d4d4d",
|
||||
"display": "standalone",
|
||||
"start_url": "."
|
||||
|
||||
+5
-24
@@ -6,13 +6,6 @@
|
||||
/>
|
||||
|
||||
<Tooltip />
|
||||
|
||||
<transition name="modal-anim">
|
||||
<keep-alive>
|
||||
<TrainModal />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
|
||||
<AppHeader :current-lang="currentLang" @change-lang="changeLang" />
|
||||
|
||||
<main class="app_main">
|
||||
@@ -54,7 +47,6 @@ 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 TrainModal from './components/TrainsView/TrainModal.vue';
|
||||
import Tooltip from './components/Tooltip/Tooltip.vue';
|
||||
import UpdateCard from './components/App/UpdateCard.vue';
|
||||
|
||||
@@ -67,7 +59,6 @@ export default defineComponent({
|
||||
Clock,
|
||||
StatusIndicator,
|
||||
AppHeader,
|
||||
TrainModal,
|
||||
UpdateCard,
|
||||
Tooltip
|
||||
},
|
||||
@@ -81,9 +72,7 @@ export default defineComponent({
|
||||
isUpdateCardOpen: false,
|
||||
|
||||
currentLang: 'pl',
|
||||
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app',
|
||||
|
||||
nextUpdateTime: 0
|
||||
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app'
|
||||
}),
|
||||
|
||||
created() {
|
||||
@@ -96,22 +85,13 @@ export default defineComponent({
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
if (!this.isOnProductionHost) document.title = 'Stacjownik Dev';
|
||||
|
||||
this.loadLang();
|
||||
this.setupOfflineHandling();
|
||||
this.checkAppVersion();
|
||||
|
||||
this.apiStore.setupAPIData();
|
||||
window.requestAnimationFrame(this.update);
|
||||
|
||||
if (!this.isOnProductionHost) document.title = 'Stacjownik Dev';
|
||||
},
|
||||
|
||||
update(t: number) {
|
||||
if (t >= this.nextUpdateTime) {
|
||||
this.apiStore.fetchActiveData();
|
||||
this.nextUpdateTime = t + 20000;
|
||||
}
|
||||
window.requestAnimationFrame(this.update);
|
||||
},
|
||||
|
||||
async checkAppVersion() {
|
||||
@@ -131,7 +111,7 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
this.isUpdateCardOpen =
|
||||
(storageVersion != '' && storageVersion != version) ||
|
||||
(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}`);
|
||||
@@ -158,6 +138,7 @@ export default defineComponent({
|
||||
|
||||
handleOnlineMode() {
|
||||
this.store.isOffline = false;
|
||||
this.apiStore.dataStatuses.connection = Status.Data.Loading;
|
||||
|
||||
this.apiStore.connectToAPI();
|
||||
},
|
||||
|
||||
@@ -43,7 +43,6 @@ export default defineComponent({
|
||||
|
||||
width: 6em;
|
||||
height: 1em;
|
||||
margin: 0.5em 0;
|
||||
|
||||
.bar-fg,
|
||||
.bar-bg {
|
||||
|
||||
@@ -88,8 +88,9 @@ $unknown: #b93c3c;
|
||||
.status-badge {
|
||||
border-radius: 1em;
|
||||
font-weight: 500;
|
||||
text-wrap: nowrap;
|
||||
|
||||
padding: 0.2em 0.55em;
|
||||
padding: 0.2rem 0.55rem;
|
||||
|
||||
background-color: $online;
|
||||
|
||||
@@ -106,13 +107,13 @@ $unknown: #b93c3c;
|
||||
|
||||
&.no-limit {
|
||||
background-color: $no-limit;
|
||||
font-size: 0.85em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
&.not-signed,
|
||||
&.unavailable {
|
||||
background-color: $unav;
|
||||
font-size: 0.85em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
&.afk {
|
||||
@@ -125,7 +126,7 @@ $unknown: #b93c3c;
|
||||
background-color: $no-space;
|
||||
border: 1px solid white;
|
||||
color: white;
|
||||
font-size: 0.85em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
&.unknown,
|
||||
|
||||
@@ -2,24 +2,22 @@
|
||||
<div class="stock-list">
|
||||
<ul>
|
||||
<li
|
||||
v-for="({ vehicleName, vehicleCargo, images, imagesFallbacks }, i) in thumbnailNames"
|
||||
v-for="(
|
||||
{ vehicleName, vehicleCargo, images, imagesFallbacks, vehicleString }, i
|
||||
) in thumbnailNames"
|
||||
:key="i"
|
||||
>
|
||||
<div class="stock-text">
|
||||
<p>{{ vehicleName.replace(/_/g, ' ') }}</p>
|
||||
<div>{{ vehicleName.replace(/_/g, ' ') }}</div>
|
||||
<small v-if="vehicleCargo">({{ vehicleCargo }})</small>
|
||||
</div>
|
||||
|
||||
<span>
|
||||
<img
|
||||
<VehicleThumbnail
|
||||
v-for="(thumbnailImage, imageIndex) in images"
|
||||
:data-mouseover="vehicleName"
|
||||
data-tooltip-type="VehiclePreviewTooltip"
|
||||
:data-tooltip-content="vehicleName"
|
||||
:src="`https://static.spythere.eu/thumbnails/v2/${thumbnailImage}.png`"
|
||||
@error="onImageError($event, imagesFallbacks[imageIndex])"
|
||||
@click.stop="() => {}"
|
||||
height="60"
|
||||
:vehicle-name="vehicleString"
|
||||
:img-name="thumbnailImage"
|
||||
:fallback-name="imagesFallbacks[imageIndex]"
|
||||
/>
|
||||
</span>
|
||||
</li>
|
||||
@@ -30,8 +28,11 @@
|
||||
<script lang="ts">
|
||||
import { PropType, defineComponent } from 'vue';
|
||||
import { useApiStore } from '../../store/apiStore';
|
||||
import VehicleThumbnail from './VehicleThumbnail.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { VehicleThumbnail },
|
||||
|
||||
props: {
|
||||
trainStockList: {
|
||||
type: Array as PropType<string[]>,
|
||||
@@ -64,7 +65,8 @@ export default defineComponent({
|
||||
images: [] as string[],
|
||||
imagesFallbacks: [] as string[],
|
||||
vehicleName,
|
||||
vehicleCargo
|
||||
vehicleCargo,
|
||||
vehicleString
|
||||
};
|
||||
|
||||
// Generowanie członów EN57
|
||||
@@ -143,7 +145,7 @@ export default defineComponent({
|
||||
else {
|
||||
let fallbackVehicleImage = 'unknown_cargo';
|
||||
|
||||
if (/^(EP|EU)/.test(vehicleName)) fallbackVehicleImage = 'unknown_train';
|
||||
if (/^(EP|EU|ET|201E)/.test(vehicleName)) fallbackVehicleImage = 'unknown_train';
|
||||
else if (/^(SM42)/.test(vehicleName)) fallbackVehicleImage = 'unknown_SM42';
|
||||
else if (/(\d{3}a|(Bau|Gor)\d{2}|304C)_/.test(vehicleName))
|
||||
fallbackVehicleImage = 'unknown_passenger';
|
||||
@@ -160,7 +162,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
methods: {
|
||||
onImageError(event: Event, fallbackImage: string) {
|
||||
onImageError(event: Event, fallbackImage: string) {
|
||||
(event.target as HTMLImageElement).src = `/images/${fallbackImage}.png`;
|
||||
}
|
||||
}
|
||||
@@ -178,7 +180,6 @@ export default defineComponent({
|
||||
align-items: flex-end;
|
||||
overflow: auto;
|
||||
margin: 0 auto;
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
ul > li > span {
|
||||
@@ -187,20 +188,11 @@ ul > li > span {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 60px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
img.traction-only {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.stock-text {
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 0.25em;
|
||||
padding: 0.25em 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="vehicle-thumbnail" :data-load-status="imgStatus">
|
||||
<img
|
||||
ref="imgRef"
|
||||
:src="`https://static.spythere.eu/thumbnails/v2/${imgName}.png`"
|
||||
height="60"
|
||||
loading="lazy"
|
||||
data-tooltip-type="VehiclePreviewTooltip"
|
||||
:data-tooltip-content="vehicleName"
|
||||
@error="onImageError"
|
||||
@load="onImageLoad"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Ref, ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
vehicleName: { type: String, required: true },
|
||||
imgName: { type: String, required: true },
|
||||
fallbackName: { type: String, required: true },
|
||||
placeholderName: String
|
||||
});
|
||||
|
||||
const imgRef = ref(null) as Ref<HTMLElement | null>;
|
||||
|
||||
const imgStatus = ref('loading');
|
||||
|
||||
function onImageError(event: Event) {
|
||||
(event.target as HTMLImageElement).src = `/images/${props.fallbackName}.png`;
|
||||
imgStatus.value = 'error';
|
||||
}
|
||||
|
||||
function onImageLoad() {
|
||||
if (imgStatus.value != 'error') {
|
||||
imgStatus.value = 'loaded';
|
||||
}
|
||||
|
||||
if (imgRef.value) imgRef.value.style.opacity = '1';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vehicle-thumbnail {
|
||||
position: relative;
|
||||
|
||||
&[data-load-status='loading'] {
|
||||
min-height: 60px;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
opacity: 0;
|
||||
transition: opacity 100ms ease-in-out;
|
||||
}
|
||||
</style>
|
||||
@@ -1,29 +1,28 @@
|
||||
<template>
|
||||
<section class="daily-stats">
|
||||
<span :data-active="statsStatus">
|
||||
<span class="stats-list">
|
||||
<h3>
|
||||
{{ $t('journal.daily-stats.title') }}
|
||||
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
|
||||
</h3>
|
||||
<h3>
|
||||
{{ $t('journal.daily-stats.title') }}
|
||||
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
|
||||
</h3>
|
||||
|
||||
<hr class="header-separator" />
|
||||
<hr class="header-separator" />
|
||||
|
||||
<b v-if="statsStatus == Status.Data.Loading">
|
||||
{{ $t('app.loading') }}
|
||||
</b>
|
||||
<b v-if="statsStatus == Status.Data.Loading">
|
||||
{{ $t('app.loading') }}
|
||||
</b>
|
||||
|
||||
<b class="text--error" v-else-if="statsStatus == Status.Data.Error">
|
||||
{{ $t('journal.stats-error') }}
|
||||
</b>
|
||||
<b class="text--error" v-else-if="statsStatus == Status.Data.Error">
|
||||
{{ $t('journal.stats-error') }}
|
||||
</b>
|
||||
|
||||
<b v-else-if="topDispatchers.length == 0">
|
||||
{{ $t('journal.daily-stats.info') }}
|
||||
</b>
|
||||
<b v-else-if="topDispatchers.length == 0">
|
||||
{{ $t('journal.daily-stats.info') }}
|
||||
</b>
|
||||
|
||||
<div v-else>
|
||||
<div v-if="stats.totalTimetables">
|
||||
•
|
||||
<div v-else>
|
||||
<ul class="stats-list">
|
||||
<li v-if="stats.totalTimetables">
|
||||
<i18n-t keypath="journal.daily-stats.total">
|
||||
<template #count>
|
||||
<b class="text--primary">
|
||||
@@ -36,10 +35,9 @@
|
||||
<b class="text--primary"> {{ stats.distanceSum?.toFixed(2) }} km</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<div v-if="stats.maxTimetable">
|
||||
•
|
||||
<li v-if="stats.maxTimetable">
|
||||
<i18n-t keypath="journal.daily-stats.longest">
|
||||
<template #id>
|
||||
<router-link :to="`/journal/timetables?search-train=%23${stats.maxTimetable.id}`">
|
||||
@@ -60,10 +58,9 @@
|
||||
<b class="text--primary">{{ stats.maxTimetable.routeDistance }} km</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<div v-if="topDispatchers.length == 1">
|
||||
•
|
||||
<li v-if="topDispatchers.length == 1">
|
||||
<i18n-t keypath="journal.daily-stats.most-active-dr">
|
||||
<template #dispatcher>
|
||||
<router-link
|
||||
@@ -79,10 +76,9 @@
|
||||
</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<div v-if="topDispatchers.length > 1">
|
||||
•
|
||||
<li v-if="topDispatchers.length > 1">
|
||||
<i18n-t keypath="journal.daily-stats.most-active-dr-many">
|
||||
<template #dispatchers>
|
||||
<span v-for="(disp, i) in topDispatchers" :key="i">
|
||||
@@ -103,10 +99,9 @@
|
||||
</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<div v-if="stats.longestDuties.length > 0">
|
||||
•
|
||||
<li v-if="stats.longestDuties.length > 0">
|
||||
<i18n-t keypath="journal.daily-stats.longest-duties">
|
||||
<template #dispatcher>
|
||||
<router-link
|
||||
@@ -122,10 +117,9 @@
|
||||
{{ calculateDuration(stats.longestDuties[0].duration) }}
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<div v-if="stats.mostActiveDrivers.length > 0">
|
||||
•
|
||||
<li v-if="stats.mostActiveDrivers.length > 0">
|
||||
<i18n-t keypath="journal.daily-stats.most-active-driver">
|
||||
<template #driver>
|
||||
<router-link
|
||||
@@ -138,30 +132,30 @@
|
||||
<b class="text--primary">{{ stats.mostActiveDrivers[0].distance.toFixed(2) }} km</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<hr class="section-separator" />
|
||||
<hr class="section-separator" />
|
||||
|
||||
<div class="stats-badges">
|
||||
<span
|
||||
class="stat-badge"
|
||||
v-for="key in [
|
||||
'rippedSwitches',
|
||||
'derailments',
|
||||
'skippedStopSignals',
|
||||
'radioStops',
|
||||
'kills'
|
||||
]"
|
||||
:key="key"
|
||||
>
|
||||
<span>{{ $t(`journal.daily-stats.${key}`) }}</span>
|
||||
<span>{{
|
||||
Object.entries(stats.globalDiff).find(([k, v]) => k == key)?.[1] || '--'
|
||||
}}</span>
|
||||
<div class="stats-badges">
|
||||
<span
|
||||
class="badge stat-badge"
|
||||
v-for="key in [
|
||||
'rippedSwitches',
|
||||
'derailments',
|
||||
'skippedStopSignals',
|
||||
'radioStops',
|
||||
'kills'
|
||||
]"
|
||||
:key="key"
|
||||
>
|
||||
<span>{{ $t(`journal.daily-stats.${key}`) }}</span>
|
||||
<span>
|
||||
{{ Object.entries(stats.globalDiff).find(([k, v]) => k == key)?.[1] || '--' }}
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</section>
|
||||
</template>
|
||||
@@ -178,7 +172,6 @@ export default defineComponent({
|
||||
name: 'journal-daily-stats',
|
||||
|
||||
mixins: [dateMixin],
|
||||
// emits: ['toggleStatsOpen'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
@@ -193,7 +186,6 @@ export default defineComponent({
|
||||
|
||||
activated() {
|
||||
this.startFetchingDailyStats();
|
||||
// this.$emit('toggleStatsOpen', true);
|
||||
},
|
||||
|
||||
deactivated() {
|
||||
@@ -249,14 +241,24 @@ export default defineComponent({
|
||||
.daily-stats {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.daily-stats > span[data-active='0'] {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
ul.stats-list {
|
||||
list-style: disc;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
.stats-list a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.stats-list > li {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.stats-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<li class="dispatcher-history-entry">
|
||||
<div class="entry-info">
|
||||
<span>
|
||||
<span>
|
||||
<router-link :to="`/journal/dispatchers?search-station=${entry.stationName}`">
|
||||
<b>{{ entry.stationName }}</b>
|
||||
</router-link>
|
||||
|
||||
<b class="text--grayed"> #{{ entry.stationHash }}</b>
|
||||
</span>
|
||||
•
|
||||
<b
|
||||
v-if="entry.dispatcherLevel !== null"
|
||||
class="level-badge dispatcher"
|
||||
:style="calculateExpStyle(entry.dispatcherLevel, entry.dispatcherIsSupporter)"
|
||||
>
|
||||
{{ entry.dispatcherLevel >= 2 ? entry.dispatcherLevel : 'L' }}
|
||||
</b>
|
||||
<b style="margin-left: 5px">
|
||||
<span
|
||||
v-if="apiStore.donatorsData.includes(entry.dispatcherName)"
|
||||
data-tooltip-type="DonatorTooltip"
|
||||
:data-tooltip-content="$t('donations.dispatcher-message')"
|
||||
>
|
||||
<router-link
|
||||
class="text--donator"
|
||||
:to="`/journal/dispatchers?search-dispatcher=${entry.dispatcherName}`"
|
||||
>
|
||||
{{ entry.dispatcherName }}
|
||||
</router-link>
|
||||
</span>
|
||||
|
||||
<router-link
|
||||
v-else
|
||||
:to="`/journal/dispatchers?search-dispatcher=${entry.dispatcherName}`"
|
||||
>
|
||||
{{ entry.dispatcherName }}
|
||||
</router-link>
|
||||
</b>
|
||||
|
||||
<div>
|
||||
<span v-if="entry.timestampTo">
|
||||
<b>{{ $d(entry.timestampFrom) }}</b>
|
||||
{{ timestampToString(entry.timestampFrom) }}
|
||||
-
|
||||
<b
|
||||
v-if="
|
||||
new Date(entry.timestampFrom).getDate() != new Date(entry.timestampTo).getDate()
|
||||
"
|
||||
>
|
||||
{{ $d(entry.timestampTo) }}
|
||||
</b>
|
||||
{{ timestampToString(entry.timestampTo) }} ({{
|
||||
calculateDuration(entry.currentDuration)
|
||||
}})
|
||||
</span>
|
||||
|
||||
<router-link
|
||||
:to="`/scenery?station=${entry.stationName}`"
|
||||
class="dispatcher-online"
|
||||
v-else
|
||||
>
|
||||
{{ $t('journal.online-since') }}
|
||||
<b>
|
||||
{{
|
||||
new Date().getDate() != new Date(entry.timestampFrom).getDate()
|
||||
? $d(entry.timestampFrom)
|
||||
: ''
|
||||
}}
|
||||
{{ timestampToString(entry.timestampFrom) }}
|
||||
</b>
|
||||
({{ calculateDuration(entry.currentDuration) }})
|
||||
</router-link>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<span class="entry-info-right">
|
||||
<div>
|
||||
<span>
|
||||
{{ $t('scenery.dispatcher-rate') }}
|
||||
<b class="text--primary"> {{ entry.dispatcherRate }}</b>
|
||||
</span>
|
||||
<button class="btn btn--option" @click="toggleExtraInfo">
|
||||
{{ $t('scenery.dispatcher-status-changes') }}
|
||||
<b class="text--primary">{{ entry.statusHistory.length }}</b>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<b class="region-badge" :aria-describedby="entry.region">
|
||||
REGION: {{ regions.find((r) => r.id == entry.region)?.name }}
|
||||
</b>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="entry-extra" v-if="showExtraInfo">
|
||||
<ul class="status-list">
|
||||
<li v-for="statusItem in entry.statusHistory">
|
||||
<b style="margin-right: 0.5em">{{
|
||||
timestampToString(parseInt(statusItem.split('@')[0]))
|
||||
}}</b>
|
||||
|
||||
<StationStatusBadge
|
||||
:dispatcher-status="Number(statusItem.split('@')[1])"
|
||||
:is-online="true"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import { regions } from '../../../data/options.json';
|
||||
import { API } from '../../../typings/api';
|
||||
import dateMixin from '../../../mixins/dateMixin';
|
||||
import styleMixin from '../../../mixins/styleMixin';
|
||||
import { useApiStore } from '../../../store/apiStore';
|
||||
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
entry: {
|
||||
type: Object as PropType<API.DispatcherHistory.Data>,
|
||||
required: true
|
||||
},
|
||||
showExtraInfo: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
components: { StationStatusBadge },
|
||||
mixins: [dateMixin, styleMixin],
|
||||
emits: ['toggleShowExtraInfo'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
regions,
|
||||
apiStore: useApiStore()
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleExtraInfo() {
|
||||
this.$emit('toggleShowExtraInfo', this.entry.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../../styles/responsive.scss';
|
||||
@import '../../../styles/badge.scss';
|
||||
|
||||
.region-badge {
|
||||
padding: 0 0.25em;
|
||||
}
|
||||
|
||||
.level-badge {
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
.dispatcher-online {
|
||||
color: springgreen;
|
||||
}
|
||||
|
||||
.dispatcher-history-entry {
|
||||
background-color: #1a1a1a;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.entry-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
line-height: 1.75em;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.entry-info-right {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.entry-extra {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.status-list {
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.status-list > li {
|
||||
background-color: #313131;
|
||||
padding: 0.2rem 0 0.2rem 0.5em;
|
||||
margin: 0.5em 0;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.entry-info {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -16,17 +16,17 @@
|
||||
<hr class="header-separator" />
|
||||
|
||||
<div class="info-stats">
|
||||
<span class="stat-badge" v-if="stats.services">
|
||||
<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="stat-badge" v-if="stats.services">
|
||||
<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="stat-badge" v-if="stats.services">
|
||||
<span class="badge stat-badge" v-if="stats.services">
|
||||
<span>{{ $t('journal.dispatcher-stats.service-avg') }}</span>
|
||||
<span>{{ calculateDuration(stats.services.durationAvg) }}</span>
|
||||
</span>
|
||||
@@ -35,22 +35,22 @@
|
||||
<hr class="section-separator" />
|
||||
|
||||
<div class="info-stats">
|
||||
<span class="stat-badge" v-if="stats.issuedTimetables">
|
||||
<span class="badge stat-badge" v-if="stats.issuedTimetables">
|
||||
<span>{{ $t('journal.dispatcher-stats.timetables-count') }}</span>
|
||||
<span>{{ stats.issuedTimetables.count }}</span>
|
||||
</span>
|
||||
|
||||
<span class="stat-badge" v-if="stats.issuedTimetables">
|
||||
<span class="badge stat-badge" v-if="stats.issuedTimetables">
|
||||
<span>{{ $t('journal.dispatcher-stats.timetables-sum') }}</span>
|
||||
<span>{{ stats.issuedTimetables.distanceSum.toFixed(2) }}km</span>
|
||||
</span>
|
||||
|
||||
<span class="stat-badge" v-if="stats.issuedTimetables">
|
||||
<span class="badge stat-badge" v-if="stats.issuedTimetables">
|
||||
<span>{{ $t('journal.dispatcher-stats.timetables-max') }}</span>
|
||||
<span>{{ stats.issuedTimetables.distanceMax.toFixed(2) }}km</span>
|
||||
</span>
|
||||
|
||||
<span class="stat-badge" v-if="stats.issuedTimetables">
|
||||
<span class="badge stat-badge" v-if="stats.issuedTimetables">
|
||||
<span>{{ $t('journal.dispatcher-stats.timetables-avg') }}</span>
|
||||
<span>{{ stats.issuedTimetables.distanceAvg.toFixed(2) }}km</span>
|
||||
</span>
|
||||
|
||||
@@ -1,140 +1,57 @@
|
||||
<template>
|
||||
<transition name="status-anim" mode="out-in">
|
||||
<div :key="dataStatus">
|
||||
<div class="journal_warning" v-if="store.isOffline">
|
||||
{{ $t('app.offline') }}
|
||||
</div>
|
||||
<div class="journal_warning" v-if="store.isOffline">
|
||||
{{ $t('app.offline') }}
|
||||
</div>
|
||||
|
||||
<Loading v-else-if="dataStatus == Status.Data.Loading" />
|
||||
<Loading v-else-if="dataStatus == Status.Data.Loading" />
|
||||
|
||||
<div v-else-if="dataStatus == Status.Data.Error" class="journal_warning error">
|
||||
{{ $t('app.error') }}
|
||||
</div>
|
||||
<div v-else-if="dataStatus == Status.Data.Error" class="journal_warning error">
|
||||
{{ $t('app.error') }}
|
||||
</div>
|
||||
|
||||
<div class="journal_warning" v-else-if="dispatcherHistory.length == 0">
|
||||
{{ $t('app.no-result') }}
|
||||
</div>
|
||||
<div class="journal_warning" v-else-if="dispatcherHistory.length == 0">
|
||||
{{ $t('app.no-result') }}
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<table class="dispatchers-table">
|
||||
<thead>
|
||||
<th>{{ $t('journal.history-name') }}</th>
|
||||
<th>{{ $t('journal.history-hash') }}</th>
|
||||
<th>{{ $t('journal.history-dispatcher') }}</th>
|
||||
<th>{{ $t('journal.history-level') }}</th>
|
||||
<th>{{ $t('journal.history-rate') }}</th>
|
||||
<th>{{ $t('journal.history-region') }}</th>
|
||||
<th>{{ $t('journal.history-date') }}</th>
|
||||
</thead>
|
||||
<div v-else>
|
||||
<transition-group name="list-anim" class="journal-list" tag="ul">
|
||||
<JournalDispatcherEntry
|
||||
v-for="entry in dispatcherHistory"
|
||||
:key="entry.id"
|
||||
:entry="entry"
|
||||
:onToggleShowExtraInfo="toggleExtraInfo"
|
||||
:showExtraInfo="extraInfoIndexes.includes(entry.id)"
|
||||
/>
|
||||
</transition-group>
|
||||
|
||||
<tbody>
|
||||
<transition-group name="list-anim">
|
||||
<tr v-for="historyItem in dispatcherHistory" :key="historyItem.id">
|
||||
<td>
|
||||
<router-link
|
||||
:to="`/journal/dispatchers?search-station=${historyItem.stationName}`"
|
||||
>
|
||||
<b>{{ historyItem.stationName }}</b>
|
||||
</router-link>
|
||||
</td>
|
||||
<td>#{{ historyItem.stationHash }}</td>
|
||||
<td>
|
||||
<router-link
|
||||
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
|
||||
>
|
||||
<b
|
||||
v-if="apiStore.donatorsData.includes(historyItem.dispatcherName)"
|
||||
class="text--donator"
|
||||
:title="$t('donations.dispatcher-message')"
|
||||
>
|
||||
{{ historyItem.dispatcherName }}
|
||||
</b>
|
||||
<AddDataButton
|
||||
:list="dispatcherHistory"
|
||||
:scrollDataLoaded="scrollDataLoaded"
|
||||
:scrollNoMoreData="scrollNoMoreData"
|
||||
@addHistoryData="addHistoryData"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<b v-else>
|
||||
{{ historyItem.dispatcherName }}
|
||||
</b>
|
||||
</router-link>
|
||||
</td>
|
||||
<td>
|
||||
<b
|
||||
v-if="historyItem.dispatcherLevel !== null"
|
||||
class="level-badge dispatcher"
|
||||
:style="
|
||||
calculateExpStyle(
|
||||
historyItem.dispatcherLevel,
|
||||
historyItem.dispatcherIsSupporter
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
|
||||
</b>
|
||||
</td>
|
||||
<td class="text--primary">
|
||||
<b>{{ historyItem.dispatcherRate }}</b>
|
||||
</td>
|
||||
<td>
|
||||
<b class="region-badge" :aria-describedby="historyItem.region">{{
|
||||
regions.find((r) => r.id == historyItem.region)?.value || '???'
|
||||
}}</b>
|
||||
</td>
|
||||
<td style="min-width: 200px" class="time">
|
||||
<span v-if="historyItem.timestampTo" class="text--offline">
|
||||
<b>{{ $d(historyItem.timestampFrom) }}</b>
|
||||
{{ timestampToString(historyItem.timestampFrom) }}
|
||||
- {{ timestampToString(historyItem.timestampTo) }} ({{
|
||||
calculateDuration(historyItem.currentDuration)
|
||||
}})
|
||||
</span>
|
||||
<span class="dispatcher-online" v-else>
|
||||
<b class="text--online">
|
||||
<router-link :to="`/scenery?station=${historyItem.stationName}`">{{
|
||||
$t('journal.online-since')
|
||||
}}</router-link>
|
||||
{{ timestampToString(historyItem.timestampFrom) }}
|
||||
</b>
|
||||
({{ calculateDuration(historyItem.currentDuration) }})
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</transition-group>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="journal_warning" v-if="scrollNoMoreData">
|
||||
{{ $t('journal.no-further-data') }}
|
||||
</div>
|
||||
|
||||
<AddDataButton
|
||||
:list="dispatcherHistory"
|
||||
:scrollDataLoaded="scrollDataLoaded"
|
||||
:scrollNoMoreData="scrollNoMoreData"
|
||||
@addHistoryData="addHistoryData"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="journal_warning" v-if="scrollNoMoreData">
|
||||
{{ $t('journal.no-further-data') }}
|
||||
</div>
|
||||
|
||||
<div class="journal_warning" v-else-if="!scrollDataLoaded">
|
||||
{{ $t('journal.loading-further-data') }}
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<div class="journal_warning" v-else-if="!scrollDataLoaded">
|
||||
{{ $t('journal.loading-further-data') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import { regions } from '../../../data/options.json';
|
||||
import { useMainStore } from '../../../store/mainStore';
|
||||
import { API } from '../../../typings/api';
|
||||
import { Status } from '../../../typings/common';
|
||||
import Loading from '../../Global/Loading.vue';
|
||||
import AddDataButton from '../../Global/AddDataButton.vue';
|
||||
import dateMixin from '../../../mixins/dateMixin';
|
||||
import styleMixin from '../../../mixins/styleMixin';
|
||||
import { useApiStore } from '../../../store/apiStore';
|
||||
import JournalDispatcherEntry from './JournalDispatcherEntry.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { Loading, AddDataButton },
|
||||
|
||||
mixins: [dateMixin, styleMixin],
|
||||
components: { Loading, AddDataButton, JournalDispatcherEntry },
|
||||
|
||||
props: {
|
||||
dispatcherHistory: {
|
||||
@@ -159,99 +76,23 @@ export default defineComponent({
|
||||
return {
|
||||
Status,
|
||||
store: useMainStore(),
|
||||
apiStore: useApiStore(),
|
||||
regions
|
||||
|
||||
extraInfoIndexes: [] as number[]
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
computedDispatcherHistory() {
|
||||
return this.dispatcherHistory.reduce(
|
||||
(acc, historyItem, i) => {
|
||||
if (this.isAnotherDay(i - 1, i))
|
||||
acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL'));
|
||||
acc.push(historyItem);
|
||||
|
||||
return acc;
|
||||
},
|
||||
[] as (API.DispatcherHistory.Data | string)[]
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
navigateToScenery(name: string, isOnline: boolean) {
|
||||
if (!isOnline) return;
|
||||
toggleExtraInfo(id: number) {
|
||||
const existingIdx = this.extraInfoIndexes.indexOf(id);
|
||||
|
||||
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`);
|
||||
},
|
||||
|
||||
isAnotherDay(prevIndex: number, currIndex: number) {
|
||||
if (currIndex == 0) return true;
|
||||
|
||||
return (
|
||||
new Date(this.dispatcherHistory[prevIndex].timestampFrom).getDate() !=
|
||||
new Date(this.dispatcherHistory[currIndex].timestampFrom).getDate()
|
||||
);
|
||||
if (existingIdx != -1) this.extraInfoIndexes.splice(existingIdx, 1);
|
||||
else this.extraInfoIndexes.push(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../../styles/animations.scss';
|
||||
@import '../../../styles/responsive.scss';
|
||||
@import '../../../styles/badge.scss';
|
||||
@import '../../../styles/variables.scss';
|
||||
@import '../../../styles/JournalSection.scss';
|
||||
|
||||
table.dispatchers-table {
|
||||
--_bg-table: #111;
|
||||
--_bg-head: #101010;
|
||||
--_bg-row: #2f2f2f;
|
||||
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
margin-bottom: 1em;
|
||||
|
||||
thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--_bg-head);
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
tr {
|
||||
background-color: var(--_bg-row);
|
||||
border-bottom: 2px solid black;
|
||||
|
||||
&:last-child {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.75em;
|
||||
|
||||
.level-badge {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
&--online {
|
||||
color: springgreen;
|
||||
}
|
||||
|
||||
&--offline {
|
||||
color: #ddd;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -30,7 +30,11 @@
|
||||
</div>
|
||||
|
||||
<transition name="dropdown-anim">
|
||||
<div class="dropdown_wrapper" v-if="currentStatsTab !== null">
|
||||
<div
|
||||
class="dropdown_wrapper"
|
||||
:class="{ 'dropdown-align-right': true }"
|
||||
v-if="currentStatsTab !== null"
|
||||
>
|
||||
<keep-alive>
|
||||
<component :is="currentStatsTab" :key="currentStatsTab"></component>
|
||||
</keep-alive>
|
||||
@@ -79,7 +83,10 @@ export default defineComponent({
|
||||
@import '../../styles/dropdown_filters.scss';
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
.dropdown_wrapper {
|
||||
max-width: 100%;
|
||||
.dropdown_wrapper.dropdown-align-right {
|
||||
left: auto;
|
||||
right: 0;
|
||||
max-width: 700px;
|
||||
// max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
+98
-45
@@ -1,29 +1,40 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="details-actions">
|
||||
<button class="btn--action">
|
||||
<b>{{ $t('journal.stock-info') }}</b>
|
||||
<button class="btn--action" @click="toggleExtraInfo">
|
||||
<b>{{ $t('journal.entry-details') }}</b>
|
||||
<img :src="`/images/icon-arrow-${showExtraInfo ? 'asc' : 'desc'}.svg`" alt="Arrow icon" />
|
||||
</button>
|
||||
|
||||
<router-link
|
||||
v-if="driverRouteLocation !== null"
|
||||
class="a-button btn--action btn-timetable"
|
||||
:to="driverRouteLocation"
|
||||
>
|
||||
<img src="/images/icon-train.svg" alt="train icon" />
|
||||
<b>{{ $t('journal.timetable-online-button') }}</b>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="details-body" v-if="timetable.stockString && timetable.stockMass && showExtraInfo">
|
||||
<hr />
|
||||
<div class="details-body" v-if="showExtraInfo">
|
||||
<div class="g-separator"></div>
|
||||
|
||||
<EntryStops :timetable="timetable" />
|
||||
|
||||
<div class="g-separator"></div>
|
||||
|
||||
<div class="stock-specs">
|
||||
<span class="badge">
|
||||
<span class="badge" v-if="timetable.authorName">
|
||||
<span>{{ $t('journal.dispatcher-name') }}</span>
|
||||
<span>{{ timetable.authorName }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="stock-specs">
|
||||
<span class="badge">
|
||||
<span class="badge" v-if="timetable.maxSpeed">
|
||||
<span>{{ $t('journal.stock-max-speed') }}</span>
|
||||
<span>{{ timetable.maxSpeed }}km/h</span>
|
||||
</span>
|
||||
|
||||
<span class="badge">
|
||||
<span class="badge" v-if="timetable.stockLength">
|
||||
<span>{{ $t('journal.stock-length') }}</span>
|
||||
<span>
|
||||
{{
|
||||
@@ -34,13 +45,13 @@
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="badge">
|
||||
<span class="badge" v-if="timetable.stockMass">
|
||||
<span>{{ $t('journal.stock-mass') }}</span>
|
||||
<span>
|
||||
{{
|
||||
Math.floor(
|
||||
(currentHistoryIndex == 0
|
||||
? timetable.stockMass!
|
||||
? timetable.stockMass
|
||||
: stockHistory[currentHistoryIndex].stockMass || timetable.stockMass) / 1000
|
||||
)
|
||||
}}t
|
||||
@@ -48,27 +59,56 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Historia zmian w składzie -->
|
||||
<div class="stock-history" v-if="stockHistory.length > 1">
|
||||
<button
|
||||
v-for="(sh, i) in stockHistory"
|
||||
:key="i"
|
||||
class="btn--action"
|
||||
:data-checked="i == currentHistoryIndex"
|
||||
@click.stop="currentHistoryIndex = i"
|
||||
>
|
||||
{{ sh.updatedAt }}
|
||||
</button>
|
||||
<div class="stock-dangers" v-if="timetable.twr || timetable.skr">
|
||||
<div class="g-separator"></div>
|
||||
|
||||
<b>{{ $t('journal.stock-dangers') }}:</b>
|
||||
|
||||
<ul>
|
||||
<li v-if="timetable.twr">
|
||||
<b class="text--primary">{{ $t('general.TWR') }} (TWR)</b>
|
||||
<span v-if="timetable.warningNotes">
|
||||
| <i>{{ timetable.warningNotes }}</i>
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li v-if="timetable.skr">
|
||||
<b class="text--primary">{{ $t('general.SKR') }}</b>
|
||||
<span v-if="timetable.warningNotes">
|
||||
| Komentarze: <i>{{ timetable.warningNotes }}</i>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<StockList
|
||||
:trainStockList="
|
||||
(currentHistoryIndex == 0
|
||||
? timetable.stockString
|
||||
: stockHistory[currentHistoryIndex].stockString
|
||||
).split(';')
|
||||
"
|
||||
/>
|
||||
<!-- 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-history" v-if="stockHistory.length > 1">
|
||||
<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="timetable.stockString" style="margin-top: 1em">
|
||||
<StockList
|
||||
:trainStockList="
|
||||
(currentHistoryIndex == 0
|
||||
? timetable.stockString
|
||||
: stockHistory[currentHistoryIndex].stockString
|
||||
).split(';')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -77,9 +117,14 @@
|
||||
import { PropType, defineComponent } from 'vue';
|
||||
import StockList from '../../Global/StockList.vue';
|
||||
import { API } from '../../../typings/api';
|
||||
import { RouteLocationRaw } from 'vue-router';
|
||||
import EntryStops from './EntryStops.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { StockList },
|
||||
components: { StockList, EntryStops },
|
||||
|
||||
emits: ['toggleExtraInfo'],
|
||||
|
||||
props: {
|
||||
showExtraInfo: {
|
||||
type: Boolean,
|
||||
@@ -112,12 +157,25 @@ export default defineComponent({
|
||||
stockLength: Number(historyData[3]) || undefined
|
||||
};
|
||||
});
|
||||
},
|
||||
driverRouteLocation(): RouteLocationRaw | null {
|
||||
if (this.timetable.terminated) return null;
|
||||
return {
|
||||
name: 'DriverView',
|
||||
query: {
|
||||
trainId: `${this.timetable.driverId}|${this.timetable.trainNo}|eu`
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onImageError(e: Event) {
|
||||
const imageEl = e.target as HTMLImageElement;
|
||||
imageEl.src = '/images/icon-unknown.png';
|
||||
},
|
||||
|
||||
toggleExtraInfo() {
|
||||
this.$emit('toggleExtraInfo', this.timetable.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -134,6 +192,8 @@ export default defineComponent({
|
||||
|
||||
.details-actions {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
margin-top: 1em;
|
||||
|
||||
button img {
|
||||
height: 1.25em;
|
||||
@@ -155,7 +215,6 @@ export default defineComponent({
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
|
||||
.badge {
|
||||
margin: 0;
|
||||
@@ -167,20 +226,14 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
ul.stock-list {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
overflow: auto;
|
||||
hr {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
padding-bottom: 0.5em;
|
||||
|
||||
li > div {
|
||||
margin: 1em 0;
|
||||
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.stock-dangers ul {
|
||||
list-style: disc;
|
||||
padding-left: 1em;
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
@include smallScreen() {
|
||||
+34
-31
@@ -3,13 +3,31 @@
|
||||
<span class="general-train">
|
||||
<span class="text--grayed">#{{ timetable.id }}</span>
|
||||
|
||||
<span class="badges" v-if="timetable.skr || timetable.twr">
|
||||
<span class="train-badge twr" v-if="timetable.twr" :title="$t('general.TWR')">TWR</span>
|
||||
<span class="train-badge skr" v-if="timetable.skr" :title="$t('general.SKR')">SKR</span>
|
||||
<span
|
||||
class="train-badge twr"
|
||||
v-if="timetable.twr"
|
||||
data-tooltip-type="BaseTooltip"
|
||||
:data-tooltip-content="
|
||||
$t('general.TWR') + `${timetable.warningNotes ? ':\n' + timetable.warningNotes : ''}`
|
||||
"
|
||||
>
|
||||
TWR
|
||||
</span>
|
||||
<span
|
||||
class="train-badge skr"
|
||||
v-if="timetable.skr"
|
||||
data-tooltip-type="BaseTooltip"
|
||||
:data-tooltip-content="$t('general.SKR')"
|
||||
>
|
||||
SKR
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<strong class="text--primary">
|
||||
<strong
|
||||
data-tooltip-type="BaseTooltip"
|
||||
:data-tooltip-content="getCategoryExplanation(timetable.trainCategoryCode)"
|
||||
class="text--primary tooltip-help"
|
||||
>
|
||||
{{ timetable.trainCategoryCode }}
|
||||
</strong>
|
||||
<strong> {{ timetable.trainNo }}</strong>
|
||||
@@ -23,17 +41,19 @@
|
||||
{{ timetable.driverLevel < 2 ? 'L' : `${timetable.driverLevel}` }}
|
||||
</strong>
|
||||
|
||||
<strong
|
||||
<router-link
|
||||
v-if="apiStore.donatorsData.includes(timetable.driverName)"
|
||||
class="text--donator"
|
||||
:title="$t('donations.driver-message')"
|
||||
data-tooltip-type="DonatorTooltip"
|
||||
:data-tooltip-content="$t('donations.driver-message')"
|
||||
:to="`/journal/timetables?search-driver=${timetable.driverName}`"
|
||||
>
|
||||
{{ timetable.driverName }}
|
||||
</strong>
|
||||
<strong>{{ timetable.driverName }}</strong>
|
||||
</router-link>
|
||||
|
||||
<strong v-else>
|
||||
{{ timetable.driverName }}
|
||||
</strong>
|
||||
<router-link v-else :to="`/journal/timetables?search-driver=${timetable.driverName}`">
|
||||
<strong>{{ timetable.driverName }}</strong>
|
||||
</router-link>
|
||||
</span>
|
||||
|
||||
<span class="general-time">
|
||||
@@ -61,15 +81,6 @@
|
||||
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
|
||||
}}
|
||||
</b>
|
||||
|
||||
<button
|
||||
v-if="timetable.terminated == false"
|
||||
class="btn--action btn-timetable"
|
||||
@click.stop="showTimetable(timetable, $event.currentTarget)"
|
||||
>
|
||||
<img src="/images/icon-train.svg" alt="train icon" />
|
||||
<b>{{ $t('journal.timetable-online-button') }}</b>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -79,12 +90,12 @@ import { PropType, defineComponent } from 'vue';
|
||||
|
||||
import { API } from '../../../typings/api';
|
||||
import dateMixin from '../../../mixins/dateMixin';
|
||||
import modalTrainMixin from '../../../mixins/modalTrainMixin';
|
||||
import styleMixin from '../../../mixins/styleMixin';
|
||||
import { useApiStore } from '../../../store/apiStore';
|
||||
import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [dateMixin, modalTrainMixin, styleMixin],
|
||||
mixins: [dateMixin, styleMixin, trainCategoryMixin],
|
||||
|
||||
data() {
|
||||
return {
|
||||
@@ -97,14 +108,6 @@ export default defineComponent({
|
||||
type: Object as PropType<API.TimetableHistory.Data>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
showTimetable(timetable: API.TimetableHistory.Data, target: EventTarget | null) {
|
||||
if (timetable?.terminated) return;
|
||||
|
||||
this.selectModalTrainById(`${timetable.driverName}${timetable.trainNo}`, target);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -131,7 +134,6 @@ export default defineComponent({
|
||||
gap: 0.25em;
|
||||
|
||||
cursor: pointer;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.general-time {
|
||||
@@ -174,6 +176,7 @@ export default defineComponent({
|
||||
|
||||
@include smallScreen {
|
||||
.item-general {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="item-status" style="margin: 0.5em 0">
|
||||
<div class="entry-status" style="margin: 0.5em 0">
|
||||
<ProgressBar
|
||||
:progressPercent="~~((timetable.currentDistance / timetable.routeDistance) * 100)"
|
||||
:progressType="!timetable.fulfilled && timetable.terminated ? 'abandoned' : ''"
|
||||
@@ -61,7 +61,7 @@ export default defineComponent({
|
||||
<style lang="scss" scoped>
|
||||
@import '../../../styles/responsive.scss';
|
||||
|
||||
.item-status {
|
||||
.entry-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
@@ -0,0 +1,296 @@
|
||||
<template>
|
||||
<div class="entry-stops">
|
||||
<ul class="stop-list">
|
||||
<li v-for="(stop, i) in timetableStops" :key="stop.stopName">
|
||||
<span class="stop-label" :data-confirmed="stop.isConfirmed">
|
||||
<span v-if="i > 0">></span>
|
||||
|
||||
<span class="stop-name">{{ stop.stopName }}</span>
|
||||
|
||||
<span
|
||||
class="stop-date"
|
||||
v-if="stop.scheduledArrivalTimestamp != 0"
|
||||
:data-delayed="
|
||||
stop.isConfirmed && stop.arrivalTimestamp - stop.scheduledArrivalTimestamp > 0
|
||||
"
|
||||
:data-preponed="
|
||||
stop.isConfirmed &&
|
||||
stop.arrivalTimestamp != 0 &&
|
||||
stop.arrivalTimestamp - stop.scheduledArrivalTimestamp < 0
|
||||
"
|
||||
>
|
||||
<span
|
||||
v-if="stop.isConfirmed && stop.arrivalTimestamp - stop.scheduledArrivalTimestamp != 0"
|
||||
>
|
||||
p. <s>{{ timestampToString(stop.scheduledArrivalTimestamp) }}</s>
|
||||
{{ timestampToString(stop.arrivalTimestamp) }}
|
||||
</span>
|
||||
|
||||
<span v-else>p. {{ timestampToString(stop.scheduledArrivalTimestamp) }}</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="stop-time"
|
||||
v-if="stop.stopTime > 0"
|
||||
:data-stop-ph="stop.stopType.includes('ph')"
|
||||
:data-stop-pt="stop.stopType.includes('pt')"
|
||||
:data-stop-pm="stop.stopType.includes('pm')"
|
||||
>
|
||||
/<span>{{ stop.stopTime }} {{ stop.stopType }}</span
|
||||
>/
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="stop-date"
|
||||
v-if="
|
||||
stop.scheduledDepartureTimestamp != 0 &&
|
||||
stop.scheduledArrivalTimestamp != stop.scheduledDepartureTimestamp
|
||||
"
|
||||
:data-delayed="
|
||||
stop.isConfirmed && stop.departureTimestamp - stop.scheduledDepartureTimestamp > 0
|
||||
"
|
||||
:data-preponed="
|
||||
stop.isConfirmed &&
|
||||
stop.departureTimestamp != 0 &&
|
||||
stop.departureTimestamp - stop.scheduledDepartureTimestamp < 0
|
||||
"
|
||||
>
|
||||
<span
|
||||
v-if="
|
||||
stop.isConfirmed && stop.departureTimestamp - stop.scheduledDepartureTimestamp != 0
|
||||
"
|
||||
>
|
||||
o. <s>{{ timestampToString(stop.scheduledDepartureTimestamp) }}</s>
|
||||
{{ timestampToString(stop.departureTimestamp) }}
|
||||
</span>
|
||||
|
||||
<span v-else>o. {{ timestampToString(stop.scheduledDepartureTimestamp) }}</span>
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="timetable-path-list" v-if="timetablePathDetails">
|
||||
<li
|
||||
v-for="(pathData, i) in timetablePathDetails"
|
||||
:data-visited="pathData.isVisited"
|
||||
:data-next-visited="
|
||||
i < timetablePathDetails.length - 1 && timetablePathDetails[i + 1].isVisited
|
||||
"
|
||||
>
|
||||
<span v-if="i > 0" class="path-arrow">></span>
|
||||
<span class="path-arrival" v-if="pathData.arrival">{{ pathData.arrival }}</span>
|
||||
<b class="path-scenery">{{ pathData.sceneryName }}</b>
|
||||
<span class="path-departure" v-if="pathData.departure">{{ pathData.departure }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { PropType, defineComponent } from 'vue';
|
||||
import dateMixin from '../../../mixins/dateMixin';
|
||||
import { API } from '../../../typings/api';
|
||||
|
||||
interface ITimetableStopDetails {
|
||||
stopName: string;
|
||||
arrivalTimestamp: number;
|
||||
scheduledArrivalTimestamp: number;
|
||||
departureTimestamp: number;
|
||||
scheduledDepartureTimestamp: number;
|
||||
stopTime: number;
|
||||
stopType: string;
|
||||
isConfirmed: boolean;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [dateMixin],
|
||||
|
||||
props: {
|
||||
timetable: {
|
||||
type: Object as PropType<API.TimetableHistory.Data>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
timetablePathDetails() {
|
||||
if (!this.timetable.path || this.timetable.path == '') return null;
|
||||
|
||||
return this.timetable.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', '') ?? '';
|
||||
const isVisited = this.timetable.visitedSceneries.includes(sceneryHash);
|
||||
|
||||
return {
|
||||
arrival,
|
||||
sceneryName,
|
||||
sceneryHash,
|
||||
departure,
|
||||
isVisited,
|
||||
isVisitedOffline:
|
||||
!isVisited &&
|
||||
this.timetable.visitedSceneries.includes(`${sceneryName} ${sceneryHash}.sc`)
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
timetableStops(): ITimetableStopDetails[] {
|
||||
const timetable = this.timetable;
|
||||
|
||||
const stopNames = timetable.sceneriesString.split('%');
|
||||
|
||||
return stopNames.reduce<ITimetableStopDetails[]>((acc, stopName, i, arr) => {
|
||||
const arrivalDate =
|
||||
i == arr.length - 1
|
||||
? (timetable.checkpointArrivals.at(i) ?? timetable.endDate)
|
||||
: timetable.checkpointArrivals.at(i);
|
||||
|
||||
const scheduledArrivalDate =
|
||||
i == arr.length - 1
|
||||
? (timetable.checkpointArrivalsScheduled.at(i) ?? timetable.scheduledEndDate)
|
||||
: timetable.checkpointArrivalsScheduled.at(i);
|
||||
|
||||
const departureDate =
|
||||
i == 0
|
||||
? (timetable.checkpointDepartures.at(i) ?? timetable.beginDate)
|
||||
: timetable.checkpointDepartures.at(i);
|
||||
|
||||
const scheduledDepartureDate =
|
||||
i == 0
|
||||
? (timetable.checkpointDeparturesScheduled.at(i) ?? timetable.scheduledBeginDate)
|
||||
: timetable.checkpointDeparturesScheduled.at(i);
|
||||
|
||||
const stopTime = Number(timetable.checkpointStopTypes.at(i)?.split(',')[0]) || 0;
|
||||
const stopType = timetable.checkpointStopTypes.at(i)?.split(',').slice(1).join(',') || 'pt';
|
||||
|
||||
acc.push({
|
||||
stopName,
|
||||
arrivalTimestamp: this.dateStringToTimestamp(arrivalDate),
|
||||
scheduledArrivalTimestamp: this.dateStringToTimestamp(scheduledArrivalDate),
|
||||
departureTimestamp: this.dateStringToTimestamp(departureDate),
|
||||
scheduledDepartureTimestamp: this.dateStringToTimestamp(scheduledDepartureDate),
|
||||
stopTime,
|
||||
stopType,
|
||||
isConfirmed: i < timetable.confirmedStopsCount
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../../styles/badge.scss';
|
||||
|
||||
.entry-stops {
|
||||
word-wrap: break-word;
|
||||
gap: 0.25em;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.stop-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em;
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
.stop-label {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em;
|
||||
align-items: center;
|
||||
color: white;
|
||||
|
||||
&[data-confirmed='true'] > .stop-name {
|
||||
color: lightgreen;
|
||||
}
|
||||
|
||||
&[data-confirmed='true'] > .stop-date:not([data-preponed='true']):not([data-delayed='true']) {
|
||||
color: lightgreen;
|
||||
}
|
||||
}
|
||||
|
||||
.stop-name {
|
||||
font-weight: bold;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.stop-date {
|
||||
color: #ccc;
|
||||
|
||||
s {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
&[data-delayed='true'] {
|
||||
color: salmon;
|
||||
}
|
||||
|
||||
&[data-preponed='true'] {
|
||||
color: mediumspringgreen;
|
||||
}
|
||||
}
|
||||
|
||||
.stop-time {
|
||||
&[data-stop-pt='true'] span {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
&[data-stop-ph='true'] span,
|
||||
&[data-stop-pm='true'] span {
|
||||
color: gold;
|
||||
}
|
||||
}
|
||||
|
||||
.timetable-path-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em 0;
|
||||
padding: 0.5em 0;
|
||||
color: #ccc;
|
||||
|
||||
li > .path-scenery:first-child,
|
||||
li > .path-arrival:nth-child(2) {
|
||||
border-radius: 0.5em 0 0 0.5em;
|
||||
}
|
||||
|
||||
li > :last-child {
|
||||
border-radius: 0 0.5em 0.5em 0;
|
||||
}
|
||||
}
|
||||
|
||||
.path-scenery {
|
||||
padding: 0.25em 0.5em;
|
||||
background-color: #303030;
|
||||
}
|
||||
|
||||
.path-arrival,
|
||||
.path-departure {
|
||||
padding: 0.25em;
|
||||
display: inline-block;
|
||||
background-color: #4e4e4e;
|
||||
min-width: 25px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.path-arrow {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
.timetable-path-list > li[data-visited='true'] {
|
||||
.path-arrival,
|
||||
.path-scenery,
|
||||
.path-arrow {
|
||||
color: lightgreen;
|
||||
}
|
||||
|
||||
&[data-next-visited='true'] .path-departure {
|
||||
color: lightgreen;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -12,7 +12,7 @@
|
||||
<hr class="header-separator" />
|
||||
|
||||
<div class="info-stats">
|
||||
<span class="stat-badge">
|
||||
<span class="badge stat-badge">
|
||||
<span>{{ $t('journal.driver-stats.timetables') }}</span>
|
||||
<span
|
||||
>{{ store.driverStatsData._count.fulfilled }} /
|
||||
@@ -20,17 +20,17 @@
|
||||
>
|
||||
</span>
|
||||
|
||||
<span class="stat-badge">
|
||||
<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="stat-badge">
|
||||
<span class="badge stat-badge">
|
||||
<span>{{ $t('journal.driver-stats.avg-timetable') }}</span>
|
||||
<span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
|
||||
</span>
|
||||
|
||||
<span class="stat-badge">
|
||||
<span class="badge stat-badge">
|
||||
<span>{{ $t('journal.driver-stats.distance') }}</span>
|
||||
<span>
|
||||
{{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
|
||||
@@ -38,7 +38,7 @@
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="stat-badge">
|
||||
<span class="badge stat-badge">
|
||||
<span>{{ $t('journal.driver-stats.stations') }}</span>
|
||||
<span>
|
||||
{{ store.driverStatsData._sum.confirmedStopsCount }} /
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<li class="timetable-history-entry">
|
||||
<!-- General -->
|
||||
<EntryGeneral :timetable="timetableEntry" />
|
||||
|
||||
<div @click="toggleExtraInfo" style="cursor: pointer">
|
||||
<!-- Route -->
|
||||
<div class="entry-route">
|
||||
<b>{{ timetableEntry.route.replace('|', ' - ') }}</b>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<!-- Status -->
|
||||
<EntryStatus :timetable="timetableEntry" />
|
||||
</div>
|
||||
|
||||
<!-- Extra -->
|
||||
<EntryDetails
|
||||
:timetable="timetableEntry"
|
||||
:show-extra-info="showExtraInfo"
|
||||
@toggle-extra-info="toggleExtraInfo"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import { API } from '../../../typings/api';
|
||||
import { useApiStore } from '../../../store/apiStore';
|
||||
import { Journal } from '../typings';
|
||||
|
||||
import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
|
||||
import dateMixin from '../../../mixins/dateMixin';
|
||||
import styleMixin from '../../../mixins/styleMixin';
|
||||
|
||||
import EntryGeneral from './EntryGeneral.vue';
|
||||
import EntryStatus from './EntryStatus.vue';
|
||||
import EntryDetails from './EntryDetails.vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
timetableEntry: {
|
||||
type: Object as PropType<API.TimetableHistory.Data>,
|
||||
required: true
|
||||
},
|
||||
showExtraInfo: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
components: { EntryDetails, EntryGeneral, EntryStatus },
|
||||
mixins: [trainCategoryMixin, dateMixin, styleMixin],
|
||||
emits: ['toggleShowExtraInfo'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
apiStore: useApiStore()
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
timetablePathDetails() {
|
||||
if (!this.timetableEntry.path || this.timetableEntry.path == '') return null;
|
||||
|
||||
return this.timetableEntry.path.split(';').map((pathEl, i) => {
|
||||
const [arrival, name, departure] = pathEl.split(',');
|
||||
const sceneryName = name.split(' ').slice(0, -1).join(' ');
|
||||
const sceneryHash = name.split(' ').pop()?.replace('.sc', '') ?? '';
|
||||
|
||||
return {
|
||||
arrival,
|
||||
sceneryName,
|
||||
sceneryHash,
|
||||
departure,
|
||||
isVisited: this.timetableEntry.visitedSceneries?.includes(sceneryHash) ?? false
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
timetableStops(): Journal.TimetableStopDetails[] {
|
||||
const timetableEntry = this.timetableEntry;
|
||||
|
||||
const stopNames = timetableEntry.sceneriesString.split('%');
|
||||
|
||||
return stopNames.reduce<Journal.TimetableStopDetails[]>((acc, stopName, i, arr) => {
|
||||
const arrivalDate =
|
||||
i == arr.length - 1
|
||||
? (timetableEntry.checkpointArrivals.at(i) ?? timetableEntry.endDate)
|
||||
: timetableEntry.checkpointArrivals.at(i);
|
||||
|
||||
const scheduledArrivalDate =
|
||||
i == arr.length - 1
|
||||
? (timetableEntry.checkpointArrivalsScheduled.at(i) ?? timetableEntry.scheduledEndDate)
|
||||
: timetableEntry.checkpointArrivalsScheduled.at(i);
|
||||
|
||||
const departureDate =
|
||||
i == 0
|
||||
? (timetableEntry.checkpointDepartures.at(i) ?? timetableEntry.beginDate)
|
||||
: timetableEntry.checkpointDepartures.at(i);
|
||||
|
||||
const scheduledDepartureDate =
|
||||
i == 0
|
||||
? (timetableEntry.checkpointDeparturesScheduled.at(i) ??
|
||||
timetableEntry.scheduledBeginDate)
|
||||
: timetableEntry.checkpointDeparturesScheduled.at(i);
|
||||
|
||||
const stopTime = Number(timetableEntry.checkpointStopTypes.at(i)?.split(',')[0]) || 0;
|
||||
const stopType = timetableEntry.checkpointStopTypes.at(i)?.split(',')[1] || '';
|
||||
|
||||
acc.push({
|
||||
stopName,
|
||||
arrivalTimestamp: this.dateStringToTimestamp(arrivalDate),
|
||||
scheduledArrivalTimestamp: this.dateStringToTimestamp(scheduledArrivalDate),
|
||||
departureTimestamp: this.dateStringToTimestamp(departureDate),
|
||||
scheduledDepartureTimestamp: this.dateStringToTimestamp(scheduledDepartureDate),
|
||||
stopTime,
|
||||
stopType,
|
||||
isConfirmed: i < timetableEntry.confirmedStopsCount
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleExtraInfo() {
|
||||
this.$emit('toggleShowExtraInfo');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../../styles/responsive.scss';
|
||||
|
||||
.timetable-history-entry {
|
||||
background-color: #1a1a1a;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.entry-route {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,62 +1,40 @@
|
||||
<template>
|
||||
<div>
|
||||
<transition name="status-anim" mode="out-in">
|
||||
<div :key="dataStatus">
|
||||
<div class="journal_warning" v-if="store.isOffline">
|
||||
{{ $t('app.offline') }}
|
||||
</div>
|
||||
<div class="journal_warning" v-if="store.isOffline">
|
||||
{{ $t('app.offline') }}
|
||||
</div>
|
||||
|
||||
<Loading v-else-if="dataStatus == Status.Data.Loading" />
|
||||
<Loading v-else-if="dataStatus == Status.Data.Loading" />
|
||||
|
||||
<div v-else-if="dataStatus == Status.Data.Error" class="journal_warning error">
|
||||
{{ $t('app.error') }}
|
||||
</div>
|
||||
<div v-else-if="dataStatus == Status.Data.Error" class="journal_warning error">
|
||||
{{ $t('app.error') }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="timetableHistory.length == 0" class="journal_warning">
|
||||
{{ $t('app.no-result') }}
|
||||
</div>
|
||||
<div v-else-if="timetableHistory.length == 0" class="journal_warning">
|
||||
{{ $t('app.no-result') }}
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<ul class="journal-list">
|
||||
<transition-group name="list-anim">
|
||||
<li
|
||||
v-for="{ timetable, showExtraInfo } in computedTimetableHistory"
|
||||
class="journal_item"
|
||||
:key="timetable.id"
|
||||
@click="showExtraInfo.value = !showExtraInfo.value"
|
||||
>
|
||||
<div class="journal_item-info">
|
||||
<!-- General -->
|
||||
<TimetableGeneral :timetable="timetable" />
|
||||
<!-- Route -->
|
||||
<span class="item-route">
|
||||
<b>{{ timetable.route.replace('|', ' - ') }}</b>
|
||||
</span>
|
||||
<div v-else>
|
||||
<transition-group name="list-anim" class="journal-list" tag="ul">
|
||||
<JournalTimetableEntry
|
||||
v-for="(timetableEntry, i) in timetableHistory"
|
||||
:key="timetableEntry.id"
|
||||
:timetableEntry="timetableEntry"
|
||||
:onToggleShowExtraInfo="() => toggleExtraInfo(timetableEntry.id)"
|
||||
:showExtraInfo="extraInfoIndexes.includes(timetableEntry.id)"
|
||||
/>
|
||||
</transition-group>
|
||||
|
||||
<hr />
|
||||
<!-- Stops -->
|
||||
<TimetableStops :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
|
||||
<!-- Status -->
|
||||
<TimetableStatus :timetable="timetable" />
|
||||
|
||||
<!-- Extra -->
|
||||
<TimetableDetails :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
|
||||
</div>
|
||||
</li>
|
||||
</transition-group>
|
||||
</ul>
|
||||
|
||||
<AddDataButton
|
||||
:list="timetableHistory"
|
||||
:scrollDataLoaded="scrollDataLoaded"
|
||||
:scrollNoMoreData="scrollNoMoreData"
|
||||
@addHistoryData="addHistoryData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<AddDataButton
|
||||
:list="timetableHistory"
|
||||
:scrollDataLoaded="scrollDataLoaded"
|
||||
:scrollNoMoreData="scrollNoMoreData"
|
||||
@addHistoryData="addHistoryData"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
|
||||
|
||||
<div class="journal_warning" v-else-if="!scrollDataLoaded">
|
||||
{{ $t('journal.loading-further-data') }}
|
||||
</div>
|
||||
@@ -64,28 +42,21 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, ref } from 'vue';
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
|
||||
import Loading from '../../Global/Loading.vue';
|
||||
import AddDataButton from '../../Global/AddDataButton.vue';
|
||||
import JournalTimetableEntry from './JournalTimetableEntry.vue';
|
||||
|
||||
import { useMainStore } from '../../../store/mainStore';
|
||||
import { Status } from '../../../typings/common';
|
||||
import { API } from '../../../typings/api';
|
||||
|
||||
import TimetableGeneral from './TimetableGeneral.vue';
|
||||
import TimetableStops from './TimetableStops.vue';
|
||||
import TimetableStatus from './TimetableStatus.vue';
|
||||
import TimetableDetails from './TimetableDetails.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Loading,
|
||||
AddDataButton,
|
||||
TimetableDetails,
|
||||
TimetableGeneral,
|
||||
TimetableStatus,
|
||||
TimetableStops
|
||||
JournalTimetableEntry
|
||||
},
|
||||
|
||||
props: {
|
||||
@@ -110,16 +81,29 @@ export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
Status,
|
||||
store: useMainStore()
|
||||
store: useMainStore(),
|
||||
extraInfoIndexes: [] as number[]
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
computedTimetableHistory() {
|
||||
return this.timetableHistory.map((timetable) => ({
|
||||
timetable,
|
||||
showExtraInfo: ref(false)
|
||||
}));
|
||||
watch: {
|
||||
'$route.query': {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.extraInfoIndexes.length = 0;
|
||||
|
||||
this.$nextTick(() => {
|
||||
console.log(this.$el.querySelector('ul'));
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleExtraInfo(id: number) {
|
||||
const existingIdx = this.extraInfoIndexes.indexOf(id);
|
||||
|
||||
if (existingIdx != -1) this.extraInfoIndexes.splice(existingIdx, 1);
|
||||
else this.extraInfoIndexes.push(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
<template>
|
||||
<div class="timetable-stops">
|
||||
<div class="stop-list">
|
||||
<span
|
||||
v-for="(stop, i) in timetableStops.filter((_, i) =>
|
||||
!showExtraInfo ? i == 0 || i == timetableStops.length - 1 : true
|
||||
)"
|
||||
class="stop-list-item"
|
||||
:key="stop.stopName"
|
||||
:data-confirmed="stop.confirmed"
|
||||
>
|
||||
<span v-if="i > 0">
|
||||
>
|
||||
<span v-if="!showExtraInfo && i == 1 && timetableStops.length > 2">
|
||||
... (+{{ timetableStops.length - 2 }}) >
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="stop-name">{{ stop.stopName }}</span>
|
||||
<span v-html="stop.html"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="path-details" v-if="showExtraInfo && timetablePathDetails">
|
||||
<span
|
||||
v-for="(pathData, i) in timetablePathDetails"
|
||||
:data-visited="pathData.isVisited"
|
||||
:data-next-visited="
|
||||
i < timetablePathDetails.length - 1 && timetablePathDetails[i + 1].isVisited
|
||||
"
|
||||
>
|
||||
<span class="path-arrival" v-if="pathData.arrival">/ {{ pathData.arrival }} → </span>
|
||||
<b class="path-scenery">{{ pathData.sceneryName }}</b>
|
||||
<span class="path-departure" v-if="pathData.departure">
|
||||
→ {{ pathData.departure }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { PropType, defineComponent } from 'vue';
|
||||
import dateMixin from '../../../mixins/dateMixin';
|
||||
import { API } from '../../../typings/api';
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [dateMixin],
|
||||
|
||||
props: {
|
||||
showExtraInfo: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
|
||||
timetable: {
|
||||
type: Object as PropType<API.TimetableHistory.Data>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
timetablePathDetails() {
|
||||
if (!this.timetable.path || this.timetable.path == '') return null;
|
||||
|
||||
return this.timetable.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.timetable.visitedSceneries?.includes(sceneryHash) ?? false
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
timetableStops() {
|
||||
const timetable = this.timetable;
|
||||
|
||||
const stopNames = timetable.sceneriesString.split('%');
|
||||
|
||||
const beginDateHTML = ` (o. ${
|
||||
timetable.beginDate != timetable.scheduledBeginDate
|
||||
? `<s class="text--grayed">${this.localeTime(timetable.beginDate, this.$i18n.locale)}</s>`
|
||||
: ''
|
||||
} <span>${this.localeTime(timetable.scheduledBeginDate, this.$i18n.locale)}</span>)`;
|
||||
|
||||
const endDateHTML = ` (p. ${
|
||||
timetable.endDate != timetable.scheduledEndDate && timetable.fulfilled
|
||||
? `<s class="text--grayed">${this.localeTime(timetable.endDate, this.$i18n.locale)}</s>`
|
||||
: ''
|
||||
} <span>${this.localeTime(timetable.scheduledEndDate, this.$i18n.locale)}</span>)`;
|
||||
|
||||
return stopNames.map((stopName, i) => {
|
||||
const confirmed = i < timetable.confirmedStopsCount;
|
||||
if (i == 0) return { stopName, html: beginDateHTML, confirmed };
|
||||
if (i == stopNames.length - 1) return { stopName, html: endDateHTML, confirmed };
|
||||
|
||||
const departureDateScheduled = this.stringToDate(
|
||||
timetable.checkpointDeparturesScheduled?.at(i)
|
||||
);
|
||||
const departureDateReal = this.stringToDate(timetable.checkpointDepartures?.at(i));
|
||||
const arrivalDateScheduled = this.stringToDate(
|
||||
timetable.checkpointArrivalsScheduled?.at(i)
|
||||
);
|
||||
const arrivalDateReal = this.stringToDate(timetable.checkpointArrivals?.at(i));
|
||||
const arrivalHTML =
|
||||
(arrivalDateReal &&
|
||||
arrivalDateScheduled &&
|
||||
arrivalDateReal?.getTime() != arrivalDateScheduled?.getTime()
|
||||
? `<s class="text--grayed">${this.parseDateToTimeString(arrivalDateScheduled)}</s> `
|
||||
: '') + this.parseDateToTimeString(arrivalDateReal || arrivalDateScheduled);
|
||||
const departureHTML =
|
||||
(departureDateReal &&
|
||||
departureDateScheduled &&
|
||||
departureDateReal?.getTime() != departureDateScheduled?.getTime()
|
||||
? `<s class="text--grayed">${this.parseDateToTimeString(departureDateScheduled)}</s> `
|
||||
: '') + this.parseDateToTimeString(departureDateReal || departureDateScheduled);
|
||||
let html = `${arrivalHTML}${departureHTML ? ` / ${departureHTML}` : ''}`;
|
||||
if (html) html = ` (${html})`;
|
||||
return { stopName, html, confirmed };
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.timetable-stops {
|
||||
word-wrap: break-word;
|
||||
gap: 0.25em;
|
||||
font-size: 0.95em;
|
||||
color: #adadad;
|
||||
}
|
||||
|
||||
.stop-list {
|
||||
&-item[data-confirmed='true'] {
|
||||
color: lightgreen;
|
||||
|
||||
.stop-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.path-details {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.path-details > span[data-visited='true'] {
|
||||
.path-arrival,
|
||||
.path-scenery {
|
||||
color: lightgreen;
|
||||
}
|
||||
|
||||
&[data-next-visited='true'] .path-departure {
|
||||
color: lightgreen;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,101 +0,0 @@
|
||||
<template>
|
||||
<ul class="journal-list">
|
||||
<transition-group name="list-anim">
|
||||
<li
|
||||
v-for="{ timetable, showExtraInfo } in computedTimetableHistory"
|
||||
class="journal_item"
|
||||
:key="timetable.id"
|
||||
@click="showExtraInfo.value = !showExtraInfo.value"
|
||||
>
|
||||
<div class="journal_item-info">
|
||||
<!-- General -->
|
||||
<TimetableGeneral :timetable="timetable" />
|
||||
<!-- Route -->
|
||||
<span class="item-route">
|
||||
<b>{{ timetable.route.replace('|', ' - ') }}</b>
|
||||
</span>
|
||||
|
||||
<hr />
|
||||
<!-- Stops -->
|
||||
<TimetableStops :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
|
||||
<!-- Status -->
|
||||
<TimetableStatus :timetable="timetable" />
|
||||
|
||||
<button class="btn--action btn--show">
|
||||
{{ $t('journal.stock-info') }}
|
||||
<img
|
||||
:src="`/images/icon-arrow-${showExtraInfo.value ? 'asc' : 'desc'}.svg`"
|
||||
alt="Arrow icon"
|
||||
/>
|
||||
</button>
|
||||
<!-- Extra -->
|
||||
<TimetableExtra :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
|
||||
</div>
|
||||
</li>
|
||||
</transition-group>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { PropType, defineComponent, ref } from 'vue';
|
||||
|
||||
import TimetableGeneral from './TimetableGeneral.vue';
|
||||
import TimetableStops from './TimetableStops.vue';
|
||||
import TimetableStatus from './TimetableStatus.vue';
|
||||
import TimetableExtra from './TimetableExtra.vue';
|
||||
import { API } from '../../../typings/api';
|
||||
|
||||
export default defineComponent({
|
||||
components: { TimetableGeneral, TimetableStops, TimetableStatus, TimetableExtra },
|
||||
|
||||
props: {
|
||||
timetableHistory: {
|
||||
type: Array as PropType<API.TimetableHistory.Response>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedTimetableHistory() {
|
||||
return this.timetableHistory.map((timetable) => ({
|
||||
timetable,
|
||||
showExtraInfo: ref(false)
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../../styles/variables';
|
||||
@import '../../../styles/responsive';
|
||||
@import '../../../styles/JournalSection';
|
||||
|
||||
.btn--show {
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
padding: 0.2em 0.45em;
|
||||
|
||||
img {
|
||||
height: 1.3em;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.journal_item-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.item-route {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn--show {
|
||||
margin: 1em auto 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -66,4 +66,16 @@ export namespace Journal {
|
||||
iconName: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export interface TimetableStopDetails {
|
||||
stopName: string;
|
||||
arrivalTimestamp: number;
|
||||
scheduledArrivalTimestamp: number;
|
||||
departureTimestamp: number;
|
||||
scheduledDepartureTimestamp: number;
|
||||
stopTime: number;
|
||||
stopType: string;
|
||||
isConfirmed: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
{{ $t('scenery.history-list-empty') }}
|
||||
</div>
|
||||
|
||||
<div v-else class="history-list">
|
||||
<div v-else class="journal-list">
|
||||
<div v-for="historyItem in historyList" :key="historyItem.id">
|
||||
<span>
|
||||
<span class="text--grayed" style="margin-right: 10px">
|
||||
@@ -165,14 +165,14 @@ export default defineComponent({
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
.journal-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.history-list > div {
|
||||
.journal-list > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@@ -195,7 +195,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.history-list > div {
|
||||
.journal-list > div {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
@@ -15,14 +15,13 @@
|
||||
<li
|
||||
v-for="{ train, status } in stationTrains"
|
||||
class="badge user"
|
||||
tabindex="0"
|
||||
:key="train.id"
|
||||
:data-status="status"
|
||||
@click.prevent="selectModalTrain(train, $event.currentTarget)"
|
||||
@keydown.enter="selectModalTrain(train, $event.currentTarget)"
|
||||
>
|
||||
<span class="user_train">{{ train.trainNo }}</span>
|
||||
<span class="user_name">{{ train.driverName }}</span>
|
||||
<router-link :to="train.driverRouteLocation" class="a-block">
|
||||
<span class="user_train">{{ train.trainNo }}</span>
|
||||
<span class="user_name">{{ train.driverName }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</transition-group>
|
||||
</section>
|
||||
@@ -30,14 +29,13 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { PropType, defineComponent } from 'vue';
|
||||
import modalTrainMixin from '../../../mixins/modalTrainMixin';
|
||||
import routerMixin from '../../../mixins/routerMixin';
|
||||
import { ActiveScenery, Station, StopStatus } from '../../../typings/common';
|
||||
import { ActiveScenery, Station } from '../../../typings/common';
|
||||
import { getTrainStopStatus } from '../utils';
|
||||
import { useMainStore } from '../../../store/mainStore';
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [routerMixin, modalTrainMixin],
|
||||
mixins: [routerMixin],
|
||||
|
||||
props: {
|
||||
onlineScenery: {
|
||||
@@ -99,38 +97,27 @@ ul {
|
||||
}
|
||||
|
||||
.user {
|
||||
cursor: pointer;
|
||||
|
||||
&_train {
|
||||
color: black;
|
||||
background-color: $no-timetable;
|
||||
|
||||
transition: background-color 200ms;
|
||||
-ms-transition: background-color 200ms;
|
||||
-webkit-transition: background-color 200ms;
|
||||
}
|
||||
|
||||
&[data-status='no-timetable'] .user_train {
|
||||
background-color: $no-timetable;
|
||||
}
|
||||
|
||||
&[data-status='departed'] > &_train {
|
||||
&[data-status='departed'] .user_train {
|
||||
background-color: $departed;
|
||||
}
|
||||
|
||||
&[data-status='stopped'] > &_train {
|
||||
&[data-status='stopped'] .user_train {
|
||||
background-color: $stopped;
|
||||
}
|
||||
|
||||
&[data-status='online'] > &_train {
|
||||
&[data-status='online'] .user_train {
|
||||
background-color: $online;
|
||||
}
|
||||
|
||||
&[data-status='terminated'] > &_train {
|
||||
&[data-status='terminated'] .user_train {
|
||||
background-color: $terminated;
|
||||
}
|
||||
|
||||
&[data-status='disconnected'] > &_train {
|
||||
&[data-status='disconnected'] .user_train {
|
||||
background-color: $disconnected;
|
||||
}
|
||||
|
||||
@@ -139,6 +126,16 @@ ul {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.user_train {
|
||||
color: black;
|
||||
background-color: $no-timetable;
|
||||
|
||||
transition: background-color 200ms;
|
||||
-ms-transition: background-color 200ms;
|
||||
-webkit-transition: background-color 200ms;
|
||||
}
|
||||
|
||||
.users-anim {
|
||||
&-move,
|
||||
&-enter-active,
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
</span>
|
||||
|
||||
<span class="header_links" v-if="station">
|
||||
<a :href="pragotronHref" target="_blank" :title="$t('scenery.pragotron-link')">
|
||||
<img src="/images/icon-pragotron.svg" alt="icon-pragotron" />
|
||||
</a>
|
||||
|
||||
<a :href="tabliceZbiorczeHref" target="_blank" :title="$t('scenery.tablice-link')">
|
||||
<img src="/images/icon-tablice.ico" alt="icon-tablice" />
|
||||
</a>
|
||||
@@ -21,18 +25,15 @@
|
||||
</h3>
|
||||
|
||||
<div class="timetable-checkpoints" v-if="station?.generalInfo?.checkpoints">
|
||||
<span v-for="(cp, i) in station.generalInfo.checkpoints" :key="i">
|
||||
{{ (i > 0 && '•') || '' }}
|
||||
|
||||
<button
|
||||
:key="cp"
|
||||
class="checkpoint_item"
|
||||
:class="{ current: chosenCheckpoint === cp }"
|
||||
@click="setCheckpoint(cp)"
|
||||
<template v-for="(ch, i) in station.generalInfo.checkpoints" :key="i">
|
||||
<template v-if="i > 0">•</template>
|
||||
<router-link
|
||||
class="checkpoint-item"
|
||||
:class="{ current: chosenCheckpoint === ch }"
|
||||
:to="`/scenery?station=${station.name}&checkpoint=${ch}`"
|
||||
>{{ ch }}</router-link
|
||||
>
|
||||
{{ cp }}
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -62,29 +63,36 @@
|
||||
{{ $t('scenery.no-timetables') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="timetable-item"
|
||||
<router-link
|
||||
class="timetable-item a-block"
|
||||
v-else
|
||||
v-for="(row, i) in sceneryTimetables"
|
||||
:key="row.train.id + i"
|
||||
tabindex="0"
|
||||
@click.prevent.stop="selectModalTrain(row.train, $event.currentTarget)"
|
||||
@keydown.enter.prevent="selectModalTrain(row.train, $event.currentTarget)"
|
||||
:to="row.train.driverRouteLocation"
|
||||
>
|
||||
<span class="timetable-general">
|
||||
<span class="general-info">
|
||||
<span class="info-number">
|
||||
<strong>{{ row.train.timetableData!.category }}</strong>
|
||||
{{ row.train.trainNo }}
|
||||
|
||||
<span v-if="row.checkpointStop.comments" :title="row.checkpointStop.comments">
|
||||
<div class="info-train">
|
||||
<b
|
||||
data-tooltip-type="BaseTooltip"
|
||||
:data-tooltip-content="getCategoryExplanation(row.train.timetableData!.category)"
|
||||
class="text--primary tooltip-help"
|
||||
>
|
||||
{{ row.train.timetableData!.category }}
|
||||
</b>
|
||||
<span> </span>
|
||||
<b>{{ row.train.trainNo }}</b>
|
||||
<span> • </span>
|
||||
<span>{{ row.train.driverName }}</span>
|
||||
<span
|
||||
v-if="row.checkpointStop.comments"
|
||||
data-tooltip-type="BaseTooltip"
|
||||
:data-tooltip-content="row.checkpointStop.comments"
|
||||
>
|
||||
<img src="/images/icon-warning.svg" />
|
||||
</span>
|
||||
</span>
|
||||
|
|
||||
<span>
|
||||
{{ row.train.driverName }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="info-route">
|
||||
<strong>{{ row.train.timetableData!.route.replace('|', ' - ') }}</strong>
|
||||
@@ -160,7 +168,7 @@
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</transition-group>
|
||||
</div>
|
||||
</section>
|
||||
@@ -173,12 +181,12 @@ import { useRoute } from 'vue-router';
|
||||
import Loading from '../Global/Loading.vue';
|
||||
import dateMixin from '../../mixins/dateMixin';
|
||||
import routerMixin from '../../mixins/routerMixin';
|
||||
import trainCategoryMixin from '../../mixins/trainCategoryMixin';
|
||||
import { useMainStore } from '../../store/mainStore';
|
||||
import modalTrainMixin from '../../mixins/modalTrainMixin';
|
||||
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
|
||||
import { useApiStore } from '../../store/apiStore';
|
||||
import { ActiveScenery, Station } from '../../typings/common';
|
||||
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
|
||||
import { SceneryTimetableRow } from './typings';
|
||||
import { ActiveScenery, Station } from '../../typings/common';
|
||||
import { getTrainStopStatus, stopStatusPriority } from './utils';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -186,7 +194,7 @@ export default defineComponent({
|
||||
|
||||
components: { Loading, ScheduledTrainStatus },
|
||||
|
||||
mixins: [dateMixin, routerMixin, modalTrainMixin],
|
||||
mixins: [dateMixin, routerMixin, trainCategoryMixin],
|
||||
|
||||
props: {
|
||||
station: {
|
||||
@@ -205,6 +213,12 @@ export default defineComponent({
|
||||
this.loadSelectedOption();
|
||||
},
|
||||
|
||||
watch: {
|
||||
currentURL() {
|
||||
this.loadSelectedOption();
|
||||
}
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const route = useRoute();
|
||||
const currentURL = computed(() => `${location.origin}${route.fullPath}`);
|
||||
@@ -235,6 +249,13 @@ export default defineComponent({
|
||||
return url;
|
||||
},
|
||||
|
||||
pragotronHref() {
|
||||
let url = `https://pragotron-td2.web.app/board?name=${this.station!.name}®ion=${this.mainStore.region.id}`;
|
||||
if (this.chosenCheckpoint) url += `&checkpoint=${this.chosenCheckpoint}`;
|
||||
|
||||
return url;
|
||||
},
|
||||
|
||||
sceneryTimetables(): SceneryTimetableRow[] {
|
||||
if (!this.onlineScenery) return [];
|
||||
|
||||
@@ -243,7 +264,7 @@ export default defineComponent({
|
||||
return this.onlineScenery.scheduledTrains
|
||||
.filter(
|
||||
(ct) =>
|
||||
ct.timetablePathElement.stationName == sceneryName &&
|
||||
// ct.timetablePathElement.stationName == sceneryName &&
|
||||
ct.train.region == this.mainStore.region.id &&
|
||||
this.chosenCheckpoint &&
|
||||
ct.checkpointStop.stopNameRAW.toLowerCase() == this.chosenCheckpoint.toLowerCase()
|
||||
@@ -286,7 +307,19 @@ export default defineComponent({
|
||||
loadSelectedOption() {
|
||||
if (!this.station) return;
|
||||
|
||||
this.chosenCheckpoint = this.station.generalInfo?.checkpoints[0] ?? this.station.name;
|
||||
if (!this.station.generalInfo) {
|
||||
this.chosenCheckpoint = this.station.name;
|
||||
return;
|
||||
}
|
||||
|
||||
const queryCheckpoint = this.$route.query['checkpoint']?.toString();
|
||||
|
||||
this.chosenCheckpoint =
|
||||
this.station.generalInfo.checkpoints.find(
|
||||
(ch) => ch.toLocaleLowerCase() === queryCheckpoint?.toLocaleLowerCase()
|
||||
) ??
|
||||
this.station.generalInfo.checkpoints[0] ??
|
||||
this.station.name;
|
||||
},
|
||||
|
||||
setCheckpoint(cp: string) {
|
||||
@@ -356,7 +389,6 @@ export default defineComponent({
|
||||
|
||||
background: #353535;
|
||||
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
|
||||
&.empty {
|
||||
@@ -386,30 +418,35 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
.timetable-list {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timetable-checkpoints {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5em;
|
||||
|
||||
flex-wrap: wrap;
|
||||
font-size: 1.1em;
|
||||
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
button.checkpoint_item {
|
||||
color: #aaa;
|
||||
display: inline;
|
||||
.checkpoint-item {
|
||||
color: #aaa;
|
||||
display: inline;
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.checkpoint_item.current {
|
||||
&.current {
|
||||
font-weight: bold;
|
||||
color: $accentCol;
|
||||
}
|
||||
}
|
||||
|
||||
.timetable-list {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.general-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -423,7 +460,9 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
img {
|
||||
width: 1.1em;
|
||||
height: 0.9em;
|
||||
vertical-align: middle;
|
||||
margin: 0 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
{{ $t('scenery.history-list-empty') }}
|
||||
</div>
|
||||
|
||||
<div v-else class="history-list">
|
||||
<div v-else class="journal-list">
|
||||
<div v-for="timetableHistory in historyList" :key="timetableHistory.id">
|
||||
<span>
|
||||
<div>
|
||||
@@ -219,14 +219,14 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
.history-list {
|
||||
.journal-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.history-list > div {
|
||||
.journal-list > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@@ -235,7 +235,7 @@ export default defineComponent({
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.history-list > div > button > img {
|
||||
.journal-list > div > button > img {
|
||||
width: 2em;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
@@ -6,30 +6,11 @@
|
||||
<p>[F] {{ $t('options.filters') }}</p>
|
||||
<span class="active-indicator" v-if="changedFilters.length != 0"></span>
|
||||
</button>
|
||||
|
||||
<label for="scenery-search">
|
||||
<input
|
||||
id="scenery-search"
|
||||
list="sceneries"
|
||||
:placeholder="$t('sceneries.scenery-search')"
|
||||
@focus="preventKeyDown = true"
|
||||
@blur="preventKeyDown = false"
|
||||
v-model="chosenSearchScenery"
|
||||
/>
|
||||
|
||||
<datalist id="sceneries">
|
||||
<option
|
||||
v-for="scenery in sortedStationList"
|
||||
:key="scenery.name"
|
||||
:value="scenery.name"
|
||||
></option>
|
||||
</datalist>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<transition name="card-anim">
|
||||
<div class="card" v-if="isVisible" tabindex="0" ref="cardRef" @keydown.r="resetFilters">
|
||||
<div class="card_content" @scroll="onScroll" ref="cardContentRef">
|
||||
<div class="card" v-if="isVisible" ref="cardRef" @keydown.r="resetFilters">
|
||||
<div class="card_content" tabindex="0" @scroll="onScroll" ref="cardContentRef">
|
||||
<div class="card_title flex">{{ $t('filters.title') }}</div>
|
||||
<p class="card_info" v-html="$t('filters.desc')"></p>
|
||||
|
||||
@@ -40,6 +21,31 @@
|
||||
<template v-else>{{ $t('filters.no-changed-filters') }}</template>
|
||||
</div>
|
||||
|
||||
<section class="card_sceneries-search">
|
||||
<h3 class="section-header">{{ $t('filters.sceneries-search') }}</h3>
|
||||
|
||||
<datalist id="sceneries">
|
||||
<option
|
||||
v-for="scenery in sortedStationList"
|
||||
:key="scenery.name"
|
||||
:value="scenery.name"
|
||||
></option>
|
||||
</datalist>
|
||||
|
||||
<form action="javascript:void(0);" @submit="handleSceneriesInput">
|
||||
<input
|
||||
v-model="chosenSearchScenery"
|
||||
id="scenery-search"
|
||||
list="sceneries"
|
||||
:placeholder="$t('filters.sceneries-placeholder')"
|
||||
@focus="preventKeyDown = true"
|
||||
@blur="preventKeyDown = false"
|
||||
/>
|
||||
|
||||
<button class="btn--action">{{ $t('filters.search-button-title') }}</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card_options">
|
||||
<div
|
||||
class="option-section"
|
||||
@@ -57,16 +63,15 @@
|
||||
<div class="section-filters">
|
||||
<label
|
||||
v-for="filterKey in sectionFilters"
|
||||
@click="() => (filters[filterKey] = !filters[filterKey])"
|
||||
@dblclick="setSingleSectionFilter(sectionKey, filterKey)"
|
||||
:for="filterKey"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="filters[filterKey]"
|
||||
v-model="filters[filterKey]"
|
||||
type="checkbox"
|
||||
:class="sectionKey"
|
||||
:name="filterKey"
|
||||
:id="filterKey"
|
||||
/>
|
||||
<span>
|
||||
{{ $t(`filters.${filterKey}`) }}
|
||||
@@ -111,7 +116,7 @@
|
||||
@blur="preventKeyDown = false"
|
||||
/>
|
||||
|
||||
<button class="btn--action">{{ $t('filters.authors-button-title') }}</button>
|
||||
<button class="btn--action">{{ $t('filters.search-button-title') }}</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -269,20 +274,11 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
watch: {
|
||||
chosenSearchScenery(value: string) {
|
||||
const chosenStation = this.store.stationList.find(({ name }) => name == value);
|
||||
|
||||
if (chosenStation) {
|
||||
this.$router.push(`/scenery?station=${chosenStation.name.replace(/ /g, '_')}`);
|
||||
this.chosenSearchScenery = '';
|
||||
}
|
||||
},
|
||||
|
||||
isVisible(value: boolean) {
|
||||
this.$nextTick(() => {
|
||||
if (value) {
|
||||
(this.$refs['cardRef'] as HTMLDivElement).focus();
|
||||
(this.$refs['cardContentRef'] as HTMLDivElement).scrollTop = this.scrollTop;
|
||||
(this.$refs['cardContentRef'] as HTMLDivElement).focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -300,7 +296,18 @@ export default defineComponent({
|
||||
|
||||
handleAuthorsInput() {
|
||||
this.filters['authors'] = this.authors;
|
||||
// if (this.saveOptions) StorageManager.setStringValue('authors', target.value);
|
||||
},
|
||||
|
||||
handleSceneriesInput() {
|
||||
const chosenStation = this.store.stationList.find(
|
||||
({ name }) => name == this.chosenSearchScenery
|
||||
);
|
||||
|
||||
if (chosenStation) {
|
||||
this.$router.push(`/scenery?station=${chosenStation.name.replace(/ /g, '_')}`);
|
||||
this.chosenSearchScenery = '';
|
||||
this.isVisible = false;
|
||||
}
|
||||
},
|
||||
|
||||
subHour() {
|
||||
@@ -329,6 +336,8 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
resetFilters() {
|
||||
if (this.preventKeyDown) return;
|
||||
|
||||
// Reset local model values
|
||||
this.minimumHours = 0;
|
||||
this.authors = '';
|
||||
@@ -353,7 +362,8 @@ export default defineComponent({
|
||||
|
||||
setSingleSectionFilter(sectionKey: StationFilterSection, chosenKey: string) {
|
||||
filtersSections[sectionKey].forEach((filterKey) => {
|
||||
if (filterKey != chosenKey) this.filters[filterKey] = initFilters[filterKey];
|
||||
if (typeof this.filters[filterKey] === 'boolean')
|
||||
this.filters[filterKey] = filterKey != chosenKey;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -382,6 +392,7 @@ h3.section-header {
|
||||
.card {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.card_info {
|
||||
@@ -451,8 +462,12 @@ h3.section-header {
|
||||
}
|
||||
}
|
||||
|
||||
.card_authors-search {
|
||||
.card_authors-search,
|
||||
.card_sceneries-search {
|
||||
margin: 1em 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
@@ -642,10 +657,6 @@ h3.section-header {
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.card_controls > button.card-button > p {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slider {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
@@ -1,56 +1,70 @@
|
||||
<template>
|
||||
<div class="station-stats">
|
||||
<div class="separator" />
|
||||
<div
|
||||
class="dropdown"
|
||||
@keydown.esc="showDropdown = false"
|
||||
v-click-outside="() => (showDropdown = false)"
|
||||
>
|
||||
<div class="bg" v-if="showDropdown" @click="showDropdown = false"></div>
|
||||
|
||||
<div class="stats-row">
|
||||
<div>
|
||||
<span
|
||||
>{{ $t('station-stats.u-factor') }}
|
||||
<a
|
||||
href="https://td2.info.pl/dyskusje/wspolczynnik-ugla-czy-to-ma-sens/msg81011/#msg81011"
|
||||
target="_blank"
|
||||
:data-tooltip="$t('station-stats.u-factor-tooltip')"
|
||||
>(?)</a
|
||||
>:
|
||||
</span>
|
||||
<button class="filter-button btn--filled btn--image" @click="toggleDropdown" ref="button">
|
||||
<img src="/images/icon-stats.svg" alt="Open filters icon" />
|
||||
<!-- {{ $t('train-stats.stats-button') }} -->
|
||||
<span>STATYSTYKI</span>
|
||||
</button>
|
||||
|
||||
<b class="u-factor" :style="calculateFactorStyle()">
|
||||
{{ uFactor.toFixed(2) }}
|
||||
</b>
|
||||
<transition name="dropdown-anim">
|
||||
<div class="dropdown_wrapper" v-if="showDropdown">
|
||||
<div>
|
||||
<h1 class="text--primary">
|
||||
<img src="/images/icon-stats.svg" alt="Open filters icon" />
|
||||
{{ $t('train-stats.title') }}
|
||||
</h1>
|
||||
|
||||
<hr style="margin: 0.5em 0" />
|
||||
|
||||
<ul class="stats-list">
|
||||
<li>
|
||||
<span>
|
||||
{{ $t('station-stats.u-factor') }}
|
||||
<a
|
||||
href="https://td2.info.pl/dyskusje/wspolczynnik-ugla-czy-to-ma-sens/msg81011/#msg81011"
|
||||
target="_blank"
|
||||
:data-tooltip="$t('station-stats.u-factor-tooltip')"
|
||||
>(?)</a
|
||||
>:
|
||||
</span>
|
||||
<b class="u-factor" :style="calculateFactorStyle()">
|
||||
{{ uFactor.toFixed(2) }}
|
||||
</b>
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('station-stats.avg-timetable-count') }}
|
||||
<b>{{ avgTimetableCount.toFixed(2) }}</b>
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('station-stats.single-track-count') }}
|
||||
<b>{{ trackCount.oneWay }}</b> (<b>{{ trackCount.oneWayElectric }} ⚡</b>)
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('station-stats.double-track-count') }}
|
||||
<b>{{ trackCount.twoWay }}</b>
|
||||
(<b>{{ trackCount.twoWayElectric }} ⚡</b>)
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('station-stats.cross-sceneries') }}
|
||||
<b>{{ trackCount.crossTrack }}</b> (<b>{{ trackCount.crossTrackElectric }} ⚡</b>)
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('station-stats.open-spawns') }} <b>{{ spawnCount.passenger }}</b> - PAS /
|
||||
<b>{{ spawnCount.freight }}</b> - TOW / <b>{{ spawnCount.loco }}</b> - LUZ /
|
||||
<b>{{ spawnCount.all }}</b> - ALL
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div tabindex="0" @focus="() => (showDropdown = false)"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
•
|
||||
{{ $t('station-stats.avg-timetable-count') }}
|
||||
<b>{{ avgTimetableCount.toFixed(2) }}</b>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
•
|
||||
{{ $t('station-stats.single-track-count') }}
|
||||
<b>{{ trackCount.oneWay }}</b> (<b>{{ trackCount.oneWayElectric }} ⚡</b>)
|
||||
</div>
|
||||
|
||||
<div>
|
||||
•
|
||||
{{ $t('station-stats.double-track-count') }}
|
||||
<b>{{ trackCount.twoWay }}</b>
|
||||
(<b>{{ trackCount.twoWayElectric }} ⚡</b>)
|
||||
</div>
|
||||
|
||||
<div>
|
||||
• {{ $t('station-stats.cross-sceneries') }} <b>{{ trackCount.crossTrack }}</b> (<b
|
||||
>{{ trackCount.crossTrackElectric }} ⚡</b
|
||||
>)
|
||||
</div>
|
||||
|
||||
<div>
|
||||
•
|
||||
{{ $t('station-stats.open-spawns') }} <b>{{ spawnCount.passenger }}</b> - PAS /
|
||||
<b>{{ spawnCount.freight }}</b> - TOW / <b>{{ spawnCount.loco }}</b> - LUZ /
|
||||
<b>{{ spawnCount.all }}</b> - ALL
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -61,11 +75,16 @@ import { useMainStore } from '../../store/mainStore';
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
mainStore: useMainStore()
|
||||
mainStore: useMainStore(),
|
||||
showDropdown: false
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleDropdown() {
|
||||
this.showDropdown = !this.showDropdown;
|
||||
},
|
||||
|
||||
calculateFactorStyle() {
|
||||
if (this.uFactor == 0) return '';
|
||||
|
||||
@@ -171,25 +190,15 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.separator {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
@import '../../styles/dropdown.scss';
|
||||
@import '../../styles/badge.scss';
|
||||
|
||||
h1 img {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0.5em 0;
|
||||
background-color: #aaa;
|
||||
}
|
||||
|
||||
.station-stats {
|
||||
text-align: center;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
text-wrap: pretty;
|
||||
gap: 0.25em;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
.u-factor {
|
||||
@@ -209,4 +218,20 @@ export default defineComponent({
|
||||
color: rgb(22, 245, 22);
|
||||
}
|
||||
}
|
||||
|
||||
ul.stats-list {
|
||||
list-style: disc;
|
||||
padding-left: 1em;
|
||||
margin-top: 1em;
|
||||
|
||||
& > li {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.filter-button span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -52,15 +52,14 @@
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr
|
||||
<router-link
|
||||
v-for="station in filteredStationList"
|
||||
:class="{ 'last-selected': lastSelectedStationName == station.name }"
|
||||
class="a-row"
|
||||
role="row"
|
||||
:key="station.name"
|
||||
@click.left="setScenery(station.name)"
|
||||
@click.right="openForumSite($event, station.generalInfo?.url)"
|
||||
@keydown.enter="setScenery(station.name)"
|
||||
@keydown.space="openForumSite($event, station.generalInfo?.url)"
|
||||
tabindex="0"
|
||||
@click.right.prevent="openForumSite($event, station.generalInfo?.url)"
|
||||
@keydown.space.prevent="openForumSite($event, station.generalInfo?.url)"
|
||||
:to="getSceneryRoute(station)"
|
||||
>
|
||||
<td class="station-name" :class="station.generalInfo?.availability">
|
||||
<b v-if="station.generalInfo?.project" style="color: salmon">{{
|
||||
@@ -121,7 +120,7 @@
|
||||
<span v-if="station.onlineInfo?.dispatcherName">
|
||||
<b
|
||||
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
|
||||
@click.stop="openDonationCard"
|
||||
@click.prevent="openDonationCard"
|
||||
data-tooltip-type="DonatorTooltip"
|
||||
:data-tooltip-content="$t('donations.dispatcher-message')"
|
||||
>
|
||||
@@ -294,7 +293,7 @@
|
||||
>
|
||||
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
</router-link>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -319,7 +318,7 @@ import dateMixin from '../../mixins/dateMixin';
|
||||
import styleMixin from '../../mixins/styleMixin';
|
||||
import { useApiStore } from '../../store/apiStore';
|
||||
import { useMainStore } from '../../store/mainStore';
|
||||
import { Status } from '../../typings/common';
|
||||
import { Station, Status } from '../../typings/common';
|
||||
import { useTooltipStore } from '../../store/tooltipStore';
|
||||
import { getChangedFilters } from '../../managers/stationFilterManager';
|
||||
import { ActiveSorter, HeadIdsType, headIconsIds, headIds } from './typings';
|
||||
@@ -334,7 +333,6 @@ export default defineComponent({
|
||||
data: () => ({
|
||||
headIconsIds,
|
||||
headIds,
|
||||
lastSelectedStationName: '',
|
||||
getChangedFilters
|
||||
}),
|
||||
|
||||
@@ -364,21 +362,16 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
methods: {
|
||||
setScenery(name: string) {
|
||||
const station = this.filteredStationList.find((station) => station.name === name);
|
||||
getSceneryRoute(station: Station) {
|
||||
// TODO: Hide tooltips when navigating away
|
||||
|
||||
if (!station) return;
|
||||
|
||||
this.lastSelectedStationName = station.name;
|
||||
this.tooltipStore.hide();
|
||||
|
||||
this.$router.push({
|
||||
return {
|
||||
name: 'SceneryView',
|
||||
query: {
|
||||
station: station.name.replaceAll(' ', '_'),
|
||||
station: station.name,
|
||||
region: this.$route.query.region || undefined
|
||||
}
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
openDonationCard(e: Event) {
|
||||
@@ -414,9 +407,9 @@ export default defineComponent({
|
||||
$rowCol: #424242;
|
||||
|
||||
.station_table {
|
||||
height: 80vh;
|
||||
height: calc(100vh - 11em);
|
||||
max-height: 2000px;
|
||||
min-height: 700px;
|
||||
min-height: 500px;
|
||||
overflow: auto;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -503,8 +496,10 @@ table {
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
tr,
|
||||
.a-row {
|
||||
background-color: $rowCol;
|
||||
vertical-align: middle;
|
||||
|
||||
&:nth-child(even) {
|
||||
background-color: lighten($rowCol, 5);
|
||||
|
||||
@@ -23,12 +23,13 @@ export default defineComponent({
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
white-space: pre-line;
|
||||
|
||||
padding: 0.25em 0.5em;
|
||||
border-radius: 0.25em;
|
||||
|
||||
width: 100%;
|
||||
background-color: #333;
|
||||
background-color: #1f1f1f;
|
||||
box-shadow: 0 0 5px 2px #aaa;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ import BaseTooltip from './BaseTooltip.vue';
|
||||
import SpawnsTooltip from './SpawnsTooltip.vue';
|
||||
import UsersTooltip from './UsersTooltip.vue';
|
||||
|
||||
const BOX_PADDING_PX = 20;
|
||||
|
||||
export default defineComponent({
|
||||
components: { DonatorTooltip, VehiclePreviewTooltip, BaseTooltip, SpawnsTooltip, UsersTooltip },
|
||||
|
||||
@@ -33,14 +35,14 @@ export default defineComponent({
|
||||
const boxWidth = previewEl.getBoundingClientRect().width;
|
||||
|
||||
let translateX = '0',
|
||||
translateY = '30px';
|
||||
translateY = `calc(-100% - ${BOX_PADDING_PX}px)`;
|
||||
|
||||
if (val[0] <= boxWidth / 2) {
|
||||
if (val[0] <= boxWidth / 2 + BOX_PADDING_PX) {
|
||||
previewEl.style.left = '0';
|
||||
translateX = '0px';
|
||||
} else if (val[0] >= clientWidth - boxWidth / 2) {
|
||||
translateX = BOX_PADDING_PX + 'px';
|
||||
} else if (val[0] >= clientWidth - boxWidth / 2 - BOX_PADDING_PX) {
|
||||
previewEl.style.left = '100%';
|
||||
translateX = '-100%';
|
||||
translateX = `calc(-100% - ${BOX_PADDING_PX}px)`;
|
||||
} else {
|
||||
previewEl.style.left = `${val[0]}px`;
|
||||
translateX = '-50%';
|
||||
@@ -49,10 +51,10 @@ export default defineComponent({
|
||||
previewEl.style.top = `${val[1]}px`;
|
||||
|
||||
const isOutside =
|
||||
val[1] + previewEl.getBoundingClientRect().height + 30 >=
|
||||
window.innerHeight + window.scrollY;
|
||||
val[1] - previewEl.getBoundingClientRect().height <=
|
||||
window.scrollY + BOX_PADDING_PX * 2;
|
||||
|
||||
if (isOutside) translateY = 'calc(-100% - 30px)';
|
||||
if (isOutside) translateY = BOX_PADDING_PX + 'px';
|
||||
previewEl.style.transform = `translate(${translateX}, ${translateY})`;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
watch: {
|
||||
'tooltipStore.type'(prev, val) {
|
||||
vehicleName(prev, val) {
|
||||
if (prev != val) this.imageState = 'loading';
|
||||
}
|
||||
},
|
||||
@@ -81,18 +81,6 @@ export default defineComponent({
|
||||
(c) => c.id == this.tooltipStore.content.split(':')[1]
|
||||
);
|
||||
}
|
||||
|
||||
// vehicleProps() {
|
||||
// const vehicleDataArray = this.apiStore.vehiclesData?.vehicleList.find(
|
||||
// ([name]) => name === this.vehicleName
|
||||
// );
|
||||
|
||||
// if (!vehicleDataArray) return null;
|
||||
|
||||
// return (
|
||||
// this.apiStore.vehiclesData!.vehicleProps.find((v) => v.type == vehicleDataArray[1]) ?? null
|
||||
// );
|
||||
// }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -101,7 +89,7 @@ export default defineComponent({
|
||||
.tooltip-content {
|
||||
width: 300px;
|
||||
min-height: 200px;
|
||||
background-color: #333;
|
||||
background-color: #1f1f1f;
|
||||
box-shadow: 0 0 10px 2px #aaa;
|
||||
|
||||
padding: 0.5em;
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
<template>
|
||||
<span
|
||||
class="stop-label"
|
||||
:data-minor="stop.isSBL || (stop.nameRaw.endsWith(', po.') && !stop.duration)"
|
||||
:data-minor="stop.isSBL || (stop.nameRaw.endsWith(', po') && !stop.duration)"
|
||||
>
|
||||
<span class="name" v-html="stop.nameHtml"></span>
|
||||
<router-link v-if="/(, podg$|<strong>)/.test(stop.nameHtml)" :to="sceneryHref">
|
||||
<span class="stop-name" v-html="stop.nameHtml"></span>
|
||||
</router-link>
|
||||
|
||||
<span v-else class="stop-name" v-html="stop.nameHtml"></span>
|
||||
|
||||
<span
|
||||
v-if="stop.position != 'begin'"
|
||||
class="date arrival"
|
||||
:data-status="
|
||||
stop.arrivalDelay > 0 && stop.status != 'unconfirmed'
|
||||
? 'delayed'
|
||||
: stop.arrivalDelay < 0 && stop.status != 'unconfirmed'
|
||||
? 'preponed'
|
||||
: stop.arrivalDelay == 0 && stop.status == 'confirmed'
|
||||
? 'on-time'
|
||||
: ''
|
||||
"
|
||||
:data-status-delayed="stop.arrivalDelay > 0"
|
||||
:data-status-preponed="stop.arrivalDelay < 0"
|
||||
:data-status="stop.status"
|
||||
>
|
||||
p.
|
||||
<span v-if="stop.arrivalDelay != 0 && stop.status != 'unconfirmed'">
|
||||
@@ -31,10 +29,7 @@
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="
|
||||
stop.duration ||
|
||||
(stop.status == 'stopped' && stop.position != 'begin' && stop.departureDelay > 0)
|
||||
"
|
||||
v-if="stop.duration"
|
||||
class="date stop"
|
||||
:data-stop-types="stop.type.replace(', ', '-')"
|
||||
:data-stop-status="stop.departureDelay > 0 && !stop.duration ? 'delayed' : ''"
|
||||
@@ -53,20 +48,12 @@
|
||||
(stop.duration != 0 || stop.status == 'stopped' || stop.departureDelay != stop.arrivalDelay)
|
||||
"
|
||||
class="date departure"
|
||||
:data-status="
|
||||
stop.departureDelay > 0 && stop.status == 'confirmed'
|
||||
? 'delayed'
|
||||
: stop.departureDelay < 0 && stop.status == 'confirmed'
|
||||
? 'preponed'
|
||||
: stop.departureDelay == 0 && stop.status == 'confirmed'
|
||||
? 'on-time'
|
||||
: ''
|
||||
"
|
||||
:data-status-delayed="stop.departureDelay > 0"
|
||||
:data-status-preponed="stop.departureDelay < 0"
|
||||
:data-status-confirmed="stop.status == 'confirmed'"
|
||||
>
|
||||
o.
|
||||
<span
|
||||
v-if="stop.departureDelay != 0 && (stop.status == 'confirmed' || stop.status == 'stopped')"
|
||||
>
|
||||
<span v-if="stop.departureDelay != 0 && stop.status == 'confirmed'">
|
||||
<s>{{ timestampToString(stop.departureScheduled) }}</s>
|
||||
{{ timestampToString(stop.departureReal) }}
|
||||
|
||||
@@ -93,6 +80,12 @@ export default defineComponent({
|
||||
type: Object as PropType<TrainScheduleStop>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
sceneryHref() {
|
||||
return `/scenery?station=${this.stop.sceneryName}&checkpoint=${this.stop.nameRaw}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -105,24 +98,16 @@ $stopExchangeClr: #db8e29;
|
||||
$stopDefaultClr: #252525;
|
||||
$stopNameClr: #303030;
|
||||
|
||||
s {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.stop-label {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
&[data-minor='true'] {
|
||||
.date {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.name {
|
||||
background: none;
|
||||
color: #aaa;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
.stop-name {
|
||||
background: $stopNameClr;
|
||||
border-radius: 0.5em 0 0 0.5em;
|
||||
padding: 0.3em 0.5em;
|
||||
@@ -144,6 +129,18 @@ $stopNameClr: #303030;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-minor='true'] {
|
||||
.date {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stop-name {
|
||||
background: none;
|
||||
color: #aaa;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.stop {
|
||||
&[data-stop-types='ph'],
|
||||
&[data-stop-types='ph-pm'],
|
||||
@@ -157,27 +154,48 @@ $stopNameClr: #303030;
|
||||
color: $delayedClr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arrival,
|
||||
.departure {
|
||||
&[data-status='delayed'] {
|
||||
s {
|
||||
color: #ccc;
|
||||
}
|
||||
.stop-label > a {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
color: $delayedClr;
|
||||
}
|
||||
.stop .arrival {
|
||||
&[data-status='confirmed'][data-status-delayed='true'] {
|
||||
span {
|
||||
color: $delayedClr;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-status='preponed'] {
|
||||
s {
|
||||
color: #ccc;
|
||||
}
|
||||
&[data-status='confirmed'][data-status-preponed='true'] {
|
||||
span {
|
||||
color: $preponedClr;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
color: $preponedClr;
|
||||
}
|
||||
&[data-status='stopped'][data-status-preponed='true'] {
|
||||
span {
|
||||
color: $preponedClr;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-status='stopped'][data-status-delayed='true'] {
|
||||
span {
|
||||
color: $delayedClr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stop .departure[data-status-confirmed='true'] {
|
||||
&[data-status-delayed='true'] {
|
||||
span {
|
||||
color: $delayedClr;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-status-preponed='true'] {
|
||||
span {
|
||||
color: $preponedClr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,47 +2,49 @@
|
||||
<div class="train-info" :data-extended="extended">
|
||||
<section class="train-general">
|
||||
<div class="general-top-bar">
|
||||
<div>
|
||||
<div class="top-bar-header">
|
||||
<b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
|
||||
<span class="timetable-id" v-if="train.timetableData">
|
||||
#{{ train.timetableData.timetableId }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="timetable-warnings"
|
||||
v-if="train.timetableData?.TWR || train.timetableData?.SKR"
|
||||
class="train-badge twr"
|
||||
v-if="train.timetableData?.TWR"
|
||||
data-tooltip-type="BaseTooltip"
|
||||
:data-tooltip-content="$t('general.TWR') + `:\n${train.timetableData.warningNotes}`"
|
||||
>
|
||||
<span
|
||||
class="train-badge twr"
|
||||
v-if="train.timetableData?.TWR"
|
||||
:title="$t('general.TWR')"
|
||||
>
|
||||
TWR
|
||||
</span>
|
||||
<span
|
||||
class="train-badge skr"
|
||||
v-if="train.timetableData?.SKR"
|
||||
:title="$t('general.SKR')"
|
||||
>
|
||||
SKR
|
||||
</span>
|
||||
TWR
|
||||
</span>
|
||||
|
||||
<strong>
|
||||
<span v-if="train.timetableData" class="text--primary"
|
||||
>{{ train.timetableData.category }} </span
|
||||
>
|
||||
<span class="train-number">{{ train.trainNo }}</span>
|
||||
</strong>
|
||||
<span>•</span>
|
||||
<b
|
||||
class="level-badge driver"
|
||||
:style="calculateExpStyle(train.driverLevel, train.isSupporter)"
|
||||
<span
|
||||
class="train-badge skr"
|
||||
v-if="train.timetableData?.SKR"
|
||||
data-tooltip-type="BaseTooltip"
|
||||
:data-tooltip-content="$t('general.SKR')"
|
||||
>
|
||||
{{ train.driverLevel < 2 ? 'L' : `${train.driverLevel}` }}
|
||||
SKR
|
||||
</span>
|
||||
|
||||
<b
|
||||
v-if="train.timetableData"
|
||||
data-tooltip-type="BaseTooltip"
|
||||
:data-tooltip-content="getCategoryExplanation(train.timetableData.category)"
|
||||
class="text--primary tooltip-help"
|
||||
>
|
||||
{{ train.timetableData.category }}
|
||||
</b>
|
||||
<b class="train-number">{{ train.trainNo }}</b>
|
||||
<span>•</span>
|
||||
|
||||
<div class="train-driver">
|
||||
<b
|
||||
class="level-badge driver"
|
||||
:style="calculateExpStyle(train.driverLevel, train.isSupporter)"
|
||||
>
|
||||
{{ train.driverLevel < 2 ? 'L' : `${train.driverLevel}` }}
|
||||
</b>
|
||||
|
||||
<b
|
||||
v-if="apiStore.donatorsData.includes(train.driverName)"
|
||||
data-tooltip-type="DonatorTooltip"
|
||||
@@ -55,19 +57,6 @@
|
||||
<span v-else>{{ train.driverName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="extended">
|
||||
<button class="btn-timetable btn--image btn--action" @click="navigateToJournal">
|
||||
<img src="/images/icon-train.svg" alt="train icon" />
|
||||
<span>
|
||||
{{ $t('trains.journal-button') }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button class="btn-exit btn--image btn--action" @click="closeModal">
|
||||
<img src="/images/icon-exit.svg" alt="modal exit icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="general-timetable" v-if="train.timetableData">
|
||||
@@ -88,7 +77,7 @@
|
||||
<div class="general-stops" v-if="train.timetableData">
|
||||
<span v-if="train.timetableData.followingStops.length > 2">
|
||||
{{ $t('trains.via-title') }}
|
||||
<span v-html="displayStopList(train.timetableData.followingStops)"></span>
|
||||
<span v-html="getTrainStopsHtml(train.timetableData.followingStops)"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -97,21 +86,22 @@
|
||||
<ProgressBar :progressPercent="confirmedPercentage(train.timetableData.followingStops)" />
|
||||
|
||||
<span class="progress-distance">
|
||||
{{ currentDistance(train.timetableData.followingStops) }} km /
|
||||
<span class="text--primary"> {{ train.timetableData.routeDistance }} km </span>
|
||||
|
|
||||
<span>{{ currentDistance(train.timetableData.followingStops) }} km</span>
|
||||
<span>/</span>
|
||||
<span class="text--primary">{{ train.timetableData.routeDistance }} km </span>
|
||||
<span>|</span>
|
||||
<span v-html="currentDelay(train.timetableData.followingStops)"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="status-badges">
|
||||
<div v-if="!train.currentStationHash" class="train-badge offline">
|
||||
<img src="/images/icon-offline.svg" alt="" />
|
||||
<img src="/images/icon-offline.svg" alt="offline train icon" />
|
||||
{{ $t('trains.scenery-offline') }}
|
||||
</div>
|
||||
|
||||
<div v-if="!train.online" class="train-badge offline">
|
||||
<img src="/images/icon-offline.svg" alt="" />
|
||||
<img src="/images/icon-offline.svg" alt="offline train icon" />
|
||||
Offline {{ lastSeenMessage(train.lastSeen) }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -149,6 +139,20 @@
|
||||
<div class="text--grayed" style="margin-top: 0.25em">
|
||||
{{ displayTrainPosition(train) }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="train-dangers"
|
||||
v-if="extended && (train.timetableData?.TWR || train.timetableData?.SKR)"
|
||||
>
|
||||
<div v-if="train.timetableData.TWR">
|
||||
<b style="color: var(--clr-twr)">TWR</b> - {{ $t('general.TWR') }}
|
||||
<i>({{ train.timetableData?.warningNotes }})</i>
|
||||
</div>
|
||||
|
||||
<div v-if="train.timetableData.SKR">
|
||||
<b style="color: var(--clr-skr)">SKR</b> - {{ $t('general.SKR') }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="train-stats" v-if="!extended">
|
||||
@@ -179,11 +183,11 @@ import ProgressBar from '../Global/ProgressBar.vue';
|
||||
import { useMainStore } from '../../store/mainStore';
|
||||
import { useApiStore } from '../../store/apiStore';
|
||||
import StockList from '../Global/StockList.vue';
|
||||
import modalTrainMixin from '../../mixins/modalTrainMixin';
|
||||
import { Train } from '../../typings/common';
|
||||
import trainCategoryMixin from '../../mixins/trainCategoryMixin';
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [trainInfoMixin, styleMixin, modalTrainMixin],
|
||||
mixins: [trainInfoMixin, styleMixin, trainCategoryMixin],
|
||||
components: { ProgressBar, StockList },
|
||||
|
||||
props: {
|
||||
@@ -212,19 +216,14 @@ export default defineComponent({
|
||||
|
||||
return Math.min(vehicleSpeed, acc);
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
navigateToJournal() {
|
||||
this.$router.push({
|
||||
},
|
||||
journalRouteLocation() {
|
||||
return {
|
||||
path: '/journal/timetables',
|
||||
query: {
|
||||
'search-driver': this.train.driverName
|
||||
}
|
||||
});
|
||||
|
||||
this.closeModal();
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -251,6 +250,10 @@ export default defineComponent({
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.train-dangers {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.train-info {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
@@ -266,6 +269,12 @@ export default defineComponent({
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.train-driver {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.train-driver img {
|
||||
max-height: 20px;
|
||||
vertical-align: text-bottom;
|
||||
@@ -297,24 +306,15 @@ export default defineComponent({
|
||||
.general-top-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em;
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
gap: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-timetable {
|
||||
padding: 0.25em;
|
||||
}
|
||||
.top-bar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.btn-exit {
|
||||
padding: 0.25em;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.general-status {
|
||||
@@ -361,10 +361,14 @@ export default defineComponent({
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em;
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
.progress-distance {
|
||||
margin-right: 0.25em;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.timetable-warnings {
|
||||
@@ -377,9 +381,5 @@ export default defineComponent({
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1em 0;
|
||||
}
|
||||
|
||||
.btn-timetable > span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
<template>
|
||||
<div class="train-modal" v-if="chosenTrain" @keydown.esc="closeModal">
|
||||
<div class="modal-background" @click="closeModal"></div>
|
||||
<div class="modal-content" ref="content" tabindex="0">
|
||||
<TrainInfo :train="chosenTrain" :extended="true" ref="trainInfo" />
|
||||
<TrainSchedule :train="chosenTrain" tabindex="0" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import modalTrainMixin from '../../mixins/modalTrainMixin';
|
||||
import TrainInfo from './TrainInfo.vue';
|
||||
import TrainSchedule from './TrainSchedule.vue';
|
||||
import { Train } from '../../typings/common';
|
||||
|
||||
export default defineComponent({
|
||||
components: { TrainInfo, TrainSchedule },
|
||||
mixins: [modalTrainMixin],
|
||||
|
||||
computed: {
|
||||
chosenTrain() {
|
||||
return this.store.trainList.find((train) => train.modalId == this.store.chosenModalTrainId);
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
chosenTrain(train: Train | undefined) {
|
||||
this.$nextTick(() => {
|
||||
if (train) {
|
||||
document.body.classList.add('no-scroll');
|
||||
const contentEl = this.$refs['content'] as HTMLElement;
|
||||
contentEl.focus();
|
||||
} else {
|
||||
(this.store.modalLastClickedTarget as any)?.focus();
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.classList.remove('no-scroll');
|
||||
}, 90);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/responsive.scss';
|
||||
|
||||
.train-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
color: white;
|
||||
z-index: 200;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.modal-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
overflow-y: scroll;
|
||||
|
||||
width: 95vw;
|
||||
max-height: 95vh;
|
||||
max-height: 95dvh;
|
||||
margin-top: 1em;
|
||||
|
||||
background-color: #1a1a1a;
|
||||
box-shadow: 0 0 15px 10px #0e0e0e;
|
||||
}
|
||||
|
||||
@include midScreen {
|
||||
.exit {
|
||||
margin: 0.5em;
|
||||
|
||||
img {
|
||||
width: 1.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -15,12 +15,14 @@
|
||||
<div class="search_content">
|
||||
<div class="search-box">
|
||||
<input
|
||||
v-model="searchedTrain"
|
||||
class="search-input"
|
||||
ref="initFocusedElement"
|
||||
id="train-search"
|
||||
name="train-search"
|
||||
@focus="preventKeyDown = true"
|
||||
@blur="preventKeyDown = false"
|
||||
:placeholder="$t(`options.search-train`)"
|
||||
v-model="searchedTrain"
|
||||
/>
|
||||
<button class="search-exit">
|
||||
<img
|
||||
@@ -33,11 +35,13 @@
|
||||
|
||||
<div class="search-box">
|
||||
<input
|
||||
v-model="searchedDriver"
|
||||
class="search-input"
|
||||
id="driver-search"
|
||||
name="driver-search"
|
||||
@focus="preventKeyDown = true"
|
||||
@blur="preventKeyDown = false"
|
||||
:placeholder="$t(`options.search-driver`)"
|
||||
v-model="searchedDriver"
|
||||
/>
|
||||
<button class="search-exit">
|
||||
<img
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
v-else
|
||||
src="/images/icon-we4a.png"
|
||||
:title="$t('trains.we4a-tooltip')"
|
||||
width="12"
|
||||
width="10"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
@@ -84,7 +84,7 @@
|
||||
v-else
|
||||
src="/images/icon-we4a.png"
|
||||
:title="$t('trains.we4a-tooltip')"
|
||||
width="12"
|
||||
width="10"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
@@ -165,20 +165,18 @@ export default defineComponent({
|
||||
|
||||
computed: {
|
||||
scheduleStops(): TrainScheduleStop[] {
|
||||
let currentSceneryIndex = 0;
|
||||
if (!this.train.timetableData) return [];
|
||||
|
||||
const { timetablePath } = this.train.timetableData;
|
||||
let currentPathIndex = 0;
|
||||
|
||||
return (
|
||||
this.train.timetableData?.followingStops.map((stop, i, arr) => {
|
||||
const isExternal =
|
||||
i > 0 &&
|
||||
stop.arrivalLine != null &&
|
||||
(stop.arrivalLine != arr[i - 1].departureLine ||
|
||||
(stop.arrivalLine == arr[i - 1].departureLine &&
|
||||
!/-|_|(^it\d+)|(^sbl)/gi.test(stop.arrivalLine)));
|
||||
i < arr.length - 1 &&
|
||||
stop.departureLine === timetablePath[currentPathIndex].departureRouteExt;
|
||||
|
||||
if (isExternal) currentSceneryIndex++;
|
||||
|
||||
const sceneryName = this.train.timetableData!.sceneryNames[currentSceneryIndex];
|
||||
const sceneryName = timetablePath[currentPathIndex].stationName;
|
||||
const sceneryInfo = this.apiStore.sceneryData.find((st) => st.name == sceneryName);
|
||||
|
||||
const arrivalLineInfo = sceneryInfo?.routesInfo.find(
|
||||
@@ -189,6 +187,8 @@ export default defineComponent({
|
||||
(r) => r.routeName == stop.departureLine
|
||||
);
|
||||
|
||||
if (isExternal) currentPathIndex++;
|
||||
|
||||
return {
|
||||
nameHtml: stop.stopName,
|
||||
nameRaw: stop.stopNameRAW,
|
||||
@@ -249,7 +249,7 @@ export default defineComponent({
|
||||
i < this.train.timetableData!.followingStops.length;
|
||||
i++
|
||||
) {
|
||||
if (/po\.|sbl/gi.test(this.train.timetableData!.followingStops[i].stopNameRAW))
|
||||
if (/(, po$|sbl|, pe$)/gi.test(this.train.timetableData!.followingStops[i].stopNameRAW))
|
||||
activeMinorStopList.push(i);
|
||||
else break;
|
||||
}
|
||||
@@ -286,7 +286,7 @@ $blinkAnim: 0.5s ease-in-out alternate infinite blink;
|
||||
}
|
||||
|
||||
.train-schedule {
|
||||
padding: 0 1em;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.schedule-wrapper {
|
||||
@@ -523,8 +523,17 @@ $blinkAnim: 0.5s ease-in-out alternate infinite blink;
|
||||
}
|
||||
|
||||
.scenery-route {
|
||||
display: flex;
|
||||
gap: 0.25em;
|
||||
|
||||
span:nth-child(2) {
|
||||
display: flex;
|
||||
gap: 0.25em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
width: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<div class="dropdown" @keydown.esc="showOptions = false" @focusout="showOptions = false">
|
||||
<div
|
||||
class="dropdown"
|
||||
@keydown.esc="showOptions = false"
|
||||
v-click-outside="() => (showOptions = false)"
|
||||
>
|
||||
<div class="bg" v-if="showOptions" @click="showOptions = false"></div>
|
||||
|
||||
<button class="filter-button btn--filled btn--image" @click="toggleShowOptions" ref="button">
|
||||
@@ -19,21 +23,21 @@
|
||||
<div v-if="apiStore.dataStatuses.connection == Status.Loaded && regionTrains.length > 0">
|
||||
<div class="top-list general">
|
||||
<transition-group tag="ul" name="stats-anim">
|
||||
<li class="badge" key="timetable-count">
|
||||
<li class="badge stat-badge" key="timetable-count">
|
||||
<span>{{ $t('train-stats.timetable-count') }}</span>
|
||||
<span>
|
||||
<b>{{ regionTrainsWithTT.length }}</b>
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li class="badge" key="avg-speed">
|
||||
<li class="badge stat-badge" key="avg-speed">
|
||||
<span>{{ $t('train-stats.avg-speed') }}</span>
|
||||
<span>
|
||||
<b>{{ stats.avgSpeed.toFixed(1) }} km/h</b>
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li class="badge" key="avg-distance">
|
||||
<li class="badge stat-badge" key="avg-distance">
|
||||
<span>{{ $t('train-stats.avg-timetable') }}</span>
|
||||
<span>
|
||||
<b>{{ stats.avgDistance.toFixed(1) }} km</b>
|
||||
@@ -46,7 +50,7 @@
|
||||
<h3>{{ $t('train-stats.top-categories') }}</h3>
|
||||
|
||||
<transition-group tag="ul" name="stats-anim">
|
||||
<li class="badge" v-for="top in stats.topCategories" :key="top.name">
|
||||
<li class="badge stat-badge" v-for="top in stats.topCategories" :key="top.name">
|
||||
<span>{{ top.name }}</span>
|
||||
<span>{{ top.count }}</span>
|
||||
</li>
|
||||
@@ -61,7 +65,7 @@
|
||||
<h3>{{ $t('train-stats.top-vehicles') }}</h3>
|
||||
|
||||
<transition-group tag="ul" name="stats-anim">
|
||||
<li class="badge" v-for="top in stats.topVehicles" :key="top.name">
|
||||
<li class="badge stat-badge" v-for="top in stats.topVehicles" :key="top.name">
|
||||
<span>{{ top.name }}</span>
|
||||
<span>{{ top.count }}</span>
|
||||
</li>
|
||||
@@ -76,7 +80,7 @@
|
||||
<h3>{{ $t('train-stats.top-units') }}</h3>
|
||||
|
||||
<transition-group tag="ul" name="stats-anim">
|
||||
<li class="badge" v-for="top in stats.topUnits.slice(0, 7)" :key="top.name">
|
||||
<li class="badge stat-badge" v-for="top in stats.topUnits.slice(0, 7)" :key="top.name">
|
||||
<span>{{ top.name }}</span>
|
||||
<span>{{ top.count }}</span>
|
||||
</li>
|
||||
@@ -95,6 +99,8 @@
|
||||
<div class="no-data" v-else>
|
||||
{{ $t('train-stats.no-stats') }}
|
||||
</div>
|
||||
|
||||
<div tabindex="0" @focus="() => (showOptions = false)"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@@ -236,43 +242,12 @@ h3 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em;
|
||||
|
||||
// @include smallScreen {
|
||||
// justify-content: center;
|
||||
// }
|
||||
}
|
||||
|
||||
.badge {
|
||||
margin: 0;
|
||||
|
||||
& > span:first-child {
|
||||
background-color: $accentCol;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown_wrapper {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.stats-anim {
|
||||
&-move,
|
||||
&-enter-active,
|
||||
&-leave-active {
|
||||
transition: all 250ms ease;
|
||||
}
|
||||
|
||||
&-enter-from,
|
||||
&-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
&-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
h1,
|
||||
.no-data {
|
||||
|
||||
@@ -17,11 +17,10 @@
|
||||
class="train-row"
|
||||
v-for="train in trains"
|
||||
:key="train.id"
|
||||
tabindex="0"
|
||||
@click.stop="selectModalTrain(train, $event.currentTarget)"
|
||||
@keydown.enter="selectModalTrain(train, $event.currentTarget)"
|
||||
>
|
||||
<TrainInfo :train="train" :extended="false" />
|
||||
<router-link class="a-block" :to="train.driverRouteLocation">
|
||||
<TrainInfo :train="train" :extended="false" />
|
||||
</router-link>
|
||||
</li>
|
||||
</transition-group>
|
||||
</div>
|
||||
@@ -30,7 +29,6 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, inject, PropType, Ref } from 'vue';
|
||||
import modalTrainMixin from '../../mixins/modalTrainMixin';
|
||||
import { useMainStore } from '../../store/mainStore';
|
||||
import Loading from '../Global/Loading.vue';
|
||||
import TrainInfo from './TrainInfo.vue';
|
||||
@@ -47,8 +45,6 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
|
||||
mixins: [modalTrainMixin],
|
||||
|
||||
setup() {
|
||||
const store = useMainStore();
|
||||
const apiStore = useApiStore();
|
||||
@@ -86,10 +82,10 @@ export default defineComponent({
|
||||
@import '../../styles/animations.scss';
|
||||
|
||||
.train-table {
|
||||
position: relative;
|
||||
height: calc(100vh - 11em);
|
||||
min-height: 500px;
|
||||
|
||||
height: 90vh;
|
||||
min-height: 550px;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@@ -107,7 +103,5 @@ li.train-row {
|
||||
background-color: var(--clr-secondary);
|
||||
margin-bottom: 1em;
|
||||
width: 100%;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
+64
-5
@@ -48,7 +48,50 @@
|
||||
"footer": {
|
||||
"discord": "Stacjownik Discord server"
|
||||
},
|
||||
"categories": {
|
||||
"EI": "domestic express",
|
||||
"EC": "international express",
|
||||
"EN": "domestic night express",
|
||||
|
||||
"MP": "intervoivodeship bullet",
|
||||
"MO": "intervoivodeship regio",
|
||||
"MM": "international bullet",
|
||||
"MH": "intervoivodeship night bullet",
|
||||
|
||||
"RP": "voivodeship bullet",
|
||||
"RM": "international voivodeship regio",
|
||||
"RO": "voivodeship regio",
|
||||
"RA": "voivodeship regio (urban)",
|
||||
|
||||
"PW": "empty passenger",
|
||||
"PX": "empty passenger test drive",
|
||||
|
||||
"TC": "international freight (intermodal)",
|
||||
"TG": "international freight (organized cargo)",
|
||||
"TR": "international freight (unorganized cargo)",
|
||||
"TD": "domestic freight (intermodal)",
|
||||
"TM": "domestic freight (organized cargo)",
|
||||
"TN": "domestic freight (unorganized cargo)",
|
||||
"TK": "freight (for stations & sidings)",
|
||||
"TS": "empty freight test drive",
|
||||
"TH": "locomotive rolling stock (over 3 vehicles)",
|
||||
|
||||
"LT": "freight locomotive only",
|
||||
"LP": "passenger locomotive only",
|
||||
"LS": "shunting locomotive only",
|
||||
"LZ": "shunting locomotive only",
|
||||
|
||||
"ZN": "inspection / diagnostic type",
|
||||
"ZU": "other maintenance type",
|
||||
"ZG": "emergency (deprecated)",
|
||||
|
||||
"AP": "voivodeship regio (deprecated)",
|
||||
|
||||
"E": "electric loco",
|
||||
"J": "EMU",
|
||||
"S": "diesel loco",
|
||||
"M": "DMU"
|
||||
},
|
||||
"vehicle-preview": {
|
||||
"loading": "Loading preview...",
|
||||
"error": "Oops! The vehicle preview seems to be missing! :/"
|
||||
@@ -123,7 +166,7 @@
|
||||
"search-train": "Train no.",
|
||||
"search-driver": "Driver name",
|
||||
"search-dispatcher": "Dispatcher name",
|
||||
"search-station": "Scenery name",
|
||||
"search-station": "Scenery name / #",
|
||||
"search-author": "Timetable author name",
|
||||
"search-issuedFrom": "Issuing scenery name",
|
||||
"search-via": "Via scenery name",
|
||||
@@ -252,9 +295,11 @@
|
||||
"minTwoWay": "MIN. OTHER DOUBLE TRACK ROUTES"
|
||||
},
|
||||
|
||||
"sceneries-search": "SCENERY SEARCH:",
|
||||
"sceneries-placeholder": "Enter scenery name...",
|
||||
"authors-search": "SEARCH BY AUTHOR NAME (other filters apply):",
|
||||
"authors-placeholder": "Enter the author nickname...",
|
||||
"authors-button-title": "Search",
|
||||
"search-button-title": "SEARCH",
|
||||
|
||||
"minimum-hours-title": "SHOW ONLY SCENERIES UNTIL:",
|
||||
"now": "NOW",
|
||||
@@ -360,7 +405,15 @@
|
||||
"scenery-offline": "Offline ride",
|
||||
"timeout": "An error occured while trying to refresh SWDR timetable data!",
|
||||
|
||||
"journal-button": "DRIVER'S JOURNAL"
|
||||
"driver-journal-link": "DRIVER JOURNAL",
|
||||
"driver-return-link": "GO BACK",
|
||||
|
||||
"driver-not-found-header": "Train not found! :/",
|
||||
"driver-not-found-desc-1": "This train has already been terminated, changed its number or is offline.",
|
||||
"driver-not-found-desc-2": "You can browse timetable history in the",
|
||||
"driver-not-found-journal": "TIMETABLES JOURNAL",
|
||||
"driver-not-found-others": "Player {driver} is online as:",
|
||||
"driver-not-found-return": "GO BACK TO THE MAIN SITE"
|
||||
},
|
||||
"train-stats": {
|
||||
"stats-button": "STATISTICS",
|
||||
@@ -388,6 +441,7 @@
|
||||
|
||||
"no-further-data": "No further data for current parameters",
|
||||
"loading-further-data": "Loading...",
|
||||
|
||||
|
||||
"route-length": "Route length:",
|
||||
"station-count": "Stations:",
|
||||
@@ -405,11 +459,16 @@
|
||||
"minutes": "{value} min | {value} mins",
|
||||
"seconds": "{value} s",
|
||||
|
||||
"stock-info": "DETAILS",
|
||||
"entry-details": "DETAILS",
|
||||
"no-entry-details": "NO DETAILS AVAILABLE",
|
||||
|
||||
"stock-length": "Length",
|
||||
"stock-mass": "Mass",
|
||||
"stock-max-speed": "Max. speed",
|
||||
|
||||
"stock-dangers": "ADDITIONAL NOTES",
|
||||
"stock-preview": "STOCK PREVIEW",
|
||||
|
||||
"load-data": "Load further data...",
|
||||
|
||||
"last-seen-at": "Last seen at",
|
||||
@@ -512,7 +571,7 @@
|
||||
|
||||
"forum-topic": "Official {name} forum topic",
|
||||
|
||||
"pragotron-link": "Timetable pallet board (beta)",
|
||||
"pragotron-link": "Timetable pallet board",
|
||||
"tablice-link": "Timetable summary board (by Thundo)",
|
||||
|
||||
"bottom-info": "Show full history in the Journal tab"
|
||||
|
||||
+65
-6
@@ -45,6 +45,50 @@
|
||||
"footer": {
|
||||
"discord": "Serwer Discord Stacjownika"
|
||||
},
|
||||
"categories": {
|
||||
"EI": "ekspres krajowy",
|
||||
"EC": "ekspres międzynarodowy",
|
||||
"EN": "ekspres krajowy nocny",
|
||||
|
||||
"MP": "międzywojewódzki pospieszny",
|
||||
"MO": "międzywojewódzki osobowy",
|
||||
"MM": "międzynarodowy pospieszny",
|
||||
"MH": "międzywojewódzki pospieszny (nocny)",
|
||||
|
||||
"RP": "wojewódzki pospieszny",
|
||||
"RO": "wojewódzki osobowy",
|
||||
"RM": "wojewódzki osobowy międzynarodowy",
|
||||
"RA": "wojewódzki osobowy algomeracyjny",
|
||||
|
||||
"PW": "pasażerski próżny - służbowy",
|
||||
"PX": "pasażerski próżny próbny",
|
||||
|
||||
"TC": "towarowy międzynarodowy intermodalny",
|
||||
"TG": "towarowy międzynarodowy masowy",
|
||||
"TR": "towarowy międzynarodowy niemasowy",
|
||||
"TD": "towarowy krajowy intermodalny",
|
||||
"TM": "towarowy krajowy masowy",
|
||||
"TN": "towarowy krajowy niemasowy",
|
||||
"TK": "towarowy zdawczy",
|
||||
"TS": "towarowy próżny próbny",
|
||||
"TH": "skład lokomotyw (powyżej 3 pojazdów)",
|
||||
|
||||
"LT": "lokomotywa towarowa luzem",
|
||||
"LP": "lokomotywa pasażerska luzem",
|
||||
"LS": "lokomotywa manewrowa luzem",
|
||||
"LZ": "lokomotywa dla poc. utrzymaniowo-naprawczych",
|
||||
|
||||
"ZN": "inspekcyjny / diagnostyczny",
|
||||
"ZU": "inny utrzymaniowy",
|
||||
"ZG": "ratunkowy (kat. wycofana)",
|
||||
|
||||
"AP": "wojewódzki osobowy (kat. wycofana)",
|
||||
|
||||
"E": "elektrowóz",
|
||||
"J": "EZT",
|
||||
"S": "spalinowóz",
|
||||
"M": "SZT"
|
||||
},
|
||||
"vehicle-preview": {
|
||||
"loading": "Ładowanie podglądu...",
|
||||
"error": "Ups! Nie znaleziono podglądu pojazdu! :/"
|
||||
@@ -119,7 +163,7 @@
|
||||
"search-train": "Nr pociągu / #",
|
||||
"search-driver": "Nick maszynisty",
|
||||
"search-dispatcher": "Nick dyżurnego",
|
||||
"search-station": "Nazwa scenerii",
|
||||
"search-station": "Nazwa scenerii / #",
|
||||
"search-author": "Nick autora rozkładu jazdy",
|
||||
"search-issuedFrom": "Sceneria początkowa",
|
||||
"search-via": "Przez scenerię",
|
||||
@@ -249,9 +293,11 @@
|
||||
"minTwoWay": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)"
|
||||
},
|
||||
|
||||
"authors-search": "SZUKAJ AUTORA (uwzględnia inne filtry):",
|
||||
"sceneries-search": "WYSZUKAJ SCENERIĘ:",
|
||||
"sceneries-placeholder": "Wpisz nazwę scenerii...",
|
||||
"authors-search": "WYSZUKAJ AUTORA (uwzględnia inne filtry):",
|
||||
"authors-placeholder": "Wpisz nick autora...",
|
||||
"authors-button-title": "Szukaj",
|
||||
"search-button-title": "SZUKAJ",
|
||||
"minimum-hours-title": "POKAŻ TYLKO SCENERIE DOSTĘPNE MINIMUM DO:",
|
||||
"now": "TERAZ",
|
||||
"hour": " godz.",
|
||||
@@ -345,7 +391,15 @@
|
||||
|
||||
"timeout": "Wystąpił problem z aktualizacją rozkładów jazdy z SWDR",
|
||||
|
||||
"journal-button": "DZIENNIK MASZYNISTY"
|
||||
"driver-journal-link": "DZIENNIK MASZYNISTY",
|
||||
"driver-return-link": "POWRÓT",
|
||||
|
||||
"driver-not-found-header": "Nie znaleziono pociągu! :/",
|
||||
"driver-not-found-desc-1": "Ten pociąg prawdopodobnie zakończył już swój bieg, zmienił numer lub jest offline.",
|
||||
"driver-not-found-desc-2": "Historię rozkładów jazdy możesz przejrzeć w",
|
||||
"driver-not-found-journal": "DZIENNIKU RJ",
|
||||
"driver-not-found-others": "Gracz {driver} jest online jako:",
|
||||
"driver-not-found-return": "WRÓĆ NA STRONĘ GŁÓWNĄ"
|
||||
},
|
||||
"train-stats": {
|
||||
"stats-button": "STATYSTYKI",
|
||||
@@ -389,11 +443,16 @@
|
||||
"timetable-abandoned": "PORZUCONY",
|
||||
"timetable-online-button": "RJ ONLINE",
|
||||
|
||||
"stock-info": "SZCZEGÓŁY",
|
||||
"entry-details": "SZCZEGÓŁY",
|
||||
"no-entry-details": "BRAK DOSTĘPNYCH SZCZEGÓŁÓW",
|
||||
|
||||
"stock-length": "Długość",
|
||||
"stock-mass": "Masa",
|
||||
"stock-max-speed": "Prędkość maks.",
|
||||
|
||||
"stock-dangers": "DODATKOWE UWAGI",
|
||||
"stock-preview": "PODGLĄD SKŁADU",
|
||||
|
||||
"load-data": "Pobierz dalszą historię...",
|
||||
|
||||
"last-seen-at": "Ostatnio widziany na: ",
|
||||
@@ -495,7 +554,7 @@
|
||||
|
||||
"forum-topic": "Oficjalny wątek scenerii {name}",
|
||||
|
||||
"pragotron-link": "Paletowa tablica informacyjna (beta)",
|
||||
"pragotron-link": "Paletowa tablica informacyjna",
|
||||
"tablice-link": "Tablica informacyjna zbiorcza (autorstwa Thundo)",
|
||||
|
||||
"bottom-info": "Pokaż pełną historię w zakładce Dziennika"
|
||||
|
||||
@@ -57,6 +57,10 @@ export default defineComponent({
|
||||
: '';
|
||||
},
|
||||
|
||||
dateStringToTimestamp(dateString?: string) {
|
||||
return dateString ? new Date(dateString).getTime() : 0;
|
||||
},
|
||||
|
||||
calculateDuration(timestampMs: number, showSeconds = false) {
|
||||
const secondsTotal = Math.floor(timestampMs / 1000);
|
||||
const minsTotal = Math.round(timestampMs / 60000);
|
||||
@@ -70,8 +74,8 @@ export default defineComponent({
|
||||
minsInHour
|
||||
)}`
|
||||
: showSeconds && secondsTotal <= 60
|
||||
? this.$t('journal.seconds', { value: secondsTotal }, secondsTotal)
|
||||
: this.$t('journal.minutes', { value: minsTotal }, minsTotal);
|
||||
? this.$t('journal.seconds', { value: secondsTotal }, secondsTotal)
|
||||
: this.$t('journal.minutes', { value: minsTotal }, minsTotal);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import { useMainStore } from '../store/mainStore';
|
||||
import { useTooltipStore } from '../store/tooltipStore';
|
||||
import { Train } from '../typings/common';
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
store: useMainStore(),
|
||||
tooltipStore: useTooltipStore()
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
selectModalTrain(train: Train, target?: EventTarget | null) {
|
||||
this.store.chosenModalTrainId = train.modalId;
|
||||
if (target) this.store.modalLastClickedTarget = target;
|
||||
},
|
||||
|
||||
selectModalTrainById(modalId: string, target?: EventTarget | null) {
|
||||
this.store.chosenModalTrainId = modalId;
|
||||
if (target) this.store.modalLastClickedTarget = target;
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
this.store.chosenModalTrainId = undefined;
|
||||
this.tooltipStore.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
methods: {
|
||||
getCategoryExplanation(categoryCode: string) {
|
||||
const categoryKey = categoryCode.slice(0, 2);
|
||||
const vehicleTypeKey = categoryCode.slice(-1);
|
||||
|
||||
return `${this.$t('categories.' + categoryKey)}\n(${this.$t('categories.' + vehicleTypeKey)})`;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -75,18 +75,18 @@ export default defineComponent({
|
||||
return positionString.charAt(0).toUpperCase() + positionString.slice(1);
|
||||
},
|
||||
|
||||
displayStopList(stops: TrainStop[]): string | undefined {
|
||||
getTrainStopsHtml(stops: TrainStop[]): string {
|
||||
if (!stops) return '';
|
||||
|
||||
return stops
|
||||
.reduce((acc: string[], stop: TrainStop, i: number) => {
|
||||
if (stop.stopType.includes('ph') && !stop.stopNameRAW.includes('po.'))
|
||||
if (stop.stopType.includes('ph'))
|
||||
acc.push(
|
||||
`<strong style='color:${stop.confirmed ? 'springgreen' : 'white'}'>${
|
||||
stop.stopName
|
||||
}</strong>`
|
||||
);
|
||||
else if (i > 0 && i < stops.length - 1 && !/po\.|sbl/gi.test(stop.stopNameRAW))
|
||||
else if (i > 0 && i < stops.length - 1 && !/(, po$|sbl)/gi.test(stop.stopNameRAW))
|
||||
acc.push(
|
||||
`<span style='color:${stop.confirmed ? 'springgreen' : 'lightgray'}'>${
|
||||
stop.stopName
|
||||
@@ -108,16 +108,19 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
currentDelay(stops: TrainStop[]) {
|
||||
const delay =
|
||||
stops.find(
|
||||
(stop, i) =>
|
||||
(i == 0 && !stop.confirmed) || (i > 0 && stops[i - 1].confirmed && !stop.confirmed)
|
||||
)?.departureDelay || 0;
|
||||
const lastConfirmedStop = stops.find(
|
||||
(stop, i) =>
|
||||
(i == 0 && !stop.confirmed) ||
|
||||
(i > 0 && stops[i - 1].confirmed && !stop.confirmed) ||
|
||||
(stops[i + 1] == undefined && stop.confirmed)
|
||||
);
|
||||
|
||||
if (delay > 0)
|
||||
return `<span style='color: salmon'>${this.$t('trains.delayed')} ${delay} min</span>`;
|
||||
else if (delay < 0)
|
||||
return `<span style='color: lightgreen'>${this.$t('trains.preponed')} ${delay} min</span>`;
|
||||
const lastDelay = lastConfirmedStop?.departureDelay ?? lastConfirmedStop?.arrivalDelay ?? 0;
|
||||
|
||||
if (lastDelay > 0)
|
||||
return `<span style='color: salmon'>${this.$t('trains.delayed')} ${lastDelay} min</span>`;
|
||||
else if (lastDelay < 0)
|
||||
return `<span style='color: lightgreen'>${this.$t('trains.preponed')} ${lastDelay} min</span>`;
|
||||
else return this.$t('trains.on-time');
|
||||
},
|
||||
|
||||
|
||||
+15
-1
@@ -20,6 +20,15 @@ const routes: Array<RouteRecordRaw> = [
|
||||
region: route.query.region
|
||||
})
|
||||
},
|
||||
{
|
||||
path: '/driver',
|
||||
name: 'DriverView',
|
||||
component: () => import('../views/DriverView.vue'),
|
||||
props: (route) => ({
|
||||
trainId: route.query.trainId,
|
||||
modalId: route.query.modalId
|
||||
})
|
||||
},
|
||||
{
|
||||
path: '/scenery',
|
||||
name: 'SceneryView',
|
||||
@@ -57,7 +66,12 @@ const routes: Array<RouteRecordRaw> = [
|
||||
|
||||
const router = createRouter({
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
if (to.name == 'SceneryView' && from.name !== to.name && from.query['view'] === undefined)
|
||||
if (
|
||||
(to.name == 'SceneryView' || to.name == 'DriverView') &&
|
||||
from.name !== to.name &&
|
||||
from.query['view'] === undefined &&
|
||||
!savedPosition
|
||||
)
|
||||
return { el: `.app_main`, top: -15 };
|
||||
|
||||
if (savedPosition) return savedPosition;
|
||||
|
||||
+27
-33
@@ -18,7 +18,8 @@ export const useApiStore = defineStore('apiStore', {
|
||||
donatorsData: [] as API.Donators.Response,
|
||||
sceneryData: [] as StationJSONData[],
|
||||
|
||||
lastFetchData: new Date(),
|
||||
nextUpdateTime: 0,
|
||||
nextDataCheckTime: 0,
|
||||
|
||||
client: undefined as AxiosInstance | undefined,
|
||||
|
||||
@@ -48,38 +49,41 @@ export const useApiStore = defineStore('apiStore', {
|
||||
},
|
||||
|
||||
async connectToAPI() {
|
||||
// Static data
|
||||
this.fetchDonatorsData();
|
||||
this.fetchStationsGeneralInfo();
|
||||
window.requestAnimationFrame(this.updateTick);
|
||||
},
|
||||
|
||||
// Ponowne pobieranie danych po ServiceWorkerze
|
||||
setTimeout(() => {
|
||||
this.fetchStationsGeneralInfo();
|
||||
}, Math.floor(Math.random() * 500) + 1000);
|
||||
updateTick(t: number) {
|
||||
if (this.dataStatuses.connection == Status.Data.Offline) return;
|
||||
|
||||
this.fetchVehiclesInfo();
|
||||
// Static data refresh
|
||||
if (t >= this.nextDataCheckTime) {
|
||||
this.fetchDonatorsData();
|
||||
this.fetchVehiclesInfo();
|
||||
|
||||
// Revalidation after staling
|
||||
this.fetchStationsGeneralInfo().then(() => {
|
||||
this.fetchStationsGeneralInfo();
|
||||
});
|
||||
|
||||
this.nextDataCheckTime = t + 3600000;
|
||||
}
|
||||
|
||||
// Active data fefresh
|
||||
if (t >= this.nextUpdateTime) {
|
||||
this.fetchActiveData();
|
||||
this.nextUpdateTime = t + 20000;
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(this.updateTick);
|
||||
},
|
||||
|
||||
async fetchActiveData() {
|
||||
if (import.meta.env.VITE_API_ACTIVE_DATA_MODE == 'mocking') {
|
||||
import('../../tests/data/getActiveData.json').then((data) => {
|
||||
console.warn('activeData: mocking mode');
|
||||
this.activeData = data.default as API.ActiveData.Response;
|
||||
this.lastFetchData = new Date();
|
||||
|
||||
this.dataStatuses.connection = Status.Data.Loaded;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading;
|
||||
|
||||
try {
|
||||
const response = await this.client!.get<API.ActiveData.Response>('api/getActiveData');
|
||||
|
||||
this.activeData = response.data;
|
||||
this.lastFetchData = new Date();
|
||||
this.dataStatuses.connection = Status.Data.Loaded;
|
||||
} catch (error) {
|
||||
this.dataStatuses.connection = Status.Data.Error;
|
||||
@@ -100,7 +104,7 @@ export const useApiStore = defineStore('apiStore', {
|
||||
async fetchStationsGeneralInfo() {
|
||||
try {
|
||||
const sceneryData: StationJSONData[] = (
|
||||
await this.client!.get<StationJSONData[]>('api/getSceneries')
|
||||
await this.client!.get<StationJSONData[]>(`api/getSceneries`)
|
||||
).data;
|
||||
|
||||
this.dataStatuses.sceneries = Status.Data.Loaded;
|
||||
@@ -112,16 +116,6 @@ export const useApiStore = defineStore('apiStore', {
|
||||
},
|
||||
|
||||
async fetchVehiclesInfo() {
|
||||
// if (import.meta.env.VITE_API_VEHICLES_MODE == 'mocking') {
|
||||
// import('../../tests/data/vehicles.json').then((data) => {
|
||||
// console.warn('vehicles.json: mocking mode');
|
||||
// this.vehiclesData = data.default;
|
||||
// this.dataStatuses.vehicles = Status.Data.Loaded;
|
||||
// });
|
||||
|
||||
// return;
|
||||
// }
|
||||
|
||||
try {
|
||||
const response = await this.client!.get<API.Vehicles.Response>('api/getVehicles');
|
||||
|
||||
|
||||
+22
-16
@@ -50,15 +50,6 @@ export const useMainStore = defineStore('mainStore', {
|
||||
|
||||
const timetable = train.timetable;
|
||||
|
||||
const sceneryNames =
|
||||
train.timetable?.sceneries?.map(
|
||||
(sceneryHash) =>
|
||||
apiStore.activeData?.activeSceneries?.find((st) => st.stationHash === sceneryHash)
|
||||
?.stationName ??
|
||||
apiStore.sceneryData.find((sd) => sd.hash === sceneryHash)?.name ??
|
||||
sceneryHash
|
||||
) ?? [];
|
||||
|
||||
const trainObj = {
|
||||
id: train.id,
|
||||
modalId: `${train.driverName}${train.trainNo}`, // simplified id for train modal
|
||||
@@ -86,6 +77,13 @@ export const useMainStore = defineStore('mainStore', {
|
||||
isSupporter: train.driverIsSupporter,
|
||||
driverLevel: train.driverLevel,
|
||||
|
||||
driverRouteLocation: {
|
||||
name: 'DriverView',
|
||||
query: {
|
||||
trainId: train.id
|
||||
}
|
||||
},
|
||||
|
||||
timetableData: timetable
|
||||
? {
|
||||
timetableId: timetable.timetableId,
|
||||
@@ -96,7 +94,8 @@ export const useMainStore = defineStore('mainStore', {
|
||||
followingStops: timetable.stopList,
|
||||
routeDistance: timetable.stopList[timetable.stopList.length - 1].stopDistance,
|
||||
sceneries: timetable.sceneries,
|
||||
sceneryNames: sceneryNames.reverse(),
|
||||
warningNotes: timetable.warningNotes,
|
||||
|
||||
timetablePath: timetable.path.split(';').map((pathElementString) => {
|
||||
const [arrival, station, departure] = pathElementString.split(',');
|
||||
|
||||
@@ -104,7 +103,7 @@ export const useMainStore = defineStore('mainStore', {
|
||||
arrivalRouteExt: arrival,
|
||||
departureRouteExt: departure,
|
||||
stationName: station.split(' ').slice(0, -1).join(' '),
|
||||
stationHash: station.split(' ').slice(-1).join(' ')
|
||||
stationHash: station.split(' ').slice(-1).join(' ').replace('.sc', '')
|
||||
};
|
||||
})
|
||||
}
|
||||
@@ -169,12 +168,15 @@ export const useMainStore = defineStore('mainStore', {
|
||||
const offlineActiveSceneries = this.trainList.reduce((acc, train) => {
|
||||
if (!train.timetableData) return acc;
|
||||
|
||||
train.timetableData.sceneryNames.forEach((name) => {
|
||||
train.timetableData.timetablePath.forEach((p) => {
|
||||
if (
|
||||
acc.findIndex((v) => v.name == name && v.region == train.region) != -1 ||
|
||||
acc.findIndex(
|
||||
(v) =>
|
||||
(v.name == p.stationName || v.hash == p.stationHash) && v.region == train.region
|
||||
) != -1 ||
|
||||
apiStore.activeData?.activeSceneries?.findIndex(
|
||||
(sc) =>
|
||||
sc.stationName === name &&
|
||||
(sc.stationName == p.stationName || sc.stationHash == p.stationHash) &&
|
||||
sc.region == train.region &&
|
||||
Date.now() - sc.lastSeen < 1000 * 60 * 2
|
||||
) != -1
|
||||
@@ -182,7 +184,7 @@ export const useMainStore = defineStore('mainStore', {
|
||||
return acc;
|
||||
|
||||
acc.push({
|
||||
name: name,
|
||||
name: p.stationName,
|
||||
hash: '',
|
||||
region: train.region,
|
||||
maxUsers: 0,
|
||||
@@ -281,7 +283,11 @@ export const useMainStore = defineStore('mainStore', {
|
||||
if (!scheduledTrains) return;
|
||||
|
||||
scheduledTrains.forEach(({ train, checkpointStop, timetablePathElement, ...v }) => {
|
||||
if (scenery.name != timetablePathElement.stationName) return;
|
||||
if (
|
||||
scenery.name != timetablePathElement.stationName &&
|
||||
scenery.hash != timetablePathElement.stationHash
|
||||
)
|
||||
return;
|
||||
|
||||
scenery.scheduledTrains.push({ train, checkpointStop, timetablePathElement, ...v });
|
||||
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
@import 'responsive.scss';
|
||||
@import 'animations.scss';
|
||||
|
||||
.journal-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
text-align: left;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.list_wrapper {
|
||||
overflow-y: auto;
|
||||
height: 90vh;
|
||||
min-height: 650px;
|
||||
height: calc(100vh - 12.5em);
|
||||
min-height: 500px;
|
||||
margin-top: 0.5em;
|
||||
position: relative;
|
||||
|
||||
@@ -12,7 +20,7 @@
|
||||
}
|
||||
|
||||
.journal_wrapper {
|
||||
max-width: 1500px;
|
||||
max-width: var(--max-container-width);
|
||||
width: 100%;
|
||||
|
||||
margin: 0 auto;
|
||||
@@ -38,16 +46,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.journal_item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.journal_item,
|
||||
.journal_warning {
|
||||
background-color: #1a1a1a;
|
||||
padding: 1em;
|
||||
margin-bottom: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.journal_top-bar {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@import 'variables.scss';
|
||||
@import 'responsive.scss';
|
||||
@import 'badge.scss';
|
||||
|
||||
.stats-tab {
|
||||
position: absolute;
|
||||
@@ -7,7 +8,6 @@
|
||||
z-index: 99;
|
||||
|
||||
transform: translateY(1em);
|
||||
|
||||
width: 100%;
|
||||
|
||||
background-color: #1a1a1a;
|
||||
@@ -29,26 +29,10 @@ hr.section-separator {
|
||||
.info-stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.stat-badge {
|
||||
display: flex;
|
||||
|
||||
span {
|
||||
background-color: $accentCol;
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
padding: 0.2em 0.5em;
|
||||
}
|
||||
|
||||
span:first-child {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.journal-stats {
|
||||
text-align: center;
|
||||
|
||||
@@ -41,11 +41,11 @@ $animType: ease-in-out;
|
||||
}
|
||||
|
||||
&-enter-active {
|
||||
transition: all $animDuration ease-out;
|
||||
transition: all $animDuration ease-in-out;
|
||||
}
|
||||
|
||||
&-leave-active {
|
||||
transition: all $animDuration ease-out;
|
||||
transition: all $animDuration ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-3
@@ -1,3 +1,6 @@
|
||||
@import 'variables.scss';
|
||||
@import 'responsive.scss';
|
||||
|
||||
.badge {
|
||||
font-weight: 600;
|
||||
|
||||
@@ -78,14 +81,12 @@
|
||||
.train-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
gap: 0.3em;
|
||||
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 0.2em;
|
||||
font-weight: bold;
|
||||
|
||||
font-size: 0.9em;
|
||||
|
||||
&.twr {
|
||||
background-color: var(--clr-twr);
|
||||
box-shadow: 0 0 5px 1px var(--clr-twr);
|
||||
@@ -114,3 +115,13 @@
|
||||
background-color: #007599;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-badge {
|
||||
margin: 0;
|
||||
color: white;
|
||||
|
||||
& > span:first-child {
|
||||
background-color: $accentCol;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
+32
-4
@@ -124,10 +124,12 @@ input {
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a:not(.a-block):not(.a-button):not(.a-row) {
|
||||
display: inline-block;
|
||||
|
||||
transition: color 0.3s;
|
||||
|
||||
@@ -138,6 +140,14 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
a.a-block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
a.a-row {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
@@ -184,6 +194,7 @@ a.a-button {
|
||||
color: white;
|
||||
background: none;
|
||||
border-radius: 0.25em;
|
||||
text-decoration: none;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -214,7 +225,11 @@ a.a-button {
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
background-color: #555;
|
||||
background-color: #424242;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,6 +305,7 @@ a.a-button {
|
||||
// Basic tooltip
|
||||
[data-tooltip] {
|
||||
cursor: help;
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
[data-tooltip]:hover::after,
|
||||
@@ -308,6 +324,10 @@ a.a-button {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.tooltip-help {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5em;
|
||||
@@ -329,3 +349,11 @@ a.a-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.g-separator {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: #aaa;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
+18
-11
@@ -4,6 +4,12 @@ export enum APIDataStatus {
|
||||
OK = 'OK',
|
||||
WARNING = 'WARNING'
|
||||
}
|
||||
|
||||
export interface APICache {
|
||||
lastModified: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export namespace API {
|
||||
export namespace ActiveData {
|
||||
export interface APIStatuses {
|
||||
@@ -17,6 +23,7 @@ export namespace API {
|
||||
activeSceneries?: API.ActiveSceneries.Response;
|
||||
trains?: API.ActiveTrains.Response;
|
||||
apiStatuses?: APIStatuses;
|
||||
caches: APICache[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +31,7 @@ export namespace API {
|
||||
export type Response = Data[];
|
||||
|
||||
export interface Data {
|
||||
id: string;
|
||||
id: number;
|
||||
currentDuration: number;
|
||||
dispatcherId: number;
|
||||
dispatcherName: string;
|
||||
@@ -197,6 +204,7 @@ export namespace API {
|
||||
sceneries: string[];
|
||||
|
||||
path: string;
|
||||
warningNotes: string | null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,8 +247,6 @@ export namespace API {
|
||||
authorName?: string;
|
||||
authorId?: number;
|
||||
|
||||
stopsString?: string;
|
||||
|
||||
stockString?: string;
|
||||
stockHistory: string[];
|
||||
|
||||
@@ -248,17 +254,18 @@ export namespace API {
|
||||
stockLength?: number;
|
||||
maxSpeed?: number;
|
||||
|
||||
hashesString?: string;
|
||||
currentSceneryName?: string;
|
||||
currentSceneryHash?: string;
|
||||
routeSceneries?: string;
|
||||
checkpointArrivals?: string[];
|
||||
checkpointDepartures?: string[];
|
||||
checkpointArrivalsScheduled?: string[];
|
||||
checkpointDeparturesScheduled?: string[];
|
||||
checkpointStopTypes?: string[];
|
||||
visitedSceneries?: string[];
|
||||
routeSceneries: string;
|
||||
checkpointArrivals: string[];
|
||||
checkpointDepartures: string[];
|
||||
checkpointArrivalsScheduled: string[];
|
||||
checkpointDeparturesScheduled: string[];
|
||||
checkpointStopTypes: string[];
|
||||
visitedSceneries: string[];
|
||||
sceneryNames: string[];
|
||||
path: string;
|
||||
warningNotes: string | null;
|
||||
}
|
||||
|
||||
export type Response = Data[];
|
||||
|
||||
+18
-12
@@ -1,3 +1,5 @@
|
||||
import { RouteLocationRaw } from 'vue-router';
|
||||
|
||||
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
|
||||
export type ScenerySpawnType = 'passenger' | 'freight' | 'loco' | 'all';
|
||||
|
||||
@@ -70,18 +72,22 @@ export interface Train {
|
||||
isTimeout: boolean;
|
||||
isSupporter: boolean;
|
||||
|
||||
timetableData?: {
|
||||
timetableId: number;
|
||||
category: string;
|
||||
route: string;
|
||||
followingStops: TrainStop[];
|
||||
TWR: boolean;
|
||||
SKR: boolean;
|
||||
routeDistance: number;
|
||||
sceneries: string[];
|
||||
sceneryNames: string[];
|
||||
timetablePath: TimetablePathElement[];
|
||||
};
|
||||
driverRouteLocation: RouteLocationRaw;
|
||||
|
||||
timetableData?: TrainTimetableData;
|
||||
}
|
||||
|
||||
export interface TrainTimetableData {
|
||||
timetableId: number;
|
||||
category: string;
|
||||
route: string;
|
||||
followingStops: TrainStop[];
|
||||
TWR: boolean;
|
||||
SKR: boolean;
|
||||
routeDistance: number;
|
||||
sceneries: string[];
|
||||
timetablePath: TimetablePathElement[];
|
||||
warningNotes: string | null;
|
||||
}
|
||||
|
||||
export interface Station {
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<section class="driver-view">
|
||||
<div class="view-wrapper">
|
||||
<div v-if="chosenTrain">
|
||||
<div class="actions">
|
||||
<a class="a-button btn--image" @click="$router.back()">
|
||||
<img src="/images/icon-back.svg" alt="train icon" />
|
||||
<span>
|
||||
{{ $t('trains.driver-return-link') }}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<router-link
|
||||
:to="`/journal/timetables?search-driver=${chosenTrain.driverName}`"
|
||||
class="a-button btn--image"
|
||||
>
|
||||
<span class="hidable">
|
||||
{{ $t('trains.driver-journal-link') }}
|
||||
</span>
|
||||
<img src="/images/icon-train.svg" alt="train icon" />
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="train-card">
|
||||
<TrainInfo :train="chosenTrain" :extended="true" ref="trainInfo" />
|
||||
<TrainSchedule :train="chosenTrain" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Loading v-else-if="apiStore.dataStatuses.connection == Status.Data.Loading" />
|
||||
|
||||
<div v-else class="driver-not-found">
|
||||
<h2>⦻ {{ $t('trains.driver-not-found-header') }}</h2>
|
||||
|
||||
<p class="text--grayed">
|
||||
{{ $t('trains.driver-not-found-desc-1') }} <br />
|
||||
{{ $t('trains.driver-not-found-desc-2') }}
|
||||
<router-link to="/journal/timetables"
|
||||
>{{ $t('trains.driver-not-found-journal') }} </router-link
|
||||
>!
|
||||
</p>
|
||||
|
||||
<p v-if="props.trainId && otherDriverTrains.length > 0">
|
||||
<i18n-t keypath="trains.driver-not-found-others">
|
||||
<template v-slot:driver>
|
||||
<b>{{ otherDriverTrains[0].driverName }}</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</p>
|
||||
|
||||
<div class="other-driver-trains">
|
||||
<template v-for="(train, i) in otherDriverTrains">
|
||||
<router-link :to="`/driver?trainId=${train.id}`">
|
||||
{{ train.trainNo }}
|
||||
| {{ regionsJSON.find((r) => r.id == train.region)?.name ?? 'PL1' }}
|
||||
</router-link>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1em">
|
||||
<router-link to="/"><< {{ $t('trains.driver-not-found-return') }}</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import TrainInfo from '../components/TrainsView/TrainInfo.vue';
|
||||
import TrainSchedule from '../components/TrainsView/TrainSchedule.vue';
|
||||
import Loading from '../components/Global/Loading.vue';
|
||||
import { useMainStore } from '../store/mainStore';
|
||||
import { useApiStore } from '../store/apiStore';
|
||||
import { Status } from '../typings/common';
|
||||
import { regions as regionsJSON } from '../data/options.json';
|
||||
|
||||
const props = defineProps({
|
||||
trainId: {
|
||||
type: String
|
||||
},
|
||||
|
||||
modalId: {
|
||||
type: String
|
||||
}
|
||||
});
|
||||
|
||||
const mainStore = useMainStore();
|
||||
const apiStore = useApiStore();
|
||||
|
||||
const chosenTrain = computed(() =>
|
||||
mainStore.trainList.find((train) => train.id == props.trainId || train.modalId == props.modalId)
|
||||
);
|
||||
|
||||
const otherDriverTrains = computed(() => {
|
||||
return mainStore.trainList.filter(
|
||||
(train) =>
|
||||
train.driverId == Number(props.trainId?.split('|')[0]) &&
|
||||
(train.timetableData || train.online || train.lastSeen >= Date.now() - 60000)
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../styles/responsive';
|
||||
|
||||
$viewBgCol: #1a1a1a;
|
||||
|
||||
.driver-view {
|
||||
margin: 0 auto;
|
||||
padding: 1em 0;
|
||||
max-width: var(--max-container-width);
|
||||
min-height: calc(100vh - 7em);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.actions > a {
|
||||
background-color: $viewBgCol;
|
||||
padding: 0.5em;
|
||||
border-radius: 0.5em 0.5em 0 0;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($viewBgCol, 10);
|
||||
}
|
||||
}
|
||||
|
||||
.train-card {
|
||||
background-color: $viewBgCol;
|
||||
border-radius: 0 0 0.5em 0.5em;
|
||||
}
|
||||
|
||||
.driver-not-found {
|
||||
background-color: $viewBgCol;
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
border-radius: 0.5em 0.5em;
|
||||
|
||||
p {
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.other-driver-trains {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.actions > a > span.hidable {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -267,7 +267,10 @@ export default defineComponent({
|
||||
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
|
||||
|
||||
if (dispatcher) queries.push(`dispatcherName=${dispatcher}`);
|
||||
if (station) queries.push(`stationName=${station}`);
|
||||
|
||||
if (station.startsWith("#")) queries.push(`stationHash=${station.slice(1)}`);
|
||||
else if (station.length > 0) queries.push(`stationName=${station}`);
|
||||
|
||||
if (timestampFrom && timestampTo)
|
||||
queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ import { defineComponent, provide, reactive, Ref, ref } from 'vue';
|
||||
|
||||
import dateMixin from '../mixins/dateMixin';
|
||||
import routerMixin from '../mixins/routerMixin';
|
||||
import modalTrainMixin from '../mixins/modalTrainMixin';
|
||||
|
||||
import JournalOptions from '../components/JournalView/JournalOptions.vue';
|
||||
import JournalStats from '../components/JournalView/JournalStats.vue';
|
||||
@@ -148,7 +147,7 @@ export default defineComponent({
|
||||
JournalHeader,
|
||||
JournalTimetablesList
|
||||
},
|
||||
mixins: [dateMixin, routerMixin, modalTrainMixin],
|
||||
mixins: [dateMixin, routerMixin],
|
||||
|
||||
name: 'JournalTimetables',
|
||||
|
||||
@@ -307,14 +306,6 @@ export default defineComponent({
|
||||
this.searchersValues[v as Journal.TimetableSearchKey] = options[v] ?? '';
|
||||
});
|
||||
|
||||
// this.searchersValues['search-date'] = options['search-date'] ?? '';
|
||||
// this.searchersValues['search-driver'] = options['search-driver'] ?? '';
|
||||
// this.searchersValues['search-train'] = options['search-train'] ?? '';
|
||||
// this.searchersValues['search-dispatcher'] = options['search-dispatcher'] ?? '';
|
||||
// this.searchersValues['search-issuedFrom'] = options['search-issuedFrom'] ?? '';
|
||||
// this.searchersValues['search-via'] = options['search-via'] ?? '';
|
||||
// this.searchersValues['search-terminatingAt'] = options['search-terminatingAt'] ?? '';
|
||||
|
||||
this.sorterActive.id =
|
||||
(options['sorter-active'] as Journal.TimetableSorterKey) ?? 'timetableId';
|
||||
|
||||
@@ -462,7 +453,7 @@ export default defineComponent({
|
||||
this.timetableHistory = responseData;
|
||||
|
||||
// Stats display
|
||||
this.store.driverStatsName =
|
||||
this.mainStore.driverStatsName =
|
||||
this.timetableHistory.length > 0 && this.searchersValues['search-driver'].trim()
|
||||
? this.timetableHistory[0].driverName
|
||||
: '';
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
:station="stationInfo"
|
||||
:onlineScenery="onlineSceneryInfo"
|
||||
/>
|
||||
|
||||
<SceneryInfo :station="stationInfo" :onlineScenery="onlineSceneryInfo" />
|
||||
</div>
|
||||
|
||||
@@ -229,10 +230,11 @@ button.back-btn {
|
||||
overflow: auto;
|
||||
|
||||
background-color: #181818;
|
||||
border-radius: 0.5em;
|
||||
padding: 1em 0.5em;
|
||||
|
||||
height: calc(100vh - 0.5em);
|
||||
min-height: 800px;
|
||||
min-height: 500px;
|
||||
max-height: 2000px;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
ref="filterCardRef"
|
||||
/>
|
||||
|
||||
<StationStats />
|
||||
|
||||
<button
|
||||
class="btn-donation btn--image"
|
||||
ref="btn"
|
||||
@@ -21,7 +23,6 @@
|
||||
|
||||
<DonationCard :is-card-open="isDonationCardOpen" @toggle-card="toggleDonationCard" />
|
||||
<StationTable @toggle-donation-card="toggleDonationCard" />
|
||||
<StationStats />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -96,14 +97,16 @@ export default defineComponent({
|
||||
|
||||
.stations-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em;
|
||||
|
||||
position: relative;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
button.btn-donation {
|
||||
margin-left: auto;
|
||||
|
||||
$btnColor: #254069;
|
||||
|
||||
background-color: $btnColor;
|
||||
@@ -118,4 +121,8 @@ button.btn-donation {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.count {
|
||||
padding: 0.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
import { computed, ComputedRef, defineComponent, provide, reactive, ref, watch } from 'vue';
|
||||
import TrainOptions from '../components/TrainsView/TrainOptions.vue';
|
||||
import TrainTable from '../components/TrainsView/TrainTable.vue';
|
||||
import modalTrainMixin from '../mixins/modalTrainMixin';
|
||||
import { useMainStore } from '../store/mainStore';
|
||||
import { TrainFilter, trainFilters } from '../components/TrainsView/typings';
|
||||
import { filteredTrainList } from '../managers/trainFilterManager';
|
||||
@@ -33,8 +32,6 @@ export default defineComponent({
|
||||
TrainStats
|
||||
},
|
||||
|
||||
mixins: [modalTrainMixin],
|
||||
|
||||
props: {
|
||||
train: {
|
||||
type: String,
|
||||
@@ -102,16 +99,16 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
activated() {
|
||||
// Backwards compatibility with external links leading to train modal
|
||||
if (this.trainId) {
|
||||
this.$router.replace(`/driver?modalId=${this.trainId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.train) {
|
||||
this.searchedTrain = this.train;
|
||||
this.searchedDriver = this.driver || '';
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.trainId) {
|
||||
this.selectModalTrainById(this.trainId);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -120,13 +117,12 @@ export default defineComponent({
|
||||
@import '../styles/responsive.scss';
|
||||
|
||||
.trains-view {
|
||||
min-height: 600px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.trains_wrapper {
|
||||
margin: 1rem auto;
|
||||
max-width: 1500px;
|
||||
max-width: var(--max-container-width);
|
||||
}
|
||||
|
||||
.trains_topbar {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user