Compare commits
213 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af9073ab98 | |||
| 800fc35e63 | |||
| d5649d221b | |||
| 5b35fac512 | |||
| d5bc90f668 | |||
| 6d663886f0 | |||
| 85a1a0216e | |||
| 4ac054e947 | |||
| ba70fa1316 | |||
| 77e6b20d0c | |||
| f60263c923 | |||
| 6aec1a75c9 | |||
| d28d600833 | |||
| a353eb3185 | |||
| c5735a6953 | |||
| 7930f7fc8a | |||
| 68f4d54619 | |||
| c4f9738589 | |||
| dd916afd1d | |||
| ea5c9e0028 | |||
| eb7c2d7132 | |||
| ee7c50f59b | |||
| 439f59fedc | |||
| c47d839ce3 | |||
| f77c13cbcf | |||
| dbbbd33100 | |||
| 14d13360a8 | |||
| dc862252ba | |||
| e5fe727ccd | |||
| e836bbed0c | |||
| d4438fd215 | |||
| 1550849360 | |||
| 9d1dc4ffca | |||
| 0397fa788d | |||
| 6e5696b0a6 | |||
| 4537341a57 | |||
| c35c74bd4a | |||
| 25735c5e6e | |||
| 41e60bc69e | |||
| 933bdecb3c | |||
| 10e183d96b | |||
| 5429d39f5e | |||
| ff31e7f903 | |||
| 91f4c6bc57 | |||
| c133eb060b | |||
| 7ffc169d8a | |||
| 1b85cc5f58 | |||
| 72ff857fff | |||
| 96d64e77fc | |||
| 6ceae3f161 | |||
| 8e8e27658c | |||
| 9b6ace394a | |||
| 6cfeaa91bf | |||
| 08b208aeaa | |||
| a089b5275b | |||
| 8425cd4371 | |||
| dbdc517b87 | |||
| e271358a27 | |||
| 66262e3fcd | |||
| 5b2b6bdea2 | |||
| c8587de6d9 | |||
| 1f376085f2 | |||
| f28600a7fa | |||
| d59ead87e6 | |||
| 34d91bc800 | |||
| cf9991d8a0 | |||
| 4ffb79d62b | |||
| d9f5edb4fe | |||
| 1b2112430a | |||
| 0a972a23ef | |||
| 6d52724d06 | |||
| 99415c35d3 | |||
| c3f687d439 | |||
| 266edfd6e6 | |||
| d32d5ad91b | |||
| c3481470cb | |||
| 57e88b9abc | |||
| 44ebf53798 | |||
| 145dc72b6b | |||
| b7f3761940 | |||
| ea7c49dfb3 | |||
| 5d6785813a | |||
| a0054aed14 | |||
| 471e6f5216 | |||
| a617eef00e | |||
| 38e700ecd6 | |||
| da1be0e10a | |||
| f49bb12948 | |||
| 02673a3d70 | |||
| 4ddc7345df | |||
| 5d822684c0 | |||
| 69fa15c70a | |||
| 9192067388 | |||
| 2b41e5b857 | |||
| 674680ff14 | |||
| 475bd2ff10 | |||
| 074d1eb155 | |||
| 378393de89 | |||
| 03e61083a7 | |||
| 0b746fce8c | |||
| 5883e710be | |||
| 3d0695a17b | |||
| 4adb76eeb0 | |||
| 4c41076519 | |||
| 77f61d17fd | |||
| 032a84cbcf | |||
| de9851ebcc | |||
| ff78eba927 | |||
| e4c5f6a322 | |||
| 0a78761928 | |||
| 4843043c29 | |||
| 9e1df1fb61 | |||
| 021474cfb0 | |||
| 7d0e68862c | |||
| 653d45dfc6 | |||
| 4a4e1240a4 | |||
| 14ca48a90d | |||
| a02f9804b1 | |||
| c5efc6fbac | |||
| cacd0a1e4e | |||
| 50375099ab | |||
| 6af67ec741 | |||
| c64112c86a | |||
| 0434702d3b | |||
| dd7d1b0bb0 | |||
| 68934a89a4 | |||
| b88a240ec1 | |||
| eaa34f3359 | |||
| febb22e1bc | |||
| 500f3c1223 | |||
| 221e0c7e82 | |||
| ca19f7e397 | |||
| a71ccd3e1a | |||
| d496c70fa8 | |||
| b9868ba52e | |||
| 59bd3fa2ef | |||
| e14d328ed9 | |||
| 36d71292bc | |||
| 2f6e2e7402 | |||
| e959eac6c5 | |||
| 8bedc4dfc6 | |||
| 73563d5db7 | |||
| 3f818069cd | |||
| cdf0b2a426 | |||
| c29ddeb78c | |||
| b81d98cab7 | |||
| 0e45bca5da | |||
| 715e66879f | |||
| 1747e15dc8 | |||
| 6a923a8e1d | |||
| 25a248e95e | |||
| aa7a6b220e | |||
| deb7b68985 | |||
| 633f05f690 | |||
| 73828867da | |||
| 75685c1e0e | |||
| 496ff95236 | |||
| 7e25327832 | |||
| 272c9f50f8 | |||
| 255e07372e | |||
| 279bbfa4db | |||
| a5c829faf5 | |||
| 5fdfaeac5e | |||
| 9beb30e3d5 | |||
| 48582e2eea | |||
| 2e721fb8bf | |||
| f93c1fbfec | |||
| c06e7b6468 | |||
| 22a6d266cb | |||
| 5f8a16401b | |||
| c9be01aa29 | |||
| 4ec058b33c | |||
| 27a5d2a406 | |||
| 58169e26f6 | |||
| fee1f4bbd5 | |||
| 240817acc3 | |||
| db3be87dd8 | |||
| 1665134d6f | |||
| df289ab734 | |||
| f74440ba6f | |||
| a25dbe9fd5 | |||
| 4fff136d6b | |||
| d06f2d5d2e | |||
| 9f68d628d0 | |||
| d64b906dac | |||
| f3e193e68a | |||
| 5640ce9f2b | |||
| 50100eb2f9 | |||
| e478c510b2 | |||
| 7ea558642f | |||
| 493145f7f2 | |||
| 4f72535365 | |||
| 8e3bf80715 | |||
| 6da586d08a | |||
| be53b9c7fb | |||
| 94ed1160a1 | |||
| 859d8d2631 | |||
| 5f3abd73c5 | |||
| d71c8bb6f9 | |||
| a3db13d79c | |||
| 8cb3da66f2 | |||
| 6e07897ac0 | |||
| 726b859f5c | |||
| 651c60707a | |||
| d4fee84603 | |||
| 86539cdf23 | |||
| 69772460b8 | |||
| 6988a83355 | |||
| b6425564c8 | |||
| caf0a9b4c5 | |||
| bd5f433d6e | |||
| 8d9cc721d6 | |||
| cceeffe49d |
@@ -0,0 +1,20 @@
|
||||
# This file was auto-generated by the Firebase CLI
|
||||
# https://github.com/firebase/firebase-tools
|
||||
|
||||
name: Deploy to Firebase Hosting on merge
|
||||
'on':
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
build_and_deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: npm ci && npm run build
|
||||
- uses: FirebaseExtended/action-hosting-deploy@v0
|
||||
with:
|
||||
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
||||
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STACJOWNIK_TD2 }}'
|
||||
channelId: live
|
||||
projectId: stacjownik-td2
|
||||
@@ -0,0 +1,17 @@
|
||||
# This file was auto-generated by the Firebase CLI
|
||||
# https://github.com/firebase/firebase-tools
|
||||
|
||||
name: Deploy to Firebase Hosting on PR
|
||||
'on': pull_request
|
||||
jobs:
|
||||
build_and_preview:
|
||||
if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: npm ci && npm run build
|
||||
- uses: FirebaseExtended/action-hosting-deploy@v0
|
||||
with:
|
||||
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
||||
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STACJOWNIK_TD2 }}'
|
||||
projectId: stacjownik-td2
|
||||
@@ -1,7 +1,7 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
/dev-dist
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
@@ -31,6 +31,7 @@ node_modules
|
||||
.firebase
|
||||
.firebaserc
|
||||
|
||||
# Env
|
||||
.env
|
||||
|
||||
.fake
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"hosting": {
|
||||
"public": "dist",
|
||||
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
||||
"ignore": [
|
||||
"firebase.json",
|
||||
"**/.*",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
@@ -10,4 +14,3 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
|
||||
<meta name="keywords" content="Stacjownik, TD2, Train Driver 2, stacjownik-td2" />
|
||||
<meta name="description" content="Automatycznie odświeżana strona wyświetlająca stacje w Train Driver 2!" />
|
||||
<meta name="keywords" content="Stacjownik, TD2, Train Driver 2, stacjownik-td2, stacjownik, td2.info.pl" />
|
||||
<meta name="description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
|
||||
|
||||
<title>Stacjownik</title>
|
||||
|
||||
@@ -16,29 +16,31 @@
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta name="theme-color" content="#222222" />
|
||||
|
||||
<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-32.png" sizes="32x32" type="image/png" />
|
||||
<link rel="icon" href="favicon-16.png" sizes="16x16" type="image/png" />
|
||||
<link rel="icon" href="favicon.ico" />
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;700&display=swap" rel="stylesheet" />
|
||||
<!-- 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" />
|
||||
|
||||
<script src="https://www.gstatic.com/firebasejs/8.1.1/firebase-app.js"></script>
|
||||
<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" />
|
||||
|
||||
<script>
|
||||
const firebaseConfig = {
|
||||
apiKey: 'AIzaSyBI36X2-p7vU1flxoJdCEc0noByyTe1mpw',
|
||||
authDomain: 'stacjownik-td2.firebaseapp.com',
|
||||
databaseURL: 'https://stacjownik-td2.firebaseio.com',
|
||||
projectId: 'stacjownik-td2',
|
||||
storageBucket: 'stacjownik-td2.appspot.com',
|
||||
};
|
||||
|
||||
firebase.initializeApp(firebaseConfig);
|
||||
</script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@500;700&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -1,32 +1,34 @@
|
||||
{
|
||||
"name": "stacjownik",
|
||||
"version": "1.10.8",
|
||||
"version": "1.17.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"deploy": "yarn build && firebase deploy --only hosting",
|
||||
"preview": "vite preview"
|
||||
"preview": "yarn build && vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.12.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"firebase": "^9.8.1",
|
||||
"howler": "^2.2.1",
|
||||
"pinia": "^2.0.14",
|
||||
"sass": "^1.53.0",
|
||||
"socket.io-client": "^4.4.1",
|
||||
"vue": "^3.2.37",
|
||||
"vue-i18n": "^9.1.6",
|
||||
"vue-router": "^4.0.0-0"
|
||||
"core-js": "^3.32.2",
|
||||
"dotenv": "^16.3.1",
|
||||
"firebase": "^10.4.0",
|
||||
"howler": "^2.2.4",
|
||||
"pinia": "^2.1.6",
|
||||
"sass": "^1.67.0",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.4.1",
|
||||
"vue-router": "^4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.8.3",
|
||||
"@vitejs/plugin-vue": "^3.0.0",
|
||||
"axios": "^1.1.2",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^3.0.0",
|
||||
"vue-tsc": "^1.0.3"
|
||||
"@types/node": "^20.6.2",
|
||||
"@vite-pwa/assets-generator": "^0.0.10",
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
"axios": "^1.5.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-pwa": "^0.16.5",
|
||||
"vue-tsc": "^1.8.11"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
||||
|
After Width: | Height: | Size: 932 B |
|
After Width: | Height: | Size: 953 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1020 B |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 799 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -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,7 +1,6 @@
|
||||
@import './styles/responsive.scss';
|
||||
@import './styles/variables.scss';
|
||||
@import './styles/global.scss';
|
||||
@import './styles/scenery_status.scss';
|
||||
|
||||
// VUE ROUTE CHANGE ANIMATION
|
||||
.view-anim {
|
||||
@@ -33,7 +32,8 @@
|
||||
.route {
|
||||
margin: 0 0.2em;
|
||||
|
||||
&-active {
|
||||
&-active,
|
||||
&[data-active='true'] {
|
||||
color: $accentCol;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -45,7 +45,11 @@
|
||||
font-size: 1rem;
|
||||
|
||||
@include smallScreen() {
|
||||
font-size: calc(0.55rem + 1vw);
|
||||
font-size: calc(0.55rem + 1.1vw);
|
||||
}
|
||||
|
||||
@include screenLandscape() {
|
||||
font-size: calc(0.45rem + 0.8vw);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +90,11 @@ footer.app_footer {
|
||||
max-width: 100%;
|
||||
padding: 0.5em;
|
||||
|
||||
img {
|
||||
width: 1.1em;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
z-index: 10;
|
||||
|
||||
background: #111;
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
</keep-alive>
|
||||
</transition>
|
||||
|
||||
<UpdatePrompt />
|
||||
|
||||
<AppHeader :current-lang="currentLang" @change-lang="changeLang" />
|
||||
|
||||
<main class="app_main">
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive>
|
||||
<keep-alive exclude="JournalView">
|
||||
<component :is="Component" :key="$route.name" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
@@ -19,7 +21,10 @@
|
||||
<footer class="app_footer">
|
||||
©
|
||||
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
|
||||
{{ new Date().getUTCFullYear() }} | <a :href="releaseURL" target="_blank">v{{ VERSION }}</a>
|
||||
{{ new Date().getUTCFullYear() }} |
|
||||
<a :href="releaseURL" target="_blank">v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}</a>
|
||||
<br />
|
||||
<a href="https://discord.gg/x2mpNN3svk"><img :src="getIcon('discord', 'png')" alt=""> <b>{{ $t('footer.discord') }}</b></a>
|
||||
|
||||
<div style="display: none">∫ ukryta taktyczna całka do programowania w HTMLu</div>
|
||||
</footer>
|
||||
@@ -27,7 +32,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, provide, ref, watch } from 'vue';
|
||||
import { computed, defineComponent, KeepAlive, provide, ref, watch } from 'vue';
|
||||
|
||||
import Clock from './components/App/Clock.vue';
|
||||
|
||||
@@ -41,6 +46,10 @@ import StorageManager from './scripts/managers/storageManager';
|
||||
import imageMixin from './mixins/imageMixin';
|
||||
import AppHeader from './components/App/AppHeader.vue';
|
||||
import axios from 'axios';
|
||||
import UpdatePrompt from './components/App/UpdatePrompt.vue';
|
||||
import { VERSION } from 'vue-i18n';
|
||||
import { RouterView } from 'vue-router';
|
||||
import useCustomSW from './mixins/useCustomSW';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@@ -49,6 +58,7 @@ export default defineComponent({
|
||||
SelectBox,
|
||||
TrainModal,
|
||||
AppHeader,
|
||||
UpdatePrompt,
|
||||
},
|
||||
|
||||
mixins: [imageMixin],
|
||||
@@ -57,6 +67,8 @@ export default defineComponent({
|
||||
const store = useStore();
|
||||
store.connectToAPI();
|
||||
|
||||
const { offlineReady } = useCustomSW();
|
||||
|
||||
const isFilterCardVisible = ref(false);
|
||||
|
||||
provide('isFilterCardVisible', isFilterCardVisible);
|
||||
@@ -77,10 +89,30 @@ export default defineComponent({
|
||||
|
||||
currentLang: 'pl',
|
||||
releaseURL: '',
|
||||
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app',
|
||||
}),
|
||||
|
||||
created() {
|
||||
this.loadLang();
|
||||
|
||||
this.store.isOffline = !window.navigator.onLine;
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
this.store.isOffline = true;
|
||||
|
||||
this.store.apiData = {
|
||||
stations: [],
|
||||
dispatchers: [],
|
||||
trains: [],
|
||||
connectedSocketCount: 0,
|
||||
};
|
||||
|
||||
this.store.setOnlineData();
|
||||
});
|
||||
|
||||
window.addEventListener('online', () => {
|
||||
this.store.isOffline = false;
|
||||
});
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="60" height="60" fill="#898989"/>
|
||||
<path d="M30.5 6.04878H35.2195" stroke="#BFBFBF"/>
|
||||
<path d="M27.9024 4.00303C25.2115 4.10008 24.2403 6.24494 24 7.41767H32.0488C31.8486 6.16406 30.5934 3.90598 27.9024 4.00303Z" fill="black"/>
|
||||
<path d="M33.0244 29.6688V5.47793V4.68292H34.4878V5.47793V56.5854H33.0244V32.5H27.5V28.5V28.0163L28.5 28V31.5L31.9268 31.5447H33.0244V29.6688Z" fill="#BFBFBF"/>
|
||||
<path d="M28.1463 29.2683C30.8373 29.1712 31.8085 27.0264 32.0488 25.8537H24C24.2002 27.1073 25.4554 29.3654 28.1463 29.2683Z" fill="black"/>
|
||||
<path d="M32.0488 25.8537V7.86993V7.41464H24V25.8537H32.0488Z" fill="black"/>
|
||||
<path d="M25 26V29.5L33.8781 44.9756" stroke="black"/>
|
||||
<rect x="33.0244" y="31.5447" width="1.46341" height="25.0407" fill="white"/>
|
||||
<rect x="33.0244" y="31.5447" width="1.46341" height="5.69106" fill="#FF0000"/>
|
||||
<rect x="33.0244" y="42.9268" width="1.46341" height="5.69106" fill="#FF0000"/>
|
||||
<rect x="33.0244" y="54.3089" width="1.46341" height="5.69106" fill="#FF0000"/>
|
||||
<ellipse cx="27.9024" cy="7.40022" rx="1.46341" ry="1.40022" fill="#212121"/>
|
||||
<ellipse cx="27.9024" cy="11.8343" rx="1.46341" ry="1.40022" fill="#212121"/>
|
||||
<ellipse cx="27.9024" cy="16.2683" rx="1.46341" ry="1.40022" fill="#FF0000"/>
|
||||
<ellipse cx="27.9024" cy="20.7023" rx="1.46341" ry="1.40022" fill="#212121"/>
|
||||
<ellipse cx="27.9024" cy="25.1364" rx="1.46341" ry="1.40022" fill="#212121"/>
|
||||
</svg>
|
||||
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="-0.00012207" width="60" height="60" fill="#898989"/>
|
||||
<path d="M29.0126 32.4897V10.2511V9.52028H30.4337V10.2511V57.234H29.0126V32.4897Z" fill="#BFBFBF"/>
|
||||
<path d="M26.955 29.3992V32.9949L29.7672 36.9105" stroke="black" stroke-width="0.61183"/>
|
||||
<rect x="29.0051" y="34.0686" width="1.42857" height="22.8196" fill="white"/>
|
||||
<rect x="29.0051" y="34.0686" width="1.42857" height="5.18627" fill="#FF0000"/>
|
||||
<rect x="29.0051" y="54.8137" width="1.42857" height="5.18627" fill="#FF0000"/>
|
||||
<rect x="29.0051" y="44.4412" width="1.42857" height="5.18627" fill="#FF0000"/>
|
||||
<rect x="27.8749" y="31.8649" width="3.75" height="2.17823" fill="white"/>
|
||||
<path d="M33.5 28.5111V8.61545V8.11176H26V28.5111H33.5Z" fill="black"/>
|
||||
<path d="M29.6364 5.00276C27.1289 5.09112 26.2239 7.044 26 8.11176H33.5C33.3134 6.97036 32.1438 4.91439 29.6364 5.00276Z" fill="black"/>
|
||||
<path d="M29.8636 31.6201C32.3711 31.5317 33.2761 29.5789 33.5 28.5111H26C26.1865 29.6525 27.3561 31.7085 29.8636 31.6201Z" fill="black"/>
|
||||
<ellipse cx="29.887" cy="11.8168" rx="1.38696" ry="1.28474" fill="#212121"/>
|
||||
<ellipse cx="29.887" cy="8.0135" rx="1.38696" ry="1.28474" fill="#212121"/>
|
||||
<ellipse cx="29.887" cy="15.6151" rx="1.38696" ry="1.28474" fill="#212121"/>
|
||||
<ellipse cx="29.887" cy="19.6834" rx="1.38696" ry="1.28474" fill="#212121"/>
|
||||
<ellipse cx="29.887" cy="23.7518" rx="1.38696" ry="1.28474" fill="#212121"/>
|
||||
<ellipse cx="29.887" cy="27.8201" rx="1.38696" ry="1.28474" fill="#00FF0A"/>
|
||||
<ellipse cx="29.887" cy="19.769" rx="1.38696" ry="1.28474" fill="#00FF0A"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
@@ -0,0 +1,18 @@
|
||||
<svg width="144" height="147" viewBox="0 0 144 147" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_1343_19)">
|
||||
<path d="M115.039 101.247C116.397 98.6665 115.405 95.4739 112.824 94.1167C110.243 92.7594 107.05 93.7514 105.693 96.3323L115.039 101.247ZM89.4447 44.0402L94.1179 46.4977L99.0329 37.1513L94.3597 34.6938L89.4447 44.0402ZM105.693 96.3323C95.7398 115.259 72.3278 122.534 53.4008 112.581L48.4858 121.927C72.5746 134.595 102.372 125.336 115.039 101.247L105.693 96.3323ZM53.4008 112.581C34.4739 102.627 27.1993 79.2155 37.1525 60.2885L27.8061 55.3735C15.1383 79.4623 24.397 109.259 48.4858 121.927L53.4008 112.581ZM37.1525 60.2885C47.1057 41.3616 70.5177 34.087 89.4447 44.0402L94.3597 34.6938C70.2709 22.026 40.4738 31.2846 27.8061 55.3735L37.1525 60.2885Z" fill="white"/>
|
||||
<path d="M91.2258 38.7627L101.056 20.0698L116.15 51.8695L81.3956 57.4555L91.2258 38.7627Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_1343_19" x="18.1328" y="20.0698" width="102.017" height="115.531" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1343_19"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1343_19" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.75 3.75H22.5V1.25H20V3.75H10V1.25H7.5V3.75H6.25C4.875 3.75 3.75 4.875 3.75 6.25V23.75C3.75 25.125 4.875 26.25 6.25 26.25H23.75C25.125 26.25 26.25 25.125 26.25 23.75V6.25C26.25 4.875 25.125 3.75 23.75 3.75ZM23.75 23.75H6.25V11.25H23.75V23.75ZM6.25 8.75V6.25H23.75V8.75H6.25ZM8.75 13.75H21.25V16.25H8.75V13.75ZM8.75 18.75H17.5V21.25H8.75V18.75Z" fill="#F2E147"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 477 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.75 3.75H22.5V1.25H20V3.75H10V1.25H7.5V3.75H6.25C4.875 3.75 3.75 4.875 3.75 6.25V23.75C3.75 25.125 4.875 26.25 6.25 26.25H23.75C25.125 26.25 26.25 25.125 26.25 23.75V6.25C26.25 4.875 25.125 3.75 23.75 3.75ZM23.75 23.75H6.25V11.25H23.75V23.75ZM6.25 8.75V6.25H23.75V8.75H6.25ZM8.75 13.75H21.25V16.25H8.75V13.75ZM8.75 18.75H17.5V21.25H8.75V18.75Z" fill="#66FF6C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 477 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.75 3.75H22.5V1.25H20V3.75H10V1.25H7.5V3.75H6.25C4.875 3.75 3.75 4.875 3.75 6.25V23.75C3.75 25.125 4.875 26.25 6.25 26.25H23.75C25.125 26.25 26.25 25.125 26.25 23.75V6.25C26.25 4.875 25.125 3.75 23.75 3.75ZM23.75 23.75H6.25V11.25H23.75V23.75ZM6.25 8.75V6.25H23.75V8.75H6.25ZM8.75 13.75H21.25V16.25H8.75V13.75ZM8.75 18.75H17.5V21.25H8.75V18.75Z" fill="#898989"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 477 B |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -6,23 +6,15 @@
|
||||
<img :src="getIcon('pl')" alt="icon-pl" @click="changeLang('en')" v-if="currentLang == 'pl'" />
|
||||
<img :src="getIcon('en', 'jpg')" alt="icon-en" @click="changeLang('pl')" v-else />
|
||||
</span>
|
||||
|
||||
<span class="icons-bottom">
|
||||
<a href="https://www.paypal.com/paypalme/spythere" target="_blank">
|
||||
<img :src="getIcon('dollar')" alt="icon paypal" />
|
||||
</a>
|
||||
|
||||
<a href="https://discord.gg/x2mpNN3svk" target="_blank">
|
||||
<img :src="getIcon('discord', 'png')" alt="icon discord" />
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="header_body">
|
||||
<StatusIndicator />
|
||||
|
||||
<span class="header_brand">
|
||||
<img :src="getImage('stacjownik-header-logo.svg')" alt="Stacjownik" />
|
||||
<router-link to="/">
|
||||
<img :src="getImage('stacjownik-header-logo.svg')" alt="Stacjownik" />
|
||||
</router-link>
|
||||
</span>
|
||||
|
||||
<span class="header_info">
|
||||
@@ -31,6 +23,12 @@
|
||||
<div class="info_counter">
|
||||
<img :src="getIcon('dispatcher')" alt="icon dispatcher" />
|
||||
<span class="text--primary">{{ onlineDispatchersCount }}</span>
|
||||
|
||||
<!-- <span class="g-tooltip">
|
||||
<b class="text--primary">{{ factorU }}U</b>
|
||||
<div class="content">Test</div>
|
||||
</span> -->
|
||||
|
||||
<span class="text--grayed"> / </span>
|
||||
<span class="text--primary">{{ onlineTrainsCount }}</span>
|
||||
<img :src="getIcon('train')" alt="icon train" />
|
||||
@@ -48,7 +46,12 @@
|
||||
/
|
||||
<router-link class="route" active-class="route-active" to="/trains">{{ $t('app.trains') }}</router-link>
|
||||
/
|
||||
<router-link class="route" active-class="route-active" to="/journal/timetables">
|
||||
<router-link
|
||||
class="route"
|
||||
active-class="route-active"
|
||||
:data-active="$route.path.startsWith('/journal')"
|
||||
to="/journal"
|
||||
>
|
||||
{{ $t('app.journal') }}
|
||||
</router-link>
|
||||
</span>
|
||||
@@ -66,50 +69,57 @@ import StatusIndicator from './StatusIndicator.vue';
|
||||
import Clock from './Clock.vue';
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["changeLang"],
|
||||
mixins: [imageMixin],
|
||||
props: {
|
||||
currentLang: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emits: ['changeLang'],
|
||||
mixins: [imageMixin],
|
||||
props: {
|
||||
currentLang: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
setup() {
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
store: useStore(),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
changeRegion(region: { id: string; value: string }) {
|
||||
this.store.changeRegion(region);
|
||||
},
|
||||
changeLang(lang: string) {
|
||||
this.$emit('changeLang', lang);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
onlineTrainsCount() {
|
||||
return this.store.trainList.filter((train) => train.online).length;
|
||||
},
|
||||
|
||||
onlineDispatchersCount() {
|
||||
return this.store.stationList.filter(
|
||||
(station) => station.onlineInfo && station.onlineInfo.region == this.store.region.id
|
||||
).length;
|
||||
},
|
||||
|
||||
factorU() {
|
||||
return this.onlineDispatchersCount == 0 ? '-' : (this.onlineTrainsCount / this.onlineDispatchersCount).toFixed(2);
|
||||
},
|
||||
|
||||
computedRegions() {
|
||||
return options.regions.map((region) => {
|
||||
const regionStationCount =
|
||||
this.store.apiData.stations?.filter((station) => station.region == region.id && station.isOnline).length || 0;
|
||||
const regionTrainCount =
|
||||
this.store.apiData.trains?.filter((train) => train.region == region.id && train.online).length || 0;
|
||||
return {
|
||||
store: useStore(),
|
||||
id: region.id,
|
||||
value: `${region.value} <div class='text--grayed'>${regionStationCount} / ${regionTrainCount}</div>`,
|
||||
selectedValue: region.value,
|
||||
};
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
changeRegion(region: {
|
||||
id: string;
|
||||
value: string;
|
||||
}) {
|
||||
this.store.changeRegion(region);
|
||||
},
|
||||
changeLang(lang: string) {
|
||||
this.$emit("changeLang", lang);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
onlineTrainsCount() {
|
||||
return this.store.trainList.filter((train) => train.online).length;
|
||||
},
|
||||
onlineDispatchersCount() {
|
||||
return this.store.stationList.filter((station) => station.onlineInfo && station.onlineInfo.region == this.store.region.id).length;
|
||||
},
|
||||
computedRegions() {
|
||||
return options.regions.map((region) => {
|
||||
const regionStationCount = this.store.apiData.stations?.filter((station) => station.region == region.id && station.isOnline).length || 0;
|
||||
const regionTrainCount = this.store.apiData.trains?.filter((train) => train.region == region.id && train.online).length || 0;
|
||||
return {
|
||||
id: region.id,
|
||||
value: `${region.value} <div class='text--grayed'>${regionStationCount} / ${regionTrainCount}</div>`,
|
||||
selectedValue: region.value,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
components: { SelectBox, StatusIndicator, Clock }
|
||||
},
|
||||
components: { SelectBox, StatusIndicator, Clock },
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@@ -127,21 +137,20 @@ export default defineComponent({
|
||||
|
||||
.header {
|
||||
&_body {
|
||||
max-width: 21em;
|
||||
|
||||
@include smallScreen {
|
||||
max-width: 18em;
|
||||
}
|
||||
position: relative;
|
||||
max-width: 20em;
|
||||
}
|
||||
|
||||
&_container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
||||
width: 1350px;
|
||||
padding: 0.5em 0.3em 0 0.3em;
|
||||
border-radius: 0 0 1em 1em;
|
||||
|
||||
@include smallScreen {
|
||||
position: relative;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
&_brand {
|
||||
@@ -149,6 +158,7 @@ export default defineComponent({
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
@@ -156,9 +166,7 @@ export default defineComponent({
|
||||
&_info {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
max-width: 100%;
|
||||
|
||||
font-size: 1.2em;
|
||||
font-size: 1.15em;
|
||||
}
|
||||
|
||||
&_links {
|
||||
@@ -175,57 +183,20 @@ export default defineComponent({
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
padding: 0.5em 0.5em;
|
||||
padding: 0.5em;
|
||||
|
||||
@include smallScreen() {
|
||||
right: auto;
|
||||
left: 0.75em;
|
||||
padding: 0;
|
||||
|
||||
align-items: center;
|
||||
@include smallScreen {
|
||||
transform: translateX(85%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ICONS
|
||||
.icons {
|
||||
position: relative;
|
||||
|
||||
&-top {
|
||||
img {
|
||||
width: 2.5em;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
&-bottom {
|
||||
display: flex;
|
||||
|
||||
a {
|
||||
margin-left: 0.6em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 1.9em;
|
||||
}
|
||||
|
||||
@include smallScreen() {
|
||||
flex-direction: column;
|
||||
|
||||
a {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
}
|
||||
.icons-top {
|
||||
img {
|
||||
width: 2.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,4 +234,4 @@ export default defineComponent({
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<div class="loading">{{message}}</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
props: ["message"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
min-height: 100%;
|
||||
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
font-size: calc(0.75rem + 1vw);
|
||||
|
||||
color: #fdc62f;
|
||||
}
|
||||
</style>
|
||||
@@ -161,17 +161,17 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
||||
import { useStore } from '../../store/store';
|
||||
import { StoreState } from '../../store/storeTypes';
|
||||
import { StoreState } from '../../scripts/interfaces/store/storeTypes';
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
tooltipActive: false,
|
||||
indicator: {
|
||||
offline: false,
|
||||
status: DataStatus.Loading,
|
||||
message: 'data-status.S3',
|
||||
},
|
||||
@@ -193,6 +193,7 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
dataStatus: store.dataStatuses,
|
||||
store,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -206,6 +207,13 @@ export default defineComponent({
|
||||
const trainsDataStatus = statuses.trains;
|
||||
const dispatcherDataStatus = statuses.dispatchers;
|
||||
|
||||
if (this.store.isOffline) {
|
||||
this.setSignalStatus(DataStatus.Initialized);
|
||||
this.indicator.status = DataStatus.Initialized;
|
||||
this.indicator.message = 'data-status.S1-offline';
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionStatus == DataStatus.Error) {
|
||||
this.setSignalStatus(connectionStatus);
|
||||
this.indicator.status = connectionStatus;
|
||||
@@ -252,6 +260,10 @@ export default defineComponent({
|
||||
this.orangeLight = false;
|
||||
this.redBottomLight = false;
|
||||
|
||||
if (status == DataStatus.Initialized) {
|
||||
this.redTopLight = true;
|
||||
}
|
||||
|
||||
if (status == DataStatus.Loaded) {
|
||||
this.greenLight = true;
|
||||
}
|
||||
@@ -291,10 +303,11 @@ export default defineComponent({
|
||||
|
||||
.status-indicator {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
transform: translateX(12em);
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
|
||||
transform: translateX(1.5em);
|
||||
}
|
||||
|
||||
.indicator {
|
||||
@@ -319,7 +332,7 @@ export default defineComponent({
|
||||
background-color: #171717;
|
||||
border-radius: 0.75em;
|
||||
|
||||
min-width: 13em;
|
||||
width: 13em;
|
||||
text-align: center;
|
||||
overflow: none;
|
||||
|
||||
@@ -343,22 +356,16 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
@include midScreen() {
|
||||
left: 50%;
|
||||
top: 100%;
|
||||
|
||||
transform: translate(-50%, 0);
|
||||
margin-left: 0;
|
||||
margin-top: 0.75em;
|
||||
left: auto;
|
||||
right: 200%;
|
||||
|
||||
&::before {
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-bottom: 10px solid #171717;
|
||||
border-left: 12px solid #171717;
|
||||
right: 0;
|
||||
left: auto;
|
||||
|
||||
top: 0;
|
||||
left: 50%;
|
||||
|
||||
transform: translate(-50%, -100%);
|
||||
transform: translate(100%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,3 +375,4 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="update-prompt">
|
||||
<transition name="prompt-anim">
|
||||
<div class="prompt_content" v-if="!hidePrompt && needRefresh">
|
||||
<div>{{ $t('update.title') }}</div>
|
||||
|
||||
<div class="prompt_actions">
|
||||
<button class="btn btn--filled" @click="updateServiceWorker(true)">{{ $t('update.confirm-button') }}</button>
|
||||
<button class="btn btn--filled" @click="hidePrompt = true">{{ $t('update.later-button') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import useCustomSW from '../../mixins/useCustomSW';
|
||||
|
||||
const hidePrompt = ref(false);
|
||||
const { needRefresh, updateServiceWorker } = useCustomSW();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
.update-prompt {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.prompt_content {
|
||||
margin: 1em;
|
||||
padding: 1em;
|
||||
|
||||
font-weight: bold;
|
||||
background-color: black;
|
||||
|
||||
box-shadow: 0 0 10px 1px $accentCol;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
.prompt_actions {
|
||||
display: flex;
|
||||
margin-top: 1em;
|
||||
gap: 0.5em;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// Animation
|
||||
.prompt-anim {
|
||||
&-enter-active,
|
||||
&-leave-active {
|
||||
transition: all 120ms ease-in;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&-enter-from,
|
||||
&-leave-to {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
@@ -20,7 +20,7 @@ export default defineComponent({
|
||||
.loading {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -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>
|
||||
@@ -3,6 +3,10 @@
|
||||
<div class="select-box_content">
|
||||
<button class="selected" @click="toggleBox">
|
||||
<span>{{ computedSelectedItem.selectedValue || computedSelectedItem.value }}</span>
|
||||
|
||||
<div class="arrow">
|
||||
<img :src="listOpen ? getIcon('arrow-asc') : getIcon('arrow-desc')" alt="arrow-icon" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<ul class="options" :ref="(el) => (listRef = el as Element)">
|
||||
@@ -21,10 +25,6 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="arrow">
|
||||
<img :src="listOpen ? getIcon('arrow-asc') : getIcon('arrow-desc')" alt="arrow-icon" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -129,46 +129,22 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.select-box {
|
||||
position: relative;
|
||||
width: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
padding: 0;
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
width: 1.35em;
|
||||
}
|
||||
|
||||
transform: translateY(-50%);
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
button.selected {
|
||||
background-color: transparent;
|
||||
color: paleturquoise;
|
||||
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
|
||||
padding: 0.1em 0.5em;
|
||||
margin-right: 2em;
|
||||
|
||||
display: flex;
|
||||
|
||||
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
text-align: left;
|
||||
|
||||
&:focus {
|
||||
background-color: #262626;
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,119 +1,119 @@
|
||||
<template>
|
||||
<span class="stop-date">
|
||||
<span
|
||||
class="date arrival"
|
||||
v-if="!stop.beginsHere"
|
||||
:class="{
|
||||
delayed: stop.arrivalDelay > 0 && stop.confirmed,
|
||||
preponed: stop.arrivalDelay < 0 && stop.confirmed,
|
||||
'on-time': stop.arrivalDelay == 0 && stop.confirmed,
|
||||
}"
|
||||
>
|
||||
<span v-if="stop.arrivalDelay != 0 && stop.confirmed">
|
||||
<s>{{ timestampToString(stop.arrivalTimestamp) }}</s>
|
||||
{{ timestampToString(stop.arrivalRealTimestamp) }}
|
||||
({{ stop.arrivalDelay > 0 ? '+' : '' }}{{ stop.arrivalDelay }})
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
{{ timestampToString(stop.arrivalTimestamp) }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="date stop" v-if="stop.stopTime" :class="stop.stopType.replace(', ', '-')">
|
||||
{{ stop.stopTime }} {{ stop.stopType == '' ? 'pt' : stop.stopType }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="date departure"
|
||||
v-if="!stop.terminatesHere && stop.stopTime != 0"
|
||||
:class="{
|
||||
delayed: stop.departureDelay > 0 && stop.confirmed,
|
||||
preponed: stop.departureDelay < 0 && stop.confirmed,
|
||||
}"
|
||||
>
|
||||
<span v-if="stop.departureDelay != 0 && stop.confirmed">
|
||||
<s>{{ timestampToString(stop.departureTimestamp) }}</s>
|
||||
{{ timestampToString(stop.departureRealTimestamp) }}
|
||||
|
||||
({{ stop.departureDelay > 0 ? '+' : '' }}{{ stop.departureDelay }})
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
{{ timestampToString(stop.departureTimestamp) }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import dateMixin from '../../mixins/dateMixin';
|
||||
import TrainStop from '../../scripts/interfaces/TrainStop';
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [dateMixin],
|
||||
|
||||
props: {
|
||||
stop: {
|
||||
type: Object as () => TrainStop,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$preponedClr: lime;
|
||||
$delayedClr: salmon;
|
||||
$dateClr: #525151;
|
||||
$stopExchangeClr: #db8e29;
|
||||
$stopDefaultClr: #252525;
|
||||
|
||||
.stop-date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.date {
|
||||
background: $dateClr;
|
||||
padding: 0.3em 0.5em;
|
||||
}
|
||||
|
||||
.stop {
|
||||
&.ph,
|
||||
&.ph-pm,
|
||||
&.pm {
|
||||
background: $stopExchangeClr;
|
||||
}
|
||||
|
||||
background: $stopDefaultClr;
|
||||
}
|
||||
|
||||
.arrival,
|
||||
.departure {
|
||||
&.delayed {
|
||||
s {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
span {
|
||||
color: $delayedClr;
|
||||
}
|
||||
}
|
||||
|
||||
&.preponed {
|
||||
s {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
span {
|
||||
color: $preponedClr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<span class="stop-date">
|
||||
<span
|
||||
class="date arrival"
|
||||
v-if="!stop.beginsHere"
|
||||
:class="{
|
||||
delayed: stop.arrivalDelay > 0 && (stop.confirmed || stop.stopped),
|
||||
preponed: stop.arrivalDelay < 0 && (stop.confirmed || stop.stopped),
|
||||
'on-time': stop.arrivalDelay == 0 && stop.confirmed,
|
||||
}"
|
||||
>
|
||||
<span v-if="stop.arrivalDelay != 0 && (stop.confirmed || stop.stopped)">
|
||||
<s>{{ timestampToString(stop.arrivalTimestamp) }}</s>
|
||||
{{ timestampToString(stop.arrivalRealTimestamp) }}
|
||||
({{ stop.arrivalDelay > 0 ? '+' : '' }}{{ stop.arrivalDelay }})
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
{{ timestampToString(stop.arrivalTimestamp) }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="date stop" v-if="stop.stopTime || stop.stopped" :class="stop.stopType.replace(', ', '-')">
|
||||
{{ stop.stopTime }} {{ stop.stopType == '' ? 'pt' : stop.stopType }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="date departure"
|
||||
v-if="!stop.terminatesHere && (stop.stopTime != 0 || stop.stopped)"
|
||||
:class="{
|
||||
delayed: stop.departureDelay > 0 && stop.confirmed,
|
||||
preponed: stop.departureDelay < 0 && stop.confirmed,
|
||||
}"
|
||||
>
|
||||
<span v-if="stop.departureDelay != 0 && stop.confirmed">
|
||||
<s>{{ timestampToString(stop.departureTimestamp) }}</s>
|
||||
{{ timestampToString(stop.departureRealTimestamp) }}
|
||||
|
||||
({{ stop.departureDelay > 0 ? '+' : '' }}{{ stop.departureDelay }})
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
{{ timestampToString(stop.departureTimestamp) }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import dateMixin from '../../mixins/dateMixin';
|
||||
import TrainStop from '../../scripts/interfaces/TrainStop';
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [dateMixin],
|
||||
|
||||
props: {
|
||||
stop: {
|
||||
type: Object as () => TrainStop,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$preponedClr: lime;
|
||||
$delayedClr: salmon;
|
||||
$dateClr: #525151;
|
||||
$stopExchangeClr: #db8e29;
|
||||
$stopDefaultClr: #252525;
|
||||
|
||||
.stop-date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.date {
|
||||
background: $dateClr;
|
||||
padding: 0.3em 0.5em;
|
||||
}
|
||||
|
||||
.stop {
|
||||
&.ph,
|
||||
&.ph-pm,
|
||||
&.pm {
|
||||
background: $stopExchangeClr;
|
||||
}
|
||||
|
||||
background: $stopDefaultClr;
|
||||
}
|
||||
|
||||
.arrival,
|
||||
.departure {
|
||||
&.delayed {
|
||||
s {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
span {
|
||||
color: $delayedClr;
|
||||
}
|
||||
}
|
||||
|
||||
&.preponed {
|
||||
s {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
span {
|
||||
color: $preponedClr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<section class="daily-stats">
|
||||
<span :data-active="statsStatus">
|
||||
<b v-if="statsStatus == DataStatus.Loading">
|
||||
{{ $t('app.loading') }}
|
||||
</b>
|
||||
|
||||
<b v-else-if="stats.distanceSum == null">
|
||||
{{ $t('journal.daily-stats-info') }}
|
||||
</b>
|
||||
|
||||
<span class="stats-list" v-else>
|
||||
<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">
|
||||
•
|
||||
<i18n-t keypath="journal.timetable-stats-total">
|
||||
<template #count>
|
||||
<b class="text--primary">
|
||||
{{ stats.totalTimetables }}
|
||||
{{ $t('journal.timetable-count', stats.totalTimetables) }}
|
||||
</b>
|
||||
</template>
|
||||
|
||||
<template #distance>
|
||||
<b class="text--primary"> {{ stats.distanceSum?.toFixed(2) }} km</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<div v-if="stats.timetableId">
|
||||
•
|
||||
<i18n-t keypath="journal.timetable-stats-longest">
|
||||
<template #id>
|
||||
<router-link :to="`/journal/timetables?timetableId=${stats.timetableId}`">
|
||||
<b>{{ stats.timetableId }}</b>
|
||||
</router-link>
|
||||
</template>
|
||||
<template #author>
|
||||
<router-link :to="`/journal/dispatchers?dispatcherName=${stats.timetableAuthor}`">
|
||||
<b>{{ stats.timetableAuthor }}</b>
|
||||
</router-link>
|
||||
</template>
|
||||
<template #driver>
|
||||
<b class="text--primary">{{ stats.timetableDriver }}</b>
|
||||
</template>
|
||||
<template #distance>
|
||||
<b class="text--primary">{{ stats.timetableRouteDistance }} km</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<div v-if="firstPlaceDispatchers.length == 1">
|
||||
•
|
||||
<i18n-t keypath="journal.timetable-stats-most-active-dr">
|
||||
<template #dispatcher>
|
||||
<router-link :to="`/journal/dispatchers?dispatcherName=${firstPlaceDispatchers[0].name}`">
|
||||
<b>{{ firstPlaceDispatchers[0].name }}</b>
|
||||
</router-link>
|
||||
</template>
|
||||
<template #count>
|
||||
<b class="text--primary">
|
||||
{{ firstPlaceDispatchers[0].count }}
|
||||
{{ $t('journal.timetable-count', firstPlaceDispatchers[0].count) }}
|
||||
</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<div v-if="firstPlaceDispatchers.length > 1">
|
||||
•
|
||||
<i18n-t keypath="journal.timetable-stats-most-active-dr-many">
|
||||
<template #dispatchers>
|
||||
<span v-for="(disp, i) in firstPlaceDispatchers">
|
||||
<span v-if="i == firstPlaceDispatchers.length - 1"> {{ $t('general.and') }} </span>
|
||||
|
||||
<router-link :to="`/journal/dispatchers?dispatcherName=${disp.name}`">
|
||||
<b>{{ disp.name }}</b>
|
||||
</router-link>
|
||||
|
||||
<span v-if="i < firstPlaceDispatchers.length - 2">, </span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #count>
|
||||
<b class="text--primary">
|
||||
{{ firstPlaceDispatchers[0].count }}
|
||||
{{ $t('journal.timetable-count', firstPlaceDispatchers[0].count) }}
|
||||
</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<div v-if="stats.longestDuties.length > 0">
|
||||
•
|
||||
<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">
|
||||
•
|
||||
<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>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import axios from 'axios';
|
||||
import { defineComponent } from 'vue';
|
||||
import dateMixin from '../../mixins/dateMixin';
|
||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
||||
import { ITimetablesDailyStats, ITimetablesDailyStatsResponse } from '../../scripts/interfaces/api/StatsAPIData';
|
||||
import { URLs } from '../../scripts/utils/apiURLs';
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [dateMixin],
|
||||
emits: ['toggleStatsOpen'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
DataStatus,
|
||||
statsStatus: DataStatus.Loading,
|
||||
intervalId: -1,
|
||||
|
||||
stats: {
|
||||
totalTimetables: 0,
|
||||
distanceSum: 0,
|
||||
distanceAvg: 0,
|
||||
timetableAuthor: '',
|
||||
timetableDriver: '',
|
||||
timetableId: 0,
|
||||
timetableRouteDistance: 0,
|
||||
longestDuties: [],
|
||||
mostActiveDrivers: [],
|
||||
mostActiveDispatchers: [],
|
||||
} as ITimetablesDailyStats,
|
||||
};
|
||||
},
|
||||
|
||||
activated() {
|
||||
this.startFetchingDailyStats();
|
||||
this.$emit('toggleStatsOpen', true);
|
||||
},
|
||||
|
||||
deactivated() {
|
||||
this.stopFetchingDailyStats();
|
||||
},
|
||||
|
||||
computed: {
|
||||
firstPlaceDispatchers() {
|
||||
if (this.stats.mostActiveDispatchers.length == 0) return [];
|
||||
const maxCount = this.stats.mostActiveDispatchers[0].count;
|
||||
|
||||
return this.stats.mostActiveDispatchers.filter((disp) => disp.count === maxCount);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchDailyTimetableStats() {
|
||||
try {
|
||||
const res: 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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/responsive.scss';
|
||||
|
||||
.daily-stats {
|
||||
text-align: left;
|
||||
}
|
||||
.daily-stats > span[data-active='0'] {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.stats-list a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.daily-stats {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
h3 {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
<template>
|
||||
<div class="journal-stats" v-if="store.driverStatsData?._sum.routeDistance != null">
|
||||
<h1>
|
||||
{{ $t('journal.stats-title') }} <span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
|
||||
</h1>
|
||||
|
||||
<div class="info-stats">
|
||||
<span class="stat-badge">
|
||||
<span>{{ $t('journal.stats-timetables') }}</span>
|
||||
<span>{{ store.driverStatsData._count.fulfilled }} / {{ store.driverStatsData._count._all }}</span>
|
||||
</span>
|
||||
|
||||
<span class="stat-badge">
|
||||
<span>{{ $t('journal.stats-longest-timetable') }}</span>
|
||||
<span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span>
|
||||
</span>
|
||||
|
||||
<span class="stat-badge">
|
||||
<span>{{ $t('journal.stats-avg-timetable') }}</span>
|
||||
<span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
|
||||
</span>
|
||||
|
||||
<span class="stat-badge">
|
||||
<span>{{ $t('journal.stats-distance') }}</span>
|
||||
<span>
|
||||
{{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
|
||||
{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="stat-badge">
|
||||
<span>{{ $t('journal.stats-stations') }}</span>
|
||||
<span>
|
||||
{{ store.driverStatsData._sum.confirmedStopsCount }} /
|
||||
{{ store.driverStatsData._sum.allStopsCount }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import axios from 'axios';
|
||||
import { computed, defineComponent, ref } from 'vue';
|
||||
import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData';
|
||||
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
|
||||
import { URLs } from '../../scripts/utils/apiURLs';
|
||||
import { useStore } from '../../store/store';
|
||||
|
||||
export default defineComponent({
|
||||
emits: ['closeCard'],
|
||||
|
||||
setup() {
|
||||
const store = useStore();
|
||||
return {
|
||||
store,
|
||||
driverStatsName: computed(() => store.driverStatsName),
|
||||
};
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
test: Math.random(),
|
||||
lastDispatcherName: '',
|
||||
|
||||
lastTimetables: [] as TimetableHistory[],
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
driverStatsName(value: string) {
|
||||
this.fetchDispatcherStats();
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchDispatcherStats() {
|
||||
this.store.driverStatsData = undefined;
|
||||
|
||||
if (!this.store.driverStatsName) return;
|
||||
|
||||
const statsData: DriverStatsAPIData = await (
|
||||
await axios.get(`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`)
|
||||
).data;
|
||||
|
||||
this.store.driverStatsData = statsData;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/JournalStats.scss';
|
||||
</style>
|
||||
@@ -1,62 +1,155 @@
|
||||
<template>
|
||||
<ul class="journal-list">
|
||||
<!-- <transition-group name="journal-list-anim"> -->
|
||||
<li v-for="item in computedDispatcherHistory" :class="{ sticky: typeof item == 'string' }">
|
||||
<div v-if="typeof item == 'string'" class="journal_day">
|
||||
{{ item }}
|
||||
<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 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>
|
||||
</transition>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="journal_item"
|
||||
:class="{ online: item.isOnline }"
|
||||
@click="navigateToScenery(item.stationName, item.isOnline)"
|
||||
@keydown.enter="navigateToScenery(item.stationName, item.isOnline)"
|
||||
tabindex="0"
|
||||
>
|
||||
<span>
|
||||
<b class="text--primary">{{ item.dispatcherName }}</b> • <b>{{ item.stationName }}</b>
|
||||
<span class="text--grayed"> #{{ item.stationHash }} </span>
|
||||
<span class="region-badge" :class="item.region">PL1</span>
|
||||
</span>
|
||||
<div class="journal_warning" v-if="scrollNoMoreData">
|
||||
{{ $t('journal.no-further-data') }}
|
||||
</div>
|
||||
|
||||
<span>
|
||||
<span :data-status="item.isOnline"> {{ item.isOnline ? $t('journal.online-since') : 'OFFLINE' }} </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">
|
||||
>
|
||||
{{ new Date(item.timestampTo).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
|
||||
({{ $t('journal.duty-lasted') }} {{ calculateDuration(item.currentDuration!) }})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<!-- </transition-group> -->
|
||||
</ul>
|
||||
<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 dateMixin from '../../mixins/dateMixin';
|
||||
import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
|
||||
import styleMixin from '../../mixins/styleMixin';
|
||||
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({
|
||||
components: { Loading, AddDataButton },
|
||||
|
||||
mixins: [dateMixin, styleMixin, imageMixin],
|
||||
|
||||
props: {
|
||||
dispatcherHistory: {
|
||||
type: Array as PropType<DispatcherHistory[]>,
|
||||
required: true,
|
||||
},
|
||||
scrollNoMoreData: {
|
||||
type: Boolean,
|
||||
},
|
||||
scrollDataLoaded: {
|
||||
type: Boolean,
|
||||
},
|
||||
addHistoryData: {
|
||||
type: Function as PropType<() => void>,
|
||||
},
|
||||
dataStatus: {
|
||||
type: Number as PropType<DataStatus>,
|
||||
},
|
||||
},
|
||||
|
||||
mixins: [dateMixin],
|
||||
data() {
|
||||
return {
|
||||
DataStatus,
|
||||
store: useStore(),
|
||||
regions,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
computedDispatcherHistory() {
|
||||
console.log(this.dispatcherHistory.length);
|
||||
|
||||
return this.dispatcherHistory.reduce((acc, historyItem, i) => {
|
||||
if (this.isAnotherDay(i - 1, i)) acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL'));
|
||||
acc.push(historyItem);
|
||||
@@ -86,71 +179,63 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/animations.scss';
|
||||
@import '../../styles/responsive.scss';
|
||||
@import '../../styles/badge.scss';
|
||||
@import '../../styles/variables.scss';
|
||||
@import '../../styles/JournalSection.scss';
|
||||
|
||||
.region-badge {
|
||||
padding: 0.1em 0.5em;
|
||||
border-radius: 0.5em;
|
||||
font-weight: bold;
|
||||
table.scenery-history-table {
|
||||
--_bg-table: #111;
|
||||
--_bg-head: #101010;
|
||||
--_bg-row: #2f2f2f;
|
||||
|
||||
&.eu {
|
||||
background-color: forestgreen;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
margin-bottom: 1em;
|
||||
|
||||
thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--_bg-head);
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
tr {
|
||||
background-color: var(--_bg-row);
|
||||
border-bottom: 2px solid black;
|
||||
|
||||
&:last-child {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.75em;
|
||||
|
||||
.level-badge {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
li.sticky {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.journal_item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
flex-wrap: wrap;
|
||||
|
||||
padding: 0.75em;
|
||||
|
||||
&.online {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
span[data-status='true'] {
|
||||
.text {
|
||||
&--online {
|
||||
color: springgreen;
|
||||
}
|
||||
|
||||
span[data-status='false'] {
|
||||
color: salmon;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
@include smallScreen() {
|
||||
.journal_item {
|
||||
flex-direction: column;
|
||||
|
||||
span {
|
||||
margin-top: 0.25em;
|
||||
text-align: center;
|
||||
}
|
||||
&--offline {
|
||||
color: #ddd;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="journal-stats">
|
||||
<span v-if="store.driverStatsData">
|
||||
<h3>
|
||||
{{ $t('journal.stats-title') }} <span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
|
||||
</h3>
|
||||
|
||||
<div class="info-stats">
|
||||
<span class="stat-badge">
|
||||
<span>{{ $t('journal.stats-timetables') }}</span>
|
||||
<span>{{ store.driverStatsData._count.fulfilled }} / {{ store.driverStatsData._count._all }}</span>
|
||||
</span>
|
||||
|
||||
<span class="stat-badge">
|
||||
<span>{{ $t('journal.stats-longest-timetable') }}</span>
|
||||
<span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span>
|
||||
</span>
|
||||
|
||||
<span class="stat-badge">
|
||||
<span>{{ $t('journal.stats-avg-timetable') }}</span>
|
||||
<span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
|
||||
</span>
|
||||
|
||||
<span class="stat-badge">
|
||||
<span>{{ $t('journal.stats-distance') }}</span>
|
||||
<span>
|
||||
{{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
|
||||
{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="stat-badge">
|
||||
<span>{{ $t('journal.stats-stations') }}</span>
|
||||
<span>
|
||||
{{ store.driverStatsData._sum.confirmedStopsCount }} /
|
||||
{{ store.driverStatsData._sum.allStopsCount }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<b v-else-if="store.driverStatsStatus == DataStatus.Loading">{{ $t('journal.stats-loading') }}</b>
|
||||
<b v-else-if="store.driverStatsStatus == DataStatus.Error">
|
||||
{{ $t('journal.stats-error ') }}
|
||||
</b>
|
||||
<b v-else>{{ $t('journal.driver-stats-info') }}</b>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
||||
import { useStore } from '../../store/store';
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
store: useStore(),
|
||||
DataStatus,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/JournalStats.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<section class="journal-header">
|
||||
<div class="journal-type-options">
|
||||
<router-link class="router-link" active-class="route-active" to="/journal/timetables" exact>
|
||||
{{ $t('journal.section-timetables') }}
|
||||
</router-link>
|
||||
•
|
||||
<router-link class="router-link" active-class="route-active" to="/journal/dispatchers">
|
||||
{{ $t('journal.section-dispatchers') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.journal-type-options {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
background-color: #2c2c2c;
|
||||
max-width: 18em;
|
||||
|
||||
font-size: 1.2em;
|
||||
margin: 0 auto;
|
||||
|
||||
border-radius: 0 0 0.5em 0.5em;
|
||||
padding: 0.1em 0;
|
||||
}
|
||||
|
||||
.journal-section > section {
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.router-link.active {
|
||||
color: gold;
|
||||
}
|
||||
</style>
|
||||
@@ -2,10 +2,26 @@
|
||||
<div class="filters-options" @keydown.esc="showOptions = false">
|
||||
<div class="bg" v-if="showOptions" @click="showOptions = false"></div>
|
||||
|
||||
<button class="btn--filled btn--image" @click="showOptions = !showOptions" ref="button">
|
||||
<img :src="getIcon('filter2')" alt="Open filters" />
|
||||
{{ $t('options.filters') }} [F]
|
||||
</button>
|
||||
<div class="actions-bar">
|
||||
<button class="filter-button btn--filled btn--image" @click="showOptions = !showOptions" ref="button">
|
||||
<img :src="getIcon('filter2')" alt="Open filters" />
|
||||
{{ $t('options.filters') }} [F]
|
||||
<span class="active-indicator" v-if="currentOptionsActive"></span>
|
||||
</button>
|
||||
|
||||
<button class="filter-button btn--filled btn--image" @click="refreshData">
|
||||
<img :src="getIcon('refresh')" alt="Refresh data" />
|
||||
{{ $t('general.refresh') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<datalist id="search-driver">
|
||||
<option v-for="sugg in driverSuggestions" :value="sugg"></option>
|
||||
</datalist>
|
||||
|
||||
<datalist id="search-dispatcher">
|
||||
<option v-for="sugg in dispatcherSuggestions" :value="sugg"></option>
|
||||
</datalist>
|
||||
|
||||
<transition name="options-anim">
|
||||
<div class="options_wrapper" v-if="showOptions">
|
||||
@@ -13,43 +29,26 @@
|
||||
<h1 class="option-title">{{ $t('options.search-title') }}</h1>
|
||||
<div class="search_content">
|
||||
<div class="search" v-for="(_, propName) in searchersValues" :key="propName">
|
||||
<label v-if="propName == 'search-date'" for="date">{{ $t('options.search-date') }}</label>
|
||||
<label v-if="propName == 'search-date'" for="date">{{ $t(`options.search-${optionsType}-date`) }}</label>
|
||||
|
||||
<div class="search-box">
|
||||
<input
|
||||
v-if="propName == 'search-date'"
|
||||
class="search-input"
|
||||
id="date"
|
||||
type="date"
|
||||
min="2022-02-01"
|
||||
@keydown.enter="onSearchConfirm"
|
||||
v-model="searchersValues[propName]"
|
||||
/>
|
||||
|
||||
<input
|
||||
v-else
|
||||
class="search-input"
|
||||
@keydown.enter="onSearchConfirm"
|
||||
@focus="preventKeyDown = true"
|
||||
@blur="preventKeyDown = false"
|
||||
:placeholder="$t(`options.${propName}`)"
|
||||
v-model="searchersValues[propName]"
|
||||
:type="propName == 'search-date' ? 'date' : 'text'"
|
||||
:min="propName == 'search-date' ? '2022-02-01' : undefined"
|
||||
:list="propName.toString()"
|
||||
/>
|
||||
|
||||
<button class="search-exit">
|
||||
<button class="search-exit" v-if="propName != 'search-date'">
|
||||
<img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear(propName)" />
|
||||
</button>
|
||||
</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>
|
||||
|
||||
<h1 class="option-title">{{ $t('options.sort-title') }}</h1>
|
||||
@@ -66,15 +65,31 @@
|
||||
</div>
|
||||
|
||||
<h1 class="option-title" v-if="filters.length != 0">{{ $t('options.filter-title') }}</h1>
|
||||
<div class="options_filters">
|
||||
<button
|
||||
v-for="filter in filters"
|
||||
class="filter-option btn--option"
|
||||
:class="{ checked: journalFilterActive.id === filter.id }"
|
||||
:id="filter.id"
|
||||
@click="onFilterChange(filter)"
|
||||
>
|
||||
{{ $t(`options.filter-${filter.id}`) }}
|
||||
|
||||
<div class="options_filter-sections" v-if="filters.length != 0 && filterList">
|
||||
<section class="filter-section" v-for="section in JournalFilterSection">
|
||||
<p>{{ $t(`options.filter-section-${section}`) }}</p>
|
||||
|
||||
<div class="options_filters">
|
||||
<button
|
||||
v-for="filter in filterList.filter((f) => f.filterSection == section)"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,17 +99,22 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, inject, Prop, PropType } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { defineComponent, inject, PropType } from 'vue';
|
||||
import imageMixin from '../../mixins/imageMixin';
|
||||
import keyMixin from '../../mixins/keyMixin';
|
||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
||||
import { JournalTimetableFilter } from '../../types/Journal/JournalTimetablesTypes';
|
||||
import { DriverStatsAPIData } from '../../scripts/interfaces/api/DriverStatsAPIData';
|
||||
import { URLs } from '../../scripts/utils/apiURLs';
|
||||
import { useStore } from '../../store/store';
|
||||
import ActionButton from '../Global/ActionButton.vue';
|
||||
import SelectBox from '../Global/SelectBox.vue';
|
||||
import { JournalFilterSection } from '../../scripts/enums/JournalFilterType';
|
||||
import { JournalFilter } from '../../scripts/types/JournalTimetablesTypes';
|
||||
|
||||
export default defineComponent({
|
||||
components: { SelectBox, ActionButton },
|
||||
emits: ['onSearchConfirm', 'onOptionsReset'],
|
||||
emits: ['onSearchConfirm', 'onOptionsReset', 'onRefreshData'],
|
||||
mixins: [imageMixin, keyMixin],
|
||||
|
||||
props: {
|
||||
@@ -104,7 +124,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
filters: {
|
||||
type: Array as PropType<JournalTimetableFilter[]>,
|
||||
type: Array as PropType<JournalFilter[]>,
|
||||
default: [],
|
||||
},
|
||||
|
||||
@@ -112,11 +132,29 @@ export default defineComponent({
|
||||
type: Number as PropType<DataStatus>,
|
||||
default: DataStatus.Initialized,
|
||||
},
|
||||
|
||||
currentOptionsActive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
optionsType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
showOptions: false,
|
||||
JournalFilterSection,
|
||||
|
||||
driverSuggestions: [] as string[],
|
||||
dispatcherSuggestions: [] as string[],
|
||||
|
||||
searchTimeout: 0,
|
||||
store: useStore(),
|
||||
|
||||
DataStatus,
|
||||
};
|
||||
},
|
||||
@@ -125,11 +163,16 @@ export default defineComponent({
|
||||
return {
|
||||
searchersValues: inject('searchersValues') as { [key: string]: string },
|
||||
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,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
driverStatsName() {
|
||||
return this.store.driverStatsName;
|
||||
},
|
||||
|
||||
translatedSorterOptions() {
|
||||
return this.$props.sorterOptionIds.map((id) => ({
|
||||
id,
|
||||
@@ -138,7 +181,76 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
async driverStatsName(value: string) {
|
||||
await this.fetchDriverStats();
|
||||
|
||||
// if (value) this.store.currentStatsTab = 'driver';
|
||||
},
|
||||
|
||||
async 'searchersValues.search-driver'(value: string | undefined) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
|
||||
if (!value || value == '') return;
|
||||
if (value.length < 3) return;
|
||||
|
||||
this.startSearchTimeout('driver', value);
|
||||
},
|
||||
|
||||
async 'searchersValues.search-dispatcher'(value: string | undefined) {
|
||||
if (!value || value == '') return;
|
||||
if (value.length < 3) return;
|
||||
|
||||
this.startSearchTimeout('dispatcher', value);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchDriverStats() {
|
||||
this.store.driverStatsData = undefined;
|
||||
|
||||
if (!this.store.driverStatsName) {
|
||||
this.store.driverStatsStatus = DataStatus.Initialized;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.store.driverStatsStatus = DataStatus.Loading;
|
||||
|
||||
const statsData: DriverStatsAPIData = await (
|
||||
await axios.get(`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}`)
|
||||
).data;
|
||||
|
||||
this.store.driverStatsData = statsData;
|
||||
this.store.driverStatsStatus = DataStatus.Loaded;
|
||||
} catch (error) {
|
||||
this.store.driverStatsStatus = DataStatus.Error;
|
||||
console.error('Ups! Wystąpił błąd przy próbie pobrania statystyk maszynisty! :/');
|
||||
}
|
||||
},
|
||||
|
||||
refreshData() {
|
||||
this.$emit('onRefreshData');
|
||||
},
|
||||
|
||||
startSearchTimeout(type: 'driver' | 'dispatcher', value: string) {
|
||||
if (this[`${type}Suggestions`].includes(value)) return;
|
||||
|
||||
window.clearTimeout(this.searchTimeout);
|
||||
|
||||
this.searchTimeout = setTimeout(async () => {
|
||||
try {
|
||||
const suggestions: string[] = await (
|
||||
await axios.get(`${URLs.stacjownikAPI}/api/get${type}Suggestions?name=${value}`)
|
||||
).data;
|
||||
|
||||
this[`${type}Suggestions`] = suggestions;
|
||||
} catch (error) {
|
||||
this[`${type}Suggestions`] = [];
|
||||
}
|
||||
}, 450);
|
||||
},
|
||||
|
||||
// Override keyMixin function
|
||||
onKeyDownFunction() {
|
||||
this.showOptions = !this.showOptions;
|
||||
@@ -148,18 +260,17 @@ export default defineComponent({
|
||||
});
|
||||
},
|
||||
|
||||
focusEnd() {
|
||||
console.log('focus end');
|
||||
},
|
||||
|
||||
onSorterChange(item: { id: string | number; value: string }) {
|
||||
this.sorterActive.id = item.id;
|
||||
this.sorterActive.dir = -1;
|
||||
this.$emit('onSearchConfirm');
|
||||
},
|
||||
|
||||
onFilterChange(filter: JournalTimetableFilter) {
|
||||
this.journalFilterActive = filter;
|
||||
onFilterChange(filter: JournalFilter) {
|
||||
// this.journalFilterActive = filter;
|
||||
this.filterList?.filter((f) => f.filterSection === filter.filterSection).forEach((f) => (f.isActive = false));
|
||||
filter.isActive = true;
|
||||
|
||||
this.$emit('onSearchConfirm');
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div class="journal-stats" v-if="!store.isOffline">
|
||||
<div class="tabs">
|
||||
<button
|
||||
v-for="tab in data.tabs"
|
||||
class="btn--filled"
|
||||
:data-selected="tab.name == store.currentStatsTab && areStatsOpen"
|
||||
:data-inactive="tab.inactive"
|
||||
:data-disabled="tab.inactive"
|
||||
:disabled="tab.inactive"
|
||||
@click="onTabButtonClick(tab.name)"
|
||||
>
|
||||
{{ $t(tab.titlePath) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="stats-tab" v-show="areStatsOpen">
|
||||
<keep-alive>
|
||||
<JournalDailyStats v-if="store.currentStatsTab == 'daily'" @toggleStatsOpen="toggleStatsOpen" />
|
||||
<JournalDriverStats v-else-if="store.currentStatsTab == 'driver'" />
|
||||
</keep-alive>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, KeepAlive, onMounted, reactive, Ref, ref, watch } from 'vue';
|
||||
import { useStore } from '../../store/store';
|
||||
import JournalDailyStats from './DailyStats.vue';
|
||||
import JournalDriverStats from './JournalDriverStats.vue';
|
||||
import StorageManager from '../../scripts/managers/storageManager';
|
||||
|
||||
// Types
|
||||
type TStatTab = 'daily' | 'driver';
|
||||
|
||||
// Variables
|
||||
const store = useStore();
|
||||
|
||||
const lastDailyStatsOpen = ref(false);
|
||||
const areStatsOpen = ref(false);
|
||||
const lastClickedTab: Ref<'daily' | 'driver' | null> = ref(null);
|
||||
|
||||
let data = reactive({
|
||||
tabs: [
|
||||
{
|
||||
name: 'daily',
|
||||
titlePath: 'journal.daily-stats-title',
|
||||
},
|
||||
{
|
||||
name: 'driver',
|
||||
titlePath: 'journal.driver-stats-title',
|
||||
// inactive: true,
|
||||
},
|
||||
] as { name: TStatTab; titlePath: string; inactive?: boolean }[],
|
||||
});
|
||||
|
||||
// Methods
|
||||
function onTabButtonClick(tab: TStatTab) {
|
||||
if (lastClickedTab.value == tab || !areStatsOpen.value) areStatsOpen.value = !areStatsOpen.value;
|
||||
|
||||
if (tab == 'daily') {
|
||||
StorageManager.setBooleanValue('dailyStatsOpen', areStatsOpen.value);
|
||||
lastDailyStatsOpen.value = areStatsOpen.value;
|
||||
}
|
||||
|
||||
store.currentStatsTab = tab;
|
||||
lastClickedTab.value = tab;
|
||||
|
||||
if (areStatsOpen.value == false) store.currentStatsTab = null;
|
||||
}
|
||||
|
||||
function toggleStatsOpen(open: boolean) {
|
||||
areStatsOpen.value = open;
|
||||
}
|
||||
|
||||
watch(
|
||||
computed(() => store.driverStatsData),
|
||||
(statsData) => {
|
||||
store.currentStatsTab = statsData ? 'driver' : lastClickedTab.value;
|
||||
areStatsOpen.value = statsData ? true : lastClickedTab.value !== null;
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (StorageManager.getBooleanValue('dailyStatsOpen')) {
|
||||
areStatsOpen.value = true;
|
||||
store.currentStatsTab = 'daily';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/JournalStats.scss';
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
.tabs {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
button {
|
||||
font-weight: bold;
|
||||
padding: 0.5em 0.75em;
|
||||
|
||||
&[data-inactive='true'] {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
&[data-selected='true'] {
|
||||
color: $accentCol;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
<template>
|
||||
<section class="journal-timetables">
|
||||
|
||||
<div class="journal_wrapper">
|
||||
<JournalOptions
|
||||
@on-search-confirm="searchHistory"
|
||||
@on-options-reset="resetOptions"
|
||||
:sorter-option-ids="['timetableId', 'beginDate', 'distance', 'total-stops']"
|
||||
:filters="journalTimetableFilters"
|
||||
:data-status="dataStatus"
|
||||
/>
|
||||
|
||||
<DriverStats />
|
||||
<!-- <button @click="statsCardOpen = true">Stats</button> -->
|
||||
|
||||
<div class="list_wrapper" @scroll="handleScroll">
|
||||
<!-- <transition name="warning" mode="out-in"> -->
|
||||
<!-- <div :key="dataStatus"> -->
|
||||
<Loading v-if="dataStatus == DataStatus.Initialized || 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>
|
||||
<JournalTimetablesList :timetableHistory="timetableHistory" />
|
||||
|
||||
<button
|
||||
class="btn btn--option btn--load-data"
|
||||
v-if="!scrollNoMoreData && scrollDataLoaded && timetableHistory.length >= 15"
|
||||
@click="addHistoryData"
|
||||
>
|
||||
{{ $t('journal.load-data') }}
|
||||
</button>
|
||||
</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>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, provide, reactive, Ref, ref } from 'vue';
|
||||
import axios from 'axios';
|
||||
|
||||
import DriverStats from './DriverStats.vue';
|
||||
import Loading from '../Global/Loading.vue';
|
||||
import { JournalTimetableFilter, JournalTimetableSorter } from '../../types/Journal/JournalTimetablesTypes';
|
||||
import dateMixin from '../../mixins/dateMixin';
|
||||
import routerMixin from '../../mixins/routerMixin';
|
||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
||||
import { JournalFilterType } from '../../scripts/enums/JournalFilterType';
|
||||
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
|
||||
import { URLs } from '../../scripts/utils/apiURLs';
|
||||
import { useStore } from '../../store/store';
|
||||
import JournalOptions from './JournalOptions.vue';
|
||||
import { JorunalTimetableSearchType } from '../../types/Journal/JournalTimetablesTypes';
|
||||
import modalTrainMixin from '../../mixins/modalTrainMixin';
|
||||
import imageMixin from '../../mixins/imageMixin';
|
||||
import JournalTimetablesList from './JournalTimetablesList.vue';
|
||||
import { journalTimetableFilters } from '../../constants/Journal/JournalTimetablesConsts';
|
||||
|
||||
const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`;
|
||||
|
||||
export default defineComponent({
|
||||
components: { DriverStats, Loading, JournalOptions, JournalTimetablesList },
|
||||
mixins: [dateMixin, routerMixin, modalTrainMixin, imageMixin],
|
||||
|
||||
name: 'JournalTimetables',
|
||||
|
||||
props: {
|
||||
timetableId: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
currentQuery: '',
|
||||
scrollDataLoaded: true,
|
||||
scrollNoMoreData: false,
|
||||
|
||||
showReturnButton: false,
|
||||
statsCardOpen: false,
|
||||
|
||||
timetableHistory: [] as TimetableHistory[],
|
||||
journalTimetableFilters,
|
||||
|
||||
dataStatus: DataStatus.Initialized,
|
||||
dataErrorMessage: '',
|
||||
|
||||
DataStatus,
|
||||
}),
|
||||
|
||||
setup() {
|
||||
const sorterActive: JournalTimetableSorter = reactive({ id: 'timetableId', dir: 1 });
|
||||
const journalFilterActive = ref(journalTimetableFilters[0]);
|
||||
|
||||
const searchersValues = reactive({
|
||||
'search-train': '',
|
||||
'search-driver': '',
|
||||
'search-author': '',
|
||||
'search-date': '',
|
||||
} as JorunalTimetableSearchType);
|
||||
|
||||
const countFromIndex = ref(0);
|
||||
const countLimit = 15;
|
||||
|
||||
provide('searchersValues', searchersValues);
|
||||
provide('sorterActive', sorterActive);
|
||||
provide('journalFilterActive', journalFilterActive);
|
||||
|
||||
const scrollElement: Ref<HTMLElement | null> = ref(null);
|
||||
|
||||
return {
|
||||
sorterActive,
|
||||
journalFilterActive,
|
||||
searchersValues,
|
||||
|
||||
countFromIndex,
|
||||
countLimit,
|
||||
|
||||
scrollElement,
|
||||
store: useStore(),
|
||||
};
|
||||
},
|
||||
|
||||
activated() {
|
||||
if (this.timetableId) {
|
||||
this.searchersValues['search-train'] = `#${this.timetableId}`;
|
||||
this.searchHistory();
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (!this.timetableId) this.searchHistory();
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleScroll(e: Event) {
|
||||
const listElement = e.target as HTMLElement;
|
||||
const scrollTop = listElement.scrollTop;
|
||||
const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
|
||||
|
||||
if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
|
||||
|
||||
if (scrollTop > elementHeight * 0.85) this.addHistoryData();
|
||||
},
|
||||
|
||||
resetOptions() {
|
||||
this.searchersValues['search-date'] = '';
|
||||
this.searchersValues['search-driver'] = '';
|
||||
this.searchersValues['search-train'] = '';
|
||||
this.searchersValues['search-author'] = '';
|
||||
|
||||
this.journalFilterActive = this.journalTimetableFilters[0];
|
||||
this.sorterActive.id = 'timetableId';
|
||||
|
||||
this.searchHistory();
|
||||
},
|
||||
|
||||
searchHistory() {
|
||||
this.fetchHistoryData({
|
||||
searchers: this.searchersValues,
|
||||
filter: this.journalFilterActive,
|
||||
});
|
||||
|
||||
this.scrollNoMoreData = false;
|
||||
this.scrollDataLoaded = true;
|
||||
},
|
||||
|
||||
async addHistoryData() {
|
||||
this.scrollDataLoaded = false;
|
||||
|
||||
const countFrom = this.timetableHistory.length;
|
||||
|
||||
const responseData: TimetableHistory[] = await (
|
||||
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}&countFrom=${countFrom}`)
|
||||
).data;
|
||||
|
||||
if (!responseData) return;
|
||||
|
||||
if (responseData.length == 0) {
|
||||
this.scrollNoMoreData = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.timetableHistory.push(...responseData);
|
||||
this.scrollDataLoaded = true;
|
||||
},
|
||||
|
||||
async fetchHistoryData(
|
||||
props: {
|
||||
searchers?: JorunalTimetableSearchType;
|
||||
filter?: JournalTimetableFilter;
|
||||
} = {}
|
||||
) {
|
||||
this.dataStatus = DataStatus.Loading;
|
||||
|
||||
const queries: string[] = [];
|
||||
|
||||
const driverName = props.searchers?.['search-driver'].trim();
|
||||
const trainNo = props.searchers?.['search-train'].trim();
|
||||
const authorName = props.searchers?.['search-author'].trim();
|
||||
|
||||
const dateString = props.searchers?.['search-date'].trim();
|
||||
const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
|
||||
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
|
||||
|
||||
if (driverName) queries.push(`driverName=${driverName}`);
|
||||
if (trainNo)
|
||||
queries.push(trainNo.startsWith('#') ? `timetableId=${trainNo.replace('#', '')}` : `trainNo=${trainNo}`);
|
||||
if (authorName) queries.push(`authorName=${authorName}`);
|
||||
if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
|
||||
|
||||
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
|
||||
if (this.sorterActive.id == 'distance') queries.push('sortBy=routeDistance');
|
||||
else if (this.sorterActive.id == 'total-stops') queries.push('sortBy=allStopsCount');
|
||||
else if (this.sorterActive.id == 'beginDate') queries.push('sortBy=beginDate');
|
||||
else queries.push('sortBy=timetableId');
|
||||
|
||||
queries.push('countLimit=15');
|
||||
|
||||
switch (props.filter?.id) {
|
||||
case JournalFilterType.abandoned:
|
||||
queries.push('fulfilled=0', 'terminated=1');
|
||||
break;
|
||||
|
||||
case JournalFilterType.active:
|
||||
queries.push('terminated=0');
|
||||
break;
|
||||
|
||||
case JournalFilterType.fulfilled:
|
||||
queries.push('fulfilled=1');
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.currentQuery = queries.join('&');
|
||||
|
||||
try {
|
||||
const responseData: TimetableHistory[] = await (
|
||||
await axios.get(`${TIMETABLES_API_URL}?${this.currentQuery}`)
|
||||
).data;
|
||||
|
||||
if (!responseData) {
|
||||
this.dataStatus = DataStatus.Error;
|
||||
this.dataErrorMessage = 'Brak danych!';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!responseData) return;
|
||||
|
||||
// Response data exists
|
||||
this.timetableHistory = responseData;
|
||||
|
||||
// Stats display
|
||||
this.store.driverStatsName =
|
||||
this.timetableHistory.length > 0 && this.searchersValues['search-driver'].trim()
|
||||
? this.timetableHistory[0].driverName
|
||||
: '';
|
||||
|
||||
this.dataStatus = DataStatus.Loaded;
|
||||
} catch (error) {
|
||||
this.dataStatus = DataStatus.Error;
|
||||
this.dataErrorMessage = 'Ups! Coś poszło nie tak!';
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/JournalSection.scss';
|
||||
</style>
|
||||
@@ -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> {{ timetable.trainNo }}</strong>
|
||||
</span>
|
||||
•
|
||||
<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]">(</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]">)</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">
|
||||
>
|
||||
<span v-if="!showExtraInfo && i == 1 && timetableStops.length > 2">
|
||||
... (+{{ timetableStops.length - 2 }}) >
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="stop-name">{{ stop.stopName }}</span>
|
||||
<span v-html="stop.html"></span>
|
||||
</span>
|
||||
</div>
|
||||
</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,314 +0,0 @@
|
||||
<template>
|
||||
<ul class="journal-list">
|
||||
<li
|
||||
v-for="{ timetable, sceneryList, ...item } in computedTimetableHistory"
|
||||
class="journal_item"
|
||||
:key="timetable.timetableId"
|
||||
>
|
||||
<div class="journal_item-info">
|
||||
<div class="info-top">
|
||||
<span
|
||||
tabindex="0"
|
||||
@click="showTimetable(timetable)"
|
||||
@keydown.enter="showTimetable(timetable)"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<b class="text--primary">{{ timetable.trainCategoryCode }} </b>
|
||||
<b>{{ timetable.trainNo }}</b>
|
||||
| <span>{{ timetable.driverName }}</span> |
|
||||
<span class="text--grayed">#{{ timetable.timetableId }}</span>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<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" :key="scenery.name" :class="{ confirmed: scenery.confirmed }">
|
||||
<span v-if="i > 0"> ></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" 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>
|
||||
•
|
||||
<span>
|
||||
<b>{{ $t('journal.station-count') }}</b>
|
||||
{{ timetable.confirmedStopsCount }} /
|
||||
{{ timetable.allStopsCount }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Nick dyżurnego -->
|
||||
<div v-if="timetable.authorName">
|
||||
<b class="text--grayed">{{ $t('journal.dispatcher-name') }} </b>
|
||||
<router-link class="dispatcher-link" :to="`/journal/dispatchers?dispatcherName=${timetable.authorName}`">
|
||||
<b>{{ timetable.authorName }}</b>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="timetable.stockString"
|
||||
class="btn--option btn--show"
|
||||
@click="item.showStock.value = !item.showStock.value"
|
||||
>
|
||||
{{ $t('journal.stock-info') }}
|
||||
<img :src="getIcon(`arrow-${item.showStock.value ? 'asc' : 'desc'}`)" alt="Arrow" />
|
||||
</button>
|
||||
|
||||
<div class="info-extended" v-if="timetable.stockString && item.showStock.value">
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<span class="badge info-badge">
|
||||
<span>{{ $t('journal.stock-max-speed') }}</span>
|
||||
<span>{{ timetable.maxSpeed }}km/h</span>
|
||||
</span>
|
||||
|
||||
<span class="badge info-badge">
|
||||
<span>{{ $t('journal.stock-length') }}</span>
|
||||
<span>{{ timetable.stockLength }}m</span>
|
||||
</span>
|
||||
|
||||
<span class="badge info-badge">
|
||||
<span>{{ $t('journal.stock-mass') }}</span>
|
||||
<span>{{ Math.floor(timetable.stockMass! / 1000) }}t</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul class="stock-list">
|
||||
<li v-for="(car, i) in timetable.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>
|
||||
</ul>
|
||||
</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 { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
timetableHistory: {
|
||||
type: Array as PropType<TimetableHistory[]>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
mixins: [dateMixin, imageMixin, modalTrainMixin],
|
||||
|
||||
computed: {
|
||||
computedTimetableHistory() {
|
||||
return this.timetableHistory.map((timetable) => ({
|
||||
timetable,
|
||||
sceneryList: this.getSceneryList(timetable),
|
||||
showStock: ref(false),
|
||||
}));
|
||||
},
|
||||
},
|
||||
|
||||
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>)`;
|
||||
|
||||
const abandonedDateHTML = ` (porz. ${this.localeTime(
|
||||
timetable.fulfilled ? timetable.scheduledEndDate : timetable.endDate,
|
||||
this.$i18n.locale
|
||||
)})`;
|
||||
|
||||
return { name, confirmed: i < timetable.confirmedStopsCount, beginDateHTML, endDateHTML, abandonedDateHTML };
|
||||
});
|
||||
},
|
||||
|
||||
showTimetable(timetable: TimetableHistory) {
|
||||
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/variables.scss';
|
||||
@import '../../styles/responsive.scss';
|
||||
@import '../../styles/badge.scss';
|
||||
@import '../../styles/JournalSection.scss';
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
&-top {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&-route {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
&-extended {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.info-badge {
|
||||
span:last-child {
|
||||
color: black;
|
||||
background-color: $accentCol;
|
||||
}
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.info-top {
|
||||
flex-direction: column;
|
||||
|
||||
span {
|
||||
margin: 0.1em auto;
|
||||
}
|
||||
}
|
||||
|
||||
.info-extended {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-route {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn--show {
|
||||
margin: 1em auto 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,33 +1,61 @@
|
||||
<template>
|
||||
<section class="scenery-dispatchers-history scenery-section">
|
||||
<Loading v-if="dataStatus != 2" />
|
||||
<section class="scenery-table-section">
|
||||
<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>
|
||||
<li class="list-item" v-for="historyItem in dispatcherHistoryList">
|
||||
<div>
|
||||
<router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`">
|
||||
<span class="text--grayed">#{{ historyItem.stationHash }} </span>
|
||||
<b>{{ historyItem.dispatcherName }}</b>
|
||||
</router-link>
|
||||
</div>
|
||||
<tbody>
|
||||
<tr v-for="historyItem in historyList">
|
||||
<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 style="min-width: 300px">
|
||||
<div v-if="historyItem.timestampTo">
|
||||
<b>{{ $d(historyItem.timestampFrom) }}</b>
|
||||
|
||||
<div v-if="historyItem.timestampTo">
|
||||
<b>{{ $d(historyItem.timestampFrom) }}</b>
|
||||
{{ timestampToString(historyItem.timestampFrom) }}
|
||||
- {{ timestampToString(historyItem.timestampTo) }} ({{ calculateDuration(historyItem.currentDuration) }})
|
||||
</div>
|
||||
|
||||
{{ timestampToString(historyItem.timestampFrom) }}
|
||||
- {{ timestampToString(historyItem.timestampTo) }} ({{ calculateDuration(historyItem.currentDuration) }})
|
||||
</div>
|
||||
|
||||
<div class="dispatcher-online" v-else>
|
||||
{{ $t('journal.online-since') }}
|
||||
<b>{{ timestampToString(historyItem.timestampFrom) }}</b>
|
||||
({{ calculateDuration(historyItem.currentDuration) }})
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="dispatcher-online" v-else>
|
||||
{{ $t('journal.online-since') }}
|
||||
<b>{{ timestampToString(historyItem.timestampFrom) }}</b>
|
||||
({{ calculateDuration(historyItem.currentDuration) }})
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class="bottom-info">
|
||||
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory">
|
||||
{{ $t('scenery.bottom-info') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -39,37 +67,53 @@ import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIDa
|
||||
import Station from '../../scripts/interfaces/Station';
|
||||
import { URLs } from '../../scripts/utils/apiURLs';
|
||||
import Loading from '../Global/Loading.vue';
|
||||
import styleMixin from '../../mixins/styleMixin';
|
||||
import listObserverMixin from '../../mixins/listObserverMixin';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SceneryDispatchersHistory',
|
||||
mixins: [dateMixin],
|
||||
mixins: [dateMixin, styleMixin, listObserverMixin],
|
||||
props: {
|
||||
station: {
|
||||
type: Object as PropType<Station>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
dispatcherHistoryList: [] as DispatcherHistory[],
|
||||
historyList: [] as DispatcherHistory[],
|
||||
dataStatus: DataStatus.Loading,
|
||||
DataStatus,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.fetchAPIData();
|
||||
|
||||
async activated() {
|
||||
// if (this.historyList.length == 0) {
|
||||
const fetchedHistory = await this.fetchAPIData();
|
||||
if (fetchedHistory) this.historyList = fetchedHistory;
|
||||
// }
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchAPIData(countFrom = 0, countLimit = 30) {
|
||||
async fetchAPIData(countFrom = 0, countLimit = 30): Promise<DispatcherHistory[] | null> {
|
||||
try {
|
||||
this.dataStatus = DataStatus.Loading;
|
||||
|
||||
const requestString = `${URLs.stacjownikAPI}/api/getDispatchers?stationName=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`;
|
||||
const historyAPIData: DispatcherHistory[] = await (await axios.get(requestString)).data;
|
||||
|
||||
this.dispatcherHistoryList = historyAPIData;
|
||||
this.dataStatus = DataStatus.Loaded;
|
||||
return historyAPIData;
|
||||
} catch (error) {
|
||||
this.dataStatus = DataStatus.Error;
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
navigateToHistory() {
|
||||
this.$router.push(`/journal/dispatchers?sceneryName=${this.station.name}`);
|
||||
},
|
||||
},
|
||||
components: { Loading },
|
||||
});
|
||||
@@ -77,23 +121,10 @@ export default defineComponent({
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/responsive.scss';
|
||||
@import '../../styles/SceneryView/styles.scss';
|
||||
@import '../../styles/sceneryViewTables.scss';
|
||||
|
||||
.history-list {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
.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;
|
||||
.level-badge {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.dispatcher-online {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<template>
|
||||
<section class="info-header">
|
||||
<a class="scenery-name" :href="station.generalInfo?.url">
|
||||
<a class="scenery-name" :href="station.generalInfo?.url" target="_blank">
|
||||
{{ station.name }}
|
||||
</a>
|
||||
|
||||
<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>
|
||||
</section>
|
||||
</template>
|
||||
@@ -26,18 +30,26 @@ export default defineComponent({
|
||||
@import '../../styles/variables.scss';
|
||||
@import '../../styles/responsive.scss';
|
||||
|
||||
.info-header {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.scenery-name {
|
||||
font-weight: bold;
|
||||
|
||||
position: relative;
|
||||
|
||||
font-size: 3em;
|
||||
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.scenery-abbrev {
|
||||
font-size: 1.3em;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.scenery-hash {
|
||||
margin-top: 0.5em;
|
||||
color: #aaa;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div class="scenery-info">
|
||||
<section v-if="!timetableOnly">
|
||||
<div class="info-general" v-if="station.generalInfo">
|
||||
<scenery-info-icons :station="station" />
|
||||
<div class="scenery-info-general" v-if="station.generalInfo">
|
||||
<SceneryInfoIcons :station="station" />
|
||||
|
||||
<div class="general-list">
|
||||
<div class="scenery-general-list">
|
||||
<span>
|
||||
<b>{{ $t('availability.title') }}:</b> {{ $t(`availability.${station.generalInfo.availability}`) }}
|
||||
|
||||
<span v-if="station.generalInfo.reqLevel > -1">
|
||||
- {{ $tc('scenery.req-level', station.generalInfo.reqLevel, { lvl: station.generalInfo.reqLevel }) }}
|
||||
- {{ $t('scenery.req-level', { lvl: station.generalInfo.reqLevel }, station.generalInfo.reqLevel) }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -26,36 +26,31 @@
|
||||
</span>
|
||||
<span v-if="station.generalInfo.project">
|
||||
• <b>{{ $t('scenery.project-title') }}: </b>
|
||||
<b style="color: salmon">{{ station.generalInfo.project }}</b>
|
||||
<a style="color: salmon; text-decoration: underline; font-weight: bold" :href="station.generalInfo.projectUrl" target="_blank">
|
||||
{{ station.generalInfo.project }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<scenery-info-routes :station="station" />
|
||||
<SceneryInfoRoutes :station="station" />
|
||||
|
||||
<div class="scenery-authors" v-if="station.generalInfo.authors && station.generalInfo.authors.length > 0">
|
||||
<b> {{ $tc('scenery.authors-title', station.generalInfo.authors.length) }}: </b>
|
||||
<b> {{ $t('scenery.authors-title', { authors: station.generalInfo.authors.length }, station.generalInfo.authors.length) }}: </b>
|
||||
{{ station.generalInfo.authors.join(', ') }}
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<div class="scenery-topic" v-if="station.generalInfo.url">
|
||||
<a :href="station.generalInfo.url" target="_blank">
|
||||
> {{ $t('scenery.forum-topic', { name: station.name }) }} <
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin: 2em 0; height: 2px; background-color: white" />
|
||||
<div style="margin: 2em 0; height: 2px; background-color: white"></div>
|
||||
|
||||
<!-- info dispatcher -->
|
||||
<scenery-info-dispatcher :station="station" :onlineFrom="onlineFrom" />
|
||||
<SceneryInfoDispatcher :station="station" :onlineFrom="onlineFrom" />
|
||||
|
||||
<div class="info-lists">
|
||||
<!-- user list -->
|
||||
<scenery-info-user-list :station="station" />
|
||||
<SceneryInfoUserList :station="station" />
|
||||
|
||||
<!-- spawn list -->
|
||||
<scenery-info-spawn-list :station="station" />
|
||||
<SceneryInfoSpawnList :station="station" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -72,7 +67,6 @@ import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue';
|
||||
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
|
||||
import Station from '../../scripts/interfaces/Station';
|
||||
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
SceneryInfoDispatcher,
|
||||
@@ -125,11 +119,11 @@ h3.section-header {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.info-general {
|
||||
.scenery-info-general {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.general-list {
|
||||
.scenery-general-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<section class="info-dispatcher">
|
||||
<div class="dispatcher" v-if="station.onlineInfo">
|
||||
<span class="dispatcher_level" :style="calculateExpStyle(station.onlineInfo.dispatcherExp)">
|
||||
<span
|
||||
class="dispatcher_level"
|
||||
:style="calculateExpStyle(station.onlineInfo.dispatcherExp, station.onlineInfo.dispatcherIsSupporter)"
|
||||
>
|
||||
{{ station.onlineInfo.dispatcherExp > 1 ? station.onlineInfo.dispatcherExp : 'L' }}
|
||||
</span>
|
||||
|
||||
@@ -18,18 +21,11 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span class="status-badge" v-if="station.onlineInfo && onlineFrom > 0">
|
||||
OD {{ new Date(onlineFrom).toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' }) }}
|
||||
</span>
|
||||
|
||||
<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>
|
||||
<StationStatusBadge
|
||||
:statusID="station.onlineInfo?.statusID"
|
||||
:isOnline="station.onlineInfo ? true : false"
|
||||
:statusTimestamp="station.onlineInfo?.statusTimestamp"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -40,20 +36,21 @@ import imageMixin from '../../../mixins/imageMixin';
|
||||
import routerMixin from '../../../mixins/routerMixin';
|
||||
import styleMixin from '../../../mixins/styleMixin';
|
||||
import Station from '../../../scripts/interfaces/Station';
|
||||
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [styleMixin, dateMixin, routerMixin, imageMixin],
|
||||
props: {
|
||||
station: {
|
||||
type: Object as () => Station,
|
||||
default: {},
|
||||
mixins: [styleMixin, dateMixin, routerMixin, imageMixin],
|
||||
props: {
|
||||
station: {
|
||||
type: Object as () => Station,
|
||||
default: {},
|
||||
},
|
||||
onlineFrom: {
|
||||
type: Number,
|
||||
default: -1,
|
||||
},
|
||||
},
|
||||
|
||||
onlineFrom: {
|
||||
type: Number,
|
||||
default: -1,
|
||||
},
|
||||
},
|
||||
components: { StationStatusBadge }
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -101,3 +98,4 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,114 +1,129 @@
|
||||
<template>
|
||||
<section class="info-routes" v-if="station.generalInfo">
|
||||
<div class="routes one-way" v-if="station.generalInfo.routes.oneWay.length > 0">
|
||||
<b>{{ $t('scenery.one-way-routes') }}</b>
|
||||
|
||||
<ul class="routes-list">
|
||||
<li
|
||||
v-for="route in station.generalInfo.routes.oneWay"
|
||||
:class="{ 'no-catenary': !route.catenary, internal: route.isInternal }"
|
||||
>
|
||||
{{ route.name }}
|
||||
<b v-if="route.SBL">SBL</b>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="routes two-way" v-if="station.generalInfo.routes.twoWay.length > 0">
|
||||
<b>{{ $t('scenery.two-way-routes') }}</b>
|
||||
|
||||
<ul class="routes-list">
|
||||
<li
|
||||
v-for="route in station.generalInfo.routes.twoWay"
|
||||
:class="{ 'no-catenary': !route.catenary, internal: route.isInternal }"
|
||||
>
|
||||
{{ route.name }} <b v-if="route.SBL">SBL</b>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- <div
|
||||
class="route-info"
|
||||
:class="{ 'no-catenary': !route.catenary, internal: route.isInternal }"
|
||||
v-for="route in [...station.generalInfo.routes.oneWay, ...station.generalInfo.routes.twoWay].filter(
|
||||
(route) => route.name != '-'
|
||||
)"
|
||||
:key="route.name"
|
||||
:title="`Szlak ${route.name}: ${route.isInternal ? 'wewnętrzny' : 'zewnętrzny'}, ${
|
||||
route.tracks == 2 ? 'dwutorowy' : 'jednotorowy'
|
||||
}, ${route.catenary ? 'zelektryfikowany' : 'niezelektryfikowany'} z ${route.SBL ? 'SBL' : 'PBL'} ${
|
||||
route.TWB ? 'i blokadą dwukierunkową' : ''
|
||||
}`"
|
||||
> -->
|
||||
<!-- <span class="track-name">
|
||||
<b>{{ route.name }}</b>
|
||||
</span> -->
|
||||
<!--
|
||||
<span class="track-specs">
|
||||
{{ route.tracks }}tor
|
||||
<img v-if="route.catenary" :src="icons.trackCatenary" alt="icon track catenary" />
|
||||
<img v-else :src="icons.trackNoCatenary" alt="icon track no catenary" />
|
||||
|
||||
<img v-if="route.TWB" :src="icons.trackTWB" alt="icon track twb" />
|
||||
<img v-if="route.SBL" :src="icons.trackSBL" alt="icon track sbl" />
|
||||
</span> -->
|
||||
<!-- </div> -->
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import Station from '../../../scripts/interfaces/Station';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
station: {
|
||||
type: Object as () => Station,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.info-routes {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.routes {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
padding: 0.25em;
|
||||
}
|
||||
|
||||
ul.routes-list {
|
||||
margin: 0.45em 0.25em;
|
||||
display: flex;
|
||||
|
||||
li {
|
||||
background-color: #007599;
|
||||
|
||||
padding: 0.2em 0.25em;
|
||||
margin-left: 0.25em;
|
||||
|
||||
&.no-catenary {
|
||||
background-color: #686868;
|
||||
}
|
||||
|
||||
&.internal {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
b {
|
||||
color: var(--clr-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<section class="info-routes" v-if="station.generalInfo">
|
||||
<div class="routes one-way" v-if="station.generalInfo.routes.oneWay.length > 0">
|
||||
<b>{{ $t('scenery.one-way-routes') }}</b>
|
||||
|
||||
<ul class="routes-list">
|
||||
<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 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>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="routes two-way" v-if="station.generalInfo.routes.twoWay.length > 0">
|
||||
<b>{{ $t('scenery.two-way-routes') }}</b>
|
||||
|
||||
<ul class="routes-list">
|
||||
<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 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>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import Station from '../../../scripts/interfaces/Station';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
station: {
|
||||
type: Object as () => Station,
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.info-routes {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.routes {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
padding: 0.25em;
|
||||
}
|
||||
|
||||
ul.routes-list {
|
||||
margin: 0.45em 0.25em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
li {
|
||||
margin: 0.5em 0.25em;
|
||||
cursor: pointer;
|
||||
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
span {
|
||||
padding: 0.2em 0.25em;
|
||||
background-color: #007599;
|
||||
font-weight: bold;
|
||||
|
||||
&.no-catenary {
|
||||
background-color: #686868;
|
||||
}
|
||||
|
||||
&.internal {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&.speed {
|
||||
background-color: #404040;
|
||||
color: #cfcfcf;
|
||||
}
|
||||
|
||||
&.sbl {
|
||||
color: var(--clr-primary);
|
||||
background-color: #404040;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0.5em 0.5em 0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-radius: 0.5em 0 0 0.5em;
|
||||
}
|
||||
|
||||
&:only-child {
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,52 +1,65 @@
|
||||
<template>
|
||||
<section class="info-spawn-list">
|
||||
<h3 class="spawn-header section-header">
|
||||
<img :src="getIcon('spawn')" alt="icon-spawn" />
|
||||
{{ $t('scenery.spawns') }}
|
||||
<span class="text--primary">{{ station.onlineInfo?.spawns.length || '0' }}</span>
|
||||
</h3>
|
||||
|
||||
<span v-if="station.onlineInfo">
|
||||
<span
|
||||
class="badge spawn"
|
||||
v-for="(spawn, i) in station.onlineInfo.spawns"
|
||||
:key="spawn.spawnName + station.onlineInfo?.dispatcherName + i"
|
||||
>
|
||||
<span class="spawn_name">{{ spawn.spawnName }}</span>
|
||||
<span class="spawn_length">{{ spawn.spawnLength }}m</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="badge spawn badge-none" v-if="!station.onlineInfo || station.onlineInfo.spawns.length == 0"
|
||||
>{{ $t('scenery.no-spawns') }}
|
||||
</span>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import imageMixin from '../../../mixins/imageMixin';
|
||||
import Station from '../../../scripts/interfaces/Station';
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [imageMixin],
|
||||
|
||||
props: {
|
||||
station: {
|
||||
type: Object as () => Station,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../../styles/variables.scss';
|
||||
|
||||
.spawn {
|
||||
&_length {
|
||||
background: $accentCol;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<section class="info-spawn-list">
|
||||
<h3 class="spawn-header section-header">
|
||||
<img :src="getIcon('spawn')" alt="icon-spawn" />
|
||||
{{ $t('scenery.spawns') }}
|
||||
<span class="text--primary">{{ station.onlineInfo?.spawns.length || '0' }}</span>
|
||||
</h3>
|
||||
|
||||
<span v-if="station.onlineInfo">
|
||||
<span
|
||||
class="badge spawn"
|
||||
v-for="(spawn, i) in sortedSpawns"
|
||||
: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>
|
||||
</span>
|
||||
|
||||
<span class="badge spawn badge-none" v-if="!station.onlineInfo || station.onlineInfo.spawns.length == 0"
|
||||
>{{ $t('scenery.no-spawns') }}
|
||||
</span>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import imageMixin from '../../../mixins/imageMixin';
|
||||
import Station from '../../../scripts/interfaces/Station';
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [imageMixin],
|
||||
|
||||
props: {
|
||||
station: {
|
||||
type: Object as () => Station,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
sortedSpawns() {
|
||||
return this.station.onlineInfo?.spawns.sort((s1, s2) => (s1.spawnLength < s2.spawnLength ? 1 : -1));
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../../styles/variables.scss';
|
||||
|
||||
.spawn {
|
||||
color: white;
|
||||
|
||||
&_length {
|
||||
background-color: #404040;
|
||||
color: #cfcfcf;
|
||||
}
|
||||
|
||||
&[data-electrified='true'] > &_name {
|
||||
background-color: #007599;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,133 +1,131 @@
|
||||
<template>
|
||||
<section class="info-user-list">
|
||||
<h3 class="user-header section-header">
|
||||
<img :src="getIcon('user')" alt="icon-user" />
|
||||
{{ $t('scenery.users') }}
|
||||
<span class="text--primary">{{ station.onlineInfo?.currentUsers || '0' }}</span
|
||||
> / <span class="text--primary">{{ station.onlineInfo?.maxUsers || '0' }}</span>
|
||||
</h3>
|
||||
|
||||
<div
|
||||
v-for="(train, i) in computedStationTrains"
|
||||
class="badge user"
|
||||
:class="train.stopStatus"
|
||||
:key="train.trainId"
|
||||
tabindex="0"
|
||||
@click="selectModalTrain(train.trainId)"
|
||||
@keydown.enter="selectModalTrain(train.trainId)"
|
||||
>
|
||||
<span class="user_train">{{ train.trainNo }}</span>
|
||||
<span class="user_name">{{ train.driverName }}</span>
|
||||
</div>
|
||||
|
||||
<div class="badge user badge-none" v-if="!computedStationTrains || computedStationTrains.length == 0">
|
||||
{{ $t('scenery.no-users') }}
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import imageMixin from '../../../mixins/imageMixin';
|
||||
import modalTrainMixin from '../../../mixins/modalTrainMixin';
|
||||
import routerMixin from '../../../mixins/routerMixin';
|
||||
import Station from '../../../scripts/interfaces/Station';
|
||||
import { useStore } from '../../../store/store';
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [routerMixin, imageMixin, modalTrainMixin],
|
||||
|
||||
props: {
|
||||
station: {
|
||||
type: Object as () => Station,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
|
||||
const computedStationTrains = computed(() => {
|
||||
if (!props.station) return [];
|
||||
|
||||
const station = props.station as Station;
|
||||
if (!station.onlineInfo) return [];
|
||||
if (!station.onlineInfo.stationTrains) return [];
|
||||
|
||||
return station.onlineInfo.stationTrains.map((train) => {
|
||||
const scheduledTrainStatus = station.onlineInfo?.scheduledTrains?.find((st) => st.trainNo === train.trainNo);
|
||||
|
||||
return {
|
||||
...train,
|
||||
stopStatus: scheduledTrainStatus?.stopStatus || 'no-timetable',
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return { computedStationTrains, store };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$no-timetable: #aaa;
|
||||
$departed: springgreen;
|
||||
$stopped: #ffa600;
|
||||
$online: gold;
|
||||
$terminated: salmon;
|
||||
$disconnected: slategray;
|
||||
|
||||
.info-user-list {
|
||||
width: 100%;
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
cursor: pointer;
|
||||
|
||||
&_train {
|
||||
color: black;
|
||||
background-color: $no-timetable;
|
||||
|
||||
transition: background-color 200ms;
|
||||
-ms-transition: background-color 200ms;
|
||||
-webkit-transition: background-color 200ms;
|
||||
}
|
||||
|
||||
&.no-timetable .user_train {
|
||||
background-color: $no-timetable;
|
||||
}
|
||||
|
||||
&.departed > &_train {
|
||||
background-color: $departed;
|
||||
}
|
||||
|
||||
&.stopped > &_train {
|
||||
background-color: $stopped;
|
||||
}
|
||||
|
||||
&.online > &_train {
|
||||
background-color: $online;
|
||||
}
|
||||
|
||||
&.terminated > &_train {
|
||||
background-color: $terminated;
|
||||
}
|
||||
|
||||
&.disconnected > &_train {
|
||||
background-color: $disconnected;
|
||||
}
|
||||
|
||||
&.offline {
|
||||
background: firebrick;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<section class="info-user-list">
|
||||
<h3 class="user-header section-header">
|
||||
<img :src="getIcon('user')" alt="icon-user" />
|
||||
{{ $t('scenery.users') }}
|
||||
<span class="text--primary">{{ station.onlineInfo?.currentUsers || '0' }}</span
|
||||
> / <span class="text--primary">{{ station.onlineInfo?.maxUsers || '0' }}</span>
|
||||
</h3>
|
||||
|
||||
<div
|
||||
v-for="(train, i) in computedStationTrains"
|
||||
class="badge user"
|
||||
:class="train.stopStatus"
|
||||
:key="train.trainId"
|
||||
tabindex="0"
|
||||
@click.prevent="selectModalTrain(train.trainId, $event.currentTarget)"
|
||||
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
|
||||
>
|
||||
<span class="user_train">{{ train.trainNo }}</span>
|
||||
<span class="user_name">{{ train.driverName }}</span>
|
||||
</div>
|
||||
|
||||
<div class="badge user badge-none" v-if="!computedStationTrains || computedStationTrains.length == 0">
|
||||
{{ $t('scenery.no-users') }}
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import imageMixin from '../../../mixins/imageMixin';
|
||||
import modalTrainMixin from '../../../mixins/modalTrainMixin';
|
||||
import routerMixin from '../../../mixins/routerMixin';
|
||||
import Station from '../../../scripts/interfaces/Station';
|
||||
import { useStore } from '../../../store/store';
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [routerMixin, imageMixin, modalTrainMixin],
|
||||
|
||||
props: {
|
||||
station: {
|
||||
type: Object as () => Station,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
|
||||
const computedStationTrains = computed(() => {
|
||||
if (!props.station) return [];
|
||||
|
||||
const station = props.station as Station;
|
||||
if (!station.onlineInfo) return [];
|
||||
if (!station.onlineInfo.stationTrains) return [];
|
||||
|
||||
return station.onlineInfo.stationTrains.map((train) => {
|
||||
const scheduledTrainStatus = station.onlineInfo?.scheduledTrains?.find((st) => st.trainNo === train.trainNo);
|
||||
|
||||
return {
|
||||
...train,
|
||||
stopStatus: scheduledTrainStatus?.stopStatus || 'no-timetable',
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return { computedStationTrains, store };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$no-timetable: #aaa;
|
||||
$departed: springgreen;
|
||||
$stopped: #ffa600;
|
||||
$online: gold;
|
||||
$terminated: salmon;
|
||||
$disconnected: slategray;
|
||||
|
||||
.info-user-list {
|
||||
width: 100%;
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
cursor: pointer;
|
||||
|
||||
&_train {
|
||||
color: black;
|
||||
background-color: $no-timetable;
|
||||
|
||||
transition: background-color 200ms;
|
||||
-ms-transition: background-color 200ms;
|
||||
-webkit-transition: background-color 200ms;
|
||||
}
|
||||
|
||||
&.no-timetable .user_train {
|
||||
background-color: $no-timetable;
|
||||
}
|
||||
|
||||
&.departed > &_train {
|
||||
background-color: $departed;
|
||||
}
|
||||
|
||||
&.stopped > &_train {
|
||||
background-color: $stopped;
|
||||
}
|
||||
|
||||
&.online > &_train {
|
||||
background-color: $online;
|
||||
}
|
||||
|
||||
&.terminated > &_train {
|
||||
background-color: $terminated;
|
||||
}
|
||||
|
||||
&.disconnected > &_train {
|
||||
background-color: $disconnected;
|
||||
}
|
||||
|
||||
&.offline {
|
||||
background: firebrick;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,25 +2,37 @@
|
||||
<section class="scenery-timetable">
|
||||
<div class="timetable-header">
|
||||
<h3>
|
||||
<img :src="getIcon('timetable')" alt="icon-timetable" />
|
||||
<img :src="getIcon('timetable')" alt="icon-timetable" />
|
||||
<span>{{ $t('scenery.timetables') }}</span>
|
||||
|
||||
<span class="text--primary">{{ station.onlineInfo?.scheduledTrains?.length || '0' }}</span>
|
||||
<span> / </span>
|
||||
<span class="text--grayed">
|
||||
{{ station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed).length || '0' }}
|
||||
|
||||
<span>
|
||||
<span class="text--primary">{{ station.onlineInfo?.scheduledTrains?.length || '0' }}</span>
|
||||
<span> / </span>
|
||||
<span class="text--grayed">
|
||||
{{ station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed).length || '0' }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="header_links">
|
||||
<a :href="`https://pragotron-td2.web.app/board?name=${station.name}`" target="_blank" :title="$t('scenery.pragotron-link')">
|
||||
<img :src="getIcon('pragotron')" alt="icon-pragotron" />
|
||||
</a>
|
||||
|
||||
<a :href="tabliceZbiorczeHref" target="_blank" :title="$t('scenery.tablice-link')">
|
||||
<img :src="getIcon('tablice', 'ico')" alt="icon-tablice" />
|
||||
</a>
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<div class="timetable-checkpoints" v-if="station && station.generalInfo?.checkpoints">
|
||||
<div class="timetable-checkpoints" v-if="station?.generalInfo?.checkpoints">
|
||||
<span v-for="(cp, i) in station.generalInfo.checkpoints" :key="i">
|
||||
{{ (i > 0 && '•') || '' }}
|
||||
|
||||
<button
|
||||
:key="cp.checkpointName"
|
||||
class="checkpoint_item"
|
||||
:class="{ current: selectedCheckpoint === cp.checkpointName }"
|
||||
@click="selectCheckpoint(cp)"
|
||||
:class="{ current: chosenCheckpoint === cp.checkpointName }"
|
||||
@click="setCheckpoint(cp)"
|
||||
>
|
||||
{{ cp.checkpointName }}
|
||||
</button>
|
||||
@@ -41,14 +53,14 @@
|
||||
{{ $t('scenery.no-timetables') }}
|
||||
</span>
|
||||
|
||||
<transition-group name="timetables-anim">
|
||||
<transition-group name="list-anim">
|
||||
<div
|
||||
class="timetable-item"
|
||||
v-for="(scheduledTrain, i) in computedScheduledTrains"
|
||||
:key="scheduledTrain.trainId"
|
||||
tabindex="0"
|
||||
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId)"
|
||||
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId)"
|
||||
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
|
||||
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
|
||||
>
|
||||
<span class="timetable-general">
|
||||
<span class="general-info">
|
||||
@@ -86,40 +98,29 @@
|
||||
</div>
|
||||
<div v-else>
|
||||
<div>
|
||||
<s style="margin-right: 0.2em" class="text--grayed">{{
|
||||
timestampToString(scheduledTrain.stopInfo.arrivalTimestamp)
|
||||
}}</s>
|
||||
<s style="margin-right: 0.2em" class="text--grayed">{{ timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) }}</s>
|
||||
</div>
|
||||
|
||||
<span>
|
||||
{{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }}
|
||||
({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : ''
|
||||
}}{{ scheduledTrain.stopInfo.arrivalDelay }})
|
||||
({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : '' }}{{ scheduledTrain.stopInfo.arrivalDelay }})
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="schedule-stop">
|
||||
<span class="stop-time">
|
||||
<span v-if="scheduledTrain.stopInfo.stopTime">
|
||||
{{ scheduledTrain.stopInfo.stopTime }}
|
||||
{{ scheduledTrain.stopInfo.stopType || 'pt' }}
|
||||
</span>
|
||||
|
||||
<span v-else> </span>
|
||||
<span class="stop-connection">
|
||||
{{ scheduledTrain.arrivingLine }}
|
||||
</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>
|
||||
{{ scheduledTrain.arrivingLine }}
|
||||
</span>
|
||||
<span></span>
|
||||
<span>
|
||||
{{ scheduledTrain.departureLine }}
|
||||
</span>
|
||||
<span class="stop-connection">
|
||||
{{ scheduledTrain.departureLine }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -134,15 +135,12 @@
|
||||
</div>
|
||||
<div v-else>
|
||||
<div>
|
||||
<s style="margin-right: 0.2em" class="text--grayed">{{
|
||||
timestampToString(scheduledTrain.stopInfo.departureTimestamp)
|
||||
}}</s>
|
||||
<s style="margin-right: 0.2em" class="text--grayed">{{ timestampToString(scheduledTrain.stopInfo.departureTimestamp) }}</s>
|
||||
</div>
|
||||
|
||||
<span>
|
||||
{{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }}
|
||||
({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : ''
|
||||
}}{{ scheduledTrain.stopInfo.departureDelay }})
|
||||
({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : '' }}{{ scheduledTrain.stopInfo.departureDelay }})
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
@@ -168,7 +166,6 @@ import { useStore } from '../../store/store';
|
||||
import imageMixin from '../../mixins/imageMixin';
|
||||
import modalTrainMixin from '../../mixins/modalTrainMixin';
|
||||
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
|
||||
import ScheduledTrain from '../../scripts/interfaces/ScheduledTrain';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SceneryTimetable',
|
||||
@@ -192,16 +189,22 @@ export default defineComponent({
|
||||
listOpen: false,
|
||||
}),
|
||||
|
||||
mounted() {
|
||||
this.loadSelectedOption();
|
||||
},
|
||||
|
||||
activated() {
|
||||
this.loadSelectedOption();
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const route = useRoute();
|
||||
const currentURL = computed(() => `${location.origin}${route.fullPath}`);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const selectedCheckpoint = ref(
|
||||
props.station?.generalInfo?.checkpoints?.length == 0
|
||||
? ''
|
||||
: props.station?.generalInfo?.checkpoints[0].checkpointName || ''
|
||||
const chosenCheckpoint = ref(
|
||||
props.station?.generalInfo?.checkpoints?.length == 0 ? '' : props.station?.generalInfo?.checkpoints[0].checkpointName || null
|
||||
);
|
||||
|
||||
const computedScheduledTrains = computed(() => {
|
||||
@@ -210,8 +213,7 @@ export default defineComponent({
|
||||
const station = props.station as Station;
|
||||
|
||||
let scheduledTrains =
|
||||
station.generalInfo?.checkpoints.find((cp) => cp.checkpointName === selectedCheckpoint.value)
|
||||
?.scheduledTrains ||
|
||||
station.generalInfo?.checkpoints.find((cp) => cp.checkpointName === chosenCheckpoint.value)?.scheduledTrains ||
|
||||
station.onlineInfo?.scheduledTrains ||
|
||||
[];
|
||||
|
||||
@@ -232,12 +234,21 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
currentURL,
|
||||
selectedCheckpoint,
|
||||
chosenCheckpoint,
|
||||
computedScheduledTrains,
|
||||
store,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
tabliceZbiorczeHref() {
|
||||
let url = `https://tablice-td2.web.app/?station=${this.station.name}`;
|
||||
if (this.chosenCheckpoint) url += `&checkpoint=${this.chosenCheckpoint}`;
|
||||
|
||||
return url;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
loadSelectedOption() {
|
||||
if (!this.station) return;
|
||||
@@ -245,49 +256,26 @@ export default defineComponent({
|
||||
if (!this.station.generalInfo.checkpoints) 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 }) {
|
||||
this.selectedCheckpoint = cp.checkpointName;
|
||||
setCheckpoint(cp: { checkpointName: string }) {
|
||||
this.chosenCheckpoint = cp.checkpointName;
|
||||
},
|
||||
|
||||
showTimetableOnlyView() {
|
||||
this.$router.push(`${this.$route.fullPath}&timetableOnly=1`);
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.loadSelectedOption();
|
||||
},
|
||||
|
||||
activated() {
|
||||
this.loadSelectedOption();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/responsive.scss';
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
.timetables-anim-move,
|
||||
.timetables-anim-enter-active,
|
||||
.timetables-anim-leave-active {
|
||||
transition: all 250ms ease;
|
||||
}
|
||||
|
||||
.timetables-anim-enter-from,
|
||||
.timetables-anim-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
.timetables-anim-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
@import '../../styles/animations.scss';
|
||||
|
||||
.scenery-timetable {
|
||||
height: 100%;
|
||||
@@ -296,24 +284,36 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.timetable-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 99;
|
||||
|
||||
background-color: #181818;
|
||||
|
||||
padding: 0.5em;
|
||||
|
||||
img {
|
||||
width: 25px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
h3 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
gap: 0.5em;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
}
|
||||
|
||||
.header_links {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.timetable {
|
||||
&-count {
|
||||
margin-left: 0.5em;
|
||||
@@ -325,8 +325,8 @@ export default defineComponent({
|
||||
max-width: 1100px;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 2em 0.5em;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.2em 0.5em;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
@@ -352,7 +352,9 @@ export default defineComponent({
|
||||
|
||||
&-schedule {
|
||||
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%;
|
||||
max-width: 400px;
|
||||
@@ -370,7 +372,8 @@ export default defineComponent({
|
||||
|
||||
flex-wrap: wrap;
|
||||
font-size: 1.1em;
|
||||
padding: 0.75em 0;
|
||||
|
||||
margin-top: 0.5em;
|
||||
|
||||
button.checkpoint_item {
|
||||
color: #aaa;
|
||||
@@ -383,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 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -436,47 +412,34 @@ export default defineComponent({
|
||||
|
||||
.schedule {
|
||||
&-arrival,
|
||||
&-stop,
|
||||
&-departure {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
margin: 0 0.3rem;
|
||||
font-size: 1.15em;
|
||||
}
|
||||
|
||||
&-stop {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 0.9em;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.5em;
|
||||
align-items: end;
|
||||
|
||||
padding: 0.3em 0;
|
||||
|
||||
.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-connection {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.stop-time {
|
||||
position: absolute;
|
||||
transform: translateY(-15px);
|
||||
position: relative;
|
||||
inline-size: max-content;
|
||||
align-self: center;
|
||||
font-size: 0.9em;
|
||||
|
||||
color: $accentCol;
|
||||
|
||||
&::after {
|
||||
content: '\027F6';
|
||||
display: block;
|
||||
font-size: 2.2em;
|
||||
line-height: 0.65em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -486,21 +449,6 @@ export default defineComponent({
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.scenery-timetable-list-anim {
|
||||
&-enter-from,
|
||||
&-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&-enter-active {
|
||||
transition: all 100ms ease-out;
|
||||
}
|
||||
|
||||
&-leave-active {
|
||||
transition: all 100ms ease-out 100ms;
|
||||
}
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.timetable-item {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -1,35 +1,51 @@
|
||||
<template>
|
||||
<section class="scenery-timetables-history scenery-section">
|
||||
<Loading v-if="dataStatus != 2" />
|
||||
<section class="scenery-table-section">
|
||||
<Loading v-if="dataStatus != DataStatus.Loaded" />
|
||||
<div class="no-history" v-else-if="historyList.length == 0">{{ $t('scenery.history-list-empty') }}</div>
|
||||
|
||||
<div class="list-warning" v-else-if="sceneryHistoryList.length == 0">{{ $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>
|
||||
<table class="scenery-history-table" v-else>
|
||||
<thead>
|
||||
<th>{{ $t('scenery.timetables-history-id') }}</th>
|
||||
<th>{{ $t('scenery.timetables-history-number') }}</th>
|
||||
<th>{{ $t('scenery.timetables-history-route') }}</th>
|
||||
<th>{{ $t('scenery.timetables-history-driver') }}</th>
|
||||
<th>{{ $t('scenery.timetables-history-author') }}</th>
|
||||
<th>{{ $t('scenery.timetables-history-date') }}</th>
|
||||
</thead>
|
||||
|
||||
<div>
|
||||
<router-link :to="`/journal/timetables?timetableId=${historyItem.timetableId}`">
|
||||
<span class="text--grayed"> #{{ historyItem.timetableId }} </span>
|
||||
<b class="text--primary"> {{ historyItem.trainCategoryCode }} {{ historyItem.trainNo }}</b>
|
||||
<div>{{ historyItem.driverName }}</div>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div>{{ historyItem.route.replace('|', ' -> ') }}</div>
|
||||
<!-- <div>{{ historyItem.routeDistance }} km</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>
|
||||
|
||||
<!-- <div v-if="historyItem.authorId">{{ historyItem.authorName }}</div> -->
|
||||
</li>
|
||||
</ul>
|
||||
<tbody>
|
||||
<tr v-for="historyItem in historyList">
|
||||
<td>
|
||||
<router-link :to="`/journal/timetables?timetableId=${historyItem.id}`">#{{ historyItem.id }}</router-link>
|
||||
</td>
|
||||
<td>
|
||||
<b class="text--primary">{{ historyItem.trainCategoryCode }}</b> <br />
|
||||
{{ historyItem.trainNo }}
|
||||
</td>
|
||||
<td>{{ historyItem.route.replace('|', ' -> ') }}</td>
|
||||
<td>{{ historyItem.driverName }}</td>
|
||||
<td>
|
||||
<router-link
|
||||
v-if="historyItem.authorName"
|
||||
:to="`/journal/timetables?authorName=${historyItem.authorName}`"
|
||||
>{{ historyItem.authorName }}
|
||||
</router-link>
|
||||
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i>
|
||||
</td>
|
||||
<td>
|
||||
<b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b>
|
||||
{{ localeTime(historyItem.beginDate, $i18n.locale) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class="bottom-info">
|
||||
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory()">
|
||||
{{ $t('scenery.bottom-info') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -41,37 +57,48 @@ import { TimetableHistory, SceneryTimetableHistory } from '../../scripts/interfa
|
||||
import Station from '../../scripts/interfaces/Station';
|
||||
import { URLs } from '../../scripts/utils/apiURLs';
|
||||
import Loading from '../Global/Loading.vue';
|
||||
import listObserverMixin from '../../mixins/listObserverMixin';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SceneryTimetablesHistory',
|
||||
mixins: [dateMixin],
|
||||
mixins: [dateMixin, listObserverMixin],
|
||||
props: {
|
||||
station: {
|
||||
type: Object as PropType<Station>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
sceneryHistoryList: [] as TimetableHistory[],
|
||||
historyList: [] as TimetableHistory[],
|
||||
dataStatus: DataStatus.Loading,
|
||||
DataStatus,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.fetchAPIData();
|
||||
|
||||
async activated() {
|
||||
const fetchedHistory = await this.fetchAPIData();
|
||||
if (fetchedHistory) this.historyList = fetchedHistory.timetables;
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchAPIData(countFrom = 0, countLimit = 15) {
|
||||
async fetchAPIData(countFrom = 0, countLimit = 15): Promise<SceneryTimetableHistory | null> {
|
||||
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;
|
||||
|
||||
this.sceneryHistoryList = historyAPIData.sceneryTimetables;
|
||||
this.dataStatus = DataStatus.Loaded;
|
||||
return historyAPIData;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
navigateToHistory() {
|
||||
this.$router.push(`/journal/timetables?issuedFrom=${this.station.name}`);
|
||||
},
|
||||
},
|
||||
components: { Loading },
|
||||
});
|
||||
@@ -79,34 +106,5 @@ export default defineComponent({
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/responsive.scss';
|
||||
@import '../../styles/SceneryView/styles.scss';
|
||||
|
||||
.list-warning {
|
||||
padding: 1em 0.5em;
|
||||
background-color: #444;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr 2fr 1fr;
|
||||
gap: 1em;
|
||||
align-items: center;
|
||||
|
||||
background-color: #353535;
|
||||
padding: 0.5em;
|
||||
margin: 0.5em 0;
|
||||
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.list-item {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
@import '../../styles/sceneryViewTables.scss';
|
||||
</style>
|
||||
|
||||
@@ -1,47 +1,19 @@
|
||||
<template>
|
||||
<div class="general-status">
|
||||
<span :class="scheduledTrain.stopStatus">
|
||||
<span v-if="scheduledTrain.stopStatus == 'arriving'">
|
||||
<span v-if="scheduledTrain.prevDepartureLine">({{ scheduledTrain.prevDepartureLine }})</span>
|
||||
{{ scheduledTrain.prevStationName }}
|
||||
><span v-if="scheduledTrain.nextArrivalLine"> ({{ scheduledTrain.nextArrivalLine }}) </span>
|
||||
{{ scheduledTrain.nextStationName || '---' }}
|
||||
</span>
|
||||
|
||||
<span v-else-if="scheduledTrain.stopStatus == 'departed'">
|
||||
>> <span v-if="scheduledTrain.nextArrivalLine"> ({{ scheduledTrain.nextArrivalLine }}) </span>
|
||||
{{ scheduledTrain.nextStationName }}
|
||||
</span>
|
||||
|
||||
<span v-else-if="scheduledTrain.stopStatus == 'departed-away'">
|
||||
>>>
|
||||
<span v-if="scheduledTrain.nextArrivalLine"> ({{ scheduledTrain.nextArrivalLine }}) </span>
|
||||
{{ scheduledTrain.nextStationName }}
|
||||
</span>
|
||||
|
||||
<span v-else-if="scheduledTrain.stopStatus == 'online'">
|
||||
>
|
||||
<span v-if="scheduledTrain.nextArrivalLine">
|
||||
({{ scheduledTrain.nextArrivalLine }}) {{ scheduledTrain.nextStationName }}
|
||||
</span>
|
||||
<span v-else-if="!scheduledTrain.nextStationName">{{ $t('timetables.end') }}</span>
|
||||
<span v-else>{{ scheduledTrain.nextStationName }}</span>
|
||||
</span>
|
||||
|
||||
<span v-else-if="scheduledTrain.stopStatus == 'stopped'">
|
||||
>
|
||||
<span v-if="scheduledTrain.nextArrivalLine"> ({{ scheduledTrain.nextArrivalLine }}) </span>
|
||||
{{ scheduledTrain.nextStationName }}
|
||||
</span>
|
||||
|
||||
<span v-else-if="scheduledTrain.stopStatus == 'terminated'">X {{ $t('timetables.terminated') }}</span>
|
||||
<span :class="computedScheduledTrain.stopStatus" :title="computedScheduledTrain.stopStatusDescription">
|
||||
{{ computedScheduledTrain.stopStatusIndicator }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import ScheduledTrain from '../../scripts/interfaces/ScheduledTrain';
|
||||
import { ScheduledTrain, StopStatus } from '../../scripts/interfaces/ScheduledTrain';
|
||||
|
||||
interface ScheduledTrainComp extends ScheduledTrain {
|
||||
stopStatusIndicator: string;
|
||||
stopStatusDescription: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -50,6 +22,58 @@ export default defineComponent({
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
computedScheduledTrain(): ScheduledTrainComp {
|
||||
const { prevDepartureLine, prevStationName, stopStatus, nextArrivalLine, nextStationName } = this.scheduledTrain;
|
||||
|
||||
const prevDepartureIndicator = prevDepartureLine ? `(${prevDepartureLine}) ${prevStationName}` : '---';
|
||||
const nextArrivalIndicator = nextArrivalLine ? `(${nextArrivalLine}) ${nextStationName}` : '---';
|
||||
|
||||
let stopStatusDescription = '',
|
||||
stopStatusIndicator = '';
|
||||
|
||||
switch (stopStatus) {
|
||||
case StopStatus.arriving:
|
||||
stopStatusIndicator = `${this.$t('timetables.from')}: ${prevDepartureIndicator}`;
|
||||
stopStatusDescription = this.$t('timetables.desc-arriving', { prevStationName, prevDepartureLine });
|
||||
break;
|
||||
|
||||
case StopStatus.online:
|
||||
case StopStatus.stopped:
|
||||
stopStatusIndicator = nextArrivalLine
|
||||
? `${this.$t('timetables.to')}: ${nextArrivalIndicator}`
|
||||
: `${this.$t('timetables.desc-end')}`;
|
||||
stopStatusDescription = nextArrivalLine
|
||||
? this.$t(`timetables.desc-${stopStatus}`, { nextStationName, nextArrivalLine })
|
||||
: '';
|
||||
break;
|
||||
|
||||
case StopStatus.departed:
|
||||
stopStatusIndicator = `${this.$t('timetables.to')}: ${nextArrivalIndicator}`;
|
||||
stopStatusDescription = this.$t('timetables.desc-departed', { nextStationName, nextArrivalLine });
|
||||
break;
|
||||
|
||||
case StopStatus['departed-away']:
|
||||
stopStatusIndicator = `${this.$t('timetables.to')}: ${nextArrivalIndicator}`;
|
||||
stopStatusDescription = this.$t('timetables.desc-departed-away', { nextStationName, nextArrivalLine });
|
||||
break;
|
||||
|
||||
case StopStatus.terminated:
|
||||
stopStatusIndicator = `X ${this.$t('timetables.desc-terminated')}`;
|
||||
stopStatusDescription = this.$t('timetables.desc-terminated');
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return {
|
||||
...this.scheduledTrain,
|
||||
stopStatusDescription,
|
||||
stopStatusIndicator,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -86,3 +110,4 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<button class="btn--action" :class="option.section" :data-selected="option.value" @click="handleChange">
|
||||
{{ $t(`filters.${option.id}`) }}
|
||||
</button>
|
||||
<label @dblclick="handleDbClick">
|
||||
<input v-model="option.value" type="checkbox" :class="option.section" :name="option.id" />
|
||||
<span>
|
||||
{{ $t(`filters.${option.id}`) }}
|
||||
</span>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -29,78 +32,68 @@ export default defineComponent({
|
||||
filterStore: useStationFiltersStore(),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleChange() {
|
||||
this.option.value = !this.option.value;
|
||||
|
||||
this.filterStore.changeFilterValue({
|
||||
name: this.option.name,
|
||||
value: !this.option.value,
|
||||
});
|
||||
watch: {
|
||||
'option.value'() {
|
||||
this.filterStore.changeFilterValue(this.option.name, !this.option.value);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleDbClick(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
this.filterStore.lastClickedFilterId = this.option.id;
|
||||
this.option.value = true;
|
||||
|
||||
this.filterStore.inputs.options
|
||||
.filter((option) => {
|
||||
return option.section == this.option.section && option.id != this.option.id;
|
||||
})
|
||||
.forEach((option) => {
|
||||
option.value = !this.option.value;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$accessCol: #e03b07;
|
||||
$controlCol: #0085ff;
|
||||
$signalCol: #bf7c00;
|
||||
$statusCol: #349b32;
|
||||
$saveCol: #28a826;
|
||||
$routesCol: #9049c0;
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0.4em;
|
||||
border-radius: 0.4em;
|
||||
label {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid white;
|
||||
span {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 0.25em;
|
||||
background-color: #444;
|
||||
}
|
||||
|
||||
&[data-selected='true'] {
|
||||
&.access {
|
||||
background-color: $accessCol;
|
||||
box-shadow: 0 0 6px 1px $accessCol;
|
||||
span:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
|
||||
&:checked + span {
|
||||
background-color: forestgreen;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.control {
|
||||
background-color: $controlCol;
|
||||
box-shadow: 0 0 6px 1px $controlCol;
|
||||
}
|
||||
|
||||
&.signals {
|
||||
background-color: $signalCol;
|
||||
box-shadow: 0 0 6px 1px $signalCol;
|
||||
}
|
||||
|
||||
&.routes {
|
||||
background-color: $routesCol;
|
||||
box-shadow: 0 0 6px 1px $routesCol;
|
||||
}
|
||||
|
||||
&.status {
|
||||
background-color: $statusCol;
|
||||
box-shadow: 0 0 6px 1px $statusCol;
|
||||
}
|
||||
|
||||
&.save {
|
||||
background-color: $saveCol;
|
||||
box-shadow: 0 0 6px 1px $saveCol;
|
||||
}
|
||||
|
||||
&.troll {
|
||||
background-color: firebrick;
|
||||
box-shadow: 0 0 6px 1px firebrick;
|
||||
}
|
||||
|
||||
&.mode {
|
||||
background-color: lightgreen;
|
||||
color: black;
|
||||
|
||||
font-weight: 500;
|
||||
&:focus-visible + span {
|
||||
outline: 1px solid $accentCol;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<button class="btn--filled btn--image" @click="toggleCard">
|
||||
<img class="button_icon" :src="getIcon('filter2')" alt="filter icon" />
|
||||
{{ $t('options.filters') }} [F]
|
||||
<span class="active-indicator" v-if="!filterStore.areFiltersAtDefault"></span>
|
||||
</button>
|
||||
|
||||
<label for="scenery-search">
|
||||
@@ -17,7 +18,7 @@
|
||||
/>
|
||||
|
||||
<datalist id="sceneries">
|
||||
<option v-for="scenery in store.stationList" :value="scenery.name"></option>
|
||||
<option v-for="scenery in sortedStationList" :value="scenery.name"></option>
|
||||
</datalist>
|
||||
</label>
|
||||
</div>
|
||||
@@ -26,15 +27,44 @@
|
||||
<div class="card" v-if="isVisible" tabindex="0" ref="cardEl">
|
||||
<div class="card_content">
|
||||
<div class="card_title flex">{{ $t('filters.title') }}</div>
|
||||
<p class="card_info" v-html="$t('filters.desc')"></p>
|
||||
|
||||
<section class="card_options">
|
||||
<filter-option
|
||||
v-for="(option, i) in filterStore.inputs.options"
|
||||
:option="option"
|
||||
:key="i"
|
||||
@optionChange="handleChange"
|
||||
/>
|
||||
<!-- 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">
|
||||
<h3 class="text--primary">
|
||||
{{ $t(`filters.sections.${section}`) }}
|
||||
|
||||
<button @click="filterStore.resetSectionOptions(section)">RESET</button>
|
||||
</h3>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="section-inputs">
|
||||
<FilterOption
|
||||
v-for="(option, i) in filterStore.inputs.options.filter((o) => o.section == section)"
|
||||
:option="option"
|
||||
:key="i"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card_timestamp" style="text-align: center">
|
||||
<div>{{ $t('filters.minimum-hours-title') }}</div>
|
||||
<span class="clock">
|
||||
@@ -80,18 +110,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card_actions">
|
||||
<div class="action-buttons">
|
||||
<button class="btn--action" style="width: 100%" @click="saveFilters" :data-selected="saveOptions">
|
||||
{{ $t('filters.save') }}
|
||||
</button>
|
||||
|
||||
<button class="btn--action" @click="resetFilters">{{ $t('filters.reset') }}</button>
|
||||
<button class="btn--action" @click="closeCard">{{ $t('filters.close') }}</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="card_actions">
|
||||
<div class="action-buttons">
|
||||
<button class="btn--action" style="width: 100%" @click="saveFilters" :data-selected="saveOptions">
|
||||
{{ $t('filters.save') }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn--action"
|
||||
@click="resetFilters"
|
||||
:disabled="filterStore.areFiltersAtDefault"
|
||||
:data-disabled="filterStore.areFiltersAtDefault"
|
||||
>
|
||||
{{ $t('filters.reset') }}
|
||||
</button>
|
||||
<button class="btn--action" @click="closeCard">{{ $t('filters.close') }}</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</transition>
|
||||
</section>
|
||||
@@ -150,6 +187,18 @@ export default defineComponent({
|
||||
this.currentRegion = this.store.region;
|
||||
},
|
||||
|
||||
computed: {
|
||||
sortedStationList() {
|
||||
return this.store.stationList
|
||||
.filter((s) => s.name.toLocaleLowerCase().includes(this.chosenSearchScenery.toLocaleLowerCase()))
|
||||
.sort((s1, s2) => (s1.name > s2.name ? 1 : -1));
|
||||
},
|
||||
|
||||
currentOptionsActive() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
chosenSearchScenery(value: string) {
|
||||
const chosenStation = this.store.stationList.find(({ name }) => name == value);
|
||||
@@ -173,22 +222,10 @@ export default defineComponent({
|
||||
this.isVisible = !this.isVisible;
|
||||
},
|
||||
|
||||
handleChange(change: { name: string; value: boolean }) {
|
||||
this.filterStore.changeFilterValue({
|
||||
name: change.name,
|
||||
value: !change.value,
|
||||
});
|
||||
|
||||
if (this.saveOptions) StorageManager.setBooleanValue(change.name, change.value);
|
||||
},
|
||||
|
||||
handleInput(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
|
||||
this.filterStore.changeFilterValue({
|
||||
name: target.name,
|
||||
value: target.value,
|
||||
});
|
||||
this.filterStore.changeFilterValue(target.name, target.value);
|
||||
|
||||
if (this.saveOptions) StorageManager.setStringValue(target.name, target.value);
|
||||
},
|
||||
@@ -202,11 +239,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
changeNumericFilterValue(name: string, value: number, saveToStorage = false) {
|
||||
this.filterStore.changeFilterValue({
|
||||
name,
|
||||
value,
|
||||
});
|
||||
|
||||
this.filterStore.changeFilterValue(name, value);
|
||||
if (this.saveOptions && saveToStorage) StorageManager.setNumericValue(name, value);
|
||||
},
|
||||
|
||||
@@ -273,6 +306,14 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.card {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto;
|
||||
|
||||
&_info {
|
||||
background-color: #111;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
&_controls {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
@@ -284,13 +325,13 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
&_content {
|
||||
padding: 1em 0.5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 1em;
|
||||
|
||||
max-height: 90vh;
|
||||
|
||||
padding: 1em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&_title {
|
||||
@@ -301,18 +342,6 @@ export default defineComponent({
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&_options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-rows: repeat(4, 1fr);
|
||||
gap: 0.5em;
|
||||
|
||||
@include smallScreen() {
|
||||
grid-template-columns: repeat(auto-fit, minmax(8em, 1fr));
|
||||
grid-template-rows: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&_regions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -383,6 +412,9 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
&_actions {
|
||||
width: 100%;
|
||||
padding: 0.5em;
|
||||
|
||||
.filter-option {
|
||||
max-width: 50%;
|
||||
margin: 0 auto;
|
||||
@@ -401,14 +433,39 @@ export default defineComponent({
|
||||
padding: 0.5em;
|
||||
|
||||
&[data-selected='true'] {
|
||||
background-color: lightgreen;
|
||||
color: black;
|
||||
background-color: forestgreen;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.option-section h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.25em;
|
||||
|
||||
gap: 0.5em;
|
||||
|
||||
button {
|
||||
padding: 0.15em;
|
||||
color: coral;
|
||||
}
|
||||
}
|
||||
|
||||
.section-inputs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.5em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.quick-actions div {
|
||||
display: flex;
|
||||
margin: 1em 0;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.slider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,33 +1,39 @@
|
||||
<template>
|
||||
<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">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="(id, i) in headIds" :key="id" @click="() => changeSorter(i)">
|
||||
<th
|
||||
v-for="(headerName, i) in headIds"
|
||||
:key="headerName"
|
||||
@click="changeSorter(headerName)"
|
||||
class="header-text"
|
||||
>
|
||||
<span class="header_wrapper">
|
||||
<div v-html="$t(`sceneries.${id}`)"></div>
|
||||
<div v-html="$t(`sceneries.${headerName}`)"></div>
|
||||
|
||||
<img
|
||||
class="sort-icon"
|
||||
v-if="sorterActive.index == i"
|
||||
v-if="sorterActive.headerName == headerName"
|
||||
:src="sorterActive.dir == 1 ? getIcon('arrow-asc') : getIcon('arrow-desc')"
|
||||
alt="sort icon"
|
||||
/>
|
||||
</span>
|
||||
</th>
|
||||
|
||||
<th v-for="(id, i) in headIconsIds" :key="id" @click="() => changeSorter(i + 7)">
|
||||
<th
|
||||
v-for="(headerName, i) in headIconsIds"
|
||||
:key="headerName"
|
||||
@click="changeSorter(headerName)"
|
||||
class="header-image"
|
||||
>
|
||||
<span class="header_wrapper">
|
||||
<img :src="getIcon(id)" :alt="id" :title="$t(`sceneries.${id}s`)" />
|
||||
<img :src="getIcon(headerName)" :alt="headerName" :title="$t(`sceneries.${headerName}`)" />
|
||||
|
||||
<img
|
||||
class="sort-icon"
|
||||
v-if="sorterActive.index == i + 7"
|
||||
v-if="sorterActive.headerName == headerName"
|
||||
:src="sorterActive.dir == 1 ? getIcon('arrow-asc') : getIcon('arrow-desc')"
|
||||
alt="sort icon"
|
||||
/>
|
||||
@@ -83,16 +89,11 @@
|
||||
</td>
|
||||
|
||||
<td class="station_status">
|
||||
<span class="status-badge" :class="station.onlineInfo.statusID" v-if="station.onlineInfo">
|
||||
{{ $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>
|
||||
<StationStatusBadge
|
||||
:statusID="station.onlineInfo?.statusID"
|
||||
:isOnline="station.onlineInfo ? true : false"
|
||||
:statusTimestamp="station.onlineInfo?.statusTimestamp"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td class="station_dispatcher-name">
|
||||
@@ -100,7 +101,10 @@
|
||||
</td>
|
||||
|
||||
<td class="station_dispatcher-exp">
|
||||
<span v-if="station.onlineInfo" :style="calculateExpStyle(station.onlineInfo.dispatcherExp)">
|
||||
<span
|
||||
v-if="station.onlineInfo"
|
||||
:style="calculateExpStyle(station.onlineInfo.dispatcherExp, station.onlineInfo.dispatcherIsSupporter)"
|
||||
>
|
||||
{{ 2 > station.onlineInfo.dispatcherExp ? 'L' : station.onlineInfo.dispatcherExp }}
|
||||
</span>
|
||||
</td>
|
||||
@@ -187,25 +191,31 @@
|
||||
|
||||
<td class="station_users" :class="{ inactive: !station.onlineInfo }">
|
||||
<span>
|
||||
<span class="highlight">{{ station.onlineInfo?.currentUsers || '0' }}</span>
|
||||
<span class="highlight">{{ station.onlineInfo?.currentUsers || 0 }}</span>
|
||||
/
|
||||
<span>{{ station.onlineInfo?.maxUsers || '0' }}</span>
|
||||
<span class="highlight">{{ station.onlineInfo?.maxUsers || 0 }}</span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="station_spawns" :class="{ inactive: !station.onlineInfo }">
|
||||
<span class="highlight">{{ station.onlineInfo?.spawns.length || '0' }}</span>
|
||||
<span>{{ station.onlineInfo?.spawns.length || 0 }}</span>
|
||||
</td>
|
||||
|
||||
<td class="station_schedules" :class="{ inactive: !station.onlineInfo }">
|
||||
<span>
|
||||
<span class="highlight">
|
||||
{{ station.onlineInfo?.scheduledTrains?.length || '0' }}
|
||||
</span>
|
||||
/
|
||||
<span style="color: #bbb">
|
||||
{{ station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed).length || '0' }}
|
||||
</span>
|
||||
<td class="station_schedules" style="width: 30px" :class="{ inactive: !station.onlineInfo }">
|
||||
<span class="highlight">
|
||||
{{ station.onlineInfo?.scheduledTrains?.length || 0 }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="station_schedules" style="width: 30px" :class="{ inactive: !station.onlineInfo }">
|
||||
<span style="color: #ccc">
|
||||
{{ station.onlineInfo?.scheduledTrains?.filter((train) => !train.stopInfo.confirmed).length || 0 }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="station_schedules" style="width: 30px" :class="{ inactive: !station.onlineInfo }">
|
||||
<span style="color: #66ff6c">
|
||||
{{ station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed).length || 0 }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -233,6 +243,8 @@ import Station from '../../scripts/interfaces/Station';
|
||||
import { useStationFiltersStore } from '../../store/stationFiltersStore';
|
||||
import { useStore } from '../../store/store';
|
||||
import Loading from '../Global/Loading.vue';
|
||||
import { HeadIdsTypes, headIconsIds, headIds } from '../../scripts/data/stationHeaderNames';
|
||||
import StationStatusBadge from '../Global/StationStatusBadge.vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -242,12 +254,12 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
|
||||
components: { Loading },
|
||||
components: { Loading, StationStatusBadge },
|
||||
mixins: [styleMixin, dateMixin, stationInfoMixin, returnBtnMixin, imageMixin],
|
||||
|
||||
data: () => ({
|
||||
headIds: ['station', 'min-lvl', 'status', 'dispatcher', 'dispatcher-lvl', 'routes', 'general'],
|
||||
headIconsIds: ['user', 'spawn', 'timetable'],
|
||||
headIconsIds,
|
||||
headIds,
|
||||
lastSelectedStationName: '',
|
||||
}),
|
||||
|
||||
@@ -288,8 +300,10 @@ export default defineComponent({
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
|
||||
changeSorter(i: number) {
|
||||
this.stationFiltersStore.changeSorter(i);
|
||||
changeSorter(headerName: HeadIdsTypes) {
|
||||
if (headerName == 'general' || headerName == 'routes') return;
|
||||
|
||||
this.stationFiltersStore.changeSorter(headerName);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -346,9 +360,15 @@ table {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
min-width: 75px;
|
||||
&.header-text {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
padding: 0.5em;
|
||||
&.header-image {
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
padding: 0.5em 0.25em;
|
||||
background-color: $bgCol;
|
||||
white-space: pre-wrap;
|
||||
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
<template>
|
||||
<div class="train-info" tabindex="0">
|
||||
<div class="train-info">
|
||||
<section class="train-route">
|
||||
<div class="train_general">
|
||||
<span>
|
||||
<span class="timetable-id" v-if="train.timetableData">#{{ train.timetableData.timetableId }}</span>
|
||||
|
||||
<span class="timetable_warnings">
|
||||
<span class="train-badge twr" v-if="train.timetableData?.TWR">TWR</span>
|
||||
<span class="train-badge skr" v-if="train.timetableData?.SKR">SKR</span>
|
||||
</span>
|
||||
<strong v-if="train.timetableData">{{ train.timetableData.category }} </strong>
|
||||
<strong>{{ train.trainNo }}</strong>
|
||||
<span> | {{ train.driverName }} </span>
|
||||
<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_warnings" v-if="train.timetableData?.TWR || train.timetableData?.SKR">
|
||||
<span class="train-badge twr" v-if="train.timetableData?.TWR" :title="$t('general.TWR')">TWR</span>
|
||||
<span class="train-badge skr" v-if="train.timetableData?.SKR" :title="$t('general.SKR')">SKR</span>
|
||||
</span>
|
||||
|
||||
<strong>
|
||||
<span v-if="train.timetableData" class="text--primary">{{ train.timetableData.category }} </span>
|
||||
<span class="train-number">{{ train.trainNo }}</span>
|
||||
</strong>
|
||||
<span>•</span>
|
||||
<b class="level-badge driver" :style="calculateExpStyle(train.driverLevel, train.isSupporter)">
|
||||
{{ train.driverLevel < 2 ? 'L' : `${train.driverLevel}` }}
|
||||
</b>
|
||||
<span>{{ train.driverName }}</span>
|
||||
</div>
|
||||
|
||||
<div class="timetable_route" v-if="train.timetableData">
|
||||
@@ -36,15 +41,7 @@
|
||||
</div>
|
||||
|
||||
<div class="timetable_progress" style="margin-top: 0.5em" v-if="train.timetableData">
|
||||
<!-- <span> </span> -->
|
||||
<span class="timetable_progress-bar">
|
||||
<!-- {{ confirmedPercentage(train.timetableData.followingStops) }}% -->
|
||||
<span class="bar-bg"></span>
|
||||
<span
|
||||
class="bar-fg"
|
||||
:style="{ width: `${Math.floor(confirmedPercentage(train.timetableData.followingStops))}%` }"
|
||||
></span>
|
||||
</span>
|
||||
<ProgressBar :progressPercent="confirmedPercentage(train.timetableData.followingStops)" />
|
||||
|
||||
<span class="timetable_progress-distance">
|
||||
{{ currentDistance(train.timetableData.followingStops) }} km /
|
||||
@@ -65,15 +62,13 @@
|
||||
</section>
|
||||
|
||||
<section class="train-stats">
|
||||
<div>
|
||||
<img :src="train.locoURL" loading="lazy" alt="Loco image not found" @error="onImageError" />
|
||||
</div>
|
||||
<TrainThumbnail :name="train.locoType" :onlyFirstSegment="true" />
|
||||
|
||||
<div class="text--grayed">
|
||||
{{ train.locoType }}
|
||||
<span v-if="train.cars.length > 0">
|
||||
<span v-if="train.stockList.length > 1">
|
||||
• {{ $t('trains.cars') }}:
|
||||
<span class="count">{{ train.cars.length }}</span>
|
||||
<span class="count">{{ train.stockList.length - 1 }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -90,8 +85,11 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import imageMixin from '../../mixins/imageMixin';
|
||||
import styleMixin from '../../mixins/styleMixin';
|
||||
import trainInfoMixin from '../../mixins/trainInfoMixin';
|
||||
import Train from '../../scripts/interfaces/Train';
|
||||
import ProgressBar from '../Global/ProgressBar.vue';
|
||||
import TrainThumbnail from '../Global/TrainThumbnail.vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -99,19 +97,26 @@ export default defineComponent({
|
||||
type: Object as () => Train,
|
||||
required: true,
|
||||
},
|
||||
|
||||
extended: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
mixins: [trainInfoMixin, imageMixin],
|
||||
mixins: [trainInfoMixin, imageMixin, styleMixin],
|
||||
components: { ProgressBar, TrainThumbnail },
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Global style for TrainThumbnail -->
|
||||
<style lang="scss">
|
||||
.train-stats .train-thumbnail {
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/responsive.scss';
|
||||
@import '../../styles/badge.scss';
|
||||
|
||||
.image-warning {
|
||||
height: 1em;
|
||||
@@ -122,15 +127,12 @@ export default defineComponent({
|
||||
.train-stats {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
margin: 0.5em 0;
|
||||
width: 12em;
|
||||
}
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.train-info {
|
||||
@@ -145,19 +147,16 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.timetable-id {
|
||||
margin-right: 0.3em;
|
||||
color: #d2d2d2;
|
||||
}
|
||||
|
||||
.warning-timeout {
|
||||
background-color: #be3728;
|
||||
|
||||
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
border-radius: 50%;
|
||||
|
||||
padding: 0 0.25em;
|
||||
}
|
||||
|
||||
.timetable_stops {
|
||||
@@ -168,30 +167,21 @@ export default defineComponent({
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
gap: 0.25em;
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
.train-status-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.train-badge {
|
||||
padding: 0.15em 0.35em;
|
||||
margin-right: 0.3em;
|
||||
|
||||
font-weight: bold;
|
||||
|
||||
font-size: 0.9em;
|
||||
|
||||
&.twr {
|
||||
background-color: var(--clr-twr);
|
||||
}
|
||||
|
||||
&.skr {
|
||||
background-color: var(--clr-skr);
|
||||
}
|
||||
|
||||
&.offline {
|
||||
background-color: #b83b2d;
|
||||
.train-driver {
|
||||
&.supporter {
|
||||
color: orange;
|
||||
text-shadow: orange 0 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +193,8 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.timetable_warnings {
|
||||
color: black;
|
||||
display: flex;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.timetable_progress {
|
||||
@@ -212,31 +203,6 @@ export default defineComponent({
|
||||
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 {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<div class="filters-options" @keydown.esc="showOptions = false">
|
||||
<div class="bg" v-if="showOptions" @click="showOptions = false"></div>
|
||||
|
||||
<button class="btn--filled btn--image" @click="toggleShowOptions" ref="button">
|
||||
<button class="filter-button btn--filled btn--image" @click="toggleShowOptions" ref="button">
|
||||
<img :src="getIcon('filter2')" alt="Open filters" />
|
||||
{{ $t('options.filters') }} [F]
|
||||
<span class="active-indicator" v-if="currentOptionsActive"></span>
|
||||
</button>
|
||||
|
||||
<transition name="options-anim">
|
||||
@@ -42,29 +43,34 @@
|
||||
|
||||
<h1 class="option-title">{{ $t('options.sort-title') }}</h1>
|
||||
<div class="options_sorters">
|
||||
<div v-for="opt in translatedSorterOptions">
|
||||
<button
|
||||
v-for="opt in translatedSorterOptions"
|
||||
class="sort-option btn--option"
|
||||
:data-selected="opt.id == sorterActive.id"
|
||||
@click="onSorterChange(opt)"
|
||||
>
|
||||
{{ opt.value.toUpperCase() }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h1 class="option-title" v-if="trainFilterList.length != 0">{{ $t('options.filter-title') }}</h1>
|
||||
|
||||
<div class="options_filters">
|
||||
<div v-for="section in Object.keys(TrainFilterSection)">
|
||||
<button
|
||||
class="sort-option btn--option"
|
||||
:data-selected="opt.id == sorterActive.id"
|
||||
@click="onSorterChange(opt)"
|
||||
class="btn--option"
|
||||
v-for="filter in trainFilterList.filter((f) => f.section == section)"
|
||||
:data-inactive="!filter.isActive"
|
||||
@click="onFilterChange(filter)"
|
||||
>
|
||||
{{ opt.value.toUpperCase() }}
|
||||
{{ $t(`options.filter-${filter.id}`) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="option-title" v-if="trainFilterList.length != 0">{{ $t('options.filter-title') }}</h1>
|
||||
<div class="options_filters">
|
||||
<div class="filter-option" v-for="filter in trainFilterList">
|
||||
<button class="btn--option" :data-disabled="!filter.isActive" @click="onFilterChange(filter)">
|
||||
{{ $t(`options.filter-${filter.id}`) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="filter-actions">
|
||||
<button class="btn--action" @click="clearAllFilters">{{ $t('options.filter-clear') }}</button>
|
||||
<button class="btn--action" @click="resetAllFilters">{{ $t('options.filter-reset') }}</button>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div></div>
|
||||
<button class="btn--action" @click="resetAllFilters">{{ $t('options.filter-reset') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,9 +82,10 @@
|
||||
import { defineComponent, inject, PropType } from 'vue';
|
||||
import imageMixin from '../../mixins/imageMixin';
|
||||
import keyMixin from '../../mixins/keyMixin';
|
||||
import { TrainFilter } from '../../types/Trains/TrainOptionsTypes';
|
||||
import ActionButton from '../Global/ActionButton.vue';
|
||||
import SelectBox from '../Global/SelectBox.vue';
|
||||
import { TrainFilterSection } from '../../scripts/enums/TrainFilterType';
|
||||
import { TrainFilter } from '../../scripts/interfaces/Trains/TrainFilter';
|
||||
|
||||
export default defineComponent({
|
||||
components: { SelectBox, ActionButton },
|
||||
@@ -89,11 +96,18 @@ export default defineComponent({
|
||||
type: Array as PropType<Array<string>>,
|
||||
required: true,
|
||||
},
|
||||
|
||||
currentOptionsActive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
showOptions: false,
|
||||
lastSelectedFilter: null as TrainFilter | null,
|
||||
TrainFilterSection,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -136,7 +150,11 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
onFilterChange(filter: TrainFilter) {
|
||||
// if (this.lastSelectedFilter?.id === filter.id)
|
||||
// this.trainFilterList.forEach((tf) => (tf.isActive = filter.id === tf.id));
|
||||
|
||||
filter.isActive = !filter.isActive;
|
||||
this.lastSelectedFilter = filter;
|
||||
},
|
||||
|
||||
clearAllFilters() {
|
||||
@@ -172,13 +190,24 @@ export default defineComponent({
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
.options_sorters {
|
||||
display: flex;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.options_filters > div {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
gap: 0.5em;
|
||||
|
||||
button {
|
||||
color: white;
|
||||
width: 100%;
|
||||
color: springgreen;
|
||||
font-weight: bold;
|
||||
|
||||
&[data-disabled='true'] {
|
||||
color: #888;
|
||||
&[data-inactive='true'] {
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,7 +219,7 @@ export default defineComponent({
|
||||
|
||||
margin-top: 1em;
|
||||
|
||||
button {
|
||||
> * {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,15 @@
|
||||
<template>
|
||||
<div class="train-schedule" @click="toggleShowState">
|
||||
<div class="train-stock">
|
||||
<ul class="stock-list">
|
||||
<li>
|
||||
<img class="train-image" :src="train.locoURL" alt="loco" @error="onImageError" />
|
||||
<div>{{ train.locoType }}</div>
|
||||
</li>
|
||||
<StockList :trainStockList="train.stockList" />
|
||||
|
||||
<li v-if="train.locoType.startsWith('EN')">
|
||||
<img :src="train.locoURL.replace('rb', 's')" @error="onImageError" alt="" />
|
||||
<div>{{ train.locoType }}S</div>
|
||||
<!-- <div class="train-stock"> -->
|
||||
<!-- <ul>
|
||||
<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 v-if="train.locoType.startsWith('EN71')">
|
||||
<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>
|
||||
</ul> -->
|
||||
<!-- </div> -->
|
||||
|
||||
<div class="schedule-wrapper" v-if="train.timetableData">
|
||||
<ul class="stop_list">
|
||||
@@ -60,9 +37,7 @@
|
||||
<b>{{ stop.stopNameRAW }} </b>: <span v-html="stop.comments"></span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
v-if="stop.departureLine == train.timetableData!.followingStops[i + 1].arrivalLine && !/sbl/gi.test(stop.departureLine!)"
|
||||
>
|
||||
<span v-if="stop.departureLine == train.timetableData!.followingStops[i + 1].arrivalLine && !/sbl/gi.test(stop.departureLine!)">
|
||||
{{ stop.departureLine }}
|
||||
</span>
|
||||
|
||||
@@ -89,10 +64,13 @@ import dateMixin from '../../mixins/dateMixin';
|
||||
import imageMixin from '../../mixins/imageMixin';
|
||||
import Train from '../../scripts/interfaces/Train';
|
||||
import TrainStop from '../../scripts/interfaces/TrainStop';
|
||||
import { useStore } from '../../store/store';
|
||||
import StopDate from '../Global/StopDate.vue';
|
||||
import TrainThumbnail from '../Global/TrainThumbnail.vue';
|
||||
import StockList from '../Global/StockList.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { StopDate },
|
||||
components: { StopDate, TrainThumbnail, StockList },
|
||||
props: {
|
||||
train: {
|
||||
type: Object as PropType<Train>,
|
||||
@@ -106,6 +84,8 @@ export default defineComponent({
|
||||
|
||||
setup(props) {
|
||||
return {
|
||||
store: useStore(),
|
||||
|
||||
lastConfirmed: computed(() => {
|
||||
return props.train.timetableData!.followingStops.findIndex(
|
||||
(stop, i, stops) => stop.confirmed && !stops[i + 1]?.confirmed && !stops[i + 1]?.stopped
|
||||
@@ -142,8 +122,7 @@ export default defineComponent({
|
||||
end: stop.terminatesHere,
|
||||
delayed: stop.departureDelay > 0,
|
||||
sbl: /sbl/gi.test(stop.stopName),
|
||||
[stop.stopType.replaceAll(', ', '-')]:
|
||||
stop.stopType.match(new RegExp('ph|pm|pt')) && !stop.confirmed && !stop.beginsHere,
|
||||
[stop.stopType.replaceAll(', ', '-')]: stop.stopType.match(new RegExp('ph|pm|pt')) && !stop.confirmed && !stop.beginsHere,
|
||||
'minor-stop-active': this.activeMinorStops.includes(index),
|
||||
'last-confirmed': index == this.lastConfirmed && !stop.terminatesHere,
|
||||
};
|
||||
@@ -176,31 +155,7 @@ $stopNameClr: #22a8d1;
|
||||
}
|
||||
|
||||
.train-schedule {
|
||||
padding: 0 0.25em;
|
||||
}
|
||||
|
||||
.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;
|
||||
max-width: 320px;
|
||||
}
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
.schedule-wrapper {
|
||||
|
||||
@@ -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>
|
||||
@@ -2,28 +2,28 @@
|
||||
<div class="train-table">
|
||||
<transition name="anim" mode="out-in">
|
||||
<div :key="store.dataStatuses.trains">
|
||||
<Loading v-if="trains.length == 0 && store.dataStatuses.trains == 0" />
|
||||
<div class="table-info" v-if="store.isOffline">
|
||||
{{ $t('app.offline') }}
|
||||
</div>
|
||||
|
||||
<div class="table-info no-trains" v-if="trains.length == 0 && store.dataStatuses.trains != 0">
|
||||
<Loading v-else-if="trains.length == 0 && store.dataStatuses.trains == 0" />
|
||||
|
||||
<div class="table-info no-trains" v-else-if="trains.length == 0 && store.dataStatuses.trains != 0">
|
||||
{{ $t('trains.no-trains') }}
|
||||
</div>
|
||||
|
||||
<div class="timeouts-warning" v-if="trainNumbersWithTimeouts.length != 0">
|
||||
<b class="warning-timeout">?</b>
|
||||
{{ $t('trains.timeout') }}
|
||||
</div>
|
||||
|
||||
<ul class="train-list">
|
||||
<transition-group name="list-anim" tag="ul" class="train-list" v-else>
|
||||
<li
|
||||
class="train-row"
|
||||
v-for="train in currentTrains"
|
||||
:key="train.trainId"
|
||||
@click.stop="selectModalTrain(train.trainId)"
|
||||
@keydown.enter="selectModalTrain(train.trainId)"
|
||||
tabindex="0"
|
||||
@click.stop="selectModalTrain(train.trainId, $event.currentTarget)"
|
||||
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
|
||||
>
|
||||
<TrainInfo :train="train" />
|
||||
</li>
|
||||
</ul>
|
||||
</transition-group>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@@ -67,18 +67,9 @@ export default defineComponent({
|
||||
id: string | number;
|
||||
dir: number;
|
||||
},
|
||||
distanceLimitExceeded: computed(
|
||||
() => props.trains.findIndex(({ timetableData }) => timetableData && timetableData.routeDistance > 200) != -1
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
trainNumbersWithTimeouts() {
|
||||
return this.store.trainList.filter((train) => train.isTimeout).map((train) => train.trainNo);
|
||||
},
|
||||
},
|
||||
|
||||
activated() {
|
||||
const query = this.$route.query;
|
||||
if (query.trainNo && query.driverName) {
|
||||
@@ -94,6 +85,7 @@ export default defineComponent({
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/responsive.scss';
|
||||
@import '../../styles/animations.scss';
|
||||
|
||||
.anim {
|
||||
&-enter-from,
|
||||
@@ -154,7 +146,7 @@ img.train-image {
|
||||
|
||||
.train {
|
||||
&-list {
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
|
||||
@include smallScreen() {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,28 +1,46 @@
|
||||
import { JournalFilterType } from "../../scripts/enums/JournalFilterType";
|
||||
import { JournalTimetableFilter } from "../../types/Journal/JournalTimetablesTypes";
|
||||
import { JournalFilterSection, JournalFilterType } from '../../scripts/enums/JournalFilterType';
|
||||
import { JournalFilter } from '../../scripts/types/JournalTimetablesTypes';
|
||||
|
||||
export const journalTimetableFilters: JournalTimetableFilter[] = [
|
||||
export const journalTimetableFilters: JournalFilter[] = [
|
||||
{
|
||||
id: JournalFilterType.all,
|
||||
filterSection: 'timetable-status',
|
||||
id: JournalFilterType.ALL,
|
||||
filterSection: JournalFilterSection.TIMETABLE_STATUS,
|
||||
isActive: true,
|
||||
},
|
||||
|
||||
{
|
||||
id: JournalFilterType.active,
|
||||
filterSection: 'timetable-status',
|
||||
id: JournalFilterType.ACTIVE,
|
||||
filterSection: JournalFilterSection.TIMETABLE_STATUS,
|
||||
isActive: false,
|
||||
},
|
||||
|
||||
{
|
||||
id: JournalFilterType.fulfilled,
|
||||
filterSection: 'timetable-status',
|
||||
id: JournalFilterType.FULFILLED,
|
||||
filterSection: JournalFilterSection.TIMETABLE_STATUS,
|
||||
isActive: false,
|
||||
},
|
||||
|
||||
{
|
||||
id: JournalFilterType.abandoned,
|
||||
filterSection: 'timetable-status',
|
||||
id: JournalFilterType.ABANDONED,
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,33 +1,58 @@
|
||||
import { TrainFilterType } from '../../scripts/enums/TrainFilterType';
|
||||
import { TrainFilter } from '../../types/Trains/TrainOptionsTypes';
|
||||
import { TrainFilterSection, TrainFilterType } from '../../scripts/enums/TrainFilterType';
|
||||
import { TrainFilter } from '../../scripts/interfaces/Trains/TrainFilter';
|
||||
|
||||
export const trainFilters: TrainFilter[] = [
|
||||
{
|
||||
id: TrainFilterType.twr,
|
||||
section: TrainFilterSection.TRAIN_TYPE,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: TrainFilterType.skr,
|
||||
section: TrainFilterSection.TRAIN_TYPE,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: TrainFilterType.common,
|
||||
section: TrainFilterSection.TRAIN_TYPE,
|
||||
isActive: true,
|
||||
},
|
||||
|
||||
{
|
||||
id: TrainFilterType.passenger,
|
||||
section: TrainFilterSection.TIMETABLE_TYPE,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: TrainFilterType.freight,
|
||||
section: TrainFilterSection.TIMETABLE_TYPE,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: TrainFilterType.other,
|
||||
section: TrainFilterSection.TIMETABLE_TYPE,
|
||||
isActive: true,
|
||||
},
|
||||
|
||||
{
|
||||
id: TrainFilterType.withComments,
|
||||
section: TrainFilterSection.COMMENTS,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: TrainFilterType.comments,
|
||||
id: TrainFilterType.noComments,
|
||||
section: TrainFilterSection.COMMENTS,
|
||||
isActive: true,
|
||||
},
|
||||
|
||||
{
|
||||
id: TrainFilterType.withTimetable,
|
||||
section: TrainFilterSection.TIMETABLE,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: TrainFilterType.noTimetable,
|
||||
section: TrainFilterSection.TIMETABLE,
|
||||
isActive: true,
|
||||
},
|
||||
];
|
||||
@@ -37,6 +62,10 @@ export const sorterOptions = [
|
||||
id: 'distance',
|
||||
value: 'kilometraż',
|
||||
},
|
||||
{
|
||||
id: 'id',
|
||||
value: 'id rozkładu',
|
||||
},
|
||||
{
|
||||
id: 'progress',
|
||||
value: 'przebyta trasa',
|
||||
|
||||
@@ -1,41 +1,38 @@
|
||||
{
|
||||
"optionSections": ["reality", "package-access", "access", "control", "addons", "blockades", "signals", "status"],
|
||||
|
||||
"options": [
|
||||
{
|
||||
"id": "default",
|
||||
"name": "default",
|
||||
"iconName": "td2",
|
||||
"section": "access",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"id": "not-default",
|
||||
"name": "notDefault",
|
||||
"iconName": "",
|
||||
"section": "access",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"id": "real",
|
||||
"name": "real",
|
||||
"iconName": "lock",
|
||||
"section": "access",
|
||||
"section": "reality",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"id": "fictional",
|
||||
"name": "fictional",
|
||||
"iconName": "user",
|
||||
"section": "access",
|
||||
"section": "reality",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"id": "default",
|
||||
"name": "default",
|
||||
"section": "package-access",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"id": "not-default",
|
||||
"name": "notDefault",
|
||||
"section": "package-access",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"id": "non-public",
|
||||
"name": "nonPublic",
|
||||
"iconName": "user",
|
||||
"section": "access",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
@@ -43,7 +40,6 @@
|
||||
{
|
||||
"id": "unavailable",
|
||||
"name": "unavailable",
|
||||
"iconName": "user",
|
||||
"section": "access",
|
||||
"value": false,
|
||||
"defaultValue": false
|
||||
@@ -51,7 +47,6 @@
|
||||
{
|
||||
"id": "abandoned",
|
||||
"name": "abandoned",
|
||||
"iconName": "user",
|
||||
"section": "access",
|
||||
"value": false,
|
||||
"defaultValue": false
|
||||
@@ -59,7 +54,6 @@
|
||||
{
|
||||
"id": "SPK",
|
||||
"name": "SPK",
|
||||
"iconName": "SPK",
|
||||
"section": "control",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
@@ -67,7 +61,6 @@
|
||||
{
|
||||
"id": "SCS",
|
||||
"name": "SCS",
|
||||
"iconName": "SCS",
|
||||
"section": "control",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
@@ -75,15 +68,21 @@
|
||||
{
|
||||
"id": "SPE",
|
||||
"name": "SPE",
|
||||
"iconName": "SPE",
|
||||
"section": "control",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
},
|
||||
|
||||
{
|
||||
"id": "SPK-M",
|
||||
"name": "mechaniczne+SPK",
|
||||
"section": "control",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"id": "manual",
|
||||
"name": "ręczne",
|
||||
"iconName": "ręczne",
|
||||
"id": "SCS-M",
|
||||
"name": "mechaniczne+SCS",
|
||||
"section": "control",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
@@ -91,7 +90,27 @@
|
||||
{
|
||||
"id": "mechanical",
|
||||
"name": "mechaniczne",
|
||||
"iconName": "mechaniczne",
|
||||
"section": "control",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"id": "SPK-R",
|
||||
"name": "ręczne+SPK",
|
||||
"section": "control",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"id": "SCS-R",
|
||||
"name": "ręczne+SCS",
|
||||
"section": "control",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"id": "manual",
|
||||
"name": "ręczne",
|
||||
"section": "control",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
@@ -99,23 +118,34 @@
|
||||
{
|
||||
"id": "SUP",
|
||||
"name": "SUP",
|
||||
"iconName": "SUP",
|
||||
"section": "control",
|
||||
"section": "addons",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"id": "noSUP",
|
||||
"name": "noSUP",
|
||||
"section": "addons",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"id": "SBL",
|
||||
"name": "SBL",
|
||||
"iconName": "SBL",
|
||||
"section": "routes",
|
||||
"section": "blockades",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"id": "PBL",
|
||||
"name": "PBL",
|
||||
"section": "blockades",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"id": "modern",
|
||||
"name": "współczesna",
|
||||
"iconName": "współczesna",
|
||||
"section": "signals",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
@@ -123,7 +153,6 @@
|
||||
{
|
||||
"id": "semaphores",
|
||||
"name": "kształtowa",
|
||||
"iconName": "kształtowa",
|
||||
"section": "signals",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
@@ -131,7 +160,6 @@
|
||||
{
|
||||
"id": "mixed",
|
||||
"name": "mieszana",
|
||||
"iconName": "mieszana",
|
||||
"section": "signals",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
@@ -139,7 +167,6 @@
|
||||
{
|
||||
"id": "historical",
|
||||
"name": "historyczna",
|
||||
"iconName": "historyczna",
|
||||
"section": "signals",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
@@ -148,7 +175,6 @@
|
||||
{
|
||||
"id": "free",
|
||||
"name": "free",
|
||||
"iconName": "",
|
||||
|
||||
"section": "status",
|
||||
"value": false,
|
||||
@@ -157,7 +183,6 @@
|
||||
{
|
||||
"id": "occupied",
|
||||
"name": "occupied",
|
||||
"iconName": "",
|
||||
|
||||
"section": "status",
|
||||
"value": true,
|
||||
@@ -166,7 +191,6 @@
|
||||
{
|
||||
"id": "endingStatus",
|
||||
"name": "endingStatus",
|
||||
"iconName": "",
|
||||
|
||||
"section": "status",
|
||||
"value": true,
|
||||
@@ -175,7 +199,6 @@
|
||||
{
|
||||
"id": "afkStatus",
|
||||
"name": "afkStatus",
|
||||
"iconName": "",
|
||||
|
||||
"section": "status",
|
||||
"value": true,
|
||||
@@ -184,7 +207,6 @@
|
||||
{
|
||||
"id": "noSpaceStatus",
|
||||
"name": "noSpaceStatus",
|
||||
"iconName": "",
|
||||
|
||||
"section": "status",
|
||||
"value": true,
|
||||
@@ -193,7 +215,6 @@
|
||||
{
|
||||
"id": "unavailableStatus",
|
||||
"name": "unavailableStatus",
|
||||
"iconName": "",
|
||||
|
||||
"section": "status",
|
||||
"value": true,
|
||||
@@ -254,7 +275,6 @@
|
||||
{
|
||||
"id": "include-selected",
|
||||
"name": "include-selected",
|
||||
"iconName": "",
|
||||
"section": "mode",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
@@ -262,7 +282,6 @@
|
||||
{
|
||||
"id": "save",
|
||||
"name": "save",
|
||||
"iconName": "",
|
||||
"section": "mode",
|
||||
"value": true,
|
||||
"defaultValue": true
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"general": {
|
||||
"and": " and ",
|
||||
"refresh": "REFRESH",
|
||||
"TWR": "High risk freight train",
|
||||
"SKR": "Train with exceeded gauge"
|
||||
},
|
||||
"app": {
|
||||
"sceneries": "SCENERIES",
|
||||
"trains": "TRAINS",
|
||||
@@ -8,15 +14,21 @@
|
||||
"error": "An error occured while loading data!",
|
||||
"no-result": "No results for current search!",
|
||||
"migration-warning": "Stacjownik services will be unavailable 2/06/2022 between 1-3am (CEST time) due to the migration of API hostings!",
|
||||
"migration-confirm": "Roger that!"
|
||||
"migration-confirm": "Roger that!",
|
||||
"offline": "App is in the offline mode!"
|
||||
},
|
||||
"footer": {
|
||||
"discord": "Stacjownik Discord server"
|
||||
},
|
||||
"update": {
|
||||
"title": "New Stacjownik version is available!",
|
||||
"title": "New version of the app is available!",
|
||||
"paragraph1": "Enjoy the application and may the green signal be with you!",
|
||||
"release-link": "Click here to browse version changelog (GitHub)",
|
||||
"confirm-button": "Understood!"
|
||||
"confirm-button": "UPDATE NOW",
|
||||
"later-button": "LATER"
|
||||
},
|
||||
"data-status": {
|
||||
"S1-offline": "<b>S1 signal</b> <br> The app is working in offline mode!",
|
||||
"S1a-connection": "<b>S1a signal</b> <br> Cannot connect with Stacjownik API service!",
|
||||
"S1a-sceneries": "<b>S1a signal</b> <br> Cannot load online stations data!",
|
||||
"S2": "<b>S2 signal</b> <br> All data loaded successfully!",
|
||||
@@ -28,7 +40,7 @@
|
||||
"desc": {
|
||||
"control-type": "Control type: ",
|
||||
"signals-type": "Signals type: ",
|
||||
"SBL": "This scenery has automatic line blockade system on following routes: ",
|
||||
"SBL": "This scenery has automatic block signalling (ABS/SBL) system on following routes: ",
|
||||
"SUP": "Requires the SUP application (level crossing remote control simulator)",
|
||||
"TWB-all": "This scenery has two-way route blockade on all routes",
|
||||
"TWB-routes": "This scenery has two-way route blockade on following routes: ",
|
||||
@@ -87,47 +99,75 @@
|
||||
"search-dispatcher": "Dispatcher name",
|
||||
"search-station": "Scenery name",
|
||||
"search-author": "Timetable author name",
|
||||
"search-date": "Timetable date (CEST / GMT+2)",
|
||||
"search-issuedFrom": "Origin scenery name",
|
||||
"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-speed": "speed",
|
||||
"sort-length": "length",
|
||||
"sort-distance": "distance",
|
||||
"sort-routeDistance": "route distance",
|
||||
"sort-timetable": "train no.",
|
||||
"sort-progress": "route progress",
|
||||
"sort-delay": "current delay",
|
||||
"sort-id": "timetable id",
|
||||
|
||||
"sort-total-stops": "total stops",
|
||||
"sort-allStopsCount": "total stops",
|
||||
"sort-beginDate": "date",
|
||||
"sort-timetableId": "timetable ID",
|
||||
"sort-timestampFrom": "date",
|
||||
"sort-duration": "duration",
|
||||
|
||||
"filter-comments": "COMMENTS",
|
||||
"filter-twr": "TWR",
|
||||
"filter-skr": "SKR",
|
||||
"filter-noComments": "NO COMMENTS",
|
||||
"filter-withComments": "COMMENTS",
|
||||
"filter-twr": "HIGH RISK CARGO",
|
||||
"filter-skr": "EXCEEDED GAUGE",
|
||||
"filter-twr-skr": "ALL TYPES",
|
||||
"filter-common": "NO WARNINGS",
|
||||
"filter-passenger": "PASSENGER",
|
||||
"filter-freight": "FREIGHT",
|
||||
"filter-other": "OTHER",
|
||||
"filter-noTimetable": "NO TIMETABLE",
|
||||
"filter-withTimetable": "TIMETABLE",
|
||||
|
||||
"filter-reset": "RESET FILTERS",
|
||||
"filter-clear": "CLEAR FILTERS",
|
||||
|
||||
"filter-section-timetable-status": "TIMETABLE STATUS",
|
||||
"filter-section-twrskr": "WARNINGS",
|
||||
|
||||
"filter-all": "ALL ENTRIES",
|
||||
"filter-abandoned": "ABANDONED",
|
||||
"filter-fulfilled": "FULFILLED",
|
||||
"filter-active": "ACTIVE"
|
||||
},
|
||||
"filters": {
|
||||
"desc": " • Left mouse click: select / unselect chosen filter <br /> • Double left click: unselect all filters but chosen from a <b class='text--primary'>group</b> <br /> • <span style='color: coral'>RESET</span>: reset all filters from a <b class='text--primary'>group</b>",
|
||||
|
||||
"sections": {
|
||||
"quick": "QUICK FILTERS",
|
||||
"reality": "SCENERY REALITY",
|
||||
"package-access": "IN-GAME AVAILABILITY",
|
||||
"access": "GENERAL AVAILABILITY",
|
||||
"control": "CONTROLS",
|
||||
"signals": "SIGNALLING",
|
||||
"addons": "ADDITIONAL PROGRAMS",
|
||||
"blockades": "BLOCK SIGNALLING",
|
||||
"status": "ONLINE STATUS"
|
||||
},
|
||||
|
||||
"all-available": "ALL AVAILABLE",
|
||||
"all-free": "CURRENTLY FREE",
|
||||
|
||||
"endingStatus": "ENDS SOON",
|
||||
"afkStatus": "AFK",
|
||||
"noSpaceStatus": "NO SPACE",
|
||||
"unavailableStatus": "UNAVAILABLE",
|
||||
|
||||
"title": "STATION FILTER",
|
||||
"default": "DEFAULT",
|
||||
"not-default": "OTHER",
|
||||
"title": "STATION FILTERS",
|
||||
"default": "IN-GAME",
|
||||
"not-default": "ADDITIONAL",
|
||||
"real": "REAL",
|
||||
"fictional": "FICTIONAL",
|
||||
"unavailable": "UNSUPPORTED",
|
||||
@@ -135,12 +175,22 @@
|
||||
"abandoned": "ABANDONED",
|
||||
|
||||
"SPK": "SPK",
|
||||
"SPK-R": "SPK + MANUAL",
|
||||
"SPK-M": "SPK + MECH.",
|
||||
"SCS": "SCS",
|
||||
"SCS-R": "SCS + MANUAL",
|
||||
"SCS-M": "SCS + MECH.",
|
||||
"SPE": "SPE",
|
||||
|
||||
"manual": "MANUAL",
|
||||
"mechanical": "MECHANICAL",
|
||||
"SUP": "SUP",
|
||||
"SBL": "SBL",
|
||||
|
||||
"SUP": "SUP (RASP-UZK)",
|
||||
"noSUP": "WITHOUT SUP",
|
||||
|
||||
"SBL": "AUTOMATIC (SBL)",
|
||||
"PBL": "SEMIAUTOMATIC (PBL)",
|
||||
|
||||
"modern": "MODERN",
|
||||
"semaphores": "SEMAPHORES",
|
||||
"mixed": "MIXED",
|
||||
@@ -161,7 +211,7 @@
|
||||
"hour": "h",
|
||||
"no-limit": "NO LIMIT",
|
||||
"include-selected": "INCLUDE SELECTED",
|
||||
"save": "SAVE FILTERS",
|
||||
"save": "REMEMBER FILTERS",
|
||||
"reset": "RESET FILTERS",
|
||||
"close": "CLOSE FILTERS"
|
||||
},
|
||||
@@ -173,9 +223,11 @@
|
||||
"dispatcher-lvl": "Dispatcher\nlevel",
|
||||
"routes": "Routes\ndouble / single",
|
||||
"general": "General info",
|
||||
"users": "Drivers online",
|
||||
"spawns": "Spawns online",
|
||||
"timetables": "Active timetables",
|
||||
"user": "Drivers online",
|
||||
"spawn": "Spawns online",
|
||||
"timetableAll": "Active timetables",
|
||||
"timetableConfirmed": "Confirmed timetables",
|
||||
"timetableUnconfirmed": "Unconfirmed timetables",
|
||||
"no-stations": "No stations to show here!",
|
||||
"scenery-search": "Search for scenery..."
|
||||
},
|
||||
@@ -226,6 +278,7 @@
|
||||
"title": "DISPATCHER HISTORY",
|
||||
"loading": "Loading dispatcher history data...",
|
||||
"no-history": "No dispatcher history found!",
|
||||
"data-refreshed-at": "Data refreshed at",
|
||||
|
||||
"section-timetables": "TIMETABLES",
|
||||
"section-dispatchers": "DISPATCHERS",
|
||||
@@ -235,7 +288,7 @@
|
||||
|
||||
"route-length": "Route length:",
|
||||
"station-count": "Stations:",
|
||||
"dispatcher-name": "Created by",
|
||||
"dispatcher-name": "Author",
|
||||
"timetable-day": "Timetable created at",
|
||||
"timetable-active": "ACTIVE",
|
||||
"timetable-fulfilled": "FULFILLED",
|
||||
@@ -243,23 +296,58 @@
|
||||
|
||||
"online-since": "ONLINE SINCE",
|
||||
"duty-lasted": "The duty lasted",
|
||||
"minutes": "{minutes} mins",
|
||||
"hours": "{hours}h {minutes} mins",
|
||||
|
||||
"stock-info": "STOCK INFO",
|
||||
"hours": "{value} hour | {value} hours",
|
||||
"minutes": "{value} min | {value} mins",
|
||||
"seconds": "{value} s",
|
||||
|
||||
"stock-info": "EXTRA INFO",
|
||||
"stock-length": "Length",
|
||||
"stock-mass": "Mass",
|
||||
"stock-max-speed": "Maximum registered speed",
|
||||
"stock-max-speed": "Max. speed",
|
||||
|
||||
"load-data": "Load further data...",
|
||||
|
||||
"last-seen-at": "Last seen at",
|
||||
"currently-at": "Currently at",
|
||||
|
||||
"stats-title": "DRIVING STATISTICS OF",
|
||||
|
||||
"stats-timetables": "TIMETABLES",
|
||||
"stats-longest-timetable": "LONGEST TIMETABLE",
|
||||
"stats-avg-timetable": "AVERAGE TIMETABLE LENGTH",
|
||||
"stats-distance": "DISTANCE",
|
||||
"stats-stations": "STATIONS"
|
||||
"stats-stations": "STATIONS",
|
||||
|
||||
"timetable-stats-title": "Daily stats on {date}",
|
||||
"timetable-stats-total": "Issued timetables: {count} (total distance: {distance})",
|
||||
"timetable-stats-longest": "The longest timetable: #{id} (made by {author} for {driver}, distance: {distance})",
|
||||
"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",
|
||||
|
||||
"daily-stats-title": "DAILY STATS",
|
||||
"daily-stats-info": "Today's statistics are unavailable yet!",
|
||||
|
||||
"driver-stats-title": "DRIVER STATS",
|
||||
"driver-stats-info": "Enter a proper nickname into filters [F] to see user's driving statistics!",
|
||||
|
||||
"stats-loading": "Fetching 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": {
|
||||
"users": "PLAYERS ONLINE",
|
||||
@@ -274,22 +362,41 @@
|
||||
"history-btn": "View the dispatcher history",
|
||||
"info-btn": "Return to the scenery view",
|
||||
"authors-title": "Scenery author | Scenery authors",
|
||||
"abbrev": "Station symbol:",
|
||||
"lines-title": "Real lines",
|
||||
"project-title": "Project name",
|
||||
"one-way-routes": "One way routes",
|
||||
"two-way-routes": "Two way routes",
|
||||
|
||||
"option-active-timetables": "Active timetables",
|
||||
"option-timetables-history": "Scenery timetables history",
|
||||
"option-dispatchers-history": "Scenery dispatchers history",
|
||||
"option-timetables-history": "Timetables history",
|
||||
"option-dispatchers-history": "Dispatchers history",
|
||||
|
||||
"timetable-author-title": "Issued by",
|
||||
"timetable-author-unknown": "Author unknown",
|
||||
|
||||
"timetables-history-id": "ID",
|
||||
"timetables-history-number": "Number",
|
||||
"timetables-history-route": "Route",
|
||||
"timetables-history-driver": "Driver",
|
||||
"timetables-history-author": "TT author",
|
||||
"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",
|
||||
"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)",
|
||||
"tablice-link": "Timetable summary board (by Thundo)",
|
||||
|
||||
"bottom-info": "Show full history in the Journal tab"
|
||||
},
|
||||
"availability": {
|
||||
"title": "Availability",
|
||||
@@ -304,7 +411,19 @@
|
||||
"end": "Timetable terminates here",
|
||||
"terminated": "Timetable terminated",
|
||||
"begins": "BEGINS HERE",
|
||||
"terminates": "TERMINATES\nHERE"
|
||||
"terminates": "TERMINATES\nHERE",
|
||||
|
||||
"from": "FROM",
|
||||
"to": "TO",
|
||||
|
||||
"desc-arriving": "The train is not here yet. It's going to come from: {prevStationName} (szlak {prevDepartureLine})",
|
||||
"desc-online": "The train is at the station. It's going to leave to: {nextStationName} (szlak {nextArrivalLine})",
|
||||
"desc-stopped": "The train is at the station and is stopped. It's going to leave towards: {nextStationName} (szlak {nextArrivalLine})",
|
||||
"desc-next-arrival": "Leaves towards: {nextStationName} (szlak {nextArrivalLine})",
|
||||
"desc-departed": "The train is at the station and it's been departed. Leaves towards: {nextStationName} (szlak {nextArrivalLine})",
|
||||
"desc-departed-away": "The train has been departed to: {nextStationName} (szlak {nextArrivalLine})",
|
||||
"desc-end": "The train terminates here",
|
||||
"desc-terminated": "The train has been terminated"
|
||||
},
|
||||
"history": {
|
||||
"title": "TIMETABLE JOURNAL",
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"general": {
|
||||
"and": " oraz ",
|
||||
"refresh": "ODŚWIEŻ",
|
||||
"TWR": "Towar niebezpieczny wysokiego ryzyka",
|
||||
"SKR": "Przekroczona skrajnia"
|
||||
},
|
||||
"app": {
|
||||
"sceneries": "SCENERIE",
|
||||
"trains": "POCIĄGI",
|
||||
@@ -8,17 +14,21 @@
|
||||
"error": "Wystąpił problem z załadowaniem danych!",
|
||||
"no-result": "Brak wyników o podanych kryteriach!",
|
||||
"migration-warning": "Usługi Stacjownika będą niedostępne w godzinach 1:00-3:00 2 czerwca 2022r. z powodu migracji hostingów API!",
|
||||
"migration-confirm": "Przyjąłem!"
|
||||
"migration-confirm": "Przyjąłem!",
|
||||
"offline": "Aplikacja w trybie offline!"
|
||||
},
|
||||
"footer": {
|
||||
"discord": "Serwer Discord Stacjownika"
|
||||
},
|
||||
|
||||
"update": {
|
||||
"title": "Nowa wersja Stacjownika jest dostępna!",
|
||||
"paragraph1": "Miłego korzystania z aplikacji i niech S2 będzie z wami!",
|
||||
"release-link": "Kliknij, aby przejrzeć listę zmian (GitHub)",
|
||||
"confirm-button": "Przyjąłem!"
|
||||
"confirm-button": "ZAKTUALIZUJ",
|
||||
"later-button": "PÓŹNIEJ"
|
||||
},
|
||||
|
||||
"data-status": {
|
||||
"S1-offline": "<b>Sygnał S1</b> <br> Aplikacja działa w trybie offline!",
|
||||
"S1a-connection": "<b>Sygnał S1a</b> <br> Błąd podczas próby połączenia się z API Stacjownika!",
|
||||
"S1a-sceneries": "<b>Sygnał S1a</b> <br> Błąd podczas pobierania danych o sceneriach online!",
|
||||
"S2": "<b>Sygnał S2</b> <br> Pomyślnie załadowano dane!",
|
||||
@@ -89,14 +99,18 @@
|
||||
"search-dispatcher": "Nick dyżurnego",
|
||||
"search-station": "Nazwa scenerii",
|
||||
"search-author": "Nick autora rozkładu jazdy",
|
||||
"search-date": "Data rozkładu jazdy (czas polski)",
|
||||
"search-issuedFrom": "Sceneria początkowa",
|
||||
"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-total-stops": "stacje",
|
||||
"sort-routeDistance": "kilometraż",
|
||||
"sort-allStopsCount": "stacje",
|
||||
"sort-beginDate": "data",
|
||||
"sort-timetableId": "ID rozkładu",
|
||||
"sort-timestampFrom": "data",
|
||||
"sort-duration": "czas dyżuru",
|
||||
"sort-id": "id rozkładu",
|
||||
|
||||
"sort-mass": "masa",
|
||||
"sort-speed": "prędkość",
|
||||
@@ -106,23 +120,47 @@
|
||||
"sort-delay": "opóźnienie",
|
||||
"sort-comments": "uwagi ekspl.",
|
||||
|
||||
"filter-comments": "UWAGI EKSPLOATACYJNE",
|
||||
"filter-twr": "TWR",
|
||||
"filter-skr": "PRZEKR. SKRAJNIA",
|
||||
"filter-withComments": "UWAGI EKSPLOATACYJNE",
|
||||
"filter-noComments": "BEZ UWAG",
|
||||
"filter-twr": "WYS. RYZYKA",
|
||||
"filter-skr": "SKRAJNIA",
|
||||
"filter-twr-skr": "WSZYSTKIE",
|
||||
"filter-common": "ZWYKŁE",
|
||||
"filter-passenger": "PASAŻERSKIE",
|
||||
"filter-freight": "TOWAROWE",
|
||||
"filter-other": "INNE",
|
||||
"filter-noTimetable": "BEZ RJ",
|
||||
"filter-withTimetable": "ROZKŁAD JAZDY",
|
||||
|
||||
"filter-reset": "ZRESETUJ FILTRY",
|
||||
"filter-clear": "WYŁĄCZ FILTRY",
|
||||
|
||||
"filter-section-timetable-status": "STATUS ROZKŁADU JAZDY",
|
||||
"filter-section-twrskr": "UWAGI",
|
||||
|
||||
"filter-all": "WSZYSTKIE",
|
||||
"filter-abandoned": "PORZUCONE",
|
||||
"filter-fulfilled": "WYPEŁNIONE",
|
||||
"filter-active": "AKTYWNE"
|
||||
},
|
||||
"filters": {
|
||||
"desc": " • Kliknięcie: zaznaczenie / odznaczenie filtru <br /> • Podwójne kliknięcie: odznaczenie reszty filtrów z <b class='text--primary'>grupy</b> <br /> • <span style='color: coral'>RESET</span>: zresetowanie filtrów z <b class='text--primary'>grupy</b>",
|
||||
|
||||
"sections": {
|
||||
"quick": "SZYBKIE FILTRY",
|
||||
"reality": "FIKCYJNOŚĆ SCENERII",
|
||||
"package-access": "DOSTĘPNOŚĆ W PACZCE",
|
||||
"access": "DOSTĘPNOŚĆ OGÓLNA",
|
||||
"control": "TYP STEROWANIA",
|
||||
"signals": "TYP SYGNALIZACJI",
|
||||
"addons": "DODATKOWE PROGRAMY",
|
||||
"blockades": "BLOKADY LINIOWE",
|
||||
"status": "STATUS ONLINE"
|
||||
},
|
||||
|
||||
"all-available": "WSZYSTKIE DOSTĘPNE",
|
||||
"all-free": "WSZYSTKIE WOLNE",
|
||||
|
||||
"endingStatus": "KOŃCZY",
|
||||
"afkStatus": "Z/W",
|
||||
"noSpaceStatus": "BRAK MIEJSCA",
|
||||
@@ -138,18 +176,29 @@
|
||||
"abandoned": "WYCOFANA",
|
||||
|
||||
"SPK": "SPK",
|
||||
"SPK-R": "SPK + RĘCZNE",
|
||||
"SPK-M": "SPK + MECH.",
|
||||
"SCS": "SCS",
|
||||
"SCS-R": "SCS + RĘCZNE",
|
||||
"SCS-M": "SCS + MECH.",
|
||||
"SPE": "SPE",
|
||||
"manual": "RĘCZNE",
|
||||
"SUP": "SUP",
|
||||
"SBL": "SBL",
|
||||
|
||||
"SUP": "SUP (RASP-UZK)",
|
||||
"noSUP": "BEZ SUP",
|
||||
|
||||
"SBL": "SAMOCZYNNA",
|
||||
"PBL": "PÓŁSAMOCZYNNA",
|
||||
|
||||
"mechanical": "MECHANICZNE",
|
||||
"modern": "WSPÓŁCZESNA",
|
||||
"semaphores": "KSZTAŁTOWA",
|
||||
"mixed": "MIESZANA",
|
||||
"historical": "HISTORYCZNA",
|
||||
|
||||
"free": "WOLNA",
|
||||
"occupied": "ZAJĘTA",
|
||||
|
||||
"sliders": {
|
||||
"min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO",
|
||||
"max-lvl": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
|
||||
@@ -158,27 +207,31 @@
|
||||
"routes-2t-cat": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)",
|
||||
"routes-2t-other": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)"
|
||||
},
|
||||
|
||||
"authors-search": "Szukaj autora (uwzględnia inne filtry)",
|
||||
"minimum-hours-title": "POKAŻ TYLKO SCENERIE DOSTĘPNE MINIMUM DO:",
|
||||
"now": "TERAZ",
|
||||
"hour": " godz.",
|
||||
"no-limit": "BEZ LIMITU",
|
||||
"include-selected": "POKAŻ ZAZNACZONE",
|
||||
"save": "ZAPISZ FILTRY",
|
||||
"save": "ZAPAMIĘTAJ FILTRY",
|
||||
"reset": "RESETUJ FILTRY",
|
||||
"close": "ZAMKNIJ FILTRY"
|
||||
},
|
||||
"sceneries": {
|
||||
"station": "Stacja",
|
||||
"abbr": "Skrót\nposterunku",
|
||||
"min-lvl": "Min. poziom\ndyżurnego",
|
||||
"status": "Status",
|
||||
"dispatcher": "Dyżurny",
|
||||
"dispatcher-lvl": "Poziom\ndyżurnego",
|
||||
"routes": "Szlaki\n2tor / 1tor",
|
||||
"general": "Informacje\nogólne",
|
||||
"users": "Maszyniści online",
|
||||
"spawns": "Otwarte spawny",
|
||||
"timetables": "Aktywne rozkłady jazdy",
|
||||
"user": "Maszyniści online",
|
||||
"spawn": "Otwarte spawny",
|
||||
"timetableAll": "Aktywne rozkłady jazdy",
|
||||
"timetableConfirmed": "Zatwierdzone rozkłady jazdy",
|
||||
"timetableUnconfirmed": "Niezatwierdzone rozkłady jazdy",
|
||||
"no-stations": "Brak stacji do wyświetlenia!",
|
||||
"scenery-search": "Wyszukaj scenerię..."
|
||||
},
|
||||
@@ -230,6 +283,7 @@
|
||||
"title": "HISTORIA DYŻURÓW",
|
||||
"loading": "Ładowanie historii dyżurów...",
|
||||
"no-history": "Brak historii dyżurów dla tej scenerii!",
|
||||
"data-refreshed-at": "Dane odświeżone o",
|
||||
|
||||
"section-timetables": "ROZKŁADY JAZDY",
|
||||
"section-dispatchers": "DYŻURNI",
|
||||
@@ -239,31 +293,64 @@
|
||||
|
||||
"online-since": "ONLINE OD",
|
||||
"duty-lasted": "Dyżur trwał",
|
||||
"minutes": "{minutes} min.",
|
||||
"hours": "{hours} godz. {minutes} min.",
|
||||
"hours": "{value} godz.",
|
||||
"minutes": "{value} min.",
|
||||
"seconds": "{value} sek.",
|
||||
|
||||
"route-length": "Kilometraż:",
|
||||
"station-count": "Stacje:",
|
||||
"dispatcher-name": "Wystawiony przez dyżurnego",
|
||||
"dispatcher-name": "Autor",
|
||||
"timetable-day": "Rozkład z dnia",
|
||||
"timetable-active": "AKTYWNY",
|
||||
"timetable-fulfilled": "WYPEŁNIONY",
|
||||
"timetable-abandoned": "PORZUCONY",
|
||||
|
||||
"stock-info": "INFORMACJE O SKŁADZIE",
|
||||
"stock-info": "DODATKOWE INFORMACJE",
|
||||
"stock-length": "Długość",
|
||||
"stock-mass": "Masa",
|
||||
"stock-max-speed": "Maks. zarejestrowana prędkość",
|
||||
"stock-max-speed": "Prędkość maks.",
|
||||
|
||||
"load-data": "Pobierz dalszą historię...",
|
||||
|
||||
"stats-title": "STATYSTYKI MASZYNISTY",
|
||||
|
||||
"last-seen-at": "Ostatnio widziany na: ",
|
||||
"currently-at": "Obecnie na scenerii: ",
|
||||
|
||||
"stats-timetables": "ROZKŁADY JAZDY",
|
||||
"stats-longest-timetable": "NAJDŁUŻSZY RJ",
|
||||
"stats-avg-timetable": "ŚREDNIA DŁUGOŚĆ RJ",
|
||||
"stats-distance": "DYSTANS",
|
||||
"stats-stations": "STACJE"
|
||||
"stats-stations": "STACJE",
|
||||
|
||||
"timetable-stats-total": "Stworzone rozkłady jazdy: {count} (łączny dystans: {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-dr": "Najaktywniejszy dyżurny: {dispatcher} (stworzył {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",
|
||||
|
||||
"daily-stats-title": "STATYSTYKI DNIA",
|
||||
"daily-stats-info": "Dzisiejsze statystyki nie są jeszcze dostępne!",
|
||||
|
||||
"driver-stats-title": "STATYSTYKI GRACZA",
|
||||
"driver-stats-info": "Wpisz nazwę użytkownika w filtrach [F], aby zobaczyć jego statystyki maszynisty!",
|
||||
|
||||
"stats-loading": "Pobieranie 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": {
|
||||
"users": "GRACZE ONLINE",
|
||||
@@ -276,24 +363,43 @@
|
||||
"no-scenery": "Ups! Ta sceneria nie istnieje!",
|
||||
"return-btn": "Wróć na stronę główną",
|
||||
"history-btn": "Przejdź do widoku historii dyżurnych ruchu",
|
||||
"info-btn": "Wróc do widoku scenerii",
|
||||
"info-btn": "Wróć do widoku scenerii",
|
||||
"authors-title": "Autor scenerii | Autorzy scenerii",
|
||||
"abbrev": "Skrót posterunku:",
|
||||
"lines-title": "Rzeczywiste linie",
|
||||
"project-title": "Projekt",
|
||||
"one-way-routes": "Szlaki jednotorowe",
|
||||
"two-way-routes": "Szlaki dwutorowe",
|
||||
|
||||
"option-active-timetables": "Aktywne rozkłady jazdy",
|
||||
"option-timetables-history": "Historia rozkładów scenerii",
|
||||
"option-dispatchers-history": "Historia dyżurów scenerii",
|
||||
"option-timetables-history": "Historia rozkładów",
|
||||
"option-dispatchers-history": "Historia dyżurów",
|
||||
|
||||
"timetable-author-title": "Wydany przez",
|
||||
"timetable-author-unknown": "Autor nieznany",
|
||||
|
||||
"timetables-history-id": "ID",
|
||||
"timetables-history-number": "Numer",
|
||||
"timetables-history-route": "Trasa",
|
||||
"timetables-history-driver": "Maszynista",
|
||||
"timetables-history-author": "Autor RJ",
|
||||
"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",
|
||||
"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)",
|
||||
"tablice-link": "Tablica informacyjna zbiorcza (autorstwa Thundo)",
|
||||
|
||||
"bottom-info": "Pokaż pełną historię w zakładce Dziennika"
|
||||
},
|
||||
"availability": {
|
||||
"title": "Dostępność",
|
||||
@@ -308,7 +414,19 @@
|
||||
"end": "Koniec rozkładu jazdy",
|
||||
"terminated": "Rozkład jazdy zakończony",
|
||||
"begins": "ROZPOCZYNA\nBIEG",
|
||||
"terminates": "KOŃCZY BIEG"
|
||||
"terminates": "KOŃCZY BIEG",
|
||||
|
||||
"from": "Z",
|
||||
"to": "DO",
|
||||
|
||||
"desc-arriving": "Pociągu nie ma jeszcze na tej scenerii. Przyjedzie z: {prevStationName} (szlak {prevDepartureLine})",
|
||||
"desc-online": "Pociąg jest na tej scenerii. Odjedzie do: {nextStationName} (szlak {nextArrivalLine})",
|
||||
"desc-stopped": "Pociąg jest na tej scenerii i odbywa postój. Odjedzie do: {nextStationName} (szlak {nextArrivalLine})",
|
||||
"desc-next-arrival": "Odjeżdża do: {nextStationName} (szlak {nextArrivalLine})",
|
||||
"desc-departed": "Pociąg jest na tej scenerii i został odprawiony. Odjeżdża do: {nextStationName} (szlak {nextArrivalLine})",
|
||||
"desc-departed-away": "Pociąg został odprawiony i odjechał do: {nextStationName} (szlak {nextArrivalLine})",
|
||||
"desc-end": "Pociąg kończy bieg",
|
||||
"desc-terminated": "Pociąg skończył bieg"
|
||||
},
|
||||
"history": {
|
||||
"title": "DZIENNIK ROZKŁADÓW JAZDY"
|
||||
|
||||
@@ -10,6 +10,8 @@ import { createPinia } from 'pinia';
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: 'pl',
|
||||
legacy: false,
|
||||
warnHtmlMessage: false,
|
||||
fallbackLocale: 'pl',
|
||||
messages: {
|
||||
en: enLang,
|
||||
|
||||
@@ -1,50 +1,77 @@
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
methods: {
|
||||
localeDate(dateString: string, locale: string) {
|
||||
return new Date(dateString).toLocaleDateString(locale == 'pl' ? 'pl-PL' : 'en-GB', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
},
|
||||
|
||||
localeDay(dateString: string, locale: string) {
|
||||
return new Date(dateString).toLocaleDateString(locale == 'pl' ? 'pl-PL' : 'en-GB', {
|
||||
day: 'numeric',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
});
|
||||
},
|
||||
|
||||
localeTime(dateString: string, locale: string) {
|
||||
return new Date(dateString).toLocaleTimeString(locale == 'pl' ? 'pl-PL' : 'en-GB', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
},
|
||||
|
||||
timestampToString(timestamp: number | null) {
|
||||
return timestamp
|
||||
? new Date(timestamp).toLocaleTimeString('pl-PL', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
: '';
|
||||
},
|
||||
|
||||
calculateDuration(timestampMs: number) {
|
||||
const minsTotal = Math.round(timestampMs / 60000);
|
||||
const hoursTotal = Math.floor(minsTotal / 60);
|
||||
const minsInHour = minsTotal % 60;
|
||||
|
||||
return minsTotal > 60
|
||||
? this.$t('journal.hours', { hours: hoursTotal, minutes: minsInHour })
|
||||
: this.$t('journal.minutes', { minutes: minsTotal });
|
||||
},
|
||||
},
|
||||
});
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
methods: {
|
||||
localeDate(dateString: string, locale: string) {
|
||||
return new Date(dateString).toLocaleDateString(locale == 'pl' ? 'pl-PL' : 'en-GB', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
},
|
||||
|
||||
localeDay(dateString: string, locale: string) {
|
||||
return new Date(dateString).toLocaleDateString(locale == 'pl' ? 'pl-PL' : 'en-GB', {
|
||||
day: 'numeric',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
});
|
||||
},
|
||||
|
||||
localeDateTime(dateString: string, locale: string) {
|
||||
return new Date(dateString).toLocaleString(locale == 'pl' ? 'pl-PL' : 'en-GB', {
|
||||
timeStyle: 'short',
|
||||
dateStyle: 'medium'
|
||||
});
|
||||
},
|
||||
|
||||
localeTime(dateString: string, locale: string) {
|
||||
return new Date(dateString).toLocaleTimeString(locale == 'pl' ? 'pl-PL' : 'en-GB', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
},
|
||||
|
||||
stringToDate(dateString?: string) {
|
||||
return dateString ? new Date(dateString) : null;
|
||||
},
|
||||
|
||||
parseDateToTimeString(date: Date | null) {
|
||||
return (
|
||||
date?.toLocaleTimeString('pl-PL', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}) || ''
|
||||
);
|
||||
},
|
||||
|
||||
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);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import { Ref, defineComponent } from 'vue';
|
||||
import { useStore } from '../store/store';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
data() {
|
||||
return {
|
||||
store: useStore(),
|
||||
};
|
||||
@@ -15,15 +15,17 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
methods: {
|
||||
selectModalTrain(trainId: string) {
|
||||
selectModalTrain(trainId: string, target?: EventTarget | null) {
|
||||
this.store.chosenModalTrainId = trainId;
|
||||
document.body.classList.add('no-scroll');
|
||||
if (target) this.store.modalLastClickedTarget = target;
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
this.store.chosenModalTrainId = undefined;
|
||||
|
||||
setTimeout(() => {
|
||||
(this.store.modalLastClickedTarget as any)?.focus();
|
||||
document.body.classList.remove('no-scroll');
|
||||
}, 150);
|
||||
},
|
||||
|
||||
@@ -4,11 +4,17 @@ export default defineComponent({
|
||||
methods: {
|
||||
calculateExpStyle(exp: number, isSupporter = false): string {
|
||||
const bgColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 85%, 50%)`) : '#666';
|
||||
|
||||
const fontColor = exp > 15 || exp == -1 ? 'white' : 'black';
|
||||
const boxShadow = isSupporter ? `box-shadow: 0 0 10px 2px ${bgColor};` : '';
|
||||
|
||||
return `background-color: ${bgColor}; color: ${fontColor}; ${boxShadow}`;
|
||||
|
||||
const fontColor = exp > 14 || exp == -1 ? 'white' : 'black';
|
||||
const boxShadow = isSupporter ? `box-shadow: 0 0 6px 2px ${bgColor};` : '';
|
||||
|
||||
return `background-color: ${bgColor}; color: ${fontColor}; ${boxShadow};`;
|
||||
},
|
||||
|
||||
calculateTextExpStyle(exp: number, isSupporter = false): string {
|
||||
const textColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 75%, 50%)`) : '#666';
|
||||
|
||||
return `color: ${textColor}; ${isSupporter ? 'text-shadow: 0 0 6px ' + textColor : ''};`;
|
||||
},
|
||||
|
||||
statusClasses(occupiedTo: string) {
|
||||
@@ -41,6 +47,6 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
return className;
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useRegisterSW } from 'virtual:pwa-register/vue';
|
||||
|
||||
export default () => {
|
||||
const { needRefresh, updateServiceWorker, offlineReady } = useRegisterSW({
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
return {
|
||||
needRefresh,
|
||||
updateServiceWorker,
|
||||
offlineReady,
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
|
||||
import JournalDispatchersVue from '../components/JournalView/JournalDispatchers.vue';
|
||||
import JournalTimetablesVue from '../components/JournalView/JournalTimetables.vue';
|
||||
import JournalDispatchersVue from '../views/JournalDispatchers.vue';
|
||||
import JournalTimetablesVue from '../views/JournalTimetables.vue';
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
@@ -21,32 +21,23 @@ const routes: Array<RouteRecordRaw> = [
|
||||
},
|
||||
{
|
||||
path: '/journal',
|
||||
name: 'JournalView',
|
||||
component: () => import('../views/JournalView.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'JournalTimetables',
|
||||
component: JournalTimetablesVue,
|
||||
alias: '/timetables',
|
||||
},
|
||||
{
|
||||
path: 'dispatchers',
|
||||
name: 'JournalDispatchers',
|
||||
component: JournalDispatchersVue,
|
||||
props: (route) => ({ sceneryName: route.query.sceneryName, dispatcherName: route.query.dispatcherName }),
|
||||
},
|
||||
{
|
||||
path: 'timetables',
|
||||
name: 'JournalTimetables',
|
||||
component: JournalTimetablesVue,
|
||||
props: (route) => ({
|
||||
trainNo: route.query.trainNo,
|
||||
driverName: route.query.driverName,
|
||||
timetableId: route.query.timetableId,
|
||||
}),
|
||||
},
|
||||
],
|
||||
redirect: '/journal/timetables'
|
||||
},
|
||||
{
|
||||
path: '/journal/timetables',
|
||||
name: 'JournalTimetables',
|
||||
component: JournalTimetablesVue,
|
||||
props: (route) => ({
|
||||
trainNo: route.query.trainNo,
|
||||
driverName: route.query.driverName,
|
||||
timetableId: route.query.timetableId,
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/journal/dispatchers',
|
||||
name: 'JournalDispatchers',
|
||||
component: JournalDispatchersVue,
|
||||
props: (route) => ({ sceneryName: route.query.sceneryName, dispatcherName: route.query.dispatcherName }),
|
||||
},
|
||||
{
|
||||
path: '/:catchAll(.*)',
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import Filter from "../../interfaces/Filter";
|
||||
|
||||
export const filterInitStates: Filter = {
|
||||
default: false,
|
||||
notDefault: false,
|
||||
real: false,
|
||||
fictional: false,
|
||||
SPK: false,
|
||||
SCS: false,
|
||||
SPE: false,
|
||||
SUP: false,
|
||||
noSUP: false,
|
||||
ręczne: false,
|
||||
'ręczne+SPK': false,
|
||||
'ręczne+SCS': false,
|
||||
mechaniczne: false,
|
||||
'mechaniczne+SPK': false,
|
||||
'mechaniczne+SCS': false,
|
||||
współczesna: false,
|
||||
kształtowa: false,
|
||||
historyczna: false,
|
||||
mieszana: false,
|
||||
SBL: false,
|
||||
PBL: false,
|
||||
minLevel: 0,
|
||||
maxLevel: 20,
|
||||
minOneWayCatenary: 0,
|
||||
minOneWay: 0,
|
||||
minTwoWayCatenary: 0,
|
||||
minTwoWay: 0,
|
||||
'include-selected': false,
|
||||
'no-1track': false,
|
||||
'no-2track': false,
|
||||
free: true,
|
||||
occupied: false,
|
||||
ending: false,
|
||||
nonPublic: false,
|
||||
unavailable: true,
|
||||
abandoned: true,
|
||||
afkStatus: false,
|
||||
endingStatus: false,
|
||||
noSpaceStatus: false,
|
||||
unavailableStatus: false,
|
||||
unsignedStatus: false,
|
||||
|
||||
authors: '',
|
||||
|
||||
onlineFromHours: 0,
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export const headIds = ['station', 'min-lvl', 'status', 'dispatcher', 'dispatcher-lvl', 'routes', 'general'] as const;
|
||||
|
||||
export const headIconsIds = ['user', 'spawn', 'timetableAll', 'timetableUnconfirmed', 'timetableConfirmed'] as const;
|
||||
|
||||
export type HeadIdsTypes = typeof headIds[number] | typeof headIconsIds[number];
|
||||