Compare commits

..

86 Commits

Author SHA1 Message Date
Spythere af9073ab98 Merge do wersji 1.17.1
Wersja 1.17.1
2023-10-03 21:58:23 +02:00
Spythere 800fc35e63 pliki lock 2023-10-03 21:53:59 +02:00
Spythere d5649d221b hotfixy 2023-10-03 21:51:00 +02:00
Spythere 5b35fac512 miniaturki c.d. 2023-10-03 21:31:06 +02:00
Spythere d5bc90f668 mock data 2023-10-02 22:09:27 +02:00
Spythere 6d663886f0 bump wersji 2023-10-02 22:06:58 +02:00
Spythere 85a1a0216e poprawki miniatur 2023-10-02 22:05:54 +02:00
Spythere 4ac054e947 miniaturki 2EN57 2023-10-01 15:11:53 +02:00
Spythere ba70fa1316 miniaturki pojazdów c.d. 2023-10-01 15:08:01 +02:00
Spythere 77e6b20d0c obsługa niezaładowanych miniaturek pojazdów 2023-10-01 01:41:14 +02:00
Spythere f60263c923 mocking danych 2023-10-01 00:23:17 +02:00
Spythere 6aec1a75c9 cleanup c.d. 2023-09-29 16:49:37 +02:00
Spythere d28d600833 code cleanup dziennika 2023-09-29 03:03:59 +02:00
Spythere a353eb3185 Meta tagi; aktualizacja paczek
Meta tagi; aktualizacja paczek
2023-09-26 14:44:48 +02:00
Spythere c5735a6953 pliki lock 2023-09-26 14:42:11 +02:00
Spythere 7930f7fc8a package.json 2023-09-26 14:36:01 +02:00
Spythere 68f4d54619 index: meta tagi 2023-09-26 14:33:58 +02:00
Spythere c4f9738589 Merge #54 (hotfix)
hotfix: wersja WS
2023-09-07 15:21:28 +02:00
Spythere dd916afd1d hotfix: wersja WS 2023-09-07 15:20:37 +02:00
Spythere ea5c9e0028 Merge do wersji 1.17.0
Wersja 1.17.0
2023-09-06 15:43:52 +02:00
Spythere eb7c2d7132 revamp postoju i strzałek w RJ scenerii 2023-09-05 16:48:26 +02:00
Spythere ee7c50f59b szybkie filtry (wip) 2023-09-05 16:10:38 +02:00
Spythere 439f59fedc poprawki filtrów scenerii 2023-09-05 15:40:32 +02:00
Spythere c47d839ce3 poprawki kolorów 2023-09-04 18:50:12 +02:00
Spythere f77c13cbcf srjp: poprawki dostępności modalu 2023-09-04 18:37:52 +02:00
Spythere dbbbd33100 poprawki dzienników 2023-09-03 19:24:32 +02:00
Spythere 14d13360a8 dziennik dr: animacje 2023-09-03 18:47:01 +02:00
Spythere dc862252ba dziennik dr: kolumna regionów 2023-09-03 18:34:45 +02:00
Spythere e5fe727ccd aktualizacja URLi 2023-09-03 17:58:55 +02:00
Spythere e836bbed0c szybkie filtry (wip) 2023-09-02 19:36:49 +02:00
Spythere d4438fd215 station filters active indicator 2023-09-02 18:53:23 +02:00
Spythere 1550849360 dzienniki 2023-09-02 18:47:01 +02:00
Spythere 9d1dc4ffca bump 1.17 2023-08-31 22:46:41 +02:00
Spythere 0397fa788d zmiana wyglądu listy dzienników 2023-08-31 22:45:14 +02:00
Spythere 6e5696b0a6 daily stats hotfix 2023-08-31 21:46:05 +02:00
Spythere 4537341a57 odświeżony wygląd dziennika RJ 2023-08-30 20:31:40 +02:00
Spythere c35c74bd4a Merge pull request #52
fix: data wcześniejszego przyjazdu dla 0pt
2023-08-27 23:07:01 +02:00
Spythere 25735c5e6e fix: data wcześniejszego przyjazdu dla 0pt 2023-08-27 22:45:36 +02:00
Spythere 41e60bc69e Merge do wersji 1.16.3 2023-07-06 01:54:21 +02:00
Spythere 933bdecb3c bump: 1.16.3 2023-07-02 14:50:55 +02:00
Spythere 10e183d96b zamiana infinite scrolla na przyciski 2023-07-02 14:50:18 +02:00
Spythere 5429d39f5e tłumaczenia historii dr scenerii 2023-07-01 23:39:08 +02:00
Spythere ff31e7f903 bump: 1.16.2.1 2023-07-01 23:29:48 +02:00
Spythere 91f4c6bc57 Merge do wersji 1.16.2 2023-07-01 01:25:10 +02:00
Spythere c133eb060b bump: 1.16.2 2023-07-01 01:20:04 +02:00
Spythere 7ffc169d8a hotfix 2023-07-01 01:19:50 +02:00
Spythere 1b85cc5f58 poprawki dziennika rj / dr 2023-06-30 20:50:03 +02:00
Spythere 72ff857fff dodatkowe info o postojach w dzienniku RJ 2023-06-27 02:52:41 +02:00
Spythere 96d64e77fc feature: nieskończona lista historii dr/rj scenerii 2023-06-24 13:46:37 +02:00
Spythere 6ceae3f161 revamp tabeli historii dyżurów 2023-06-23 14:19:28 +02:00
Spythere 8e8e27658c Merge do wersji 1.16.1
Wersja 1.16.1
2023-06-22 19:27:39 +02:00
Spythere 9b6ace394a bump v.1.16.1 2023-06-21 18:36:21 +02:00
Spythere 6cfeaa91bf responsywność dailyStats 2023-06-21 18:35:23 +02:00
Spythere 08b208aeaa fix tłumaczeń 2023-06-21 18:31:16 +02:00
Spythere a089b5275b hotfix daily stats 2023-06-21 18:30:02 +02:00
Spythere 8425cd4371 przyjazdy/odjazdy stacji pośrednich RJ w dzienniku 2023-06-21 18:26:16 +02:00
Spythere dbdc517b87 fix tłumaczeń 2023-06-21 17:32:36 +02:00
Spythere e271358a27 fix timetable id 2023-06-21 17:19:31 +02:00
Spythere 66262e3fcd dodatkowe statystyki dnia 2023-06-21 17:16:02 +02:00
Spythere 5b2b6bdea2 bump 1.16.0.1 2023-06-16 01:17:33 +02:00
Spythere c8587de6d9 npm update 2023-06-15 15:28:32 +02:00
Spythere 1f376085f2 feature: info o elektryfikacji spawnu na scenerii 2023-06-15 15:28:12 +02:00
Spythere f28600a7fa Merge do wersji 1.16.0
Wersja 1.16.0
2023-06-12 01:33:45 +02:00
Spythere d59ead87e6 bump v1.16.0 2023-06-12 01:21:49 +02:00
Spythere 34d91bc800 poprawki w pokazywaniu statystyk 2023-06-12 01:19:31 +02:00
Spythere cf9991d8a0 layout filtrów dzienników 2023-06-12 00:51:17 +02:00
Spythere 4ffb79d62b poprawki pobierania danych 2023-06-11 21:47:50 +02:00
Spythere d9f5edb4fe poprawki tłumaczeń 2023-06-11 21:47:22 +02:00
Spythere 1b2112430a feature: długości szlaków po kliknięciu 2023-06-08 23:35:57 +02:00
Spythere 0a972a23ef fix: asynchroniczność pobierania danych z API 2023-06-04 13:35:53 +02:00
Spythere 6d52724d06 zapamiętywanie stanu statystyk dnia 2023-06-04 12:19:46 +02:00
Spythere 99415c35d3 rozbudowane filtry dziennika RJ 2023-06-04 12:06:15 +02:00
Spythere c3f687d439 hotfixy dzienników 2023-06-04 01:45:58 +02:00
Spythere 266edfd6e6 reorder typów danych 2023-06-04 00:33:43 +02:00
Spythere d32d5ad91b poprawki dzienników 2023-06-03 18:55:44 +02:00
Spythere c3481470cb optymalizacja zapytań; filtr scenerii pocz. 2023-06-03 15:49:15 +02:00
Spythere 57e88b9abc Merge do wersji 1.15.1
Wersja 1.15.1
2023-06-02 20:09:43 +02:00
Spythere 44ebf53798 poprawki pwa 2023-06-02 20:06:25 +02:00
Spythere 145dc72b6b pwa: zmiana na autoUpdate 2023-06-02 19:41:33 +02:00
Spythere b7f3761940 Merge do wersji 1.15.0
Wersja 1.15.0
2023-06-02 01:16:37 +02:00
Spythere ea7c49dfb3 bump: 1.15.0 2023-06-02 01:10:01 +02:00
Spythere 5d6785813a fix: odznaki TWR/SKR; dodano do dziennika 2023-06-02 01:07:44 +02:00
Spythere a0054aed14 fix: optymalizacja zapytań historii RJ scenerii 2023-06-02 00:51:03 +02:00
Spythere 471e6f5216 Wersja 1.14.3
Wersja 1.14.3
2023-05-18 02:42:27 +02:00
Spythere a617eef00e bump 1.14.3 2023-05-18 02:38:13 +02:00
Spythere 38e700ecd6 migracja z route na routeNames 2023-05-18 02:37:27 +02:00
108 changed files with 32804 additions and 17427 deletions
+1
View File
@@ -31,6 +31,7 @@ node_modules
.firebase .firebase
.firebaserc .firebaserc
# Env
.env .env
.fake .fake
+18 -3
View File
@@ -16,14 +16,30 @@
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" /> <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" /> <meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#222222" />
<link rel="icon" href="favicon-64.png" sizes="64x64" type="image/png" /> <link rel="icon" href="favicon-64.png" sizes="64x64" type="image/png" />
<link rel="icon" href="favicon-32.png" sizes="32x32" type="image/png" />
<link rel="icon" href="favicon-62.png" sizes="62x62" type="image/png" /> <link rel="icon" href="favicon-62.png" sizes="62x62" type="image/png" />
<link rel="icon" href="favicon-32.png" sizes="32x32" type="image/png" />
<link rel="icon" href="favicon-16.png" sizes="16x16" type="image/png" /> <link rel="icon" href="favicon-16.png" sizes="16x16" type="image/png" />
<link rel="icon" href="favicon.ico" /> <link rel="icon" href="favicon.ico" />
<!-- Static OpenGraph meta -->
<meta name="description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
<meta property="og:url" content="https://stacjownik-td2.web.app/" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Stacjownik" />
<meta property="og:description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
<meta property="og:image" content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:site_name" content="Stacjownik" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Stacjownik" />
<meta name="twitter:description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
<meta name="twitter:image" content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg" />
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@500;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@500;700&display=swap" rel="stylesheet" />
</head> </head>
@@ -32,4 +48,3 @@
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>
+12050 -11386
View File
File diff suppressed because it is too large Load Diff
+19 -18
View File
@@ -1,6 +1,6 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.14.2", "version": "1.17.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -9,25 +9,26 @@
"preview": "yarn build && vite preview" "preview": "yarn build && vite preview"
}, },
"dependencies": { "dependencies": {
"core-js": "^3.12.1", "core-js": "^3.32.2",
"dotenv": "^16.0.3", "dotenv": "^16.3.1",
"firebase": "^9.8.1", "firebase": "^10.4.0",
"howler": "^2.2.1", "howler": "^2.2.4",
"pinia": "^2.0.14", "pinia": "^2.1.6",
"sass": "^1.53.0", "sass": "^1.67.0",
"socket.io-client": "^4.4.1", "socket.io-client": "^4.7.2",
"vue": "^3.2.37", "vue": "^3.3.4",
"vue-i18n": "^9.1.6", "vue-i18n": "^9.4.1",
"vue-router": "^4.0.0-0" "vue-router": "^4.2.4"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.11.17", "@types/node": "^20.6.2",
"@vitejs/plugin-vue": "^4.0.0", "@vite-pwa/assets-generator": "^0.0.10",
"axios": "^1.2.1", "@vitejs/plugin-vue": "^4.3.4",
"typescript": "^4.9.4", "axios": "^1.5.0",
"vite": "^4.0.3", "typescript": "^5.2.2",
"vite-plugin-pwa": "^0.14.0", "vite": "^4.4.9",
"vue-tsc": "^1.0.18" "vite-plugin-pwa": "^0.16.5",
"vue-tsc": "^1.8.11"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 B

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00251 14.9297L0 1.07422H6.14651L8.00251 4.27503L9.84583 1.07422H16L8.00251 14.9297Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 215 B

-1
View File
@@ -1,7 +1,6 @@
@import './styles/responsive.scss'; @import './styles/responsive.scss';
@import './styles/variables.scss'; @import './styles/variables.scss';
@import './styles/global.scss'; @import './styles/global.scss';
@import './styles/scenery_status.scss';
// VUE ROUTE CHANGE ANIMATION // VUE ROUTE CHANGE ANIMATION
.view-anim { .view-anim {
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

+42
View File
@@ -0,0 +1,42 @@
<template>
<button
class="btn btn--option btn--load-data"
v-if="!scrollNoMoreData && scrollDataLoaded && list.length >= 15"
@click="addHistoryData"
>
{{ $t('journal.load-data') }}
</button>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
export default defineComponent({
props: {
scrollNoMoreData: {
type: Boolean,
required: true,
},
scrollDataLoaded: {
type: Boolean,
required: true,
},
list: {
type: Array as PropType<any[]>,
required: true,
},
},
emits: ['addHistoryData'],
methods: {
addHistoryData() {
this.$emit('addHistoryData');
},
},
});
</script>
<style scoped></style>
+62
View File
@@ -0,0 +1,62 @@
<template>
<div class="progress-bar">
<span class="bar-bg"></span>
<span class="bar-fg" :style="{ width: `${~~progressPercent}%`, backgroundColor: bgColor }"></span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
progressPercent: {
type: Number,
required: true,
},
progressType: {
type: String,
required: false,
},
},
computed: {
bgColor() {
switch (this.progressType) {
case 'abandoned':
return 'salmon';
default:
return 'springgreen';
}
},
},
});
</script>
<style lang="scss" scoped>
.progress-bar {
position: relative;
width: 6em;
height: 1em;
margin: 0.5em 0;
.bar-fg,
.bar-bg {
position: absolute;
height: 1em;
width: 100%;
left: 0;
}
.bar-fg {
background-color: springgreen;
}
.bar-bg {
background-color: #5b5b5b;
}
}
</style>
@@ -0,0 +1,90 @@
<template>
<span class="status-badge" :class="statusID" v-if="isOnline">
{{ $t(`status.${statusID}`) }}
{{ statusID == 'online' ? timestampToString(statusTimestamp!) : '' }}
</span>
<span class="status-badge free" v-else>
{{ $t('status.free') }}
</span>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import dateMixin from '../../mixins/dateMixin';
export default defineComponent({
props: {
statusID: {
type: String,
},
statusTimestamp: {
type: Number,
},
isOnline: {
type: Boolean,
},
},
mixins: [dateMixin],
});
</script>
<style lang="scss" scoped>
$free: #8a8a8a;
$ending: #e6c300;
$no-limit: #117fc9;
$unav: #ff3d5d;
$brb: #e6a100;
$no-space: #222;
$online: #09a116;
$unknown: rgb(185, 60, 60);
.status-badge {
border-radius: 1rem;
font-weight: 500;
padding: 0.2em 0.55em;
background-color: $online;
&.free {
background-color: $free;
font-size: 0.95em;
}
&.ending {
background-color: $ending;
color: black;
font-size: 0.9em;
}
&.no-limit {
background-color: $no-limit;
font-size: 0.85em;
}
&.not-signed,
&.unavailable {
background-color: $unav;
font-size: 0.85em;
}
&.brb {
background-color: $brb;
color: black;
font-size: 0.95em;
}
&.no-space {
background-color: $no-space;
border: 1px solid white;
color: white;
font-size: 0.85em;
}
&.unknown {
background-color: $unknown;
font-size: 0.95em;
}
}
</style>
+106
View File
@@ -0,0 +1,106 @@
<template>
<div class="stock-list">
<ul>
<li v-for="(stockName, i) in trainStockList">
<p>{{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }} {{ stockName.split(':')[1] }}</p>
<span>
<img
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}${/^EN/.test(stockName) ? 'rb' : ''}.png`"
@error="onImageError($event, stockName)"
width="400"
height="60"
/>
<img
v-if="/^(EN|2EN)/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`"
@error="(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')"
/>
<img
class="train-thumbnail"
v-if="/^EN71/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`"
@error="(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')"
/>
<img
class="train-thumbnail"
v-if="/^(EN|2EN)/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}ra.png`"
@error="(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-ra.png')"
/>
</span>
</li>
</ul>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import { useStore } from '../../store/store';
import { RollingStockInfo } from '../../scripts/interfaces/github_api/StockInfoGithubData';
import imageMixin from '../../mixins/imageMixin';
export default defineComponent({
mixins: [imageMixin],
props: {
trainStockList: {
type: Array as PropType<string[]>,
required: true,
},
},
data() {
return {
store: useStore(),
};
},
methods: {
onImageError(event: Event, stockName: string) {
const fallbackName =
Object.keys(this.store.rollingStockData!.info).find((type) => {
return this.store.rollingStockData!.info[type as keyof RollingStockInfo].find((v) => v[0] === stockName.split(':')[0]);
}) || 'vehicle-unknown';
(event.target as HTMLImageElement).src = `/images/icon-${fallbackName}.png`;
},
},
});
</script>
<style lang="scss" scoped>
.stock-list {
display: flex;
justify-content: center;
}
.stock-list ul {
display: flex;
align-items: flex-end;
overflow: auto;
margin: 0 auto;
padding: 1em 0;
}
ul > li > span {
display: flex;
align-items: flex-end;
}
img {
max-height: 60px;
width: auto;
height: auto;
}
p {
text-align: center;
color: #aaa;
font-size: 0.9em;
margin-bottom: 1em;
}
</style>
+119 -119
View File
@@ -1,119 +1,119 @@
<template> <template>
<span class="stop-date"> <span class="stop-date">
<span <span
class="date arrival" class="date arrival"
v-if="!stop.beginsHere" v-if="!stop.beginsHere"
:class="{ :class="{
delayed: stop.arrivalDelay > 0 && stop.confirmed, delayed: stop.arrivalDelay > 0 && (stop.confirmed || stop.stopped),
preponed: stop.arrivalDelay < 0 && stop.confirmed, preponed: stop.arrivalDelay < 0 && (stop.confirmed || stop.stopped),
'on-time': stop.arrivalDelay == 0 && stop.confirmed, 'on-time': stop.arrivalDelay == 0 && stop.confirmed,
}" }"
> >
<span v-if="stop.arrivalDelay != 0 && stop.confirmed"> <span v-if="stop.arrivalDelay != 0 && (stop.confirmed || stop.stopped)">
<s>{{ timestampToString(stop.arrivalTimestamp) }}</s> <s>{{ timestampToString(stop.arrivalTimestamp) }}</s>
{{ timestampToString(stop.arrivalRealTimestamp) }} {{ timestampToString(stop.arrivalRealTimestamp) }}
({{ stop.arrivalDelay > 0 ? '+' : '' }}{{ stop.arrivalDelay }}) ({{ stop.arrivalDelay > 0 ? '+' : '' }}{{ stop.arrivalDelay }})
</span> </span>
<span v-else> <span v-else>
{{ timestampToString(stop.arrivalTimestamp) }} {{ timestampToString(stop.arrivalTimestamp) }}
</span> </span>
</span> </span>
<span class="date stop" v-if="stop.stopTime" :class="stop.stopType.replace(', ', '-')"> <span class="date stop" v-if="stop.stopTime || stop.stopped" :class="stop.stopType.replace(', ', '-')">
{{ stop.stopTime }} {{ stop.stopType == '' ? 'pt' : stop.stopType }} {{ stop.stopTime }} {{ stop.stopType == '' ? 'pt' : stop.stopType }}
</span> </span>
<span <span
class="date departure" class="date departure"
v-if="!stop.terminatesHere && stop.stopTime != 0" v-if="!stop.terminatesHere && (stop.stopTime != 0 || stop.stopped)"
:class="{ :class="{
delayed: stop.departureDelay > 0 && stop.confirmed, delayed: stop.departureDelay > 0 && stop.confirmed,
preponed: stop.departureDelay < 0 && stop.confirmed, preponed: stop.departureDelay < 0 && stop.confirmed,
}" }"
> >
<span v-if="stop.departureDelay != 0 && stop.confirmed"> <span v-if="stop.departureDelay != 0 && stop.confirmed">
<s>{{ timestampToString(stop.departureTimestamp) }}</s> <s>{{ timestampToString(stop.departureTimestamp) }}</s>
{{ timestampToString(stop.departureRealTimestamp) }} {{ timestampToString(stop.departureRealTimestamp) }}
({{ stop.departureDelay > 0 ? '+' : '' }}{{ stop.departureDelay }}) ({{ stop.departureDelay > 0 ? '+' : '' }}{{ stop.departureDelay }})
</span> </span>
<span v-else> <span v-else>
{{ timestampToString(stop.departureTimestamp) }} {{ timestampToString(stop.departureTimestamp) }}
</span> </span>
</span> </span>
</span> </span>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import TrainStop from '../../scripts/interfaces/TrainStop'; import TrainStop from '../../scripts/interfaces/TrainStop';
export default defineComponent({ export default defineComponent({
mixins: [dateMixin], mixins: [dateMixin],
props: { props: {
stop: { stop: {
type: Object as () => TrainStop, type: Object as () => TrainStop,
required: true, required: true,
}, },
}, },
setup() { setup() {
return {}; return {};
}, },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
$preponedClr: lime; $preponedClr: lime;
$delayedClr: salmon; $delayedClr: salmon;
$dateClr: #525151; $dateClr: #525151;
$stopExchangeClr: #db8e29; $stopExchangeClr: #db8e29;
$stopDefaultClr: #252525; $stopDefaultClr: #252525;
.stop-date { .stop-date {
display: flex; display: flex;
align-items: center; align-items: center;
.date { .date {
background: $dateClr; background: $dateClr;
padding: 0.3em 0.5em; padding: 0.3em 0.5em;
} }
.stop { .stop {
&.ph, &.ph,
&.ph-pm, &.ph-pm,
&.pm { &.pm {
background: $stopExchangeClr; background: $stopExchangeClr;
} }
background: $stopDefaultClr; background: $stopDefaultClr;
} }
.arrival, .arrival,
.departure { .departure {
&.delayed { &.delayed {
s { s {
color: #999; color: #999;
} }
span { span {
color: $delayedClr; color: $delayedClr;
} }
} }
&.preponed { &.preponed {
s { s {
color: #999; color: #999;
} }
span { span {
color: $preponedClr; color: $preponedClr;
} }
} }
} }
} }
</style> </style>
+82
View File
@@ -0,0 +1,82 @@
<template>
<img class="train-thumbnail" :src="placeholderUrl" v-if="isNotFound" />
<img
class="train-thumbnail"
v-else
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${name.split(':')[0]}${stockType == 'loco-ezt' ? 'rb' : ''}.png`"
@error="onImageError"
@load="onImageLoad"
width="220"
height="60"
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import imageMixin from '../../mixins/imageMixin';
import { useStore } from '../../store/store';
import { RollingStockInfo } from '../../scripts/interfaces/github_api/StockInfoGithubData';
export default defineComponent({
props: {
name: {
type: String,
required: true,
},
onlyFirstSegment: {
type: Boolean,
default: false,
},
},
data() {
return {
store: useStore(),
isNotFound: false,
isLoaded: false,
};
},
computed: {
url() {
return `https://rj.td2.info.pl/dist/img/thumbnails/${this.name.split(':')[0]}.png`;
},
placeholderUrl() {
return `/images/icon-${this.stockType}.png`;
},
stockType() {
if (!this.store.rollingStockData) return 'vehicle-unknown';
return (
Object.keys(this.store.rollingStockData.info).find((type) => {
return this.store.rollingStockData?.info[type as keyof RollingStockInfo].find((v) => v[0] === this.name.split(':')[0]);
}) || 'vehicle-unknown'
);
},
},
methods: {
onImageError() {
this.isNotFound = true;
this.isLoaded = false;
},
onImageLoad() {
this.isNotFound = false;
this.isLoaded = true;
},
},
});
</script>
<style lang="scss" scoped>
.train-thumbnail {
width: auto;
height: auto;
max-height: 60px;
}
</style>
+148 -80
View File
@@ -1,56 +1,62 @@
<template> <template>
<section class="daily-stats"> <section class="daily-stats">
<span :data-active="data.statsStatus"> <span :data-active="statsStatus">
<b v-if="data.statsStatus == DataStatus.Loading"> <b v-if="statsStatus == DataStatus.Loading">
{{ $t('app.loading') }} {{ $t('app.loading') }}
</b> </b>
<b v-else-if="data.stats.distanceSum == null"> <b v-else-if="stats.distanceSum == null">
{{ $t('journal.daily-stats-info') }} {{ $t('journal.daily-stats-info') }}
</b> </b>
<span> <span class="stats-list" v-else>
<div v-if="data.stats.totalTimetables"> <h3>
{{ $t('journal.daily-stats-title') }}
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
</h3>
<hr style="margin-bottom: 0.5em" />
<div v-if="stats.totalTimetables">
&bull; &bull;
<i18n-t keypath="journal.timetable-stats-total"> <i18n-t keypath="journal.timetable-stats-total">
<template #count> <template #count>
<b class="text--primary"> <b class="text--primary">
{{ data.stats.totalTimetables }} {{ stats.totalTimetables }}
{{ $t('journal.timetable-count', data.stats.totalTimetables) }} {{ $t('journal.timetable-count', stats.totalTimetables) }}
</b> </b>
</template> </template>
<template #distance> <template #distance>
<b class="text--primary"> {{ data.stats.distanceSum?.toFixed(2) }} km </b> <b class="text--primary"> {{ stats.distanceSum?.toFixed(2) }} km</b>
</template> </template>
</i18n-t> </i18n-t>
</div> </div>
<div v-if="data.stats.timetableId"> <div v-if="stats.timetableId">
&bull; &bull;
<i18n-t keypath="journal.timetable-stats-longest"> <i18n-t keypath="journal.timetable-stats-longest">
<template #id> <template #id>
<router-link :to="`/journal/timetables?timetableId=${data.stats.timetableId}`"> <router-link :to="`/journal/timetables?timetableId=${stats.timetableId}`">
<b>{{ data.stats.timetableId }}</b> <b>{{ stats.timetableId }}</b>
</router-link> </router-link>
</template> </template>
<template #author> <template #author>
<router-link :to="`/journal/dispatchers?dispatcherName=${data.stats.timetableAuthor}`"> <router-link :to="`/journal/dispatchers?dispatcherName=${stats.timetableAuthor}`">
<b>{{ data.stats.timetableAuthor }}</b> <b>{{ stats.timetableAuthor }}</b>
</router-link> </router-link>
</template> </template>
<template #driver> <template #driver>
<b>{{ data.stats.timetableDriver }}</b> <b class="text--primary">{{ stats.timetableDriver }}</b>
</template> </template>
<template #distance> <template #distance>
<b class="text--primary">{{ data.stats.timetableRouteDistance }} km</b> <b class="text--primary">{{ stats.timetableRouteDistance }} km</b>
</template> </template>
</i18n-t> </i18n-t>
</div> </div>
<div v-if="firstPlaceDispatchers.length == 1"> <div v-if="firstPlaceDispatchers.length == 1">
&bull; &bull;
<i18n-t keypath="journal.timetable-stats-most-active"> <i18n-t keypath="journal.timetable-stats-most-active-dr">
<template #dispatcher> <template #dispatcher>
<router-link :to="`/journal/dispatchers?dispatcherName=${firstPlaceDispatchers[0].name}`"> <router-link :to="`/journal/dispatchers?dispatcherName=${firstPlaceDispatchers[0].name}`">
<b>{{ firstPlaceDispatchers[0].name }}</b> <b>{{ firstPlaceDispatchers[0].name }}</b>
@@ -67,7 +73,7 @@
<div v-if="firstPlaceDispatchers.length > 1"> <div v-if="firstPlaceDispatchers.length > 1">
&bull; &bull;
<i18n-t keypath="journal.timetable-stats-most-active-many"> <i18n-t keypath="journal.timetable-stats-most-active-dr-many">
<template #dispatchers> <template #dispatchers>
<span v-for="(disp, i) in firstPlaceDispatchers"> <span v-for="(disp, i) in firstPlaceDispatchers">
<span v-if="i == firstPlaceDispatchers.length - 1"> {{ $t('general.and') }} </span> <span v-if="i == firstPlaceDispatchers.length - 1"> {{ $t('general.and') }} </span>
@@ -88,95 +94,157 @@
</template> </template>
</i18n-t> </i18n-t>
</div> </div>
<div v-if="stats.longestDuties.length > 0">
&bull;
<i18n-t keypath="journal.timetable-stats-longest-duties">
<template #dispatcher>
<router-link :to="`/journal/dispatchers?dispatcherName=${stats.longestDuties[0].name}`">
<b>{{ stats.longestDuties[0].name }}</b>
</router-link>
</template>
<template #station>{{ stats.longestDuties[0].station }}</template>
<template #duration>
{{ calculateDuration(stats.longestDuties[0].duration) }}
</template>
</i18n-t>
</div>
<div v-if="stats.mostActiveDrivers.length > 0">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active-driver">
<template #driver>
<b class="text--primary">{{ stats.mostActiveDrivers[0].name }}</b>
</template>
<template #distance>
<b class="text--primary">{{ stats.mostActiveDrivers[0].distance.toFixed(2) }} km</b>
</template>
</i18n-t>
</div>
</span> </span>
</span> </span>
</section> </section>
</template> </template>
<script setup lang="ts"> <script lang="ts">
import axios from 'axios'; import axios from 'axios';
import { computed, reactive, ref } from 'vue'; import { defineComponent } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import { DataStatus } from '../../scripts/enums/DataStatus'; import { DataStatus } from '../../scripts/enums/DataStatus';
import { ITimetablesDailyStats, ITimetablesDailyStatsResponse } from '../../scripts/interfaces/api/StatsAPIData'; import { ITimetablesDailyStats, ITimetablesDailyStatsResponse } from '../../scripts/interfaces/api/StatsAPIData';
import { URLs } from '../../scripts/utils/apiURLs'; import { URLs } from '../../scripts/utils/apiURLs';
const intervalId = ref(-1); export default defineComponent({
mixins: [dateMixin],
emits: ['toggleStatsOpen'],
const data = reactive({ data() {
statsStatus: DataStatus.Loading, return {
DataStatus,
statsStatus: DataStatus.Loading,
intervalId: -1,
stats: { stats: {
totalTimetables: 0, totalTimetables: 0,
distanceSum: 0, distanceSum: 0,
distanceAvg: 0, distanceAvg: 0,
timetableAuthor: '', timetableAuthor: '',
timetableDriver: '', timetableDriver: '',
timetableId: 0, timetableId: 0,
timetableRouteDistance: 0, timetableRouteDistance: 0,
longestDuties: [],
mostActiveDispatchers: [], mostActiveDrivers: [],
} as ITimetablesDailyStats, mostActiveDispatchers: [],
}); } as ITimetablesDailyStats,
const firstPlaceDispatchers = computed(() => {
if (data.stats.mostActiveDispatchers.length == 0) return [];
const maxCount = data.stats.mostActiveDispatchers[0].count;
return data.stats.mostActiveDispatchers.filter((disp) => disp.count === maxCount);
});
async function fetchDailyTimetableStats() {
try {
const {
distanceAvg,
distanceSum,
maxTimetable,
totalTimetables,
mostActiveDispatchers,
}: ITimetablesDailyStatsResponse = await (
await axios.get(`${URLs.stacjownikAPI}/api/getDailyTimetableStats`)
).data;
data.stats = {
totalTimetables,
distanceSum,
distanceAvg,
timetableAuthor: maxTimetable?.authorName || '',
timetableDriver: maxTimetable?.driverName || '',
timetableId: maxTimetable?.id || 0,
timetableRouteDistance: maxTimetable?.routeDistance || 0,
mostActiveDispatchers,
}; };
},
data.statsStatus = DataStatus.Loaded; activated() {
} catch (error) { this.startFetchingDailyStats();
console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...'); this.$emit('toggleStatsOpen', true);
data.statsStatus = DataStatus.Error; },
}
}
function startFetchingDailyStats() { deactivated() {
fetchDailyTimetableStats(); this.stopFetchingDailyStats();
intervalId.value = setInterval(fetchDailyTimetableStats, 60000); },
}
function stopFetchingDailyStats() { computed: {
clearInterval(intervalId.value); firstPlaceDispatchers() {
} if (this.stats.mostActiveDispatchers.length == 0) return [];
const maxCount = this.stats.mostActiveDispatchers[0].count;
defineExpose({ return this.stats.mostActiveDispatchers.filter((disp) => disp.count === maxCount);
startFetchingDailyStats, },
stopFetchingDailyStats, },
methods: {
async fetchDailyTimetableStats() {
try {
const res: ITimetablesDailyStatsResponse = await (
await axios.get(`${URLs.stacjownikAPI}/api/getDailyTimetableStats`)
).data;
this.stats = {
totalTimetables: res.totalTimetables,
distanceSum: res.distanceSum,
distanceAvg: res.distanceAvg,
timetableAuthor: res.maxTimetable?.authorName || '',
timetableDriver: res.maxTimetable?.driverName || '',
timetableId: res.maxTimetable?.id || 0,
timetableRouteDistance: res.maxTimetable?.routeDistance || 0,
mostActiveDispatchers: res.mostActiveDispatchers,
mostActiveDrivers: res.mostActiveDrivers,
longestDuties: res.longestDuties,
};
this.statsStatus = DataStatus.Loaded;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...');
this.statsStatus = DataStatus.Error;
}
},
startFetchingDailyStats() {
this.fetchDailyTimetableStats();
if (this.intervalId != -1) return;
this.intervalId = setInterval(this.fetchDailyTimetableStats, 60000);
},
stopFetchingDailyStats() {
clearInterval(this.intervalId);
this.intervalId = -1;
},
},
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss';
.daily-stats { .daily-stats {
text-align: left; text-align: left;
} }
.daily-stats > span[data-active='0'] { .daily-stats > span[data-active='0'] {
opacity: 0.75; opacity: 0.75;
} }
.stats-list a {
text-decoration: underline;
}
@include smallScreen {
.daily-stats {
text-align: justify;
}
h3 {
text-align: center;
}
}
</style> </style>
@@ -1,57 +1,105 @@
<template> <template>
<transition-group class="journal-list" tag="ul" name="list-anim"> <div>
<li <transition name="status-anim" mode="out-in">
v-for="item in computedDispatcherHistory" <div :key="dataStatus">
:key="typeof item === 'string' ? item : item.timestampFrom + item.dispatcherId" <div class="journal_warning" v-if="store.isOffline">
:class="{ sticky: typeof item == 'string' }" {{ $t('app.offline') }}
> </div>
<div v-if="typeof item == 'string'" class="journal_day">
{{ item }} <Loading v-else-if="dataStatus == DataStatus.Loading" />
<div v-else-if="dataStatus == DataStatus.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 v-else>
<table class="scenery-history-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>
<tbody>
<transition-group name="list-anim">
<tr v-for="historyItem in dispatcherHistory" :key="historyItem.id">
<td>
<router-link :to="`/journal/dispatchers?sceneryName=${historyItem.stationName}`">
<b>{{ historyItem.stationName }}</b>
</router-link>
</td>
<td>#{{ historyItem.stationHash }}</td>
<td>
<router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`">
<b>{{ 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>
<AddDataButton
:list="dispatcherHistory"
:scrollDataLoaded="scrollDataLoaded"
:scrollNoMoreData="scrollNoMoreData"
@addHistoryData="addHistoryData"
/>
</div>
</div> </div>
</transition>
<div <div class="journal_warning" v-if="scrollNoMoreData">
v-else {{ $t('journal.no-further-data') }}
class="journal_item" </div>
:class="{ online: item.isOnline }"
@click="navigateToScenery(item.stationName, item.isOnline)"
@keydown.enter="navigateToScenery(item.stationName, item.isOnline)"
tabindex="0"
>
<span class="item-general">
<b
v-if="item.dispatcherLevel !== null"
class="level-badge dispatcher"
:style="calculateExpStyle(item.dispatcherLevel, item.dispatcherIsSupporter)"
>
{{ item.dispatcherLevel >= 2 ? item.dispatcherLevel : 'L' }}
</b>
<b class="text--primary">{{ item.dispatcherName }}</b> &bull; <b>{{ item.stationName }}</b> <div class="journal_warning" v-else-if="!scrollDataLoaded">
<span class="text--grayed">&nbsp;#{{ item.stationHash }}&nbsp;</span> {{ $t('journal.loading-further-data') }}
<span class="region-badge" :class="item.region">PL1</span> </div>
<span class="like-count" v-if="item.dispatcherRate"> </div>
<img :src="getIcon('like')" alt="like icon" />
{{ item.dispatcherRate }}
</span>
</span>
<span class="item-time">
<span :data-status="item.isOnline"> {{ item.isOnline ? $t('journal.online-since') : 'OFFLINE' }}&nbsp; </span>
<span>
{{ new Date(item.timestampFrom).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
</span>
<span v-if="item.currentDuration && item.isOnline"> ({{ calculateDuration(item.currentDuration) }}) </span>
<span v-if="item.timestampTo">
&gt;
{{ new Date(item.timestampTo).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
({{ $t('journal.duty-lasted') }} {{ calculateDuration(item.currentDuration!) }})
</span>
</span>
</div>
</li>
</transition-group>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -60,19 +108,48 @@ import dateMixin from '../../mixins/dateMixin';
import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData'; import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
import styleMixin from '../../mixins/styleMixin'; import styleMixin from '../../mixins/styleMixin';
import imageMixin from '../../mixins/imageMixin'; import imageMixin from '../../mixins/imageMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { useStore } from '../../store/store';
import Loading from '../Global/Loading.vue';
import { regions } from '../../data/options.json';
import AddDataButton from '../Global/AddDataButton.vue';
export default defineComponent({ export default defineComponent({
components: { Loading, AddDataButton },
mixins: [dateMixin, styleMixin, imageMixin],
props: { props: {
dispatcherHistory: { dispatcherHistory: {
type: Array as PropType<DispatcherHistory[]>, type: Array as PropType<DispatcherHistory[]>,
required: true, required: true,
}, },
scrollNoMoreData: {
type: Boolean,
},
scrollDataLoaded: {
type: Boolean,
},
addHistoryData: {
type: Function as PropType<() => void>,
},
dataStatus: {
type: Number as PropType<DataStatus>,
},
}, },
mixins: [dateMixin, styleMixin, imageMixin], data() {
return {
DataStatus,
store: useStore(),
regions,
};
},
computed: { computed: {
computedDispatcherHistory() { computedDispatcherHistory() {
console.log(this.dispatcherHistory.length);
return this.dispatcherHistory.reduce((acc, historyItem, i) => { return this.dispatcherHistory.reduce((acc, historyItem, i) => {
if (this.isAnotherDay(i - 1, i)) acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL')); if (this.isAnotherDay(i - 1, i)) acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL'));
acc.push(historyItem); acc.push(historyItem);
@@ -105,79 +182,60 @@ export default defineComponent({
@import '../../styles/animations.scss'; @import '../../styles/animations.scss';
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/badge.scss'; @import '../../styles/badge.scss';
@import '../../styles/JournalSection.scss';
@import '../../styles/variables.scss'; @import '../../styles/variables.scss';
@import '../../styles/JournalSection.scss';
li.sticky { table.scenery-history-table {
position: sticky; --_bg-table: #111;
top: 0; --_bg-head: #101010;
} --_bg-row: #2f2f2f;
.journal_item { width: 100%;
display: flex; border-collapse: collapse;
justify-content: space-between; position: relative;
align-items: center; text-align: center;
flex-wrap: wrap;
text-align: left;
gap: 0.5em 1em; margin-bottom: 1em;
line-height: 1.7em; thead {
padding: 0.75em; position: sticky;
top: 0;
&.online { background-color: var(--_bg-head);
cursor: pointer;
} }
span[data-status='true'] { 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;
}
}
@media screen and (max-width: 550px) {
font-size: 0.9em;
}
}
.text {
&--online {
color: springgreen; color: springgreen;
} }
span[data-status='false'] { &--offline {
color: salmon; color: #ddd;
}
}
.item-general {
display: flex;
justify-content: center;
align-items: center;
gap: 0.25em;
flex-wrap: wrap;
.level-badge {
margin-right: 0.25em;
}
}
.journal_day {
margin-bottom: 1em;
padding: 0.5em;
font-weight: bold;
background-color: #333;
span {
position: relative;
background-color: inherit;
z-index: 10;
padding-right: 1em;
font-weight: bold;
}
}
.like-count {
display: flex;
align-items: center;
gap: 0.25em;
font-size: 1.2em;
color: $accentCol;
}
@include smallScreen {
.journal_item {
flex-direction: column;
} }
} }
</style> </style>
+40 -30
View File
@@ -49,15 +49,6 @@
</button> </button>
</div> </div>
</div> </div>
<div class="search_actions">
<button class="btn--action" @click="onResetButtonClick">
{{ $t('options.reset-button') }}
</button>
<button class="btn--action" @click="onSearchButtonConfirm">
{{ $t('options.search-button') }}
</button>
</div>
</div> </div>
<h1 class="option-title">{{ $t('options.sort-title') }}</h1> <h1 class="option-title">{{ $t('options.sort-title') }}</h1>
@@ -74,15 +65,31 @@
</div> </div>
<h1 class="option-title" v-if="filters.length != 0">{{ $t('options.filter-title') }}</h1> <h1 class="option-title" v-if="filters.length != 0">{{ $t('options.filter-title') }}</h1>
<div class="options_filters">
<button <div class="options_filter-sections" v-if="filters.length != 0 && filterList">
v-for="filter in filters" <section class="filter-section" v-for="section in JournalFilterSection">
class="filter-option btn--option" <p>{{ $t(`options.filter-section-${section}`) }}</p>
:class="{ checked: journalFilterActive.id === filter.id }"
:id="filter.id" <div class="options_filters">
@click="onFilterChange(filter)" <button
> v-for="filter in filterList.filter((f) => f.filterSection == section)"
{{ $t(`options.filter-${filter.id}`) }} class="filter-option btn--option"
:class="{ checked: filter.isActive }"
:id="filter.id"
@click="onFilterChange(filter)"
>
{{ $t(`options.filter-${filter.id}`) }}
</button>
</div>
</section>
</div>
<div class="options_actions">
<button class="btn--action" @click="onResetButtonClick">
{{ $t('options.reset-button') }}
</button>
<button class="btn--action" @click="onSearchButtonConfirm">
{{ $t('options.search-button') }}
</button> </button>
</div> </div>
</div> </div>
@@ -100,9 +107,10 @@ import { DataStatus } from '../../scripts/enums/DataStatus';
import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData'; import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData';
import { URLs } from '../../scripts/utils/apiURLs'; import { URLs } from '../../scripts/utils/apiURLs';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import { JournalTimetableFilter } from '../../types/Journal/JournalTimetablesTypes';
import ActionButton from '../Global/ActionButton.vue'; import ActionButton from '../Global/ActionButton.vue';
import SelectBox from '../Global/SelectBox.vue'; import SelectBox from '../Global/SelectBox.vue';
import { JournalFilterSection } from '../../scripts/enums/JournalFilterType';
import { JournalFilter } from '../../scripts/types/JournalTimetablesTypes';
export default defineComponent({ export default defineComponent({
components: { SelectBox, ActionButton }, components: { SelectBox, ActionButton },
@@ -116,7 +124,7 @@ export default defineComponent({
}, },
filters: { filters: {
type: Array as PropType<JournalTimetableFilter[]>, type: Array as PropType<JournalFilter[]>,
default: [], default: [],
}, },
@@ -132,13 +140,14 @@ export default defineComponent({
optionsType: { optionsType: {
type: String, type: String,
required: true required: true,
} },
}, },
data() { data() {
return { return {
showOptions: false, showOptions: false,
JournalFilterSection,
driverSuggestions: [] as string[], driverSuggestions: [] as string[],
dispatcherSuggestions: [] as string[], dispatcherSuggestions: [] as string[],
@@ -154,7 +163,8 @@ export default defineComponent({
return { return {
searchersValues: inject('searchersValues') as { [key: string]: string }, searchersValues: inject('searchersValues') as { [key: string]: string },
sorterActive: inject('sorterActive') as { id: string | number; dir: number }, sorterActive: inject('sorterActive') as { id: string | number; dir: number },
journalFilterActive: inject('journalFilterActive') as JournalTimetableFilter, // journalFilterActive: inject('journalFilterActive') as JournalFilter,
filterList: inject('filterList') as JournalFilter[] | undefined,
}; };
}, },
@@ -174,7 +184,8 @@ export default defineComponent({
watch: { watch: {
async driverStatsName(value: string) { async driverStatsName(value: string) {
await this.fetchDriverStats(); await this.fetchDriverStats();
this.store.currentStatsTab = value ? 'driver' : 'daily';
// if (value) this.store.currentStatsTab = 'driver';
}, },
async 'searchersValues.search-driver'(value: string | undefined) { async 'searchersValues.search-driver'(value: string | undefined) {
@@ -249,18 +260,17 @@ export default defineComponent({
}); });
}, },
focusEnd() {
console.log('focus end');
},
onSorterChange(item: { id: string | number; value: string }) { onSorterChange(item: { id: string | number; value: string }) {
this.sorterActive.id = item.id; this.sorterActive.id = item.id;
this.sorterActive.dir = -1; this.sorterActive.dir = -1;
this.$emit('onSearchConfirm'); this.$emit('onSearchConfirm');
}, },
onFilterChange(filter: JournalTimetableFilter) { onFilterChange(filter: JournalFilter) {
this.journalFilterActive = filter; // this.journalFilterActive = filter;
this.filterList?.filter((f) => f.filterSection === filter.filterSection).forEach((f) => (f.isActive = false));
filter.isActive = true;
this.$emit('onSearchConfirm'); this.$emit('onSearchConfirm');
}, },
+27 -21
View File
@@ -1,20 +1,22 @@
<template> <template>
<div class="journal-stats" v-show="!store.isOffline"> <div class="journal-stats" v-if="!store.isOffline">
<div class="tabs"> <div class="tabs">
<button <button
v-for="tab in data.tabs" v-for="tab in data.tabs"
class="btn--filled" class="btn--filled"
:data-selected="tab.name == store.currentStatsTab && areStatsOpen" :data-selected="tab.name == store.currentStatsTab && areStatsOpen"
:data-inactive="tab.inactive" :data-inactive="tab.inactive"
:data-disabled="tab.inactive"
:disabled="tab.inactive"
@click="onTabButtonClick(tab.name)" @click="onTabButtonClick(tab.name)"
> >
{{ $t(tab.titlePath) }} {{ $t(tab.titlePath) }}
</button> </button>
</div> </div>
<div class="stats-tab" v-show="areStatsOpen"> <div class="stats-tab" v-show="areStatsOpen">
<keep-alive> <keep-alive>
<JournalDailyStats v-if="store.currentStatsTab == 'daily'" ref="dailyStatsComp" /> <JournalDailyStats v-if="store.currentStatsTab == 'daily'" @toggleStatsOpen="toggleStatsOpen" />
<JournalDriverStats v-else-if="store.currentStatsTab == 'driver'" /> <JournalDriverStats v-else-if="store.currentStatsTab == 'driver'" />
</keep-alive> </keep-alive>
</div> </div>
@@ -22,22 +24,21 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, KeepAlive, onActivated, onDeactivated, reactive, Ref, ref, watch } from 'vue'; import { computed, KeepAlive, onMounted, reactive, Ref, ref, watch } from 'vue';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import JournalDailyStats from './DailyStats.vue'; import JournalDailyStats from './DailyStats.vue';
import JournalDriverStats from './JournalDriverStats.vue'; import JournalDriverStats from './JournalDriverStats.vue';
import StorageManager from '../../scripts/managers/storageManager';
// Types // Types
type TStatTab = 'daily' | 'driver'; type TStatTab = 'daily' | 'driver';
// Variables // Variables
const store = useStore(); const store = useStore();
const dailyStatsComp: Ref<InstanceType<typeof JournalDailyStats> | null> = ref(null);
const lastDailyStatsOpen = ref(false); const lastDailyStatsOpen = ref(false);
const areStatsOpen = ref(false); const areStatsOpen = ref(false);
const lastClickedTab = ref('daily'); const lastClickedTab: Ref<'daily' | 'driver' | null> = ref(null);
let data = reactive({ let data = reactive({
tabs: [ tabs: [
@@ -48,7 +49,7 @@ let data = reactive({
{ {
name: 'driver', name: 'driver',
titlePath: 'journal.driver-stats-title', titlePath: 'journal.driver-stats-title',
inactive: true, // inactive: true,
}, },
] as { name: TStatTab; titlePath: string; inactive?: boolean }[], ] as { name: TStatTab; titlePath: string; inactive?: boolean }[],
}); });
@@ -57,30 +58,35 @@ let data = reactive({
function onTabButtonClick(tab: TStatTab) { function onTabButtonClick(tab: TStatTab) {
if (lastClickedTab.value == tab || !areStatsOpen.value) areStatsOpen.value = !areStatsOpen.value; if (lastClickedTab.value == tab || !areStatsOpen.value) areStatsOpen.value = !areStatsOpen.value;
if (tab == 'daily') lastDailyStatsOpen.value = areStatsOpen.value; if (tab == 'daily') {
StorageManager.setBooleanValue('dailyStatsOpen', areStatsOpen.value);
lastDailyStatsOpen.value = areStatsOpen.value;
}
store.currentStatsTab = tab; store.currentStatsTab = tab;
lastClickedTab.value = tab; lastClickedTab.value = tab;
if (areStatsOpen.value == false) store.currentStatsTab = null;
} }
onActivated(() => { function toggleStatsOpen(open: boolean) {
dailyStatsComp.value?.startFetchingDailyStats(); areStatsOpen.value = open;
}); }
onDeactivated(() => {
dailyStatsComp.value?.stopFetchingDailyStats();
});
watch( watch(
computed(() => store.driverStatsData), computed(() => store.driverStatsData),
(statsData) => { (statsData) => {
data.tabs[1].inactive = statsData ? false : true; store.currentStatsTab = statsData ? 'driver' : lastClickedTab.value;
areStatsOpen.value = statsData ? true : lastClickedTab.value !== null;
lastClickedTab.value = statsData ? 'driver' : 'daily';
if (statsData) areStatsOpen.value = true;
if (!statsData) areStatsOpen.value = lastDailyStatsOpen.value;
} }
); );
onMounted(() => {
if (StorageManager.getBooleanValue('dailyStatsOpen')) {
areStatsOpen.value = true;
store.currentStatsTab = 'daily';
}
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -0,0 +1,82 @@
<template>
<div>
<transition name="status-anim" mode="out-in">
<div :key="dataStatus">
<div class="journal_warning" v-if="store.isOffline">
{{ $t('app.offline') }}
</div>
<Loading v-else-if="dataStatus == DataStatus.Loading" />
<div v-else-if="dataStatus == DataStatus.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>
<TimetableHistoryList :timetableHistory="timetableHistory" />
<AddDataButton
:list="timetableHistory"
:scrollDataLoaded="scrollDataLoaded"
:scrollNoMoreData="scrollNoMoreData"
@addHistoryData="addHistoryData"
/>
</div>
</div>
</transition>
<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>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { DataStatus } from '../../../scripts/enums/DataStatus';
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
import { useStore } from '../../../store/store';
import Loading from '../../Global/Loading.vue';
import ProgressBar from '../../Global/ProgressBar.vue';
import AddDataButton from '../../Global/AddDataButton.vue';
import TimetableHistoryList from './TimetableHistoryList.vue';
export default defineComponent({
components: { ProgressBar, Loading, AddDataButton, TimetableHistoryList },
props: {
timetableHistory: {
type: Array as PropType<TimetableHistory[]>,
required: true,
},
scrollNoMoreData: {
type: Boolean,
},
scrollDataLoaded: {
type: Boolean,
},
addHistoryData: {
type: Function as PropType<() => void>,
},
dataStatus: {
type: Number as PropType<DataStatus>,
},
},
data() {
return {
DataStatus,
store: useStore(),
};
},
});
</script>
<style lang="scss" scoped>
@import '../../../styles/JournalSection.scss';
@import '../../../styles/animations.scss';
</style>
@@ -0,0 +1,165 @@
<template>
<div class="item-extra" v-if="timetable.stockString && timetable.stockMass && showExtraInfo">
<hr />
<div class="stock-specs">
<span class="badge">
<span>{{ $t('journal.dispatcher-name') }}</span>
<span>{{ timetable.authorName }}</span>
</span>
</div>
<div class="stock-specs">
<span class="badge">
<span>{{ $t('journal.stock-max-speed') }}</span>
<span>{{ timetable.maxSpeed }}km/h</span>
</span>
<span class="badge">
<span>{{ $t('journal.stock-length') }}</span>
<span>
{{ currentHistoryIndex == 0 ? timetable.stockLength : stockHistory[currentHistoryIndex].stockLength || timetable.stockLength }}m
</span>
</span>
<span class="badge">
<span>{{ $t('journal.stock-mass') }}</span>
<span>
{{
Math.floor((currentHistoryIndex == 0 ? timetable.stockMass! : stockHistory[currentHistoryIndex].stockMass || timetable.stockMass) / 1000)
}}t
</span>
</span>
</div>
<!-- Historia zmian w składzie -->
<div class="stock-history" v-if="stockHistory.length > 1">
<button class="btn--action" v-for="(sh, i) in stockHistory" :data-checked="i == currentHistoryIndex" @click.stop="currentHistoryIndex = i">
{{ sh.updatedAt }}
</button>
</div>
<!-- <StockList :trainStockList="currentHistoryIndex == 0 ? timetable.stockString : stockHistory[currentHistoryIndex].stockString).split(';')" /> -->
<StockList :trainStockList="(currentHistoryIndex == 0 ? timetable.stockString : stockHistory[currentHistoryIndex].stockString).split(';') " />
<!-- <ul class="stock-list">
<li
v-for="(stockName, i) in (currentHistoryIndex == 0 ? timetable.stockString : stockHistory[currentHistoryIndex].stockString).split(';')"
:key="i"
>
<div>{{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }} {{ stockName.split(':')[1] }}</div>
<TrainThumbnail :name="stockName" />
</li>
</ul> -->
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
import imageMixin from '../../../mixins/imageMixin';
import TrainThumbnail from '../../Global/TrainThumbnail.vue';
import StockList from '../../Global/StockList.vue';
export default defineComponent({
mixins: [imageMixin],
props: {
showExtraInfo: {
type: Boolean,
required: true,
},
timetable: {
type: Object as PropType<TimetableHistory>,
required: true,
},
},
data() {
return {
currentHistoryIndex: 0,
};
},
computed: {
stockHistory() {
return this.timetable.stockHistory
.slice()
.reverse()
.map((h) => {
const historyData = h.split('@');
return {
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, {
hour: '2-digit',
minute: '2-digit',
}),
stockString: historyData[1],
stockMass: Number(historyData[2]) || undefined,
stockLength: Number(historyData[3]) || undefined,
};
});
},
},
methods: {
onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = this.getImage('unknown.png');
},
},
components: { TrainThumbnail, StockList },
});
</script>
<style lang="scss" scoped>
@import '../../../styles/variables.scss';
@import '../../../styles/responsive.scss';
@import '../../../styles/badge.scss';
.item-extra {
margin-top: 0.5em;
}
.stock-history {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 1em;
button[data-checked='true'] {
color: $accentCol;
}
}
.stock-specs {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 0.5em;
.badge {
margin: 0;
span:last-child {
color: black;
background-color: $accentCol;
}
}
@include smallScreen() {
justify-content: center;
}
}
ul.stock-list {
display: flex;
align-items: flex-end;
overflow: auto;
padding-bottom: 0.5em;
li > div {
margin: 1em 0;
text-align: center;
color: #aaa;
font-size: 0.9em;
}
}
</style>
@@ -0,0 +1,137 @@
<template>
<div class="item-general">
<span
class="general-train"
tabindex="0"
@click.stop="showTimetable(timetable, $event.currentTarget)"
@keydown.enter="showTimetable(timetable, $event.currentTarget)"
>
<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>
<span>
<strong class="text--primary">
{{ timetable.trainCategoryCode }}
</strong>
<strong>&nbsp;{{ timetable.trainNo }}</strong>
</span>
&bull;
<strong
v-if="timetable.driverLevel !== null"
class="level-badge driver"
:style="calculateExpStyle(timetable.driverLevel, timetable.driverIsSupporter)"
>
{{ timetable.driverLevel < 2 ? 'L' : `${timetable.driverLevel}` }}
</strong>
<strong>{{ timetable.driverName }}</strong>
</span>
<span class="general-time">
<b class="info-date"
>{{
new Date(timetable.createdAt).getTime() - new Date(timetable.beginDate).getTime() < 0
? localeDateTime(timetable.createdAt, $i18n.locale)
: localeDateTime(timetable.beginDate, $i18n.locale)
}}
</b>
<b
class="info-badge"
:class="{
fulfilled: timetable.fulfilled,
terminated: timetable.terminated && !timetable.fulfilled,
active: !timetable.terminated,
}"
>
{{
!timetable.terminated
? $t('journal.timetable-active')
: timetable.fulfilled
? $t('journal.timetable-fulfilled')
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
}}
</b>
</span>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
import dateMixin from '../../../mixins/dateMixin';
import modalTrainMixin from '../../../mixins/modalTrainMixin';
import styleMixin from '../../../mixins/styleMixin';
export default defineComponent({
mixins: [dateMixin, modalTrainMixin, styleMixin],
props: {
timetable: {
type: Object as PropType<TimetableHistory>,
required: true,
},
},
methods: {
showTimetable(timetable: TimetableHistory, target: EventTarget | null) {
if (timetable?.terminated) return;
this.selectModalTrain(timetable.driverName + timetable.trainNo.toString(), target);
},
},
});
</script>
<style lang="scss" scoped>
@import '../../../styles/responsive.scss';
@import '../../../styles/badge.scss';
.item-general {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
margin-bottom: 0.5em;
@include smallScreen() {
justify-content: center;
}
}
.info-date {
margin-right: 0.5em;
}
.info-badge {
padding: 0.05em 0.35em;
color: black;
&.terminated {
background-color: salmon;
}
&.fulfilled {
background-color: lightgreen;
}
&.active {
background-color: lightblue;
}
}
.general-train {
cursor: pointer;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25em;
}
</style>
@@ -0,0 +1,100 @@
<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--option btn--show">
{{ $t('journal.stock-info') }}
<img :src="getIcon(`arrow-${showExtraInfo.value ? 'asc' : 'desc'}`)" alt="Arrow" />
</button>
<!-- Extra -->
<TimetableExtra :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
</div>
</li>
</transition-group>
</ul>
</template>
<script lang="ts">
import { PropType, defineComponent, ref } from 'vue';
import imageMixin from '../../../mixins/imageMixin';
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
import TimetableGeneral from './TimetableGeneral.vue';
import TimetableStops from './TimetableStops.vue';
import TimetableStatus from './TimetableStatus.vue';
import TimetableExtra from './TimetableExtra.vue';
export default defineComponent({
mixins: [imageMixin],
props: {
timetableHistory: {
type: Array as PropType<TimetableHistory[]>,
required: true,
},
},
computed: {
computedTimetableHistory() {
return this.timetableHistory.map((timetable) => ({
timetable,
showExtraInfo: ref(false),
}));
},
},
methods: {},
components: { TimetableGeneral, TimetableStops, TimetableStatus, TimetableExtra },
});
</script>
<style lang="scss" scoped>
@import '../../../styles/variables.scss';
@import '../../../styles/responsive.scss';
@import '../../../styles/JournalSection.scss';
.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>
@@ -0,0 +1,68 @@
<template>
<div class="item-status" style="margin: 0.5em 0">
<ProgressBar
:progressPercent="~~((timetable.currentDistance / timetable.routeDistance) * 100)"
:progressType="!timetable.fulfilled && timetable.terminated ? 'abandoned' : ''"
/>
<span>
<span :style="{ color: timetable.fulfilled ? 'lightgreen' : timetable.terminated ? 'salmon' : '' }">
{{ timetable.currentDistance + ' km' }}
</span>
<span> / </span>
<span class="text--primary">{{ timetable.routeDistance }} km</span>
|
<span class="text--grayed">{{ timetable.confirmedStopsCount }}/{{ timetable.allStopsCount }}</span>
</span>
<span class="text--grayed" v-if="timetable.currentSceneryName">
<b>
{{ $t(`journal.${timetable.terminated ? 'last-seen-at' : 'currently-at'}`) }}
{{ timetable.currentSceneryName.replace(/.[a-zA-Z0-9]+.sc/, '') }}
<span v-if="timetable.currentLocation[0] || timetable.currentLocation[1]">&lpar;</span>
<span v-if="timetable.currentLocation[1]">
{{ $t('journal.timetable-location-route') }} {{ timetable.currentLocation[1] }}
</span>
<span v-else-if="timetable.currentLocation[0]">
{{ $t('journal.timetable-location-signal') }} {{ timetable.currentLocation[0] }}
</span>
<span v-if="timetable.currentLocation[0] || timetable.currentLocation[1]">&rpar;</span>
</b>
</span>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
import ProgressBar from '../../Global/ProgressBar.vue';
export default defineComponent({
components: { ProgressBar },
props: {
timetable: {
type: Object as PropType<TimetableHistory>,
required: true,
},
},
});
</script>
<style lang="scss" scoped>
@import '../../../styles/responsive.scss';
.item-status {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
@include smallScreen() {
justify-content: center;
}
}
</style>
@@ -0,0 +1,107 @@
<template>
<div class="stop-list" v-if="showExtraInfo == true">
<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">
&gt;
<span v-if="!showExtraInfo && i == 1 && timetableStops.length > 2">
... (+{{ timetableStops.length - 2 }}) &gt;
</span>
</span>
<span class="stop-name">{{ stop.stopName }}</span>
<span v-html="stop.html"></span>
</span>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import dateMixin from '../../../mixins/dateMixin';
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
export default defineComponent({
mixins: [dateMixin],
props: {
showExtraInfo: {
type: Boolean,
required: true
},
timetable: {
type: Object as PropType<TimetableHistory>,
required: true,
},
},
computed: {
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>
.stop-list {
word-wrap: break-word;
gap: 0.25em;
font-size: 0.95em;
color: #adadad;
&-item[data-confirmed='true'] {
color: lightgreen;
.stop-name {
font-weight: bold;
}
}
}
</style>
@@ -1,438 +0,0 @@
<template>
<transition-group class="journal-list" tag="ul" name="list-anim">
<li
v-for="{ timetable, sceneryList, stockHistoryComp, ...item } in computedTimetableHistory"
class="journal_item"
:key="timetable.id"
@click="item.showExtra.value = !item.showExtra.value"
>
<div class="journal_item-info">
<div class="info-general">
<span
class="general-train"
tabindex="0"
@click.stop="showTimetable(timetable)"
@keydown.enter="showTimetable(timetable)"
style="cursor: pointer"
>
<span class="text--grayed">#{{ timetable.id }}</span>
<span>
<strong class="text--primary">
{{ timetable.trainCategoryCode }}
</strong>
<strong>&nbsp;{{ timetable.trainNo }}</strong>
</span>
&bull;
<strong
v-if="timetable.driverLevel !== null"
class="level-badge driver"
:style="calculateExpStyle(timetable.driverLevel, timetable.driverIsSupporter)"
>
{{ timetable.driverLevel < 2 ? 'L' : `${timetable.driverLevel}` }}
</strong>
<strong>{{ timetable.driverName }}</strong>
</span>
<span class="general-time">
<b class="info-date">{{ localeDay(timetable.beginDate, $i18n.locale) }}</b>
<b
class="info-status"
:class="{
fulfilled: timetable.fulfilled || timetable.currentDistance >= timetable.routeDistance * 0.9,
terminated: timetable.terminated && !timetable.fulfilled,
active: !timetable.terminated,
}"
>
{{
!timetable.terminated
? $t('journal.timetable-active')
: timetable.fulfilled || timetable.currentDistance >= timetable.routeDistance * 0.9
? $t('journal.timetable-fulfilled')
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
}}
</b>
</span>
</div>
<div class="info-route">
<b>{{ timetable.route.replace('|', ' - ') }}</b>
</div>
<hr />
<div class="scenery-list">
<span
v-for="(scenery, i) in sceneryList.filter((_, i) =>
!item.showExtra.value ? i == 0 || i == sceneryList.length - 1 : true
)"
:key="scenery.name"
:class="{ confirmed: scenery.confirmed }"
>
<span v-if="i > 0">
&gt;
<span v-if="!item.showExtra.value && i == 1 && sceneryList.length > 2"
>... (+{{ sceneryList.length - 2 }}) &gt;</span
>
</span>
{{ scenery.name }}
<!-- Data odjazdu ze stacji początkowej -->
<span v-if="i == 0" v-html="scenery.beginDateHTML"></span>
<!-- Data przyjazdu do stacji końcowej -->
<span
v-if="i == sceneryList.length - 1 || (i == 1 && !item.showExtra.value)"
v-html="scenery.endDateHTML"
></span>
</span>
</div>
<!-- Status RJ -->
<div style="margin: 0.5em 0">
<span>
<b>{{ $t('journal.route-length') }}</b>
{{ !timetable.fulfilled ? timetable.currentDistance + ' /' : '' }}
{{ timetable.routeDistance }} km
</span>
&bull;
<span>
<b>{{ $t('journal.station-count') }}</b>
{{ timetable.confirmedStopsCount }} /
{{ timetable.allStopsCount }}
</span>
<span class="text--grayed" v-if="!timetable.fulfilled && timetable.currentSceneryName">
&bull;
<b>
{{ $t(`journal.${timetable.terminated ? 'last-seen-at' : 'currently-at'}`) }}
{{ timetable.currentSceneryName.replace(/.[a-zA-Z0-9]+.sc/, '') }}
</b>
</span>
</div>
<!-- Nick dyżurnego -->
<div v-if="timetable.authorName">
<b class="text--grayed">{{ $t('journal.dispatcher-name') }}&nbsp;</b>
<router-link class="dispatcher-link" :to="`/journal/dispatchers?dispatcherName=${timetable.authorName}`">
<b>{{ timetable.authorName }}</b>
</router-link>
<span class="text--grayed">
({{
(new Date(timetable.createdAt).getTime() - new Date(timetable.beginDate).getTime() < 0
? new Date(timetable.createdAt)
: new Date(timetable.beginDate)
).toLocaleString($i18n.locale, { timeStyle: 'short', dateStyle: 'full' })
}})
</span>
</div>
<button class="btn--option btn--show">
{{ $t('journal.stock-info') }}
<img :src="getIcon(`arrow-${item.showExtra.value ? 'asc' : 'desc'}`)" alt="Arrow" />
</button>
<!-- Dodatkowe informacje -->
<div class="info-extended" v-if="timetable.stockString && timetable.stockMass && item.showExtra.value">
<hr />
<div class="stock-specs">
<span class="badge specs-badge">
<span>{{ $t('journal.stock-max-speed') }}</span>
<span>{{ timetable.maxSpeed }}km/h</span>
</span>
<span class="badge specs-badge">
<span>{{ $t('journal.stock-length') }}</span>
<span>
{{
item.currentHistoryIndex.value == 0
? timetable.stockLength
: stockHistoryComp[item.currentHistoryIndex.value].stockLength || timetable.stockLength
}}m
</span>
</span>
<span class="badge specs-badge">
<span>{{ $t('journal.stock-mass') }}</span>
<span>
{{
Math.floor(
(item.currentHistoryIndex.value == 0
? timetable.stockMass!
: stockHistoryComp[item.currentHistoryIndex.value].stockMass || timetable.stockMass) / 1000
)
}}t
</span>
</span>
</div>
<div class="stock-history" v-if="stockHistoryComp.length > 1">
<button
class="btn--action"
v-for="(sh, i) in stockHistoryComp"
:data-checked="i == item.currentHistoryIndex.value"
@click.stop="item.currentHistoryIndex.value = i"
>
{{ sh.updatedAt }}
</button>
</div>
<ul class="stock-list">
<li
v-for="(car, i) in (item.currentHistoryIndex.value == 0
? timetable.stockString
: stockHistoryComp[item.currentHistoryIndex.value].stockString
).split(';')"
:key="i"
>
<img
@error="onImageError"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${car.split(':')[0]}.png`"
:alt="car"
/>
<div>{{ car.replace(/_/g, ' ').split(':')[0] }}</div>
</li>
</ul>
</div>
</div>
</li>
</transition-group>
</template>
<script lang="ts">
import { defineComponent, PropType, ref } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import imageMixin from '../../mixins/imageMixin';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import styleMixin from '../../mixins/styleMixin';
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
export default defineComponent({
props: {
timetableHistory: {
type: Array as PropType<TimetableHistory[]>,
required: true,
},
},
mixins: [dateMixin, imageMixin, modalTrainMixin, styleMixin],
computed: {
computedTimetableHistory() {
return this.timetableHistory.map((timetable) => ({
timetable,
sceneryList: this.getSceneryList(timetable),
stockHistoryComp: timetable.stockHistory
.slice()
.reverse()
.map((h) => {
const historyData = h.split('@');
return {
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, {
hour: '2-digit',
minute: '2-digit',
}),
stockString: historyData[1],
stockMass: Number(historyData[2]) || undefined,
stockLength: Number(historyData[3]) || undefined,
};
}),
showExtra: ref(false),
currentHistoryIndex: ref(0),
}));
},
},
methods: {
getSceneryList(timetable: TimetableHistory) {
return timetable.sceneriesString.split('%').map((name, i) => {
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.fulfilled ? timetable.endDate : timetable.scheduledEndDate,
this.$i18n.locale
)}</s> `
: '') +
`<span>${this.localeTime(
timetable.fulfilled || (timetable.terminated && !timetable.fulfilled)
? timetable.scheduledEndDate
: timetable.endDate,
this.$i18n.locale
)}</span>)`;
return { name, confirmed: i < timetable.confirmedStopsCount, beginDateHTML, endDateHTML };
});
},
showTimetable(timetable: TimetableHistory) {
if (!timetable) return;
if (timetable.terminated) return;
this.selectModalTrain(timetable.driverName + timetable.trainNo.toString());
},
onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = this.getImage('unknown.png');
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/animations.scss';
@import '../../styles/variables.scss';
@import '../../styles/responsive.scss';
@import '../../styles/badge.scss';
@import '../../styles/JournalSection.scss';
.journal_item {
cursor: pointer;
}
hr {
margin: 0.25em 0;
}
.info {
&-date {
margin-right: 0.5em;
}
&-status {
padding: 0.05em 0.35em;
color: black;
&.terminated {
background-color: salmon;
}
&.fulfilled {
background-color: lightgreen;
}
&.active {
background-color: lightblue;
}
}
&-general {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
margin-bottom: 0.5em;
}
&-route {
margin: 0.25em 0;
}
&-extended {
margin-top: 0.5em;
}
}
.general-train {
display: flex;
align-items: center;
gap: 0.25em;
}
ul.stock-list {
display: flex;
align-items: flex-end;
overflow: auto;
padding-bottom: 0.5em;
margin-top: 1em;
li > div {
text-align: center;
color: #aaa;
font-size: 0.9em;
}
li > img {
vertical-align: text-bottom;
max-height: 60px;
}
}
.stock-specs {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 0.5em;
.specs-badge {
margin: 0;
span:last-child {
color: black;
background-color: $accentCol;
}
}
}
.stock-history {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 1em;
button[data-checked='true'] {
color: $accentCol;
}
}
.scenery-list {
color: #adadad;
span.confirmed {
color: #a3eba3;
}
}
.btn--show {
display: flex;
margin-top: 1em;
font-weight: bold;
padding: 0.2em 0.45em;
img {
height: 1.3em;
}
}
@include smallScreen {
.info-general {
flex-direction: column;
}
.info-extended {
text-align: center;
}
.info-route {
display: flex;
justify-content: center;
}
.btn--show {
margin: 1em auto 0 auto;
}
.stock-specs {
justify-content: center;
}
.stock-history {
justify-content: center;
}
}
</style>
@@ -1,39 +1,61 @@
<template> <template>
<section class="scenery-dispatchers-history scenery-section"> <section class="scenery-table-section">
<Loading v-if="dataStatus != 2" /> <Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" />
<div class="no-history" v-else-if="historyList.length == 0">{{ $t('scenery.history-list-empty') }}</div>
<div class="list-warning" v-else-if="dispatcherHistoryList.length == 0">{{ $t('scenery.history-list-empty') }}</div> <table class="scenery-history-table" v-else="historyList.length">
<thead>
<th>{{ $t('scenery.dispatchers-history-hash') }}</th>
<th>{{ $t('scenery.dispatchers-history-dispatcher') }}</th>
<th>{{ $t('scenery.dispatchers-history-level') }}</th>
<th>{{ $t('scenery.dispatchers-history-rate') }}</th>
<th>{{ $t('scenery.dispatchers-history-date') }}</th>
</thead>
<ul class="history-list" v-else> <tbody>
<li class="list-item" v-for="item in dispatcherHistoryList"> <tr v-for="historyItem in historyList">
<router-link class="item-general" :to="`/journal/dispatchers?dispatcherName=${item.dispatcherName}`"> <td>#{{ historyItem.stationHash }}</td>
<span class="text--grayed">#{{ item.stationHash }}&nbsp;</span> <td>
<b <router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`">
v-if="item.dispatcherLevel !== null" <b>{{ historyItem.dispatcherName }}</b>
class="level-badge dispatcher" </router-link>
:style="calculateExpStyle(item.dispatcherLevel, item.dispatcherIsSupporter)" </td>
> <td>
{{ item.dispatcherLevel >= 2 ? item.dispatcherLevel : 'L' }} <b
</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 style="min-width: 300px">
<div v-if="historyItem.timestampTo">
<b>{{ $d(historyItem.timestampFrom) }}</b>
<b>{{ item.dispatcherName }}</b> {{ timestampToString(historyItem.timestampFrom) }}
</router-link> - {{ timestampToString(historyItem.timestampTo) }} ({{ calculateDuration(historyItem.currentDuration) }})
</div>
<div v-if="item.timestampTo"> <div class="dispatcher-online" v-else>
<b>{{ $d(item.timestampFrom) }}</b> {{ $t('journal.online-since') }}
<b>{{ timestampToString(historyItem.timestampFrom) }}</b>
{{ timestampToString(item.timestampFrom) }} ({{ calculateDuration(historyItem.currentDuration) }})
- {{ timestampToString(item.timestampTo) }} ({{ calculateDuration(item.currentDuration) }}) </div>
</div> </td>
</tr>
<div class="dispatcher-online" v-else> </tbody>
{{ $t('journal.online-since') }} </table>
<b>{{ timestampToString(item.timestampFrom) }}</b>
({{ calculateDuration(item.currentDuration) }})
</div>
</li>
</ul>
</section> </section>
<div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory">
{{ $t('scenery.bottom-info') }}
</button>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -46,37 +68,52 @@ import Station from '../../scripts/interfaces/Station';
import { URLs } from '../../scripts/utils/apiURLs'; import { URLs } from '../../scripts/utils/apiURLs';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import styleMixin from '../../mixins/styleMixin'; import styleMixin from '../../mixins/styleMixin';
import listObserverMixin from '../../mixins/listObserverMixin';
export default defineComponent({ export default defineComponent({
name: 'SceneryDispatchersHistory', name: 'SceneryDispatchersHistory',
mixins: [dateMixin, styleMixin], mixins: [dateMixin, styleMixin, listObserverMixin],
props: { props: {
station: { station: {
type: Object as PropType<Station>, type: Object as PropType<Station>,
required: true, required: true,
}, },
}, },
data() { data() {
return { return {
dispatcherHistoryList: [] as DispatcherHistory[], historyList: [] as DispatcherHistory[],
dataStatus: DataStatus.Loading, dataStatus: DataStatus.Loading,
DataStatus,
}; };
}, },
activated() {
this.fetchAPIData(); async activated() {
// if (this.historyList.length == 0) {
const fetchedHistory = await this.fetchAPIData();
if (fetchedHistory) this.historyList = fetchedHistory;
// }
}, },
methods: { methods: {
async fetchAPIData(countFrom = 0, countLimit = 30) { async fetchAPIData(countFrom = 0, countLimit = 30): Promise<DispatcherHistory[] | null> {
try { try {
this.dataStatus = DataStatus.Loading;
const requestString = `${URLs.stacjownikAPI}/api/getDispatchers?stationName=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`; const requestString = `${URLs.stacjownikAPI}/api/getDispatchers?stationName=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`;
const historyAPIData: DispatcherHistory[] = await (await axios.get(requestString)).data; const historyAPIData: DispatcherHistory[] = await (await axios.get(requestString)).data;
this.dispatcherHistoryList = historyAPIData;
this.dataStatus = DataStatus.Loaded; this.dataStatus = DataStatus.Loaded;
return historyAPIData;
} catch (error) { } catch (error) {
this.dataStatus = DataStatus.Error;
console.error(error); console.error(error);
return null;
} }
}, },
navigateToHistory() {
this.$router.push(`/journal/dispatchers?sceneryName=${this.station.name}`);
},
}, },
components: { Loading }, components: { Loading },
}); });
@@ -84,30 +121,10 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/SceneryView/styles.scss'; @import '../../styles/sceneryViewTables.scss';
.history-list { .level-badge {
padding: 0 0.5em; margin: 0 auto;
}
.list-item {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
text-align: left;
background-color: #353535;
padding: 0.5em;
margin: 0.5em 0;
line-height: 1.5em;
}
.item-general {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.25em;
} }
.dispatcher-online { .dispatcher-online {
+7 -1
View File
@@ -4,7 +4,9 @@
{{ station.name }} {{ station.name }}
</a> </a>
<div class="scenery-abbrev">{{ $t('scenery.abbrev') }} <b>{{ station.generalInfo?.abbr }}</b></div> <div class="scenery-abbrev">
{{ $t('scenery.abbrev') }} <b>{{ station.generalInfo?.abbr }}</b>
</div>
<div class="scenery-hash" v-if="station.onlineInfo?.hash">#{{ station.onlineInfo.hash }}</div> <div class="scenery-hash" v-if="station.onlineInfo?.hash">#{{ station.onlineInfo.hash }}</div>
</section> </section>
@@ -28,6 +30,10 @@ export default defineComponent({
@import '../../styles/variables.scss'; @import '../../styles/variables.scss';
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
.info-header {
margin-top: 1em;
}
.scenery-name { .scenery-name {
font-weight: bold; font-weight: bold;
font-size: 3em; font-size: 3em;
+7 -19
View File
@@ -2,7 +2,7 @@
<div class="scenery-info"> <div class="scenery-info">
<section v-if="!timetableOnly"> <section v-if="!timetableOnly">
<div class="scenery-info-general" v-if="station.generalInfo"> <div class="scenery-info-general" v-if="station.generalInfo">
<scenery-info-icons :station="station" /> <SceneryInfoIcons :station="station" />
<div class="scenery-general-list"> <div class="scenery-general-list">
<span> <span>
@@ -26,28 +26,16 @@
</span> </span>
<span v-if="station.generalInfo.project"> <span v-if="station.generalInfo.project">
&bull; <b>{{ $t('scenery.project-title') }}: </b> &bull; <b>{{ $t('scenery.project-title') }}: </b>
<a <a style="color: salmon; text-decoration: underline; font-weight: bold" :href="station.generalInfo.projectUrl" target="_blank">
style="color: salmon; text-decoration: underline; font-weight: bold"
:href="station.generalInfo.projectUrl"
target="_blank"
>
{{ station.generalInfo.project }} {{ station.generalInfo.project }}
</a> </a>
</span> </span>
</div> </div>
<scenery-info-routes :station="station" /> <SceneryInfoRoutes :station="station" />
<div class="scenery-authors" v-if="station.generalInfo.authors && station.generalInfo.authors.length > 0"> <div class="scenery-authors" v-if="station.generalInfo.authors && station.generalInfo.authors.length > 0">
<b> <b> {{ $t('scenery.authors-title', { authors: station.generalInfo.authors.length }, station.generalInfo.authors.length) }}: </b>
{{
$t(
'scenery.authors-title',
{ authors: station.generalInfo.authors.length },
station.generalInfo.authors.length
)
}}:
</b>
{{ station.generalInfo.authors.join(', ') }} {{ station.generalInfo.authors.join(', ') }}
</div> </div>
</div> </div>
@@ -55,14 +43,14 @@
<div style="margin: 2em 0; height: 2px; background-color: white"></div> <div style="margin: 2em 0; height: 2px; background-color: white"></div>
<!-- info dispatcher --> <!-- info dispatcher -->
<scenery-info-dispatcher :station="station" :onlineFrom="onlineFrom" /> <SceneryInfoDispatcher :station="station" :onlineFrom="onlineFrom" />
<div class="info-lists"> <div class="info-lists">
<!-- user list --> <!-- user list -->
<scenery-info-user-list :station="station" /> <SceneryInfoUserList :station="station" />
<!-- spawn list --> <!-- spawn list -->
<scenery-info-spawn-list :station="station" /> <SceneryInfoSpawnList :station="station" />
</div> </div>
</section> </section>
</div> </div>
@@ -21,18 +21,11 @@
</span> </span>
</div> </div>
<span class="status-badge" v-if="station.onlineInfo && onlineFrom > 0"> <StationStatusBadge
OD {{ new Date(onlineFrom).toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' }) }} :statusID="station.onlineInfo?.statusID"
</span> :isOnline="station.onlineInfo ? true : false"
:statusTimestamp="station.onlineInfo?.statusTimestamp"
<span class="status-badge" v-if="station.onlineInfo" :class="station.onlineInfo.statusID"> />
{{ $t(`status.${station.onlineInfo.statusID}`) }}
{{ station.onlineInfo.statusID == 'online' ? timestampToString(station.onlineInfo.statusTimestamp) : '' }}
</span>
<span class="status-badge free" v-else>
{{ $t('status.free') }}
</span>
</section> </section>
</template> </template>
@@ -43,20 +36,21 @@ import imageMixin from '../../../mixins/imageMixin';
import routerMixin from '../../../mixins/routerMixin'; import routerMixin from '../../../mixins/routerMixin';
import styleMixin from '../../../mixins/styleMixin'; import styleMixin from '../../../mixins/styleMixin';
import Station from '../../../scripts/interfaces/Station'; import Station from '../../../scripts/interfaces/Station';
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
export default defineComponent({ export default defineComponent({
mixins: [styleMixin, dateMixin, routerMixin, imageMixin], mixins: [styleMixin, dateMixin, routerMixin, imageMixin],
props: { props: {
station: { station: {
type: Object as () => Station, type: Object as () => Station,
default: {}, default: {},
},
onlineFrom: {
type: Number,
default: -1,
},
}, },
components: { StationStatusBadge }
onlineFrom: {
type: Number,
default: -1,
},
},
}); });
</script> </script>
@@ -104,3 +98,4 @@ export default defineComponent({
} }
} }
</style> </style>
@@ -4,9 +4,11 @@
<b>{{ $t('scenery.one-way-routes') }}</b> <b>{{ $t('scenery.one-way-routes') }}</b>
<ul class="routes-list"> <ul class="routes-list">
<li v-for="route in station.generalInfo.routes.oneWay"> <li v-for="route in station.generalInfo.routes.oneWay" @click="setActiveShowLength(route.name)">
<span :class="{ 'no-catenary': !route.catenary, internal: route.isInternal }"> {{ route.name }}</span> <span :class="{ 'no-catenary': !route.catenary, internal: route.isInternal }"> {{ route.name }}</span>
<span v-if="route.speed" class="speed">{{ route.speed }}</span> <span v-if="route.speed" class="speed">
{{ activeShowLength.includes(route.name) ? route.length + 'm' : route.speed }}
</span>
<span v-if="route.SBL" class="sbl">SBL</span> <span v-if="route.SBL" class="sbl">SBL</span>
</li> </li>
</ul> </ul>
@@ -16,9 +18,11 @@
<b>{{ $t('scenery.two-way-routes') }}</b> <b>{{ $t('scenery.two-way-routes') }}</b>
<ul class="routes-list"> <ul class="routes-list">
<li v-for="route in station.generalInfo.routes.twoWay"> <li v-for="(route, i) in station.generalInfo.routes.twoWay" @click="setActiveShowLength(route.name)">
<span :class="{ 'no-catenary': !route.catenary, internal: route.isInternal }">{{ route.name }}</span> <span :class="{ 'no-catenary': !route.catenary, internal: route.isInternal }">{{ route.name }}</span>
<span v-if="route.speed" class="speed">{{ route.speed }}</span> <span v-if="route.speed" class="speed">
{{ activeShowLength.includes(route.name) ? route.length + 'm' : route.speed }}
</span>
<span v-if="route.SBL" class="sbl">SBL</span> <span v-if="route.SBL" class="sbl">SBL</span>
</li> </li>
</ul> </ul>
@@ -37,6 +41,19 @@ export default defineComponent({
default: {}, default: {},
}, },
}, },
methods: {
setActiveShowLength(name: string) {
if (this.activeShowLength.includes(name)) this.activeShowLength.splice(this.activeShowLength.indexOf(name), 1);
else this.activeShowLength.push(name);
},
},
data() {
return {
activeShowLength: [] as string[],
};
},
}); });
</script> </script>
@@ -66,6 +83,11 @@ ul.routes-list {
li { li {
margin: 0.5em 0.25em; margin: 0.5em 0.25em;
cursor: pointer;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
span { span {
padding: 0.2em 0.25em; padding: 0.2em 0.25em;
@@ -100,7 +122,6 @@ ul.routes-list {
&:only-child { &:only-child {
border-radius: 0.5em; border-radius: 0.5em;
} }
} }
} }
@@ -1,52 +1,65 @@
<template> <template>
<section class="info-spawn-list"> <section class="info-spawn-list">
<h3 class="spawn-header section-header"> <h3 class="spawn-header section-header">
<img :src="getIcon('spawn')" alt="icon-spawn" /> <img :src="getIcon('spawn')" alt="icon-spawn" />
&nbsp;{{ $t('scenery.spawns') }} &nbsp; &nbsp;{{ $t('scenery.spawns') }} &nbsp;
<span class="text--primary">{{ station.onlineInfo?.spawns.length || '0' }}</span> <span class="text--primary">{{ station.onlineInfo?.spawns.length || '0' }}</span>
</h3> </h3>
<span v-if="station.onlineInfo"> <span v-if="station.onlineInfo">
<span <span
class="badge spawn" class="badge spawn"
v-for="(spawn, i) in station.onlineInfo.spawns" v-for="(spawn, i) in sortedSpawns"
:key="spawn.spawnName + station.onlineInfo?.dispatcherName + i" :key="spawn.spawnName + station.onlineInfo?.dispatcherName + i"
> :data-electrified="spawn.isElectrified"
<span class="spawn_name">{{ spawn.spawnName }}</span> >
<span class="spawn_length">{{ spawn.spawnLength }}m</span> <span class="spawn_name">{{ spawn.spawnName }}</span>
</span> <span class="spawn_length">{{ spawn.spawnLength }}m</span>
</span> </span>
</span>
<span class="badge spawn badge-none" v-if="!station.onlineInfo || station.onlineInfo.spawns.length == 0"
>{{ $t('scenery.no-spawns') }} <span class="badge spawn badge-none" v-if="!station.onlineInfo || station.onlineInfo.spawns.length == 0"
</span> >{{ $t('scenery.no-spawns') }}
</section> </span>
</template> </section>
</template>
<script lang="ts">
import { defineComponent } from 'vue'; <script lang="ts">
import imageMixin from '../../../mixins/imageMixin'; import { defineComponent } from 'vue';
import Station from '../../../scripts/interfaces/Station'; import imageMixin from '../../../mixins/imageMixin';
import Station from '../../../scripts/interfaces/Station';
export default defineComponent({
mixins: [imageMixin], export default defineComponent({
mixins: [imageMixin],
props: {
station: { props: {
type: Object as () => Station, station: {
default: {}, type: Object as () => Station,
}, default: {},
}, },
}); },
</script>
computed: {
<style lang="scss" scoped> sortedSpawns() {
@import '../../../styles/variables.scss'; return this.station.onlineInfo?.spawns.sort((s1, s2) => (s1.spawnLength < s2.spawnLength ? 1 : -1));
},
.spawn { },
&_length { });
background: $accentCol; </script>
color: black;
} <style lang="scss" scoped>
} @import '../../../styles/variables.scss';
</style>
.spawn {
color: white;
&_length {
background-color: #404040;
color: #cfcfcf;
}
&[data-electrified='true'] > &_name {
background-color: #007599;
}
}
</style>
@@ -1,133 +1,131 @@
<template> <template>
<section class="info-user-list"> <section class="info-user-list">
<h3 class="user-header section-header"> <h3 class="user-header section-header">
<img :src="getIcon('user')" alt="icon-user" /> <img :src="getIcon('user')" alt="icon-user" />
&nbsp;{{ $t('scenery.users') }} &nbsp; &nbsp;{{ $t('scenery.users') }} &nbsp;
<span class="text--primary">{{ station.onlineInfo?.currentUsers || '0' }}</span <span class="text--primary">{{ station.onlineInfo?.currentUsers || '0' }}</span
>&nbsp;/&nbsp;<span class="text--primary">{{ station.onlineInfo?.maxUsers || '0' }}</span> >&nbsp;/&nbsp;<span class="text--primary">{{ station.onlineInfo?.maxUsers || '0' }}</span>
</h3> </h3>
<div <div
v-for="(train, i) in computedStationTrains" v-for="(train, i) in computedStationTrains"
class="badge user" class="badge user"
:class="train.stopStatus" :class="train.stopStatus"
:key="train.trainId" :key="train.trainId"
tabindex="0" tabindex="0"
@click="selectModalTrain(train.trainId)" @click.prevent="selectModalTrain(train.trainId, $event.currentTarget)"
@keydown.enter="selectModalTrain(train.trainId)" @keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
> >
<span class="user_train">{{ train.trainNo }}</span> <span class="user_train">{{ train.trainNo }}</span>
<span class="user_name">{{ train.driverName }}</span> <span class="user_name">{{ train.driverName }}</span>
</div> </div>
<div class="badge user badge-none" v-if="!computedStationTrains || computedStationTrains.length == 0"> <div class="badge user badge-none" v-if="!computedStationTrains || computedStationTrains.length == 0">
{{ $t('scenery.no-users') }} {{ $t('scenery.no-users') }}
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent } from 'vue';
import { computed, defineComponent } from 'vue'; import imageMixin from '../../../mixins/imageMixin';
import imageMixin from '../../../mixins/imageMixin'; import modalTrainMixin from '../../../mixins/modalTrainMixin';
import modalTrainMixin from '../../../mixins/modalTrainMixin'; import routerMixin from '../../../mixins/routerMixin';
import routerMixin from '../../../mixins/routerMixin'; import Station from '../../../scripts/interfaces/Station';
import Station from '../../../scripts/interfaces/Station'; import { useStore } from '../../../store/store';
import { useStore } from '../../../store/store';
export default defineComponent({
export default defineComponent({ mixins: [routerMixin, imageMixin, modalTrainMixin],
mixins: [routerMixin, imageMixin, modalTrainMixin],
props: {
props: { station: {
station: { type: Object as () => Station,
type: Object as () => Station, default: {},
default: {}, },
}, },
},
setup(props) {
setup(props) { const store = useStore();
const store = useStore();
const computedStationTrains = computed(() => {
const computedStationTrains = computed(() => { if (!props.station) return [];
if (!props.station) return [];
const station = props.station as Station;
const station = props.station as Station; if (!station.onlineInfo) return [];
if (!station.onlineInfo) return []; if (!station.onlineInfo.stationTrains) return [];
if (!station.onlineInfo.stationTrains) return [];
return station.onlineInfo.stationTrains.map((train) => {
return station.onlineInfo.stationTrains.map((train) => { const scheduledTrainStatus = station.onlineInfo?.scheduledTrains?.find((st) => st.trainNo === train.trainNo);
const scheduledTrainStatus = station.onlineInfo?.scheduledTrains?.find((st) => st.trainNo === train.trainNo);
return {
return { ...train,
...train, stopStatus: scheduledTrainStatus?.stopStatus || 'no-timetable',
stopStatus: scheduledTrainStatus?.stopStatus || 'no-timetable', };
}; });
}); });
});
return { computedStationTrains, store };
return { computedStationTrains, store }; },
}, });
}); </script>
</script>
<style lang="scss" scoped>
<style lang="scss" scoped> $no-timetable: #aaa;
$no-timetable: #aaa; $departed: springgreen;
$departed: springgreen; $stopped: #ffa600;
$stopped: #ffa600; $online: gold;
$online: gold; $terminated: salmon;
$terminated: salmon; $disconnected: slategray;
$disconnected: slategray;
.info-user-list {
.info-user-list { width: 100%;
width: 100%;
ul {
ul { display: flex;
display: flex; flex-wrap: wrap;
flex-wrap: wrap; justify-content: center;
justify-content: center; }
} }
}
.user {
.user { cursor: pointer;
cursor: pointer;
&_train {
&_train { color: black;
color: black; background-color: $no-timetable;
background-color: $no-timetable;
transition: background-color 200ms;
transition: background-color 200ms; -ms-transition: background-color 200ms;
-ms-transition: background-color 200ms; -webkit-transition: background-color 200ms;
-webkit-transition: background-color 200ms; }
}
&.no-timetable .user_train {
&.no-timetable .user_train { background-color: $no-timetable;
background-color: $no-timetable; }
}
&.departed > &_train {
&.departed > &_train { background-color: $departed;
background-color: $departed; }
}
&.stopped > &_train {
&.stopped > &_train { background-color: $stopped;
background-color: $stopped; }
}
&.online > &_train {
&.online > &_train { background-color: $online;
background-color: $online; }
}
&.terminated > &_train {
&.terminated > &_train { background-color: $terminated;
background-color: $terminated; }
}
&.disconnected > &_train {
&.disconnected > &_train { background-color: $disconnected;
background-color: $disconnected; }
}
&.offline {
&.offline { background: firebrick;
background: firebrick; pointer-events: none;
pointer-events: none; }
} }
} </style>
</style>
+65 -119
View File
@@ -14,19 +14,11 @@
</span> </span>
<span class="header_links"> <span class="header_links">
<a <a :href="`https://pragotron-td2.web.app/board?name=${station.name}`" target="_blank" :title="$t('scenery.pragotron-link')">
:href="`https://pragotron-td2.web.app/board?name=${station.name}`"
target="_blank"
:title="$t('scenery.pragotron-link')"
>
<img :src="getIcon('pragotron')" alt="icon-pragotron" /> <img :src="getIcon('pragotron')" alt="icon-pragotron" />
</a> </a>
<a <a :href="tabliceZbiorczeHref" target="_blank" :title="$t('scenery.tablice-link')">
:href="`https://tablice-td2.web.app/?station=${station.name}`"
target="_blank"
:title="$t('scenery.tablice-link')"
>
<img :src="getIcon('tablice', 'ico')" alt="icon-tablice" /> <img :src="getIcon('tablice', 'ico')" alt="icon-tablice" />
</a> </a>
</span> </span>
@@ -39,8 +31,8 @@
<button <button
:key="cp.checkpointName" :key="cp.checkpointName"
class="checkpoint_item" class="checkpoint_item"
:class="{ current: selectedCheckpoint === cp.checkpointName }" :class="{ current: chosenCheckpoint === cp.checkpointName }"
@click="selectCheckpoint(cp)" @click="setCheckpoint(cp)"
> >
{{ cp.checkpointName }} {{ cp.checkpointName }}
</button> </button>
@@ -67,8 +59,8 @@
v-for="(scheduledTrain, i) in computedScheduledTrains" v-for="(scheduledTrain, i) in computedScheduledTrains"
:key="scheduledTrain.trainId" :key="scheduledTrain.trainId"
tabindex="0" tabindex="0"
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId)" @click.prevent.stop="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId)" @keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
> >
<span class="timetable-general"> <span class="timetable-general">
<span class="general-info"> <span class="general-info">
@@ -106,40 +98,29 @@
</div> </div>
<div v-else> <div v-else>
<div> <div>
<s style="margin-right: 0.2em" class="text--grayed">{{ <s style="margin-right: 0.2em" class="text--grayed">{{ timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) }}</s>
timestampToString(scheduledTrain.stopInfo.arrivalTimestamp)
}}</s>
</div> </div>
<span> <span>
{{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }} {{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }}
({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : '' ({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : '' }}{{ scheduledTrain.stopInfo.arrivalDelay }})
}}{{ scheduledTrain.stopInfo.arrivalDelay }})
</span> </span>
</div> </div>
</span> </span>
</span> </span>
<span class="schedule-stop"> <span class="schedule-stop">
<span class="stop-time"> <span class="stop-connection">
<span v-if="scheduledTrain.stopInfo.stopTime"> {{ scheduledTrain.arrivingLine }}
{{ scheduledTrain.stopInfo.stopTime }}
{{ scheduledTrain.stopInfo.stopType || 'pt' }}
</span>
<span v-else>&nbsp;</span>
</span> </span>
<span class="arrow"></span> <span class="stop-time">
{{ scheduledTrain.stopInfo.stopTime || '' }}
{{ scheduledTrain.stopInfo.stopTime ? scheduledTrain.stopInfo.stopType || 'pt' : '' }}
</span>
<span class="stop-line"> <span class="stop-connection">
<span> {{ scheduledTrain.departureLine }}
{{ scheduledTrain.arrivingLine }}
</span>
<span></span>
<span>
{{ scheduledTrain.departureLine }}
</span>
</span> </span>
</span> </span>
@@ -154,15 +135,12 @@
</div> </div>
<div v-else> <div v-else>
<div> <div>
<s style="margin-right: 0.2em" class="text--grayed">{{ <s style="margin-right: 0.2em" class="text--grayed">{{ timestampToString(scheduledTrain.stopInfo.departureTimestamp) }}</s>
timestampToString(scheduledTrain.stopInfo.departureTimestamp)
}}</s>
</div> </div>
<span> <span>
{{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }} {{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }}
({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : '' ({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : '' }}{{ scheduledTrain.stopInfo.departureDelay }})
}}{{ scheduledTrain.stopInfo.departureDelay }})
</span> </span>
</div> </div>
</span> </span>
@@ -211,16 +189,22 @@ export default defineComponent({
listOpen: false, listOpen: false,
}), }),
mounted() {
this.loadSelectedOption();
},
activated() {
this.loadSelectedOption();
},
setup(props) { setup(props) {
const route = useRoute(); const route = useRoute();
const currentURL = computed(() => `${location.origin}${route.fullPath}`); const currentURL = computed(() => `${location.origin}${route.fullPath}`);
const store = useStore(); const store = useStore();
const selectedCheckpoint = ref( const chosenCheckpoint = ref(
props.station?.generalInfo?.checkpoints?.length == 0 props.station?.generalInfo?.checkpoints?.length == 0 ? '' : props.station?.generalInfo?.checkpoints[0].checkpointName || null
? ''
: props.station?.generalInfo?.checkpoints[0].checkpointName || ''
); );
const computedScheduledTrains = computed(() => { const computedScheduledTrains = computed(() => {
@@ -229,8 +213,7 @@ export default defineComponent({
const station = props.station as Station; const station = props.station as Station;
let scheduledTrains = let scheduledTrains =
station.generalInfo?.checkpoints.find((cp) => cp.checkpointName === selectedCheckpoint.value) station.generalInfo?.checkpoints.find((cp) => cp.checkpointName === chosenCheckpoint.value)?.scheduledTrains ||
?.scheduledTrains ||
station.onlineInfo?.scheduledTrains || station.onlineInfo?.scheduledTrains ||
[]; [];
@@ -251,12 +234,21 @@ export default defineComponent({
return { return {
currentURL, currentURL,
selectedCheckpoint, chosenCheckpoint,
computedScheduledTrains, computedScheduledTrains,
store, store,
}; };
}, },
computed: {
tabliceZbiorczeHref() {
let url = `https://tablice-td2.web.app/?station=${this.station.name}`;
if (this.chosenCheckpoint) url += `&checkpoint=${this.chosenCheckpoint}`;
return url;
},
},
methods: { methods: {
loadSelectedOption() { loadSelectedOption() {
if (!this.station) return; if (!this.station) return;
@@ -264,27 +256,19 @@ export default defineComponent({
if (!this.station.generalInfo.checkpoints) return; if (!this.station.generalInfo.checkpoints) return;
if (this.station.generalInfo.checkpoints.length == 0) return; if (this.station.generalInfo.checkpoints.length == 0) return;
if (this.selectedCheckpoint != '') return; if (this.chosenCheckpoint != '') return;
this.selectedCheckpoint = this.station.generalInfo.checkpoints[0].checkpointName; this.chosenCheckpoint = this.station.generalInfo.checkpoints[0].checkpointName;
}, },
selectCheckpoint(cp: { checkpointName: string }) { setCheckpoint(cp: { checkpointName: string }) {
this.selectedCheckpoint = cp.checkpointName; this.chosenCheckpoint = cp.checkpointName;
}, },
showTimetableOnlyView() { showTimetableOnlyView() {
this.$router.push(`${this.$route.fullPath}&timetableOnly=1`); this.$router.push(`${this.$route.fullPath}&timetableOnly=1`);
}, },
}, },
mounted() {
this.loadSelectedOption();
},
activated() {
this.loadSelectedOption();
},
}); });
</script> </script>
@@ -341,8 +325,8 @@ export default defineComponent({
max-width: 1100px; max-width: 1100px;
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2em 0.5em; gap: 1.2em 0.5em;
overflow: hidden; overflow: hidden;
@@ -368,7 +352,9 @@ export default defineComponent({
&-schedule { &-schedule {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(30px, 1fr)); grid-template-columns: repeat(3, 1fr);
gap: 0.2em;
align-items: center;
width: 100%; width: 100%;
max-width: 400px; max-width: 400px;
@@ -400,33 +386,6 @@ export default defineComponent({
} }
} }
.arrow {
border: solid white;
border-width: 0 2px 2px 0;
display: inline-block;
padding: 2px;
margin-left: 50px;
position: relative;
transform: rotate(-45deg);
&::before {
content: '';
position: absolute;
display: block;
width: 55px;
height: 3px;
top: 4px;
left: 4px;
transform: translate(-100%, -1px) rotate(45deg);
transform-origin: right bottom;
background: white;
}
}
.general-info { .general-info {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -453,47 +412,34 @@ export default defineComponent({
.schedule { .schedule {
&-arrival, &-arrival,
&-stop,
&-departure { &-departure {
display: flex;
justify-content: center;
align-items: center;
margin: 0 0.3rem;
font-size: 1.15em; font-size: 1.15em;
} }
&-stop { &-stop {
position: relative; display: grid;
display: flex; grid-template-columns: repeat(3, 1fr);
flex-direction: column; gap: 0.5em;
font-size: 0.9em; align-items: end;
padding: 0.3em 0; .stop-connection {
font-size: 0.95em;
.stop-line {
display: flex;
position: absolute;
span {
width: 65px;
word-break: break-all;
}
span:first-child {
text-align: right;
}
span:last-child {
text-align: left;
}
} }
.stop-time { .stop-time {
position: absolute; position: relative;
transform: translateY(-15px); inline-size: max-content;
align-self: center;
font-size: 0.9em;
color: $accentCol; color: $accentCol;
&::after {
content: '\027F6';
display: block;
font-size: 2.2em;
line-height: 0.65em;
}
} }
} }
} }
@@ -1,19 +1,20 @@
<template> <template>
<section class="scenery-timetables-history scenery-section"> <section class="scenery-table-section">
<Loading v-if="dataStatus != 2" /> <Loading v-if="dataStatus != DataStatus.Loaded" />
<div class="no-history" v-else-if="historyList.length == 0">{{ $t('scenery.history-list-empty') }}</div>
<table v-else-if="sceneryHistoryList.length"> <table class="scenery-history-table" v-else>
<thead> <thead>
<th>{{ $t('scenery.timetables-history-id') }}</th> <th>{{ $t('scenery.timetables-history-id') }}</th>
<th>{{ $t('scenery.timetables-history-number')}}</th> <th>{{ $t('scenery.timetables-history-number') }}</th>
<th>{{ $t('scenery.timetables-history-route')}}</th> <th>{{ $t('scenery.timetables-history-route') }}</th>
<th>{{ $t('scenery.timetables-history-driver')}}</th> <th>{{ $t('scenery.timetables-history-driver') }}</th>
<th>{{ $t('scenery.timetables-history-author')}}</th> <th>{{ $t('scenery.timetables-history-author') }}</th>
<th>{{ $t('scenery.timetables-history-date')}}</th> <th>{{ $t('scenery.timetables-history-date') }}</th>
</thead> </thead>
<tbody> <tbody>
<tr v-for="historyItem in sceneryHistoryList" @click="test"> <tr v-for="historyItem in historyList">
<td> <td>
<router-link :to="`/journal/timetables?timetableId=${historyItem.id}`">#{{ historyItem.id }}</router-link> <router-link :to="`/journal/timetables?timetableId=${historyItem.id}`">#{{ historyItem.id }}</router-link>
</td> </td>
@@ -26,7 +27,7 @@
<td> <td>
<router-link <router-link
v-if="historyItem.authorName" v-if="historyItem.authorName"
:to="`/journal/dispatchers?dispatcherName=${historyItem.authorName}`" :to="`/journal/timetables?authorName=${historyItem.authorName}`"
>{{ historyItem.authorName }} >{{ historyItem.authorName }}
</router-link> </router-link>
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i> <i v-else>{{ $t('scenery.timetable-author-unknown') }}</i>
@@ -38,34 +39,13 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="list-warning" v-else>{{ $t('scenery.history-list-empty') }}</div>
<!-- <ul class="history-list" v-else>
<li class="list-item" v-for="historyItem in sceneryHistoryList">
<div>
<b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b>
{{ localeTime(historyItem.beginDate, $i18n.locale) }}
</div>
<div>
<router-link :to="`/journal/timetables?timetableId=${historyItem.id}`">
<span class="text--grayed"> #{{ historyItem.id }} </span>
<b class="text--primary">&nbsp;{{ historyItem.trainCategoryCode }} {{ historyItem.trainNo }}</b>
<div>{{ historyItem.driverName }}</div>
</router-link>
</div>
<div>{{ historyItem.route.replace('|', ' -> ') }}</div>
<div>
{{ $t('scenery.timetable-author-title') }}:
<b v-if="historyItem.authorName">{{ historyItem.authorName }}</b>
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i>
</div>
</li>
</ul> -->
</section> </section>
<div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory()">
{{ $t('scenery.bottom-info') }}
</button>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -77,40 +57,47 @@ import { TimetableHistory, SceneryTimetableHistory } from '../../scripts/interfa
import Station from '../../scripts/interfaces/Station'; import Station from '../../scripts/interfaces/Station';
import { URLs } from '../../scripts/utils/apiURLs'; import { URLs } from '../../scripts/utils/apiURLs';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import listObserverMixin from '../../mixins/listObserverMixin';
export default defineComponent({ export default defineComponent({
name: 'SceneryTimetablesHistory', name: 'SceneryTimetablesHistory',
mixins: [dateMixin], mixins: [dateMixin, listObserverMixin],
props: { props: {
station: { station: {
type: Object as PropType<Station>, type: Object as PropType<Station>,
required: true, required: true,
}, },
}, },
data() { data() {
return { return {
sceneryHistoryList: [] as TimetableHistory[], historyList: [] as TimetableHistory[],
dataStatus: DataStatus.Loading, dataStatus: DataStatus.Loading,
DataStatus,
}; };
}, },
activated() {
this.fetchAPIData(); async activated() {
const fetchedHistory = await this.fetchAPIData();
if (fetchedHistory) this.historyList = fetchedHistory.timetables;
}, },
methods: { methods: {
async fetchAPIData(countFrom = 0, countLimit = 15) { async fetchAPIData(countFrom = 0, countLimit = 15): Promise<SceneryTimetableHistory | null> {
try { try {
const requestString = `${URLs.stacjownikAPI}/api/getSceneryTimetables?name=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`; const requestString = `${URLs.stacjownikAPI}/api/getIssuedTimetables?name=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`;
const historyAPIData: SceneryTimetableHistory = await (await axios.get(requestString)).data; const historyAPIData: SceneryTimetableHistory = await (await axios.get(requestString)).data;
this.sceneryHistoryList = historyAPIData.sceneryTimetables;
this.dataStatus = DataStatus.Loaded; this.dataStatus = DataStatus.Loaded;
return historyAPIData;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return null;
} }
}, },
test() { navigateToHistory() {
console.log('test'); this.$router.push(`/journal/timetables?issuedFrom=${this.station.name}`);
}, },
}, },
components: { Loading }, components: { Loading },
@@ -119,46 +106,5 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/SceneryView/styles.scss'; @import '../../styles/sceneryViewTables.scss';
.list-warning {
padding: 1em 0.5em;
background-color: #444;
font-size: 1.2em;
}
.history-list {
padding: 0 0.5em;
}
table {
width: 100%;
border-collapse: collapse;
thead {
position: sticky;
top: 0;
background-color: #222222;
}
th {
padding: 0.5em;
}
tr {
background-color: #353535;
border: none;
}
td {
padding: 0.75em;
border-bottom: solid 5px #111;
}
}
@include smallScreen {
.list-item {
grid-template-columns: 1fr 1fr;
}
}
</style> </style>
+41 -43
View File
@@ -1,13 +1,10 @@
<template> <template>
<button <label @dblclick="handleDbClick">
class="btn--action" <input v-model="option.value" type="checkbox" :class="option.section" :name="option.id" />
:class="option.section" <span>
:data-selected="option.value" {{ $t(`filters.${option.id}`) }}
@click="handleLeftClick" </span>
@dblclick="handleDbClick" </label>
>
{{ $t(`filters.${option.id}`) }}
</button>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -36,38 +33,24 @@ export default defineComponent({
}; };
}, },
methods: { watch: {
handleLeftClick() { 'option.value'() {
this.option.value = !this.option.value; this.filterStore.changeFilterValue(this.option.name, !this.option.value);
this.filterStore.lastClickedFilterId = '';
this.filterStore.changeFilterValue({
name: this.option.name,
value: !this.option.value,
});
}, },
},
methods: {
handleDbClick(e: Event) { handleDbClick(e: Event) {
e.preventDefault(); e.preventDefault();
this.filterStore.lastClickedFilterId = this.option.id; this.filterStore.lastClickedFilterId = this.option.id;
this.option.value = true; this.option.value = true;
this.filterStore.changeFilterValue({
name: this.option.name,
value: !this.option.value,
});
this.filterStore.inputs.options this.filterStore.inputs.options
.filter((option) => { .filter((option) => {
return option.section == this.option.section && option.id != this.option.id; return option.section == this.option.section && option.id != this.option.id;
}) })
.forEach((option) => { .forEach((option) => {
this.filterStore.changeFilterValue({
name: option.name,
value: this.option.value,
});
option.value = !this.option.value; option.value = !this.option.value;
}); });
}, },
@@ -76,25 +59,40 @@ export default defineComponent({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
$realityCol: #e03b07; @import '../../styles/variables.scss';
$accessCol: #e03b07;
$controlCol: #0085ff;
$signalCol: #bf7c00;
$statusCol: #349b32;
$saveCol: #28a826;
$routesCol: #9049c0;
button { label {
padding: 0.25em; position: relative;
border-radius: 0; user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
&:focus-visible { span {
outline: 1px solid white; cursor: pointer;
display: inline-block;
width: 100%;
text-align: center;
padding: 0.25em;
background-color: #444;
} }
&[data-selected='true'] { span:hover {
background-color: forestgreen; background-color: #555;
font-weight: bold; }
input[type='checkbox'] {
cursor: pointer;
position: absolute;
opacity: 0;
&:checked + span {
background-color: forestgreen;
font-weight: bold;
}
&:focus-visible + span {
outline: 1px solid $accentCol;
}
} }
} }
</style> </style>
@@ -4,6 +4,7 @@
<button class="btn--filled btn--image" @click="toggleCard"> <button class="btn--filled btn--image" @click="toggleCard">
<img class="button_icon" :src="getIcon('filter2')" alt="filter icon" /> <img class="button_icon" :src="getIcon('filter2')" alt="filter icon" />
{{ $t('options.filters') }} [F] {{ $t('options.filters') }} [F]
<span class="active-indicator" v-if="!filterStore.areFiltersAtDefault"></span>
</button> </button>
<label for="scenery-search"> <label for="scenery-search">
@@ -29,6 +30,22 @@
<p class="card_info" v-html="$t('filters.desc')"></p> <p class="card_info" v-html="$t('filters.desc')"></p>
<section class="card_options"> <section class="card_options">
<!-- QUICK ACTIONS (TODO) -->
<!-- <div class="quick-actions">
<h3 class="text--primary">{{ $t('filters.sections.quick') }}</h3>
<hr />
<div>
<button class="btn--action" style="width: 100%" @click="filterStore.handleQuickAction('all-available')">
{{ $t('filters.all-available') }}
</button>
<button class="btn--action" style="width: 100%" @click="filterStore.handleQuickAction('all-free')">
{{ $t('filters.all-free') }}
</button>
</div>
</div> -->
<div class="option-section" v-for="section in filterStore.inputs.optionSections"> <div class="option-section" v-for="section in filterStore.inputs.optionSections">
<h3 class="text--primary"> <h3 class="text--primary">
{{ $t(`filters.sections.${section}`) }} {{ $t(`filters.sections.${section}`) }}
@@ -39,7 +56,7 @@
<hr /> <hr />
<div class="section-inputs"> <div class="section-inputs">
<filter-option <FilterOption
v-for="(option, i) in filterStore.inputs.options.filter((o) => o.section == section)" v-for="(option, i) in filterStore.inputs.options.filter((o) => o.section == section)"
:option="option" :option="option"
:key="i" :key="i"
@@ -176,6 +193,10 @@ export default defineComponent({
.filter((s) => s.name.toLocaleLowerCase().includes(this.chosenSearchScenery.toLocaleLowerCase())) .filter((s) => s.name.toLocaleLowerCase().includes(this.chosenSearchScenery.toLocaleLowerCase()))
.sort((s1, s2) => (s1.name > s2.name ? 1 : -1)); .sort((s1, s2) => (s1.name > s2.name ? 1 : -1));
}, },
currentOptionsActive() {
return true;
},
}, },
watch: { watch: {
@@ -204,10 +225,7 @@ export default defineComponent({
handleInput(e: Event) { handleInput(e: Event) {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
this.filterStore.changeFilterValue({ this.filterStore.changeFilterValue(target.name, target.value);
name: target.name,
value: target.value,
});
if (this.saveOptions) StorageManager.setStringValue(target.name, target.value); if (this.saveOptions) StorageManager.setStringValue(target.name, target.value);
}, },
@@ -221,11 +239,7 @@ export default defineComponent({
}, },
changeNumericFilterValue(name: string, value: number, saveToStorage = false) { changeNumericFilterValue(name: string, value: number, saveToStorage = false) {
this.filterStore.changeFilterValue({ this.filterStore.changeFilterValue(name, value);
name,
value,
});
if (this.saveOptions && saveToStorage) StorageManager.setNumericValue(name, value); if (this.saveOptions && saveToStorage) StorageManager.setNumericValue(name, value);
}, },
@@ -426,33 +440,30 @@ export default defineComponent({
} }
} }
.card_options { .option-section h3 {
.option-section h3 { display: flex;
display: flex; align-items: center;
align-items: center; margin-bottom: 0.25em;
margin-bottom: 0.25em;
gap: 0.5em; gap: 0.5em;
button { button {
padding: 0.15em; padding: 0.15em;
color: coral; color: coral;
}
} }
}
.section-inputs { .section-inputs {
display: grid; display: grid;
// flex-wrap: wrap; grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.5em;
// grid-template-rows: repeat(3, 1fr); margin: 1em 0;
gap: 0.5em; }
margin: 1em 0;
// @include smallScreen() { .quick-actions div {
// grid-template-columns: repeat(auto-fit, minmax(8em, 1fr)); display: flex;
// grid-template-rows: auto; margin: 1em 0;
// } gap: 1em;
}
} }
.slider { .slider {
+7 -15
View File
@@ -1,9 +1,5 @@
<template> <template>
<section class="station_table"> <section class="station_table">
<button class="return-btn" @click="scrollToTop" v-if="showReturnButton">
<img :src="icons.arrow" alt="return arrow" />
</button>
<div class="table_wrapper"> <div class="table_wrapper">
<table> <table>
<thead> <thead>
@@ -93,16 +89,11 @@
</td> </td>
<td class="station_status"> <td class="station_status">
<span class="status-badge" :class="station.onlineInfo.statusID" v-if="station.onlineInfo"> <StationStatusBadge
{{ $t(`status.${station.onlineInfo.statusID}`) }} :statusID="station.onlineInfo?.statusID"
{{ :isOnline="station.onlineInfo ? true : false"
station.onlineInfo.statusID == 'online' ? timestampToString(station.onlineInfo.statusTimestamp) : '' :statusTimestamp="station.onlineInfo?.statusTimestamp"
}} />
</span>
<span class="status-badge free" v-else>
{{ $t('status.free') }}
</span>
</td> </td>
<td class="station_dispatcher-name"> <td class="station_dispatcher-name">
@@ -253,6 +244,7 @@ import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import { HeadIdsTypes, headIconsIds, headIds } from '../../scripts/data/stationHeaderNames'; import { HeadIdsTypes, headIconsIds, headIds } from '../../scripts/data/stationHeaderNames';
import StationStatusBadge from '../Global/StationStatusBadge.vue';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -262,7 +254,7 @@ export default defineComponent({
}, },
}, },
components: { Loading }, components: { Loading, StationStatusBadge },
mixins: [styleMixin, dateMixin, stationInfoMixin, returnBtnMixin, imageMixin], mixins: [styleMixin, dateMixin, stationInfoMixin, returnBtnMixin, imageMixin],
data: () => ({ data: () => ({
+20 -71
View File
@@ -1,13 +1,13 @@
<template> <template>
<div class="train-info" tabindex="0"> <div class="train-info">
<section class="train-route"> <section class="train-route">
<div class="train_general"> <div class="train_general">
<b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b> <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-id" v-if="train.timetableData">#{{ train.timetableData.timetableId }}</span>
<span class="timetable_warnings" v-if="train.timetableData?.TWR || train.timetableData?.SKR"> <span class="timetable_warnings" v-if="train.timetableData?.TWR || train.timetableData?.SKR">
<span class="train-badge twr" v-if="train.timetableData?.TWR">TWR</span> <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">SKR</span> <span class="train-badge skr" v-if="train.timetableData?.SKR" :title="$t('general.SKR')">SKR</span>
</span> </span>
<strong> <strong>
@@ -41,13 +41,7 @@
</div> </div>
<div class="timetable_progress" style="margin-top: 0.5em" v-if="train.timetableData"> <div class="timetable_progress" style="margin-top: 0.5em" v-if="train.timetableData">
<span class="timetable_progress-bar"> <ProgressBar :progressPercent="confirmedPercentage(train.timetableData.followingStops)" />
<span class="bar-bg"></span>
<span
class="bar-fg"
:style="{ width: `${Math.floor(confirmedPercentage(train.timetableData.followingStops))}%` }"
></span>
</span>
<span class="timetable_progress-distance"> <span class="timetable_progress-distance">
&nbsp; {{ currentDistance(train.timetableData.followingStops) }} km / &nbsp; {{ currentDistance(train.timetableData.followingStops) }} km /
@@ -68,15 +62,13 @@
</section> </section>
<section class="train-stats"> <section class="train-stats">
<div> <TrainThumbnail :name="train.locoType" :onlyFirstSegment="true" />
<img :src="train.locoURL" loading="lazy" alt="Loco image not found" @error="onImageError" />
</div>
<div class="text--grayed"> <div class="text--grayed">
{{ train.locoType }} {{ train.locoType }}
<span v-if="train.cars.length > 0"> <span v-if="train.stockList.length > 1">
&nbsp;&bull; {{ $t('trains.cars') }}: &nbsp;&bull; {{ $t('trains.cars') }}:
<span class="count">{{ train.cars.length }}</span> <span class="count">{{ train.stockList.length - 1 }}</span>
</span> </span>
</div> </div>
@@ -96,6 +88,8 @@ import imageMixin from '../../mixins/imageMixin';
import styleMixin from '../../mixins/styleMixin'; import styleMixin from '../../mixins/styleMixin';
import trainInfoMixin from '../../mixins/trainInfoMixin'; import trainInfoMixin from '../../mixins/trainInfoMixin';
import Train from '../../scripts/interfaces/Train'; import Train from '../../scripts/interfaces/Train';
import ProgressBar from '../Global/ProgressBar.vue';
import TrainThumbnail from '../Global/TrainThumbnail.vue';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -103,22 +97,27 @@ export default defineComponent({
type: Object as () => Train, type: Object as () => Train,
required: true, required: true,
}, },
extended: { extended: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
}, },
mixins: [trainInfoMixin, imageMixin, styleMixin], mixins: [trainInfoMixin, imageMixin, styleMixin],
components: { ProgressBar, TrainThumbnail },
}); });
</script> </script>
<!-- Global style for TrainThumbnail -->
<style lang="scss">
.train-stats .train-thumbnail {
max-width: 100%;
}
</style>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/badge.scss'; @import '../../styles/badge.scss';
.image-warning { .image-warning {
height: 1em; height: 1em;
@@ -128,15 +127,12 @@ export default defineComponent({
.train-stats { .train-stats {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-content: center; align-items: center;
flex-direction: column; flex-direction: column;
text-align: center; text-align: center;
img { gap: 0.25em;
margin: 0.5em 0;
width: 12em;
}
} }
.train-info { .train-info {
@@ -182,26 +178,6 @@ export default defineComponent({
gap: 0.25em; gap: 0.25em;
} }
.train-badge {
padding: 0.1em 0.2em;
border-radius: 0.2em;
font-weight: bold;
font-size: 0.9em;
&.twr {
background-color: var(--clr-twr);
}
&.skr {
background-color: var(--clr-skr);
}
&.offline {
background-color: #9c362b;
}
}
.train-driver { .train-driver {
&.supporter { &.supporter {
color: orange; color: orange;
@@ -218,9 +194,7 @@ export default defineComponent({
.timetable_warnings { .timetable_warnings {
display: flex; display: flex;
gap: 0.2em; gap: 0.25em;
color: black;
} }
.timetable_progress { .timetable_progress {
@@ -229,31 +203,6 @@ export default defineComponent({
flex-wrap: wrap; flex-wrap: wrap;
} }
.timetable_progress-bar {
position: relative;
width: 6em;
height: 1em;
margin: 0.5em 0;
.bar-fg,
.bar-bg {
position: absolute;
height: 1em;
width: 100%;
left: 0;
}
.bar-fg {
background-color: springgreen;
}
.bar-bg {
background-color: #5b5b5b;
}
}
.timetable_progress-distance { .timetable_progress-distance {
margin-right: 0.25em; margin-right: 0.25em;
} }
+1 -1
View File
@@ -82,10 +82,10 @@
import { defineComponent, inject, PropType } from 'vue'; import { defineComponent, inject, PropType } from 'vue';
import imageMixin from '../../mixins/imageMixin'; import imageMixin from '../../mixins/imageMixin';
import keyMixin from '../../mixins/keyMixin'; import keyMixin from '../../mixins/keyMixin';
import { TrainFilter } from '../../types/Trains/TrainOptionsTypes';
import ActionButton from '../Global/ActionButton.vue'; import ActionButton from '../Global/ActionButton.vue';
import SelectBox from '../Global/SelectBox.vue'; import SelectBox from '../Global/SelectBox.vue';
import { TrainFilterSection } from '../../scripts/enums/TrainFilterType'; import { TrainFilterSection } from '../../scripts/enums/TrainFilterType';
import { TrainFilter } from '../../scripts/interfaces/Trains/TrainFilter';
export default defineComponent({ export default defineComponent({
components: { SelectBox, ActionButton }, components: { SelectBox, ActionButton },
+14 -62
View File
@@ -1,38 +1,15 @@
<template> <template>
<div class="train-schedule" @click="toggleShowState"> <div class="train-schedule" @click="toggleShowState">
<div class="train-stock"> <StockList :trainStockList="train.stockList" />
<ul class="stock-list">
<li>
<img class="train-image" :src="train.locoURL" alt="loco" @error="onImageError" />
<div>{{ train.locoType }}</div>
</li>
<li v-if="train.locoType.startsWith('EN')"> <!-- <div class="train-stock"> -->
<img :src="train.locoURL.replace('rb', 's')" @error="onImageError" alt="" /> <!-- <ul>
<div>{{ train.locoType }}S</div> <li v-for="(stockName, i) in train.stockList" :key="i">
<p>{{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }} {{ stockName.split(':')[1] }}</p>
<TrainThumbnail :name="stockName" />
</li> </li>
</ul> -->
<li v-if="train.locoType.startsWith('EN71')"> <!-- </div> -->
<img :src="train.locoURL.replace('rb', 's')" @error="onImageError" alt="" />
<div>{{ train.locoType }}S</div>
</li>
<li v-if="train.locoType.startsWith('EN')">
<img :src="train.locoURL.replace('rb', 'ra')" @error="onImageError" alt="" />
<div>{{ train.locoType }}RA</div>
</li>
<li v-for="(car, i) in train.cars" :key="i">
<img
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${car.split(':')[0]}.png`"
@error="onImageError"
alt="car"
/>
<div>{{ car.replace(/_/g, ' ').split(':')[0] }}</div>
</li>
</ul>
</div>
<div class="schedule-wrapper" v-if="train.timetableData"> <div class="schedule-wrapper" v-if="train.timetableData">
<ul class="stop_list"> <ul class="stop_list">
@@ -60,9 +37,7 @@
<b>{{ stop.stopNameRAW }} </b>: <span v-html="stop.comments"></span> <b>{{ stop.stopNameRAW }} </b>: <span v-html="stop.comments"></span>
</div> </div>
<span <span v-if="stop.departureLine == train.timetableData!.followingStops[i + 1].arrivalLine && !/sbl/gi.test(stop.departureLine!)">
v-if="stop.departureLine == train.timetableData!.followingStops[i + 1].arrivalLine && !/sbl/gi.test(stop.departureLine!)"
>
{{ stop.departureLine }} {{ stop.departureLine }}
</span> </span>
@@ -91,9 +66,11 @@ import Train from '../../scripts/interfaces/Train';
import TrainStop from '../../scripts/interfaces/TrainStop'; import TrainStop from '../../scripts/interfaces/TrainStop';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import StopDate from '../Global/StopDate.vue'; import StopDate from '../Global/StopDate.vue';
import TrainThumbnail from '../Global/TrainThumbnail.vue';
import StockList from '../Global/StockList.vue';
export default defineComponent({ export default defineComponent({
components: { StopDate }, components: { StopDate, TrainThumbnail, StockList },
props: { props: {
train: { train: {
type: Object as PropType<Train>, type: Object as PropType<Train>,
@@ -145,8 +122,7 @@ export default defineComponent({
end: stop.terminatesHere, end: stop.terminatesHere,
delayed: stop.departureDelay > 0, delayed: stop.departureDelay > 0,
sbl: /sbl/gi.test(stop.stopName), sbl: /sbl/gi.test(stop.stopName),
[stop.stopType.replaceAll(', ', '-')]: [stop.stopType.replaceAll(', ', '-')]: stop.stopType.match(new RegExp('ph|pm|pt')) && !stop.confirmed && !stop.beginsHere,
stop.stopType.match(new RegExp('ph|pm|pt')) && !stop.confirmed && !stop.beginsHere,
'minor-stop-active': this.activeMinorStops.includes(index), 'minor-stop-active': this.activeMinorStops.includes(index),
'last-confirmed': index == this.lastConfirmed && !stop.terminatesHere, 'last-confirmed': index == this.lastConfirmed && !stop.terminatesHere,
}; };
@@ -179,30 +155,7 @@ $stopNameClr: #22a8d1;
} }
.train-schedule { .train-schedule {
padding: 0 0.25em; padding: 0 1em;
}
.train-stock {
padding: 0.25em 0.5em;
display: flex;
justify-content: center;
}
ul.stock-list {
display: flex;
align-items: flex-end;
overflow: auto;
padding-bottom: 1em;
li > div {
text-align: center;
color: #aaa;
font-size: 0.9em;
}
img {
max-height: 60px;
}
} }
.schedule-wrapper { .schedule-wrapper {
@@ -426,4 +379,3 @@ ul.stop_list > li.stop {
} }
} }
</style> </style>
-337
View File
@@ -1,337 +0,0 @@
<template>
<div class="train-stats" v-click-outside="closeStats">
<action-button class="stats_button" @click="toggleStatsOpen">
<img :src="getIcon('stats')" :alt="$t('trains.stats')" />
<p>{{ $t('trains.stats') }}</p>
</action-button>
<transition name="stats-anim" class="stats_wrapper" tag="div">
<div class="stats-body" v-if="trainStatsOpen">
<h2 class="stats-header">
<img :src="getIcon('stats')" :alt="$t('trains.stats')" />
{{ $t('trains.stats') }}
</h2>
<div class="stats-speed">
<div class="title stats-title">
{{ $t('trains.stats-speed') }}
</div>
<div class="stats-content">{{ speedStats.min }} | {{ speedStats.avg }} | {{ speedStats.max }}</div>
</div>
<div class="stats-length">
<div class="title stats-title">
{{ $t('trains.stats-length') }}
</div>
<div class="stats-content">
{{ timetableStats.min }} | {{ timetableStats.avg }} |
{{ timetableStats.max }}
</div>
</div>
<div class="stats-categories">
<div class="title stats-title">
{{ $t('trains.stats-categories') }}
</div>
<div class="category-list">
<span class="category" v-for="[key, value] of categoryList" :key="key">
<span class="category-type">{{ key }}</span>
<span class="category-count">{{ value }}</span>
</span>
</div>
<div class="special-list">
<span class="special twr">
<span class="special-type">{{ $t('trains.stats-special-twr') }}</span>
<span class="special-count">{{ specialTrainCount[0] }}</span>
</span>
<span class="special skr">
<span class="special-type">{{ $t('trains.stats-special-skr') }}</span>
<span class="special-count">{{ specialTrainCount[1] }}</span>
</span>
</div>
</div>
<div class="stats-locos">
<div class="title stats-title">{{ $t('trains.stats-locos') }}</div>
<div class="loco-list stats-content">
<div class="loco-item" v-for="(loco, i) in locoList" :key="i">{{ loco[0] }} | {{ loco[1] }}</div>
</div>
</div>
</div>
</transition>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, inject } from 'vue';
import imageMixin from '../../mixins/imageMixin';
import Train from '../../scripts/interfaces/Train';
import ActionButton from '../Global/ActionButton.vue';
export default defineComponent({
components: { ActionButton },
mixins: [imageMixin],
props: {
trains: {
type: Array as () => Train[],
required: true,
},
},
data: () => ({
trainStatsOpen: false,
}),
methods: {
toggleStatsOpen() {
this.trainStatsOpen = !this.trainStatsOpen;
},
closeStats() {
this.trainStatsOpen = false;
},
},
setup(props) {
const speedStats = computed(() => {
if (props.trains.length == 0) return { avg: '0', min: '0', max: '0' };
const trainList = props.trains.filter((train) => train.timetableData);
const avg = (trainList.reduce((acc, train) => acc + train.speed, 0) / trainList.length).toFixed(2);
const minMaxSpeed = trainList.reduce((acc, train) => {
if (!train.timetableData) return acc;
acc[0] = !acc[0] || train.speed < acc[0] ? train.speed : acc[0];
acc[1] = !acc[1] || train.speed > acc[1] ? train.speed : acc[1];
return acc;
}, [] as any);
return {
avg,
min: minMaxSpeed[0].toString(),
max: minMaxSpeed[1].toString(),
};
});
const timetableStats = computed(() => {
if (props.trains.length == 0) return { avg: '0', min: '0', max: '0' };
const activeTrainsLength = props.trains.filter((train) => train.timetableData).length;
const avg = (
props.trains.reduce((acc, train) => (train.timetableData ? acc + train.timetableData.routeDistance : acc), 0) /
activeTrainsLength
).toFixed(2);
const minMaxDistance = props.trains.reduce((acc, train) => {
if (!train.timetableData) return acc;
acc[0] = !acc[0] || train.timetableData.routeDistance < acc[0] ? train.timetableData.routeDistance : acc[0];
acc[1] = !acc[1] || train.timetableData.routeDistance > acc[1] ? train.timetableData.routeDistance : acc[1];
return acc;
}, [] as any);
return {
avg,
min: minMaxDistance[0].toString(),
max: minMaxDistance[1].toString(),
};
});
const categoryList = computed(() => {
const map = props.trains.reduce((acc, train) => {
if (!train.timetableData || !train.timetableData.category) return acc;
acc.set(
train.timetableData.category,
acc.get(train.timetableData.category) ? acc.get(train.timetableData.category) + 1 : 1
);
return acc;
}, new Map());
return new Map([...map.entries()].sort((a, b) => b[1] - a[1]));
});
const locoList = computed(() => {
const map: Map<string, number> = props.trains.reduce((acc, train) => {
if (!train.timetableData || !train.locoType) return acc;
acc.set(train.locoType, acc.get(train.locoType) ? acc.get(train.locoType) + 1 : 1);
return acc;
}, new Map());
const sorted = [...map.entries()].sort((a, b) => b[1] - a[1]).filter((v, i) => i < 3);
return sorted;
});
const specialTrainCount = computed(() => {
const twrList = props.trains.filter((train) => train.timetableData && train.timetableData.TWR);
const skrList = props.trains.filter((train) => train.timetableData && train.timetableData.SKR);
return [twrList.length, skrList.length];
});
/* Inject list from TrainsView for category filter */
const chosenTrainCategories = inject('chosenTrainCategories') as string[];
return {
speedStats,
timetableStats,
categoryList,
locoList,
specialTrainCount,
chosenTrainCategories,
};
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive';
.stats-anim {
&-enter-active,
&-leave-active {
transition: all 150ms ease-out;
}
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateY(30px);
}
}
.train-stats {
position: relative;
top: 0;
z-index: 15;
}
.stats {
&_wrapper {
margin-bottom: 0.5em;
outline: none;
}
&-header {
display: flex;
margin-bottom: 0.85em;
img {
vertical-align: middle;
margin-right: 0.35em;
}
}
&-body {
position: absolute;
display: inline-block;
max-width: 700px;
width: 100%;
top: 100%;
left: 0;
background: #222;
border-radius: 0 1em 1em 1em;
padding: 1em;
}
&-content {
color: #ddd;
}
}
/* .category {
cursor: pointer;
} */
.category,
.special {
&-list {
display: flex;
flex-wrap: wrap;
}
margin-right: 0.4em;
margin-bottom: 0.4em;
&-type,
&-count {
display: inline-block;
padding: 0.2em 0.4em;
}
&-type {
background: #585858;
font-weight: 600;
}
&-count {
background: #ffc014;
color: black;
}
}
.special {
&-list {
font-size: 0.85em;
}
&-count {
background: gray;
color: white;
}
&.twr > &-type {
background-color: var(--clr-twr);
color: black;
}
&.skr > &-type {
background-color: var(--clr-skr);
color: white;
}
}
.warning {
display: inline-block;
margin-right: 0.4em;
padding: 0.2em 0.3em;
color: black;
font-weight: bold;
font-size: 0.85em;
}
@include smallScreen {
.stats-body {
display: block;
width: 100%;
border-radius: 0 0 1em 1em;
}
.train-stats {
display: flex;
justify-content: center;
}
}
</style>
+3 -2
View File
@@ -17,8 +17,9 @@
class="train-row" class="train-row"
v-for="train in currentTrains" v-for="train in currentTrains"
:key="train.trainId" :key="train.trainId"
@click.stop="selectModalTrain(train.trainId)" tabindex="0"
@keydown.enter="selectModalTrain(train.trainId)" @click.stop="selectModalTrain(train.trainId, $event.currentTarget)"
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
> >
<TrainInfo :train="train" /> <TrainInfo :train="train" />
</li> </li>
@@ -1,28 +1,46 @@
import { JournalFilterType } from "../../scripts/enums/JournalFilterType"; import { JournalFilterSection, JournalFilterType } from '../../scripts/enums/JournalFilterType';
import { JournalTimetableFilter } from "../../types/Journal/JournalTimetablesTypes"; import { JournalFilter } from '../../scripts/types/JournalTimetablesTypes';
export const journalTimetableFilters: JournalTimetableFilter[] = [ export const journalTimetableFilters: JournalFilter[] = [
{ {
id: JournalFilterType.all, id: JournalFilterType.ALL,
filterSection: 'timetable-status', filterSection: JournalFilterSection.TIMETABLE_STATUS,
isActive: true, isActive: true,
}, },
{ {
id: JournalFilterType.active, id: JournalFilterType.ACTIVE,
filterSection: 'timetable-status', filterSection: JournalFilterSection.TIMETABLE_STATUS,
isActive: false, isActive: false,
}, },
{ {
id: JournalFilterType.fulfilled, id: JournalFilterType.FULFILLED,
filterSection: 'timetable-status', filterSection: JournalFilterSection.TIMETABLE_STATUS,
isActive: false, isActive: false,
}, },
{ {
id: JournalFilterType.abandoned, id: JournalFilterType.ABANDONED,
filterSection: 'timetable-status', filterSection: JournalFilterSection.TIMETABLE_STATUS,
isActive: false,
},
{
id: JournalFilterType.TWR_SKR,
filterSection: JournalFilterSection.TWRSKR,
isActive: true,
},
{
id: JournalFilterType.TWR,
filterSection: JournalFilterSection.TWRSKR,
isActive: false,
},
{
id: JournalFilterType.SKR,
filterSection: JournalFilterSection.TWRSKR,
isActive: false, isActive: false,
}, },
]; ];
+1 -1
View File
@@ -1,5 +1,5 @@
import { TrainFilterSection, TrainFilterType } from '../../scripts/enums/TrainFilterType'; import { TrainFilterSection, TrainFilterType } from '../../scripts/enums/TrainFilterType';
import { TrainFilter } from '../../types/Trains/TrainOptionsTypes'; import { TrainFilter } from '../../scripts/interfaces/Trains/TrainFilter';
export const trainFilters: TrainFilter[] = [ export const trainFilters: TrainFilter[] = [
{ {
File diff suppressed because it is too large Load Diff
+51 -14
View File
@@ -1,7 +1,9 @@
{ {
"general": { "general": {
"and": " and ", "and": " and ",
"refresh": "REFRESH" "refresh": "REFRESH",
"TWR": "High risk freight train",
"SKR": "Train with exceeded gauge"
}, },
"app": { "app": {
"sceneries": "SCENERIES", "sceneries": "SCENERIES",
@@ -97,19 +99,21 @@
"search-dispatcher": "Dispatcher name", "search-dispatcher": "Dispatcher name",
"search-station": "Scenery name", "search-station": "Scenery name",
"search-author": "Timetable author name", "search-author": "Timetable author name",
"search-timetables-date": "Timetable date (CEST / GMT+2)", "search-issuedFrom": "Origin scenery name",
"search-dispatchers-date": "Service date (CEST / GMT+2)", "search-timetables-date": "Timetable date (UTC+2 / CEST)",
"search-dispatchers-date": "Service date (UTC+2 / CEST)",
"search-date": "Date (UTC+2 / CEST)",
"sort-mass": "mass", "sort-mass": "mass",
"sort-speed": "speed", "sort-speed": "speed",
"sort-length": "length", "sort-length": "length",
"sort-distance": "distance", "sort-routeDistance": "route distance",
"sort-timetable": "train no.", "sort-timetable": "train no.",
"sort-progress": "route progress", "sort-progress": "route progress",
"sort-delay": "current delay", "sort-delay": "current delay",
"sort-id": "timetable id", "sort-id": "timetable id",
"sort-total-stops": "total stops", "sort-allStopsCount": "total stops",
"sort-beginDate": "date", "sort-beginDate": "date",
"sort-timetableId": "timetable ID", "sort-timetableId": "timetable ID",
"sort-timestampFrom": "date", "sort-timestampFrom": "date",
@@ -119,6 +123,7 @@
"filter-withComments": "COMMENTS", "filter-withComments": "COMMENTS",
"filter-twr": "HIGH RISK CARGO", "filter-twr": "HIGH RISK CARGO",
"filter-skr": "EXCEEDED GAUGE", "filter-skr": "EXCEEDED GAUGE",
"filter-twr-skr": "ALL TYPES",
"filter-common": "NO WARNINGS", "filter-common": "NO WARNINGS",
"filter-passenger": "PASSENGER", "filter-passenger": "PASSENGER",
"filter-freight": "FREIGHT", "filter-freight": "FREIGHT",
@@ -129,6 +134,9 @@
"filter-reset": "RESET FILTERS", "filter-reset": "RESET FILTERS",
"filter-clear": "CLEAR FILTERS", "filter-clear": "CLEAR FILTERS",
"filter-section-timetable-status": "TIMETABLE STATUS",
"filter-section-twrskr": "WARNINGS",
"filter-all": "ALL ENTRIES", "filter-all": "ALL ENTRIES",
"filter-abandoned": "ABANDONED", "filter-abandoned": "ABANDONED",
"filter-fulfilled": "FULFILLED", "filter-fulfilled": "FULFILLED",
@@ -138,6 +146,7 @@
"desc": " &bull; Left mouse click: select / unselect chosen filter <br /> &bull; Double left click: unselect all filters but chosen from a <b class='text--primary'>group</b> <br /> &bull; <span style='color: coral'>RESET</span>: reset all filters from a <b class='text--primary'>group</b>", "desc": " &bull; Left mouse click: select / unselect chosen filter <br /> &bull; Double left click: unselect all filters but chosen from a <b class='text--primary'>group</b> <br /> &bull; <span style='color: coral'>RESET</span>: reset all filters from a <b class='text--primary'>group</b>",
"sections": { "sections": {
"quick": "QUICK FILTERS",
"reality": "SCENERY REALITY", "reality": "SCENERY REALITY",
"package-access": "IN-GAME AVAILABILITY", "package-access": "IN-GAME AVAILABILITY",
"access": "GENERAL AVAILABILITY", "access": "GENERAL AVAILABILITY",
@@ -148,6 +157,9 @@
"status": "ONLINE STATUS" "status": "ONLINE STATUS"
}, },
"all-available": "ALL AVAILABLE",
"all-free": "CURRENTLY FREE",
"endingStatus": "ENDS SOON", "endingStatus": "ENDS SOON",
"afkStatus": "AFK", "afkStatus": "AFK",
"noSpaceStatus": "NO SPACE", "noSpaceStatus": "NO SPACE",
@@ -266,6 +278,7 @@
"title": "DISPATCHER HISTORY", "title": "DISPATCHER HISTORY",
"loading": "Loading dispatcher history data...", "loading": "Loading dispatcher history data...",
"no-history": "No dispatcher history found!", "no-history": "No dispatcher history found!",
"data-refreshed-at": "Data refreshed at",
"section-timetables": "TIMETABLES", "section-timetables": "TIMETABLES",
"section-dispatchers": "DISPATCHERS", "section-dispatchers": "DISPATCHERS",
@@ -275,7 +288,7 @@
"route-length": "Route length:", "route-length": "Route length:",
"station-count": "Stations:", "station-count": "Stations:",
"dispatcher-name": "Created by", "dispatcher-name": "Author",
"timetable-day": "Timetable created at", "timetable-day": "Timetable created at",
"timetable-active": "ACTIVE", "timetable-active": "ACTIVE",
"timetable-fulfilled": "FULFILLED", "timetable-fulfilled": "FULFILLED",
@@ -283,8 +296,10 @@
"online-since": "ONLINE SINCE", "online-since": "ONLINE SINCE",
"duty-lasted": "The duty lasted", "duty-lasted": "The duty lasted",
"minutes": "{minutes} mins",
"hours": "{hours}h {minutes} mins", "hours": "{value} hour | {value} hours",
"minutes": "{value} min | {value} mins",
"seconds": "{value} s",
"stock-info": "EXTRA INFO", "stock-info": "EXTRA INFO",
"stock-length": "Length", "stock-length": "Length",
@@ -304,10 +319,13 @@
"stats-distance": "DISTANCE", "stats-distance": "DISTANCE",
"stats-stations": "STATIONS", "stats-stations": "STATIONS",
"timetable-stats-total": "Today, dispatchers made so far {count} with total distance of {distance}", "timetable-stats-title": "Daily stats on {date}",
"timetable-stats-longest": "The longest timetable today is #{id} made by {author} for {driver} - {distance}", "timetable-stats-total": "Issued timetables: {count} (total distance: {distance})",
"timetable-stats-most-active": "The most active dispatcher today is {dispatcher} who created {count}", "timetable-stats-longest": "The longest timetable: #{id} (made by {author} for {driver}, distance: {distance})",
"timetable-stats-most-active-many": "The most active dispatchers today are {dispatchers} who created {count} each", "timetable-stats-most-active-dr": "The most active dispatcher: {dispatcher} (created {count})",
"timetable-stats-most-active-dr-many": "The most active dispatchers: {dispatchers} (created {count} each)",
"timetable-stats-most-active-driver": "The most active driver: {driver} (total driven distance: {distance})",
"timetable-stats-longest-duties": "The longest service: {dispatcher} at {station} (duration: {duration})",
"timetable-count": "timetable | timetables", "timetable-count": "timetable | timetables",
@@ -318,7 +336,18 @@
"driver-stats-info": "Enter a proper nickname into filters [F] to see user's driving statistics!", "driver-stats-info": "Enter a proper nickname into filters [F] to see user's driving statistics!",
"stats-loading": "Fetching statistics...", "stats-loading": "Fetching statistics...",
"stats-error": "Oops! An unexpected error occurred while trying to fetch statistics! :/" "stats-error": "Oops! An unexpected error occurred while trying to fetch statistics! :/",
"timetable-location-signal": "signal:",
"timetable-location-route": "route:",
"history-name": "Scenery name",
"history-hash": "Hash",
"history-dispatcher": "Dispatcher",
"history-level": "Level",
"history-rate": "Rate",
"history-region": "Region",
"history-date": "Service date"
}, },
"scenery": { "scenery": {
"users": "PLAYERS ONLINE", "users": "PLAYERS ONLINE",
@@ -353,13 +382,21 @@
"timetables-history-author": "TT author", "timetables-history-author": "TT author",
"timetables-history-date": "Date", "timetables-history-date": "Date",
"dispatchers-history-hash": "Hash",
"dispatchers-history-dispatcher": "Dispatcher",
"dispatchers-history-level": "Level",
"dispatchers-history-rate": "Rate",
"dispatchers-history-date": "Service date",
"req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required", "req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required",
"history-list-empty": "No recorded scenery history!", "history-list-empty": "No recorded scenery history!",
"forum-topic": "Official {name} forum topic", "forum-topic": "Official {name} forum topic",
"pragotron-link": "Timetable pallet board (beta)", "pragotron-link": "Timetable pallet board (beta)",
"tablice-link": "Timetable summary board (by Thundo)" "tablice-link": "Timetable summary board (by Thundo)",
"bottom-info": "Show full history in the Journal tab"
}, },
"availability": { "availability": {
"title": "Availability", "title": "Availability",
+49 -14
View File
@@ -1,7 +1,9 @@
{ {
"general": { "general": {
"and": " oraz ", "and": " oraz ",
"refresh": "ODŚWIEŻ" "refresh": "ODŚWIEŻ",
"TWR": "Towar niebezpieczny wysokiego ryzyka",
"SKR": "Przekroczona skrajnia"
}, },
"app": { "app": {
"sceneries": "SCENERIE", "sceneries": "SCENERIE",
@@ -97,11 +99,13 @@
"search-dispatcher": "Nick dyżurnego", "search-dispatcher": "Nick dyżurnego",
"search-station": "Nazwa scenerii", "search-station": "Nazwa scenerii",
"search-author": "Nick autora rozkładu jazdy", "search-author": "Nick autora rozkładu jazdy",
"search-timetables-date": "Data rozkładu jazdy (czas polski)", "search-issuedFrom": "Sceneria początkowa",
"search-dispatchers-date": "Data służby (czas polski)", "search-timetables-date": "Data rozkładu jazdy (UTC+2 / CEST)",
"search-dispatchers-date": "Data służby (UTC+2 / CEST)",
"search-date": "Data (UTC+2 / CEST)",
"sort-distance": "kilometraż", "sort-routeDistance": "kilometraż",
"sort-total-stops": "stacje", "sort-allStopsCount": "stacje",
"sort-beginDate": "data", "sort-beginDate": "data",
"sort-timetableId": "ID rozkładu", "sort-timetableId": "ID rozkładu",
"sort-timestampFrom": "data", "sort-timestampFrom": "data",
@@ -120,6 +124,7 @@
"filter-noComments": "BEZ UWAG", "filter-noComments": "BEZ UWAG",
"filter-twr": "WYS. RYZYKA", "filter-twr": "WYS. RYZYKA",
"filter-skr": "SKRAJNIA", "filter-skr": "SKRAJNIA",
"filter-twr-skr": "WSZYSTKIE",
"filter-common": "ZWYKŁE", "filter-common": "ZWYKŁE",
"filter-passenger": "PASAŻERSKIE", "filter-passenger": "PASAŻERSKIE",
"filter-freight": "TOWAROWE", "filter-freight": "TOWAROWE",
@@ -130,6 +135,9 @@
"filter-reset": "ZRESETUJ FILTRY", "filter-reset": "ZRESETUJ FILTRY",
"filter-clear": "WYŁĄCZ FILTRY", "filter-clear": "WYŁĄCZ FILTRY",
"filter-section-timetable-status": "STATUS ROZKŁADU JAZDY",
"filter-section-twrskr": "UWAGI",
"filter-all": "WSZYSTKIE", "filter-all": "WSZYSTKIE",
"filter-abandoned": "PORZUCONE", "filter-abandoned": "PORZUCONE",
"filter-fulfilled": "WYPEŁNIONE", "filter-fulfilled": "WYPEŁNIONE",
@@ -139,6 +147,7 @@
"desc": " &bull; Kliknięcie: zaznaczenie / odznaczenie filtru <br /> &bull; Podwójne kliknięcie: odznaczenie reszty filtrów z <b class='text--primary'>grupy</b> <br /> &bull; <span style='color: coral'>RESET</span>: zresetowanie filtrów z <b class='text--primary'>grupy</b>", "desc": " &bull; Kliknięcie: zaznaczenie / odznaczenie filtru <br /> &bull; Podwójne kliknięcie: odznaczenie reszty filtrów z <b class='text--primary'>grupy</b> <br /> &bull; <span style='color: coral'>RESET</span>: zresetowanie filtrów z <b class='text--primary'>grupy</b>",
"sections": { "sections": {
"quick": "SZYBKIE FILTRY",
"reality": "FIKCYJNOŚĆ SCENERII", "reality": "FIKCYJNOŚĆ SCENERII",
"package-access": "DOSTĘPNOŚĆ W PACZCE", "package-access": "DOSTĘPNOŚĆ W PACZCE",
"access": "DOSTĘPNOŚĆ OGÓLNA", "access": "DOSTĘPNOŚĆ OGÓLNA",
@@ -149,6 +158,9 @@
"status": "STATUS ONLINE" "status": "STATUS ONLINE"
}, },
"all-available": "WSZYSTKIE DOSTĘPNE",
"all-free": "WSZYSTKIE WOLNE",
"endingStatus": "KOŃCZY", "endingStatus": "KOŃCZY",
"afkStatus": "Z/W", "afkStatus": "Z/W",
"noSpaceStatus": "BRAK MIEJSCA", "noSpaceStatus": "BRAK MIEJSCA",
@@ -271,6 +283,7 @@
"title": "HISTORIA DYŻURÓW", "title": "HISTORIA DYŻURÓW",
"loading": "Ładowanie historii dyżurów...", "loading": "Ładowanie historii dyżurów...",
"no-history": "Brak historii dyżurów dla tej scenerii!", "no-history": "Brak historii dyżurów dla tej scenerii!",
"data-refreshed-at": "Dane odświeżone o",
"section-timetables": "ROZKŁADY JAZDY", "section-timetables": "ROZKŁADY JAZDY",
"section-dispatchers": "DYŻURNI", "section-dispatchers": "DYŻURNI",
@@ -280,12 +293,13 @@
"online-since": "ONLINE OD", "online-since": "ONLINE OD",
"duty-lasted": "Dyżur trwał", "duty-lasted": "Dyżur trwał",
"minutes": "{minutes} min.", "hours": "{value} godz.",
"hours": "{hours} godz. {minutes} min.", "minutes": "{value} min.",
"seconds": "{value} sek.",
"route-length": "Kilometraż:", "route-length": "Kilometraż:",
"station-count": "Stacje:", "station-count": "Stacje:",
"dispatcher-name": "Wystawiony przez dyżurnego", "dispatcher-name": "Autor",
"timetable-day": "Rozkład z dnia", "timetable-day": "Rozkład z dnia",
"timetable-active": "AKTYWNY", "timetable-active": "AKTYWNY",
"timetable-fulfilled": "WYPEŁNIONY", "timetable-fulfilled": "WYPEŁNIONY",
@@ -309,10 +323,12 @@
"stats-distance": "DYSTANS", "stats-distance": "DYSTANS",
"stats-stations": "STACJE", "stats-stations": "STACJE",
"timetable-stats-total": "Dyżurni stworzyli dziś {count} o łącznym dystansie {distance}", "timetable-stats-total": "Stworzone rozkłady jazdy: {count} (łączny dystans: {distance})",
"timetable-stats-longest": "Najdłuższym rozkładem jazdy jest dzisiaj #{id} stworzony przez dyżurnego {author} dla maszynisty {driver} - {distance}", "timetable-stats-longest": "Najdłuższy rozkład jazdy: #{id} (stworzony przez dyżurnego {author} dla maszynisty {driver} o dystansie {distance})",
"timetable-stats-most-active": "Dzisiejszym najaktywniejszym dyżurnym jest {dispatcher}, który stworzył {count}", "timetable-stats-most-active-dr": "Najaktywniejszy dyżurny: {dispatcher} (stworzył {count})",
"timetable-stats-most-active-many": "Dzisiejszymi najaktywniejszymi dyżurnymi są {dispatchers}, którzy stworzyli po {count}", "timetable-stats-most-active-dr-many": "Najaktywniejsi dyżurni: {dispatchers} (stworzyli po {count})",
"timetable-stats-most-active-driver": "Najaktywniejszy maszynista: {driver} (łączny przejechany dystans: {distance})",
"timetable-stats-longest-duties": "Najdłuższa służba: {dispatcher} na scenerii {station} (czas trwania: {duration})",
"timetable-count": "rozkład jazdy | rozkładów jazdy", "timetable-count": "rozkład jazdy | rozkładów jazdy",
@@ -323,7 +339,18 @@
"driver-stats-info": "Wpisz nazwę użytkownika w filtrach [F], aby zobaczyć jego statystyki maszynisty!", "driver-stats-info": "Wpisz nazwę użytkownika w filtrach [F], aby zobaczyć jego statystyki maszynisty!",
"stats-loading": "Pobieranie statystyk...", "stats-loading": "Pobieranie statystyk...",
"stats-error": "Ups! Wystąpił błąd podczas próby pobrania statystyk! :/" "stats-error": "Ups! Wystąpił błąd podczas próby pobrania statystyk! :/",
"timetable-location-signal": "semafor:",
"timetable-location-route": "szlak:",
"history-name": "Sceneria",
"history-hash": "Hash",
"history-dispatcher": "Dyżurny",
"history-level": "Poziom",
"history-rate": "Ocena",
"history-region": "Region",
"history-date": "Data służby"
}, },
"scenery": { "scenery": {
"users": "GRACZE ONLINE", "users": "GRACZE ONLINE",
@@ -358,13 +385,21 @@
"timetables-history-author": "Autor RJ", "timetables-history-author": "Autor RJ",
"timetables-history-date": "Data", "timetables-history-date": "Data",
"dispatchers-history-hash": "Hash",
"dispatchers-history-dispatcher": "Dyżurny",
"dispatchers-history-level": "Poziom",
"dispatchers-history-rate": "Ocena",
"dispatchers-history-date": "Data służby",
"req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego", "req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego",
"history-list-empty": "Brak historii dla tej scenerii!", "history-list-empty": "Brak historii dla tej scenerii!",
"forum-topic": "Oficjalny wątek scenerii {name}", "forum-topic": "Oficjalny wątek scenerii {name}",
"pragotron-link": "Paletowa tablica informacyjna (beta)", "pragotron-link": "Paletowa tablica informacyjna (beta)",
"tablice-link": "Tablica informacyjna zbiorcza (autorstwa Thundo)" "tablice-link": "Tablica informacyjna zbiorcza (autorstwa Thundo)",
"bottom-info": "Pokaż pełną historię w zakładce Dziennika"
}, },
"availability": { "availability": {
"title": "Dostępność", "title": "Dostępność",
-10
View File
@@ -7,7 +7,6 @@ import plLang from './locales/pl.json';
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
import { registerSW } from 'virtual:pwa-register';
const i18n = createI18n({ const i18n = createI18n({
locale: 'pl', locale: 'pl',
@@ -21,15 +20,6 @@ const i18n = createI18n({
enableLegacy: false, enableLegacy: false,
}); });
registerSW({
onRegistered(r) {
r &&
setInterval(() => {
r.update();
}, 60 * 60 * 1000);
},
});
const clickOutsideDirective: Directive = { const clickOutsideDirective: Directive = {
mounted(el, binding) { mounted(el, binding) {
el.clickOutsideEvent = (event: Event) => { el.clickOutsideEvent = (event: Event) => {
+77 -50
View File
@@ -1,50 +1,77 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
export default defineComponent({ export default defineComponent({
methods: { methods: {
localeDate(dateString: string, locale: string) { localeDate(dateString: string, locale: string) {
return new Date(dateString).toLocaleDateString(locale == 'pl' ? 'pl-PL' : 'en-GB', { return new Date(dateString).toLocaleDateString(locale == 'pl' ? 'pl-PL' : 'en-GB', {
weekday: 'long', weekday: 'long',
day: 'numeric', day: 'numeric',
month: '2-digit', month: '2-digit',
year: 'numeric', year: 'numeric',
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
}); });
}, },
localeDay(dateString: string, locale: string) { localeDay(dateString: string, locale: string) {
return new Date(dateString).toLocaleDateString(locale == 'pl' ? 'pl-PL' : 'en-GB', { return new Date(dateString).toLocaleDateString(locale == 'pl' ? 'pl-PL' : 'en-GB', {
day: 'numeric', day: 'numeric',
month: '2-digit', month: '2-digit',
year: 'numeric', year: 'numeric',
}); });
}, },
localeTime(dateString: string, locale: string) { localeDateTime(dateString: string, locale: string) {
return new Date(dateString).toLocaleTimeString(locale == 'pl' ? 'pl-PL' : 'en-GB', { return new Date(dateString).toLocaleString(locale == 'pl' ? 'pl-PL' : 'en-GB', {
hour: '2-digit', timeStyle: 'short',
minute: '2-digit', dateStyle: 'medium'
}); });
}, },
timestampToString(timestamp: number | null) { localeTime(dateString: string, locale: string) {
return timestamp return new Date(dateString).toLocaleTimeString(locale == 'pl' ? 'pl-PL' : 'en-GB', {
? new Date(timestamp).toLocaleTimeString('pl-PL', { hour: '2-digit',
hour: '2-digit', minute: '2-digit',
minute: '2-digit', });
}) },
: '';
}, stringToDate(dateString?: string) {
return dateString ? new Date(dateString) : null;
calculateDuration(timestampMs: number) { },
const minsTotal = Math.round(timestampMs / 60000);
const hoursTotal = Math.floor(minsTotal / 60); parseDateToTimeString(date: Date | null) {
const minsInHour = minsTotal % 60; return (
date?.toLocaleTimeString('pl-PL', {
return minsTotal > 60 hour: '2-digit',
? this.$t('journal.hours', { hours: hoursTotal, minutes: minsInHour }) minute: '2-digit',
: this.$t('journal.minutes', { minutes: minsTotal }); }) || ''
}, );
}, },
});
timestampToString(timestamp: number | null) {
return timestamp
? new Date(timestamp).toLocaleTimeString('pl-PL', {
hour: '2-digit',
minute: '2-digit',
})
: '';
},
calculateDuration(timestampMs: number, showSeconds = false) {
const secondsTotal = Math.floor(timestampMs / 1000);
const minsTotal = Math.round(timestampMs / 60000);
const hoursTotal = Math.floor(minsTotal / 60);
const minsInHour = minsTotal % 60;
return minsTotal >= 60
? `${this.$t('journal.hours', { value: hoursTotal }, hoursTotal)} ${this.$t(
'journal.minutes',
{ value: minsInHour },
minsInHour
)}`
: showSeconds && secondsTotal <= 60
? this.$t('journal.seconds', { value: secondsTotal }, secondsTotal)
: this.$t('journal.minutes', { value: minsTotal }, minsTotal);
},
},
});
+26
View File
@@ -0,0 +1,26 @@
import { defineComponent } from 'vue';
export default defineComponent({
data: () => ({
observer: null as IntersectionObserver | null,
observerTarget: null as Element | null,
}),
methods: {
mountObserver(actionFunction: () => void, target: Element) {
this.observer = new IntersectionObserver((entries) => {
console.log(entries);
if (entries[0].intersectionRatio > 0.5) actionFunction();
}, { threshold: 0.2 });
this.observer.observe(target);
},
unmountObserver() {
if (!this.observerTarget) return;
this.observer?.unobserve(this.observerTarget);
},
},
});
+4 -2
View File
@@ -1,4 +1,4 @@
import { defineComponent } from 'vue'; import { Ref, defineComponent } from 'vue';
import { useStore } from '../store/store'; import { useStore } from '../store/store';
export default defineComponent({ export default defineComponent({
@@ -15,15 +15,17 @@ export default defineComponent({
}, },
methods: { methods: {
selectModalTrain(trainId: string) { selectModalTrain(trainId: string, target?: EventTarget | null) {
this.store.chosenModalTrainId = trainId; this.store.chosenModalTrainId = trainId;
document.body.classList.add('no-scroll'); document.body.classList.add('no-scroll');
if (target) this.store.modalLastClickedTarget = target;
}, },
closeModal() { closeModal() {
this.store.chosenModalTrainId = undefined; this.store.chosenModalTrainId = undefined;
setTimeout(() => { setTimeout(() => {
(this.store.modalLastClickedTarget as any)?.focus();
document.body.classList.remove('no-scroll'); document.body.classList.remove('no-scroll');
}, 150); }, 150);
}, },
@@ -1,49 +1,49 @@
import Filter from "../../scripts/interfaces/Filter"; import Filter from "../../interfaces/Filter";
export const filterInitStates: Filter = { export const filterInitStates: Filter = {
default: false, default: false,
notDefault: false, notDefault: false,
real: false, real: false,
fictional: false, fictional: false,
SPK: false, SPK: false,
SCS: false, SCS: false,
SPE: false, SPE: false,
SUP: false, SUP: false,
noSUP: false, noSUP: false,
ręczne: false, ręczne: false,
'ręczne+SPK': false, 'ręczne+SPK': false,
'ręczne+SCS': false, 'ręczne+SCS': false,
mechaniczne: false, mechaniczne: false,
'mechaniczne+SPK': false, 'mechaniczne+SPK': false,
'mechaniczne+SCS': false, 'mechaniczne+SCS': false,
współczesna: false, współczesna: false,
kształtowa: false, kształtowa: false,
historyczna: false, historyczna: false,
mieszana: false, mieszana: false,
SBL: false, SBL: false,
PBL: false, PBL: false,
minLevel: 0, minLevel: 0,
maxLevel: 20, maxLevel: 20,
minOneWayCatenary: 0, minOneWayCatenary: 0,
minOneWay: 0, minOneWay: 0,
minTwoWayCatenary: 0, minTwoWayCatenary: 0,
minTwoWay: 0, minTwoWay: 0,
'include-selected': false, 'include-selected': false,
'no-1track': false, 'no-1track': false,
'no-2track': false, 'no-2track': false,
free: true, free: true,
occupied: false, occupied: false,
ending: false, ending: false,
nonPublic: false, nonPublic: false,
unavailable: true, unavailable: true,
abandoned: true, abandoned: true,
afkStatus: false, afkStatus: false,
endingStatus: false, endingStatus: false,
noSpaceStatus: false, noSpaceStatus: false,
unavailableStatus: false, unavailableStatus: false,
unsignedStatus: false, unsignedStatus: false,
authors: '', authors: '',
onlineFromHours: 0, onlineFromHours: 0,
}; };
+14 -6
View File
@@ -1,6 +1,14 @@
export const enum JournalFilterType { export const enum JournalFilterType {
active = "active", ACTIVE = 'active',
fulfilled = "fulfilled", FULFILLED = 'fulfilled',
abandoned = "abandoned", ABANDONED = 'abandoned',
all = "all" ALL = 'all',
} TWR = 'twr',
SKR = 'skr',
TWR_SKR = 'twr-skr',
}
export enum JournalFilterSection {
TIMETABLE_STATUS = 'timetable-status',
TWRSKR = 'twrskr',
}
+1 -1
View File
@@ -41,7 +41,7 @@ export default interface Station {
maxUsers: number; maxUsers: number;
currentUsers: number; currentUsers: number;
spawns: { spawnName: string; spawnLength: number }[]; spawns: { spawnName: string; spawnLength: number; isElectrified: boolean }[];
dispatcherRate: number; dispatcherRate: number;
dispatcherName: string; dispatcherName: string;
dispatcherExp: number; dispatcherExp: number;
+1 -2
View File
@@ -15,12 +15,11 @@ export default interface Train {
driverLevel: number; driverLevel: number;
currentStationName: string; currentStationName: string;
currentStationHash: string; currentStationHash: string;
locoURL: string;
locoType: string; locoType: string;
online: boolean; online: boolean;
lastSeen: number; lastSeen: number;
region: string; region: string;
cars: string[]; stockList: string[];
isTimeout: boolean; isTimeout: boolean;
isSupporter: boolean; isSupporter: boolean;
@@ -1,4 +1,4 @@
import { TrainFilterSection, TrainFilterType } from '../../scripts/enums/TrainFilterType'; import { TrainFilterSection, TrainFilterType } from '../../enums/TrainFilterType'
export interface TrainFilter { export interface TrainFilter {
id: TrainFilterType; id: TrainFilterType;
@@ -7,6 +7,7 @@ export interface DispatcherHistory {
dispatcherLevel: number | null; dispatcherLevel: number | null;
dispatcherRate: number; dispatcherRate: number;
dispatcherIsSupporter: boolean; dispatcherIsSupporter: boolean;
dispatcherStatus?: number;
isOnline: boolean; isOnline: boolean;
lastOnlineTimestamp: number; lastOnlineTimestamp: number;
region: string; region: string;
+1 -1
View File
@@ -11,7 +11,7 @@ export default interface StationAPIData {
lastSeen: number; lastSeen: number;
dispatcherExp: number; dispatcherExp: number;
nameFromHeader: string; nameFromHeader: string;
spawnString: string; spawnString: string | null;
networkConnectionString: string; networkConnectionString: string;
isOnline: number; isOnline: number;
dispatcherRate: number; dispatcherRate: number;
@@ -14,6 +14,17 @@ export interface ITimetablesDailyStats {
name: string; name: string;
count: number; count: number;
}[]; }[];
mostActiveDrivers: {
name: string;
distance: number;
}[];
longestDuties: {
name: string;
duration: number;
station: string;
}[];
} }
export interface ITimetablesDailyStatsResponse { export interface ITimetablesDailyStatsResponse {
@@ -26,5 +37,16 @@ export interface ITimetablesDailyStatsResponse {
name: string; name: string;
count: number; count: number;
}[]; }[];
mostActiveDrivers: {
name: string;
distance: number;
}[];
longestDuties: {
name: string;
duration: number;
station: string;
}[];
} }
@@ -16,6 +16,7 @@ export interface TimetableHistory {
twr: number; twr: number;
skr: number; skr: number;
sceneriesString: string; sceneriesString: string;
currentLocation: string[];
routeDistance: number; routeDistance: number;
currentDistance: number; currentDistance: number;
@@ -47,10 +48,20 @@ export interface TimetableHistory {
hashesString?: string; hashesString?: string;
currentSceneryName?: string; currentSceneryName?: string;
currentSceneryHash?: string; currentSceneryHash?: string;
routeSceneries?: string;
checkpointArrivals?: string[];
checkpointDepartures?: string[];
checkpointArrivalsScheduled?: string[];
checkpointDeparturesScheduled?: string[];
checkpointStopTypes?: string[];
} }
export interface SceneryTimetableHistory { export interface SceneryTimetableHistory {
sceneryTimetables: TimetableHistory[]; timetables: TimetableHistory[];
totalCount: number; // totalCount: number;
sceneryName: string; // sceneryName: string;
} }
@@ -0,0 +1,23 @@
import { JournalTimetableSorter } from '../../types/JournalTimetablesTypes';
export interface TimetablesQueryParams {
driverName?: string;
trainNo?: string;
timetableId?: string;
authorName?: string;
timestampFrom?: number;
timestampTo?: number;
issuedFrom?: string;
countFrom?: number;
countLimit?: number;
fulfilled?: number;
terminated?: number;
twr?: number;
skr?: number;
sortBy?: JournalTimetableSorter['id'];
}
@@ -0,0 +1,13 @@
export interface RollingStockGithubData {
usage: Record<string, string>;
info: RollingStockInfo;
}
export interface RollingStockInfo {
'loco-e': [string, string, string, string, boolean][];
'loco-s': [string, string, string, string, boolean][];
'loco-szt': [string, string, string, string, boolean][];
'loco-ezt': [string, string, string, string, boolean][];
'car-passenger': [string, string, boolean, boolean, string][];
'car-cargo': [string, string, boolean, boolean, string][];
}
+93 -79
View File
@@ -1,79 +1,93 @@
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { DataStatus } from '../../enums/DataStatus'; import { DataStatus } from '../../enums/DataStatus';
import StationAPIData from '../api/StationAPIData'; import StationAPIData from '../api/StationAPIData';
import { TrainAPIData } from '../api/TrainAPIData'; import { TrainAPIData } from '../api/TrainAPIData';
import Station from '../Station'; import Station from '../Station';
import Train from '../Train'; import Train from '../Train';
import { DispatcherStatsAPIData } from '../api/DispatcherStatsAPIData'; import { DispatcherStatsAPIData } from '../api/DispatcherStatsAPIData';
import { DriverStatsAPIData } from '../api/DriverStatsAPIData'; import { DriverStatsAPIData } from '../api/DriverStatsAPIData';
import { RollingStockGithubData } from '../github_api/StockInfoGithubData';
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
export interface StoreState {
stationList: Station[]; export interface StoreState {
trainList: Train[]; stationList: Station[];
apiData: APIData; trainList: Train[];
apiData: APIData;
lastDispatcherStatuses: { hash: string; statusTimestamp: number; statusID: string }[]; rollingStockData?: RollingStockGithubData;
sceneryData: any[][]; lastDispatcherStatuses: { hash: string; statusTimestamp: number; statusID: string }[];
region: { id: string; value: string }; sceneryData: any[][];
trainCount: number;
stationCount: number; region: { id: string; value: string };
trainCount: number;
webSocket?: Socket; stationCount: number;
isOffline: boolean;
webSocket?: Socket;
dispatcherStatsName: string; isOffline: boolean;
dispatcherStatsData?: DispatcherStatsAPIData;
dispatcherStatsName: string;
driverStatsName: string; dispatcherStatsData?: DispatcherStatsAPIData;
driverStatsData?: DriverStatsAPIData;
driverStatsStatus: DataStatus; driverStatsName: string;
driverStatsData?: DriverStatsAPIData;
chosenModalTrainId?: string; driverStatsStatus: DataStatus;
currentStatsTab: 'daily' | 'driver'; chosenModalTrainId?: string;
dataStatuses: { currentStatsTab: 'daily' | 'driver' | null;
connection: DataStatus;
sceneries: DataStatus; dataStatuses: {
timetables: DataStatus; connection: DataStatus;
dispatchers: DataStatus; sceneries: DataStatus;
trains: DataStatus; timetables: DataStatus;
}; dispatchers: DataStatus;
trains: DataStatus;
listenerLaunched: boolean; };
blockScroll: boolean;
} listenerLaunched: boolean;
blockScroll: boolean;
export interface APIData { modalLastClickedTarget: EventTarget | null;
stations?: StationAPIData[]; }
dispatchers?: string[][];
trains?: TrainAPIData[]; export interface APIData {
connectedSocketCount: number; stations?: StationAPIData[];
} dispatchers?: string[][];
trains?: TrainAPIData[];
export interface StationJSONData { connectedSocketCount: number;
name: string; }
abbr: string;
url: string; export interface StationRoutesInfo {
lines: string; routeName: string;
project: string; isElectric: boolean;
projectUrl: string; isInternal: boolean;
isRouteSBL: boolean;
reqLevel: number; routeLength: number;
routeSpeed: number;
signalType: string; routeTracks: number;
controlType: string; }
SUP: boolean; export interface StationJSONData {
name: string;
routes: string; abbr: string;
url: string;
checkpoints: string | null; lines: string;
authors?: string; project: string;
projectUrl: string;
availability: Availability;
} reqLevel: number;
signalType: string;
controlType: string;
SUP: boolean;
// routes: string;
routesInfo: StationRoutesInfo[];
checkpoints: string | null;
authors?: string;
availability: Availability;
}
+3 -3
View File
@@ -1,4 +1,4 @@
import { TrainFilter } from '../../types/Trains/TrainOptionsTypes'; import { TrainFilter } from '../interfaces/Trains/TrainFilter';
import { TrainFilterType } from '../enums/TrainFilterType'; import { TrainFilterType } from '../enums/TrainFilterType';
import Train from '../interfaces/Train'; import Train from '../interfaces/Train';
import TrainStop from '../interfaces/TrainStop'; import TrainStop from '../interfaces/TrainStop';
@@ -44,7 +44,7 @@ function filterTrainList(trainList: Train[], searchedTrain: string, searchedDriv
return !train.timetableData?.SKR; return !train.timetableData?.SKR;
case TrainFilterType.common: case TrainFilterType.common:
return train.timetableData?.SKR || train.timetableData?.TWR; return train.timetableData?.SKR || train.timetableData?.TWR;
case TrainFilterType.passenger: case TrainFilterType.passenger:
return !/^[AMRE]\D{2}$/.test(train.timetableData?.category || ''); return !/^[AMRE]\D{2}$/.test(train.timetableData?.category || '');
@@ -81,7 +81,7 @@ function sortTrainList(trainList: Train[], sorterActive: { id: string; dir: numb
if (a.mass > b.mass) return sorterActive.dir; if (a.mass > b.mass) return sorterActive.dir;
return -sorterActive.dir; return -sorterActive.dir;
case 'distance': case 'routeDistance':
if ((a.timetableData?.routeDistance || -1) > (b.timetableData?.routeDistance || -1)) return sorterActive.dir; if ((a.timetableData?.routeDistance || -1) > (b.timetableData?.routeDistance || -1)) return sorterActive.dir;
return -sorterActive.dir; return -sorterActive.dir;
@@ -0,0 +1,25 @@
import { JournalFilterType } from '../../scripts/enums/JournalFilterType';
export type JournalTimetableSearchKey =
| 'search-driver'
| 'search-train'
| 'search-date'
| 'search-dispatcher'
| 'search-issuedFrom';
export type JournalTimetableSorterKey = 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
export type JournalTimetableSearchType = {
[key in JournalTimetableSearchKey]: string;
};
export interface JournalFilter {
id: JournalFilterType;
filterSection: string;
isActive: boolean;
}
export interface JournalTimetableSorter {
id: JournalTimetableSorterKey;
dir: 'asc' | 'desc';
}
+2 -2
View File
@@ -1,7 +1,7 @@
export const URLs = { export const URLs = {
stacjownikAPI: stacjownikAPI:
import.meta.env.VITE_APP_API_DEV == 1 && !import.meta.env.PROD import.meta.env.VITE_APP_API_DEV === "1" && !import.meta.env.PROD
? 'http://localhost:3001' ? 'http://localhost:3001'
: 'https://spythere.pl', : 'https://stacjownik.spythere.pl',
stacjownikAPIDev: 'localhost:3000', stacjownikAPIDev: 'localhost:3000',
}; };
@@ -1,156 +1,156 @@
import { HeadIdsTypes } from '../../scripts/data/stationHeaderNames'; import { HeadIdsTypes } from '../data/stationHeaderNames';
import Filter from '../../scripts/interfaces/Filter'; import Filter from '../interfaces/Filter';
import Station from '../../scripts/interfaces/Station'; import Station from '../interfaces/Station';
export const sortStations = (a: Station, b: Station, sorter: { headerName: HeadIdsTypes; dir: number }) => { export const sortStations = (a: Station, b: Station, sorter: { headerName: HeadIdsTypes; dir: number }) => {
let diff = 0; let diff = 0;
switch (sorter.headerName) { switch (sorter.headerName) {
case 'station': case 'station':
return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name); return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
case 'min-lvl': case 'min-lvl':
diff = (a.generalInfo?.reqLevel || 0) - (b.generalInfo?.reqLevel || 0); diff = (a.generalInfo?.reqLevel || 0) - (b.generalInfo?.reqLevel || 0);
break; break;
case 'status': case 'status':
diff = (a.onlineInfo?.statusTimestamp || 0) - (b.onlineInfo?.statusTimestamp || 0); diff = (a.onlineInfo?.statusTimestamp || 0) - (b.onlineInfo?.statusTimestamp || 0);
break; break;
case 'dispatcher': case 'dispatcher':
if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') > (b.onlineInfo?.dispatcherName.toLowerCase() || '')) if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') > (b.onlineInfo?.dispatcherName.toLowerCase() || ''))
return sorter.dir; return sorter.dir;
if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') < (b.onlineInfo?.dispatcherName.toLowerCase() || '')) if ((a.onlineInfo?.dispatcherName.toLowerCase() || '') < (b.onlineInfo?.dispatcherName.toLowerCase() || ''))
return -sorter.dir; return -sorter.dir;
break; break;
case 'dispatcher-lvl': case 'dispatcher-lvl':
diff = (a.onlineInfo?.dispatcherExp || 0) - (b.onlineInfo?.dispatcherExp || 0); diff = (a.onlineInfo?.dispatcherExp || 0) - (b.onlineInfo?.dispatcherExp || 0);
break; break;
case 'user': case 'user':
diff = (b.onlineInfo ? b.onlineInfo.currentUsers : -1) - (a.onlineInfo ? a.onlineInfo.currentUsers : -1); diff = (b.onlineInfo ? b.onlineInfo.currentUsers : -1) - (a.onlineInfo ? a.onlineInfo.currentUsers : -1);
break; break;
case 'spawn': case 'spawn':
diff = (a.onlineInfo ? a.onlineInfo.spawns.length : -1) - (b.onlineInfo ? b.onlineInfo.spawns.length : -1); diff = (a.onlineInfo ? a.onlineInfo.spawns.length : -1) - (b.onlineInfo ? b.onlineInfo.spawns.length : -1);
break; break;
case 'timetableConfirmed': case 'timetableConfirmed':
diff = diff =
(a.onlineInfo?.scheduledTrains (a.onlineInfo?.scheduledTrains
? a.onlineInfo.scheduledTrains.filter((train) => train.stopInfo.confirmed).length ? a.onlineInfo.scheduledTrains.filter((train) => train.stopInfo.confirmed).length
: -1) - : -1) -
(b.onlineInfo?.scheduledTrains (b.onlineInfo?.scheduledTrains
? b.onlineInfo.scheduledTrains.filter((train) => train.stopInfo.confirmed).length ? b.onlineInfo.scheduledTrains.filter((train) => train.stopInfo.confirmed).length
: -1); : -1);
break; break;
case 'timetableUnconfirmed': case 'timetableUnconfirmed':
diff = diff =
(a.onlineInfo?.scheduledTrains (a.onlineInfo?.scheduledTrains
? a.onlineInfo.scheduledTrains.filter((train) => !train.stopInfo.confirmed).length ? a.onlineInfo.scheduledTrains.filter((train) => !train.stopInfo.confirmed).length
: -1) - : -1) -
(b.onlineInfo?.scheduledTrains (b.onlineInfo?.scheduledTrains
? b.onlineInfo.scheduledTrains.filter((train) => !train.stopInfo.confirmed).length ? b.onlineInfo.scheduledTrains.filter((train) => !train.stopInfo.confirmed).length
: -1); : -1);
break; break;
case 'timetableAll': case 'timetableAll':
diff = diff =
(a.onlineInfo?.scheduledTrains ? a.onlineInfo.scheduledTrains.length : -1) - (a.onlineInfo?.scheduledTrains ? a.onlineInfo.scheduledTrains.length : -1) -
(b.onlineInfo?.scheduledTrains ? b.onlineInfo.scheduledTrains.length : -1); (b.onlineInfo?.scheduledTrains ? b.onlineInfo.scheduledTrains.length : -1);
break; break;
default: default:
break; break;
} }
if (diff != 0) return Math.sign(diff) * sorter.dir; if (diff != 0) return Math.sign(diff) * sorter.dir;
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
}; };
export const filterStations = (station: Station, filters: Filter) => { export const filterStations = (station: Station, filters: Filter) => {
if (!station.onlineInfo && filters['free']) return false; if (!station.onlineInfo && filters['free']) return false;
if (station.onlineInfo) { if (station.onlineInfo) {
const { statusID, statusTimestamp } = station.onlineInfo; const { statusID, statusTimestamp } = station.onlineInfo;
const isEnding = statusID == 'ending' && filters['endingStatus']; const isEnding = statusID == 'ending' && filters['endingStatus'];
const isNotSigned = (statusID == 'not-signed' || statusID == 'unavailable') && filters['unavailableStatus']; const isNotSigned = (statusID == 'not-signed' || statusID == 'unavailable') && filters['unavailableStatus'];
const isAFK = statusID == 'brb' && filters['afkStatus']; const isAFK = statusID == 'brb' && filters['afkStatus'];
const isNoSpace = statusID == 'no-space' && filters['noSpaceStatus']; const isNoSpace = statusID == 'no-space' && filters['noSpaceStatus'];
const isOccupied = station.onlineInfo && filters['occupied']; const isOccupied = station.onlineInfo && filters['occupied'];
const isOnlineInBounds = const isOnlineInBounds =
(filters['onlineFromHours'] < 8 && (filters['onlineFromHours'] < 8 &&
statusTimestamp > 0 && statusTimestamp > 0 &&
statusTimestamp <= Date.now() + filters['onlineFromHours'] * 3600000) || statusTimestamp <= Date.now() + filters['onlineFromHours'] * 3600000) ||
(filters['onlineFromHours'] > 0 && statusTimestamp <= 0) || (filters['onlineFromHours'] > 0 && statusTimestamp <= 0) ||
(filters['onlineFromHours'] == 8 && statusID != 'no-limit'); (filters['onlineFromHours'] == 8 && statusID != 'no-limit');
if (isEnding || isOnlineInBounds || isNotSigned || isAFK || isNoSpace || isOccupied) return false; if (isEnding || isOnlineInBounds || isNotSigned || isAFK || isNoSpace || isOccupied) return false;
} }
if ((station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && filters['nonPublic']) return false; if ((station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && filters['nonPublic']) return false;
if (station.generalInfo) { if (station.generalInfo) {
const { routes, availability, controlType, lines, reqLevel, signalType, SUP, authors } = station.generalInfo; const { routes, availability, controlType, lines, reqLevel, signalType, SUP, authors } = station.generalInfo;
if (availability == 'unavailable' && filters['unavailable'] && !station.onlineInfo) return false; if (availability == 'unavailable' && filters['unavailable'] && !station.onlineInfo) return false;
if (availability == 'abandoned' && filters['abandoned'] && !station.onlineInfo) return false; if (availability == 'abandoned' && filters['abandoned'] && !station.onlineInfo) return false;
if (availability == 'default' && filters['default']) return false; if (availability == 'default' && filters['default']) return false;
if ( if (
availability != 'default' && availability != 'default' &&
filters['notDefault'] && filters['notDefault'] &&
!(availability == 'abandoned' || availability == 'unavailable') !(availability == 'abandoned' || availability == 'unavailable')
) )
return false; return false;
if (filters['real'] && lines) return false; if (filters['real'] && lines) return false;
if (filters['fictional'] && !lines) return false; if (filters['fictional'] && !lines) return false;
const otherAvailability = const otherAvailability =
availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned'; availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned';
if (reqLevel + (otherAvailability ? 1 : 0) < filters['minLevel']) return false; if (reqLevel + (otherAvailability ? 1 : 0) < filters['minLevel']) return false;
if (reqLevel + (otherAvailability ? 1 : 0) > filters['maxLevel']) return false; if (reqLevel + (otherAvailability ? 1 : 0) > filters['maxLevel']) return false;
if ( if (
filters['no-1track'] && filters['no-1track'] &&
(routes.oneWayCatenaryRouteNames.length != 0 || routes.oneWayNoCatenaryRouteNames.length != 0) (routes.oneWayCatenaryRouteNames.length != 0 || routes.oneWayNoCatenaryRouteNames.length != 0)
) )
return false; return false;
if ( if (
filters['no-2track'] && filters['no-2track'] &&
(routes.twoWayCatenaryRouteNames.length != 0 || routes.twoWayNoCatenaryRouteNames.length != 0) (routes.twoWayCatenaryRouteNames.length != 0 || routes.twoWayNoCatenaryRouteNames.length != 0)
) )
return false; return false;
if (routes.oneWayCatenaryRouteNames.length < filters['minOneWayCatenary']) return false; if (routes.oneWayCatenaryRouteNames.length < filters['minOneWayCatenary']) return false;
if (routes.oneWayNoCatenaryRouteNames.length < filters['minOneWay']) return false; if (routes.oneWayNoCatenaryRouteNames.length < filters['minOneWay']) return false;
if (routes.twoWayCatenaryRouteNames.length < filters['minTwoWayCatenary']) return false; if (routes.twoWayCatenaryRouteNames.length < filters['minTwoWayCatenary']) return false;
if (routes.twoWayNoCatenaryRouteNames.length < filters['minTwoWay']) return false; if (routes.twoWayNoCatenaryRouteNames.length < filters['minTwoWay']) return false;
if (filters[controlType]) return false; if (filters[controlType]) return false;
if (filters[signalType]) return false; if (filters[signalType]) return false;
if (filters['SUP'] && SUP) return false; if (filters['SUP'] && SUP) return false;
if (filters['noSUP'] && !SUP) return false; if (filters['noSUP'] && !SUP) return false;
if (filters['SBL'] && routes.sblRouteNames.length > 0) return false; if (filters['SBL'] && routes.sblRouteNames.length > 0) return false;
if (filters['PBL'] && routes.sblRouteNames.length == 0) return false; if (filters['PBL'] && routes.sblRouteNames.length == 0) return false;
if ( if (
filters['authors'].length > 3 && filters['authors'].length > 3 &&
!authors?.map((a) => a.toLocaleLowerCase()).includes(filters['authors'].toLocaleLowerCase()) !authors?.map((a) => a.toLocaleLowerCase()).includes(filters['authors'].toLocaleLowerCase())
) )
return false; return false;
} }
return true; return true;
}; };
+4 -4
View File
@@ -2,8 +2,7 @@ import { ScheduledTrain, StopStatus } from '../interfaces/ScheduledTrain';
import Train from '../interfaces/Train'; import Train from '../interfaces/Train';
import TrainStop from '../interfaces/TrainStop'; import TrainStop from '../interfaces/TrainStop';
export const getLocoURL = (locoType: string): string => export const getLocoURL = (locoType: string): string => `https://rj.td2.info.pl/dist/img/thumbnails/${locoType.includes('EN') ? locoType + 'rb' : locoType}.png`;
`https://rj.td2.info.pl/dist/img/thumbnails/${locoType.includes('EN') ? locoType + 'rb' : locoType}.png`;
export const getStatusID = (stationStatus: any): string => { export const getStatusID = (stationStatus: any): string => {
if (!stationStatus) return 'unknown'; if (!stationStatus) return 'unknown';
@@ -58,7 +57,7 @@ export const getStatusTimestamp = (stationStatus: any): number => {
return -1; return -1;
}; };
export const parseSpawns = (spawnString: string) => { export const parseSpawns = (spawnString: string | null) => {
if (!spawnString) return []; if (!spawnString) return [];
if (spawnString === 'NO_SPAWN') return []; if (spawnString === 'NO_SPAWN') return [];
@@ -66,8 +65,9 @@ export const parseSpawns = (spawnString: string) => {
const spawnArray = spawn.split(','); const spawnArray = spawn.split(',');
const spawnName = spawnArray[6] ? spawnArray[6] : spawnArray[0]; const spawnName = spawnArray[6] ? spawnArray[6] : spawnArray[0];
const spawnLength = parseInt(spawnArray[2]); const spawnLength = parseInt(spawnArray[2]);
const isElectrified = spawnArray[3] == 'True';
return { spawnName, spawnLength }; return { spawnName, spawnLength, isElectrified };
}); });
}; };
+116 -99
View File
@@ -1,99 +1,116 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import inputData from '../data/options.json'; import inputData from '../data/options.json';
import Station from '../scripts/interfaces/Station'; import Station from '../scripts/interfaces/Station';
import StorageManager from '../scripts/managers/storageManager'; import StorageManager from '../scripts/managers/storageManager';
import { useStore } from './store'; import { useStore } from './store';
import { filterInitStates } from './constants/initFilterStates'; import { filterInitStates } from '../scripts/constants/stores/initFilterStates';
import { filterStations, sortStations } from './utils/filterUtils'; import { filterStations, sortStations } from '../scripts/utils/filterUtils';
import { HeadIdsTypes } from '../scripts/data/stationHeaderNames'; import { HeadIdsTypes } from '../scripts/data/stationHeaderNames';
export const useStationFiltersStore = defineStore('stationFiltersStore', { export const useStationFiltersStore = defineStore('stationFiltersStore', {
state() { state() {
return { return {
inputs: inputData, inputs: inputData,
filters: { ...filterInitStates }, filters: { ...filterInitStates },
sorterActive: { headerName: 'station' as HeadIdsTypes, dir: 1 }, sorterActive: { headerName: 'station' as HeadIdsTypes, dir: 1 },
store: useStore(), store: useStore(),
lastClickedFilterId: '', lastClickedFilterId: '',
}; };
}, },
getters: { getters: {
areFiltersAtDefault(state) { areFiltersAtDefault(state) {
return Object.keys(state.filters).every((f) => state.filters[f] === filterInitStates[f]); return Object.keys(state.filters).every((f) => state.filters[f] === filterInitStates[f]);
}, },
}, },
actions: { actions: {
getFilteredStationList(stationList: Station[], region: string): Station[] { getFilteredStationList(stationList: Station[], region: string): Station[] {
return stationList return stationList
.map((station) => { .map((station) => {
if (station.onlineInfo && station.onlineInfo.region != region) { if (station.onlineInfo && station.onlineInfo.region != region) {
delete station.onlineInfo; delete station.onlineInfo;
} }
return station; return station;
}) })
.filter((station) => filterStations(station, this.filters)) .filter((station) => filterStations(station, this.filters))
.sort((a, b) => sortStations(a, b, this.sorterActive)); .sort((a, b) => sortStations(a, b, this.sorterActive));
}, },
setupFilters() { setupFilters() {
if (!StorageManager.isRegistered('options_saved')) return; if (!StorageManager.isRegistered('options_saved')) return;
this.inputs.options.forEach((option) => { this.inputs.options.forEach((option) => {
if (!StorageManager.isRegistered(option.name)) return; if (!StorageManager.isRegistered(option.name)) return;
const savedValue = StorageManager.getBooleanValue(option.name); const savedValue = StorageManager.getBooleanValue(option.name);
this.filters[option.name] = savedValue; this.filters[option.name] = savedValue;
option.value = !savedValue; option.value = !savedValue;
}); });
this.inputs.sliders.forEach((slider) => { this.inputs.sliders.forEach((slider) => {
if (!StorageManager.isRegistered(slider.name)) return; if (!StorageManager.isRegistered(slider.name)) return;
const savedValue = StorageManager.getNumericValue(slider.name); const savedValue = StorageManager.getNumericValue(slider.name);
this.filters[slider.name] = savedValue; this.filters[slider.name] = savedValue;
slider.value = savedValue; slider.value = savedValue;
}); });
}, },
changeFilterValue(filter: { name: string; value: any }) { // Quick actions (TODO)
this.filters[filter.name] = filter.value; handleQuickAction(actionName: string) {
// switch (actionName) {
if (StorageManager.isRegistered('options_saved')) StorageManager.setValue(filter.name, filter.value); // case 'all-available':
}, // this.resetFilters();
// this.inputs.options
resetFilters() { // .filter((option) => /^(free|non-public)/.test(option.id))
this.filters = { ...filterInitStates }; // .forEach((option) => (option.value = !option.defaultValue));
// break;
this.inputs.options.forEach((option) => { // case 'all-free':
option.value = option.defaultValue; // this.resetFilters();
StorageManager.setBooleanValue(option.name, !option.defaultValue); // this.inputs.options
}); // .filter((option) => /^(free|occupied)/.test(option.id))
// .forEach((option) => (option.value = !option.defaultValue));
this.inputs.sliders.forEach((slider) => { // break;
slider.value = slider.defaultValue; // default:
StorageManager.setNumericValue(slider.name, slider.defaultValue); // break;
}); // }
}, },
resetSectionOptions(section: string) { changeFilterValue(name: string, value: any) {
this.inputs.options.forEach((option) => { this.filters[name] = value;
if (option.section != section) return; if (StorageManager.isRegistered('options_saved')) StorageManager.setValue(name, value);
},
option.value = option.defaultValue;
this.filters[option.id] = !option.defaultValue; resetFilters() {
this.filters = { ...filterInitStates };
StorageManager.setBooleanValue(option.name, !option.defaultValue);
}); this.inputs.options.forEach((option) => {
}, option.value = option.defaultValue;
StorageManager.setBooleanValue(option.name, !option.defaultValue);
changeSorter(headerName: HeadIdsTypes) { });
if (headerName == this.sorterActive.headerName) this.sorterActive.dir = -1 * this.sorterActive.dir;
else this.sorterActive.dir = 1; this.inputs.sliders.forEach((slider) => {
slider.value = slider.defaultValue;
this.sorterActive.headerName = headerName; StorageManager.setNumericValue(slider.name, slider.defaultValue);
}, });
}, },
});
resetSectionOptions(section: string) {
this.inputs.options
.filter((option) => option.section == section)
.forEach((option) => {
option.value = option.defaultValue;
StorageManager.setBooleanValue(option.name, !option.defaultValue);
});
},
changeSorter(headerName: HeadIdsTypes) {
if (headerName == this.sorterActive.headerName) this.sorterActive.dir = -1 * this.sorterActive.dir;
else this.sorterActive.dir = 1;
this.sorterActive.headerName = headerName;
},
},
});
+73 -102
View File
@@ -3,24 +3,21 @@ import { defineStore } from 'pinia';
import { io } from 'socket.io-client'; import { io } from 'socket.io-client';
import { DataStatus } from '../scripts/enums/DataStatus'; import { DataStatus } from '../scripts/enums/DataStatus';
import StationAPIData from '../scripts/interfaces/api/StationAPIData'; import StationAPIData from '../scripts/interfaces/api/StationAPIData';
import {ScheduledTrain} from '../scripts/interfaces/ScheduledTrain'; import { ScheduledTrain } from '../scripts/interfaces/ScheduledTrain';
import Station from '../scripts/interfaces/Station'; import Station from '../scripts/interfaces/Station';
import StationRoutes from '../scripts/interfaces/StationRoutes'; import StationRoutes from '../scripts/interfaces/StationRoutes';
import Train from '../scripts/interfaces/Train'; import Train from '../scripts/interfaces/Train';
import { URLs } from '../scripts/utils/apiURLs'; import { URLs } from '../scripts/utils/apiURLs';
import { import { getStatusTimestamp, getStatusID, getScheduledTrain, parseSpawns } from '../scripts/utils/storeUtils';
getLocoURL,
getStatusTimestamp,
getStatusID,
getScheduledTrain,
parseSpawns,
} from '../scripts/utils/storeUtils';
import { APIData, StationJSONData, StoreState } from '../scripts/interfaces/store/storeTypes'; import { APIData, StationJSONData, StoreState } from '../scripts/interfaces/store/storeTypes';
import packageInfo from '../../package.json';
import { RollingStockInfo, RollingStockGithubData } from '../scripts/interfaces/github_api/StockInfoGithubData';
export const useStore = defineStore('store', { export const useStore = defineStore('store', {
state: () => state: () =>
({ ({
apiData: {} as unknown, apiData: {} as unknown,
rollingStockData: undefined,
stationList: [], stationList: [],
trainList: [], trainList: [],
@@ -54,10 +51,11 @@ export const useStore = defineStore('store', {
trains: DataStatus.Loading, trains: DataStatus.Loading,
}, },
currentStatsTab: 'daily', currentStatsTab: null,
blockScroll: false, blockScroll: false,
listenerLaunched: false, listenerLaunched: false,
modalLastClickedTarget: null,
} as StoreState), } as StoreState),
actions: { actions: {
@@ -67,10 +65,7 @@ export const useStore = defineStore('store', {
if (!trains) return []; if (!trains) return [];
this.trainList = trains this.trainList = trains
.filter( .filter((train) => train.region === this.region.id && (train.online || train.timetable || train.lastSeen > Date.now() - 180000))
(train) =>
train.region === this.region.id && (train.online || train.timetable || train.lastSeen > Date.now() - 180000)
)
.map((train) => { .map((train) => {
const stock = train.stockString.split(';'); const stock = train.stockString.split(';');
const locoType = stock ? stock[0] : train.stockString; const locoType = stock ? stock[0] : train.stockString;
@@ -94,9 +89,8 @@ export const useStore = defineStore('store', {
currentStationName: train.currentStationName, currentStationName: train.currentStationName,
currentStationHash: train.currentStationHash, currentStationHash: train.currentStationHash,
connectedTrack: train.connectedTrack, connectedTrack: train.connectedTrack,
stockList: stock,
locoType, locoType,
locoURL: getLocoURL(locoType),
cars: stock.slice(1),
lastSeen: train.lastSeen, lastSeen: train.lastSeen,
isTimeout: train.isTimeout, isTimeout: train.isTimeout,
@@ -123,20 +117,12 @@ export const useStore = defineStore('store', {
getDispatcherStatus(onlineStationData: StationAPIData) { getDispatcherStatus(onlineStationData: StationAPIData) {
const { dispatchers } = this.apiData; const { dispatchers } = this.apiData;
const prevDispatcherStatus = this.lastDispatcherStatuses.find( const prevDispatcherStatus = this.lastDispatcherStatuses.find((dispatcher) => dispatcher.hash === onlineStationData.stationHash);
(dispatcher) => dispatcher.hash === onlineStationData.stationHash
);
const stationStatus = !dispatchers const stationStatus = !dispatchers ? undefined : dispatchers.find((status: string[]) => status[0] == onlineStationData.stationHash && status[1] == this.region.id) || -1;
? undefined
: dispatchers.find(
(status: string[]) => status[0] == onlineStationData.stationHash && status[1] == this.region.id
) || -1;
const statusTimestamp = const statusTimestamp = prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusTimestamp : getStatusTimestamp(stationStatus);
prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusTimestamp : getStatusTimestamp(stationStatus); const statusID = prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusID : getStatusID(stationStatus);
const statusID =
prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusID : getStatusID(stationStatus);
return { return {
hash: onlineStationData.stationHash, hash: onlineStationData.stationHash,
@@ -160,26 +146,17 @@ export const useStore = defineStore('store', {
const stopName = stop.stopNameRAW.toLowerCase(); const stopName = stop.stopNameRAW.toLowerCase();
if (stationName === stopName) return true; if (stationName === stopName) return true;
if (stopName.includes(stationName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.')) if (stopName.includes(stationName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.')) return true;
return true;
if (stationName.includes(stopName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.')) if (stationName.includes(stopName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.')) return true;
return true;
if ( if (stopName.includes('podg.') && stopName.split(', podg.')[0] && stationName.includes(stopName.split(', podg.')[0])) return true;
stopName.includes('podg.') &&
stopName.split(', podg.')[0] &&
stationName.includes(stopName.split(', podg.')[0])
)
return true;
if ( if (
stationGeneralInfo && stationGeneralInfo &&
stationGeneralInfo.checkpoints && stationGeneralInfo.checkpoints &&
stationGeneralInfo.checkpoints.length > 0 && stationGeneralInfo.checkpoints.length > 0 &&
stationGeneralInfo.checkpoints.some((cp) => stationGeneralInfo.checkpoints.some((cp) => cp.checkpointName.toLowerCase().includes(stop.stopNameRAW.toLowerCase()))
cp.checkpointName.toLowerCase().includes(stop.stopNameRAW.toLowerCase())
)
) )
return true; return true;
@@ -192,9 +169,7 @@ export const useStore = defineStore('store', {
if (stationGeneralInfo?.checkpoints) { if (stationGeneralInfo?.checkpoints) {
for (const checkpoint of stationGeneralInfo.checkpoints) { for (const checkpoint of stationGeneralInfo.checkpoints) {
const index = timetable.followingStops.findIndex( const index = timetable.followingStops.findIndex((stop) => stop.stopNameRAW.toLowerCase() == checkpoint.checkpointName.toLowerCase());
(stop) => stop.stopNameRAW.toLowerCase() == checkpoint.checkpointName.toLowerCase()
);
if (index == -1) continue; if (index == -1) continue;
@@ -210,10 +185,7 @@ export const useStore = defineStore('store', {
getStationTrains(stationAPIData: StationAPIData) { getStationTrains(stationAPIData: StationAPIData) {
return this.trainList return this.trainList
.filter( .filter((train) => train?.region === this.region.id && train.online && train.currentStationName === stationAPIData.stationName)
(train) =>
train?.region === this.region.id && train.online && train.currentStationName === stationAPIData.stationName
)
.map((train) => ({ .map((train) => ({
driverName: train.driverName, driverName: train.driverName,
driverId: train.driverId, driverId: train.driverId,
@@ -286,9 +258,7 @@ export const useStore = defineStore('store', {
}, },
async fetchStationsGeneralInfo() { async fetchStationsGeneralInfo() {
const sceneryData: StationJSONData[] = await ( const sceneryData: StationJSONData[] = await (await axios.get(`${URLs.stacjownikAPI}/api/getSceneries`)).data;
await axios.get(`${URLs.stacjownikAPI}/api/getSceneries?timestamp=${Math.floor(Date.now() / 1800000)}`)
).data;
if (!sceneryData) { if (!sceneryData) {
this.dataStatuses.sceneries = DataStatus.Error; this.dataStatuses.sceneries = DataStatus.Error;
@@ -303,70 +273,62 @@ export const useStore = defineStore('store', {
...scenery, ...scenery,
authors: scenery.authors?.split(',').map((a) => a.trim()), authors: scenery.authors?.split(',').map((a) => a.trim()),
routes: routes:
scenery.routes scenery.routesInfo.reduce(
?.split(';') (acc, route) => {
.filter((routeString) => routeString) const propName: keyof StationRoutes = `${route.routeTracks == 2 ? 'twoWay' : 'oneWay'}${route.isElectric ? '' : 'No'}CatenaryRouteNames`;
.reduce(
(acc, routeString) => {
const specs1 = routeString.split('_')[0];
const isInternal = specs1.startsWith('!');
const name = isInternal ? specs1.replace('!', '') : specs1;
const specs2 = routeString.split('_')[1].split(''); acc[route.routeTracks == 2 ? 'twoWay' : 'oneWay'].push({
const twoWay = specs2[0] == '2'; name: route.routeName,
const catenary = specs2[1] == 'E'; SBL: route.isRouteSBL,
const SBL = specs2[2] == 'S'; TWB: false,
const TWB = specs2[3] ? true : false; catenary: route.isElectric,
const speed = Number(routeString.split(':')[1]) || 0; isInternal: route.isInternal,
const length = Number(routeString.split(':')[2]) || 0; tracks: route.routeTracks,
length: route.routeLength,
speed: route.routeSpeed,
});
const propName = twoWay if (!route.isInternal) acc[propName].push(route.routeName);
? catenary
? 'twoWayCatenaryRouteNames'
: 'twoWayNoCatenaryRouteNames'
: catenary
? 'oneWayCatenaryRouteNames'
: 'oneWayNoCatenaryRouteNames';
acc[twoWay ? 'twoWay' : 'oneWay'].push({ if (route.isRouteSBL) acc['sblRouteNames'].push(route.routeName);
name,
SBL,
TWB,
catenary,
isInternal,
tracks: twoWay ? 2 : 1,
length,
speed,
});
if (!isInternal) acc[propName].push(name);
if (SBL) acc['sblRouteNames'].push(name); return acc;
},
return acc; {
}, oneWay: [],
{ twoWay: [],
oneWay: [], sblRouteNames: [],
twoWay: [], oneWayCatenaryRouteNames: [],
sblRouteNames: [], oneWayNoCatenaryRouteNames: [],
oneWayCatenaryRouteNames: [], twoWayCatenaryRouteNames: [],
oneWayNoCatenaryRouteNames: [], twoWayNoCatenaryRouteNames: [],
twoWayCatenaryRouteNames: [], } as StationRoutes
twoWayNoCatenaryRouteNames: [], ) || {},
} as StationRoutes checkpoints: scenery.checkpoints ? scenery.checkpoints.split(';').map((sub) => ({ checkpointName: sub, scheduledTrains: [] })) : [],
) || {},
checkpoints: scenery.checkpoints
? scenery.checkpoints.split(';').map((sub) => ({ checkpointName: sub, scheduledTrains: [] }))
: [],
}, },
}; };
}); });
}, },
connectToWebsocket() { async connectToWebsocket() {
if (import.meta.env.VITE_APP_WS_DEV === '1') {
const mockWebsocketData = await import('../data/mockWebsocketData.json');
this.dataStatuses.connection = DataStatus.Loaded;
this.apiData = mockWebsocketData as any;
this.setOnlineData();
console.warn('Stacjownik działa w trybie mockowania danych z WS');
return;
}
const socket = io(URLs.stacjownikAPI, { const socket = io(URLs.stacjownikAPI, {
// transports: ['websocket', 'polling'], // transports: ['websocket', 'polling'],
rememberUpgrade: true, rememberUpgrade: true,
reconnection: true, reconnection: true,
extraHeaders: {
version: packageInfo.version,
},
}); });
socket.on('connect_error', (err) => { socket.on('connect_error', (err) => {
@@ -379,7 +341,7 @@ export const useStore = defineStore('store', {
this.setOnlineData(); this.setOnlineData();
}); });
socket.emit('FETCH_DATA', {}, (data: APIData) => { socket.emit('FETCH_DATA', { version: packageInfo.version }, (data: APIData) => {
this.dataStatuses.connection = DataStatus.Loaded; this.dataStatuses.connection = DataStatus.Loaded;
this.apiData = data; this.apiData = data;
@@ -391,6 +353,7 @@ export const useStore = defineStore('store', {
async connectToAPI() { async connectToAPI() {
await this.fetchStationsGeneralInfo(); await this.fetchStationsGeneralInfo();
await this.fetchStockInfoData();
this.connectToWebsocket(); this.connectToWebsocket();
}, },
@@ -401,6 +364,14 @@ export const useStore = defineStore('store', {
await this.setOnlineData(); await this.setOnlineData();
}, },
async fetchStockInfoData() {
try {
this.rollingStockData = (await axios.get<RollingStockGithubData>('https://raw.githubusercontent.com/Spythere/api/main/td2/data/stockInfo.json')).data;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania informacji o taborze z API:', error);
}
},
async setOnlineData() { async setOnlineData() {
if (!this.apiData.stations) { if (!this.apiData.stations) {
this.dataStatuses.sceneries = DataStatus.Error; this.dataStatuses.sceneries = DataStatus.Error;
+14 -16
View File
@@ -1,6 +1,5 @@
@import 'responsive.scss'; @import 'responsive.scss';
@import 'animations.scss'; @import 'animations.scss';
//Styles
.list_wrapper { .list_wrapper {
overflow-y: auto; overflow-y: auto;
@@ -10,10 +9,6 @@
padding-right: 0.2em; padding-right: 0.2em;
} }
.journal-list {
position: relative;
}
.journal_wrapper { .journal_wrapper {
max-width: 1350px; max-width: 1350px;
width: 100%; width: 100%;
@@ -23,6 +18,15 @@
padding: 1em 0; padding: 1em 0;
} }
.journal_refreshed-date {
background-color: #333;
color: #ddd;
text-align: end;
padding: 0.25em;
margin: 0.5em 0;
}
.journal_warning { .journal_warning {
text-align: center; text-align: center;
font-size: 1.3em; font-size: 1.3em;
@@ -32,8 +36,8 @@
} }
} }
.schedule-dates > * { .journal_item {
margin-right: 0.25em; cursor: pointer;
} }
.journal_item, .journal_item,
@@ -41,6 +45,7 @@
background-color: #1a1a1a; background-color: #1a1a1a;
padding: 1em; padding: 1em;
margin-bottom: 1em; margin-bottom: 1em;
cursor: pointer;
} }
.journal_top-bar { .journal_top-bar {
@@ -50,7 +55,6 @@
gap: 0.5em; gap: 0.5em;
position: relative; position: relative;
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
@@ -63,18 +67,12 @@
} }
@include smallScreen() { @include smallScreen() {
.list_wrapper {
font-size: 1.1em;
}
.journal_top-bar { .journal_top-bar {
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
} }
}
@media (orientation: landscape) { .journal_refreshed-date {
.list_wrapper { text-align: center;
font-size: 1em;
} }
} }
-11
View File
@@ -1,11 +0,0 @@
.scenery-section {
position: relative;
height: 100%;
overflow-y: scroll;
}
.list-warning {
padding: 1em 0.5em;
background-color: #444;
font-size: 1.2em;
}
+45 -3
View File
@@ -30,7 +30,6 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
font-size: 0.9em;
&.driver { &.driver {
border-radius: 50%; border-radius: 50%;
@@ -47,11 +46,54 @@
} }
.region-badge { .region-badge {
padding: 0 0.5em; padding: 0.25em 0.5em;
border-radius: 0.5em; border-radius: 0.5em;
font-weight: bold; font-weight: bold;
color: white;
&.eu { &[aria-describedby='eu'] {
background-color: forestgreen; background-color: forestgreen;
} }
&[aria-describedby='cae'] {
background-color: lightcoral;
color: black;
}
&[aria-describedby='usw'] {
background-color: lightblue;
color: black;
}
&[aria-describedby='us'] {
background-color: lightblue;
color: black;
}
&[aria-describedby='ru'] {
background-color: lightslategray;
}
}
.train-badge {
padding: 0.1em 0.2em;
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);
color: black;
}
&.skr {
background-color: var(--clr-skr);
box-shadow: 0 0 5px 1px var(--clr-skr);
}
&.offline {
background-color: #be3728;
}
} }
+20 -26
View File
@@ -11,15 +11,6 @@
gap: 0.5em; gap: 0.5em;
} }
.filter-button .active-indicator {
width: 7px;
height: 7px;
background-color: lightgreen;
border-radius: 50%;
margin-left: 10px;
}
h1.option-title { h1.option-title {
position: relative; position: relative;
font-size: 1.1em; font-size: 1.1em;
@@ -82,12 +73,16 @@ h1.option-title {
padding: 0.25em 0.25em 0 0; padding: 0.25em 0.25em 0 0;
} }
.options_filter-sections section {
margin: 0.5em 0;
}
.options_filters { .options_filters {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5em; gap: 0.5em;
margin: 0.5em 0 0 0; margin: 0.25em 0;
} }
.sort-option, .sort-option,
@@ -118,17 +113,6 @@ h1.option-title {
margin: 0.5em auto; margin: 0.5em auto;
} }
.search_actions {
display: flex;
gap: 0.5em;
margin: 1em 0;
width: 100%;
button {
width: 100%;
}
}
.search-box { .search-box {
.search-exit { .search-exit {
position: absolute; position: absolute;
@@ -139,6 +123,17 @@ h1.option-title {
} }
} }
.options_actions {
display: flex;
gap: 0.5em;
width: 100%;
margin-top: 1em;
button {
width: 100%;
}
}
@include smallScreen() { @include smallScreen() {
h1 { h1 {
text-align: center; text-align: center;
@@ -155,13 +150,12 @@ h1.option-title {
max-width: 100%; max-width: 100%;
} }
.filter-option,
.sort-option {
margin: 0.25em 0.25em;
}
.options_filters, .options_filters,
.options_sorters { .options_sorters {
justify-content: center; justify-content: center;
} }
.filter-section {
text-align: center;
}
} }
+9
View File
@@ -138,6 +138,15 @@ input {
padding: 0.35em 0; padding: 0.35em 0;
} }
.active-indicator {
width: 7px;
height: 7px;
background-color: lightgreen;
border-radius: 50%;
margin-left: 10px;
}
a { a {
display: inline-block; display: inline-block;
+46
View File
@@ -0,0 +1,46 @@
.scenery-table-section {
position: relative;
height: 100%;
overflow-y: scroll;
}
table.scenery-history-table {
width: 100%;
border-collapse: collapse;
thead {
position: sticky;
top: 0;
background-color: #222222;
}
th {
padding: 0.5em;
}
tr {
background-color: #353535;
border: none;
}
td {
padding: 0.75em;
border-bottom: solid 5px #111;
}
}
.no-history {
padding: 1em 0.5em;
background-color: #444;
font-size: 1.2em;
color: #ccc;
}
.bottom-info {
display: flex;
justify-content: center;
button {
padding: 0.5em;
}
}
-56
View File
@@ -1,56 +0,0 @@
$free: #8a8a8a;
$ending: #e6c300;
$no-limit: #117fc9;
$unav: #ff3d5d;
$brb: #e6a100;
$no-space: #222;
$taken: #09a116;
$unknown: rgb(185, 60, 60);
.status-badge {
border-radius: 1rem;
font-weight: 500;
padding: 0.2em .55em;
background-color: $taken;
&.free {
background-color: $free;
font-size: 0.95em;
}
&.ending {
background-color: $ending;
color: black;
font-size: 0.9em;
}
&.no-limit {
background-color: $no-limit;
font-size: 0.85em;
}
&.not-signed,
&.unavailable {
background-color: $unav;
font-size: 0.85em;
}
&.brb {
background-color: $brb;
color: black;
font-size: 0.95em;
}
&.no-space {
background-color: $no-space;
color: white;
font-size: 0.85em;
}
&.unknown {
background-color: $unknown;
font-size: 0.95em;
}
}
@@ -1,18 +0,0 @@
import { JournalFilterType } from '../../scripts/enums/JournalFilterType';
export type JournalTimetableSearchKey = 'search-driver' | 'search-train' | 'search-date' | 'search-dispatcher';
export type JournalTimetableSearchType = {
[key in JournalTimetableSearchKey]: string;
};
export interface JournalTimetableFilter {
id: JournalFilterType;
filterSection: string;
isActive: boolean;
}
export interface JournalTimetableSorter {
id: 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
dir: -1 | 1;
}

Some files were not shown because too many files have changed in this diff Show More