Compare commits

...

39 Commits

Author SHA1 Message Date
Spythere 87631d1f74 Merge pull request #73 from Spythere/development
Wersja 1.20
2023-12-23 14:08:08 +01:00
Spythere 86bb9fcc2e hotfix redirectu do zakładki dziennika 2023-12-22 16:39:42 +01:00
Spythere b85e3bfe1d ukryto ikonę pragotronu 2023-12-22 16:16:19 +01:00
Spythere dd15072813 linting 2023-12-22 16:13:45 +01:00
Spythere 2f8376c996 dodane nowe statystyki dnia; poprawki bugów 2023-12-22 15:46:47 +01:00
Spythere 514723cf74 poprawki typów; ułożenie elementów w karcie filtrów scenerii 2023-12-21 22:16:03 +01:00
Spythere 0995ce15bc poprawki filtrów dziennia DR 2023-12-21 22:11:06 +01:00
Spythere 3b3c3bda31 poprawki wskaźników ładowania 2023-12-21 19:53:34 +01:00
Spythere 2027b85450 lokalne fonty; poprawki offline i cachingu pwa 2023-12-21 19:27:27 +01:00
Spythere 0c6b55146f bump: 1.20.0 2023-12-20 23:56:18 +01:00
Spythere 3c728e3cfa poprawki requestów statystyk; organizacja kodu 2023-12-20 23:55:42 +01:00
Spythere adce339392 statystyki dr (c.d.) + tłumaczenia 2023-12-19 22:11:17 +01:00
Spythere 00a4a840b0 statystyki DR 2023-12-18 16:00:18 +01:00
Spythere 1e705ea496 filtry URL dziennika DR 2023-12-17 19:51:52 +01:00
Spythere e8ed36df16 reaktywne filtry URL w dzienniku RJ 2023-12-17 16:10:13 +01:00
Spythere f4be32aa39 Statystyki DR (wip) 2023-12-16 17:49:54 +01:00
Spythere e0d3d2585d zmiana wyglądu statystyk dzienników 2023-12-14 18:42:13 +01:00
Spythere ebfaf06a44 Merge pull request #72 from Spythere/development
Wersja 1.19.4
2023-12-11 13:02:05 +01:00
Spythere 5a651aedf8 imports hotfix 2023-12-11 12:58:41 +01:00
Spythere b66af014b9 lock sync 2023-12-10 15:29:29 +01:00
Spythere 634c9e1514 bump wersji: 1.19.4 2023-12-10 15:23:54 +01:00
Spythere c4132a9be2 cleanup http c.d. 2023-12-10 15:23:27 +01:00
Spythere 82a9a9165f cleanup http 2023-12-10 15:22:33 +01:00
Spythere fcac03c0a4 Merge pull request #71 from Spythere/development
Wersja 1.19.3
2023-12-10 00:56:21 +01:00
Spythere 39c3cf2329 restrukturyzacja storów 2023-12-09 16:18:23 +01:00
Spythere 59f4a0cb66 zmniejszenie czasu odpytywania z serwera 2023-12-08 20:00:17 +01:00
Spythere e2b42d16a4 poprawki scrollBehavior 2023-12-08 18:55:06 +01:00
Spythere e23663ed28 fix: brak ładowania danych o historii scenerii przy bezpośrednim wejściu z poz. URL 2023-12-08 18:15:49 +01:00
Spythere dc7846c31e bump wersji: 1.19.3 2023-12-08 17:15:16 +01:00
Spythere d875433d56 ulepszone zapamiętywanie zakładek statystyk w dzienniku 2023-12-08 17:14:49 +01:00
Spythere 71e5044cb4 poprawki designu dziennika RJ 2023-12-08 16:20:17 +01:00
Spythere e83aa40f82 aktualizacja endpointu API statystyk 2023-12-08 16:16:14 +01:00
Spythere d1c0e0b898 Merge pull request #70 from Spythere/development
Wersja 1.19.2
2023-12-07 16:29:53 +01:00
Spythere 26a7c69886 bump: 1.19.2 2023-12-07 16:18:21 +01:00
Spythere 0dc2c505db poprawki do braku pokazywania się niezapisanych scenerii 2023-12-07 16:16:06 +01:00
Spythere 188857d335 fix: rozkłady jazdy pokazują się na innych serwerach 2023-12-06 20:29:28 +01:00
Spythere 3dbbb3b4f9 Merge pull request #69 from Spythere/development
Wersja 1.19.1
2023-12-02 23:04:55 +01:00
Spythere 07a77c463b bump 1.19.1 2023-12-02 23:04:24 +01:00
Spythere 1a8e2231dd przejście z WS na komunikację http 2023-12-02 23:02:08 +01:00
74 changed files with 12283 additions and 1691 deletions
-5
View File
@@ -50,11 +50,6 @@
name="twitter:image" name="twitter:image"
content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg" content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg"
/> />
<link
href="https://fonts.googleapis.com/css2?family=Quicksand:wght@500;700&display=swap"
rel="stylesheet"
/>
</head> </head>
<body> <body>
+20 -157
View File
@@ -1,12 +1,12 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.19.0", "version": "1.19.4",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "stacjownik", "name": "stacjownik",
"version": "1.19.0", "version": "1.19.4",
"dependencies": { "dependencies": {
"core-js": "^3.32.2", "core-js": "^3.32.2",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
@@ -14,7 +14,6 @@
"howler": "^2.2.4", "howler": "^2.2.4",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"sass": "^1.67.0", "sass": "^1.67.0",
"socket.io-client": "^4.7.2",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-i18n": "^9.4.1", "vue-i18n": "^9.4.1",
"vue-router": "^4.2.4" "vue-router": "^4.2.4"
@@ -2841,12 +2840,6 @@
"integrity": "sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA==", "integrity": "sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA==",
"dev": true "dev": true
}, },
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==",
"license": "MIT"
},
"node_modules/@surma/rollup-plugin-off-main-thread": { "node_modules/@surma/rollup-plugin-off-main-thread": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
@@ -3857,9 +3850,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001503", "version": "1.0.30001565",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001503.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz",
"integrity": "sha512-Sf9NiF+wZxPfzv8Z3iS0rXM1Do+iOy2Lxvib38glFX+08TCYYYGR5fRJXk4d77C4AYwhUjgYgMsMudbh2TqCKw==", "integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -3874,8 +3867,7 @@
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ]
"license": "CC-BY-4.0"
}, },
"node_modules/chalk": { "node_modules/chalk": {
"version": "2.4.2", "version": "2.4.2",
@@ -4164,6 +4156,7 @@
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "2.1.2" "ms": "2.1.2"
@@ -4406,28 +4399,6 @@
"once": "^1.4.0" "once": "^1.4.0"
} }
}, },
"node_modules/engine.io-client": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz",
"integrity": "sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.11.0",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
"integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/es-abstract": { "node_modules/es-abstract": {
"version": "1.20.5", "version": "1.20.5",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.5.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.5.tgz",
@@ -6341,6 +6312,7 @@
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/muggle-string": { "node_modules/muggle-string": {
@@ -7566,34 +7538,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/socket.io-client": {
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz",
"integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.5.2",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.8.0-beta.0", "version": "0.8.0-beta.0",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz",
@@ -8208,11 +8152,10 @@
"dev": true "dev": true
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "4.4.9", "version": "4.5.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz",
"integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.18.10", "esbuild": "^0.18.10",
"postcss": "^8.4.27", "postcss": "^8.4.27",
@@ -8784,27 +8727,6 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/ws": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml-name-validator": { "node_modules/xml-name-validator": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
@@ -8814,14 +8736,6 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@@ -10808,11 +10722,6 @@
"integrity": "sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA==", "integrity": "sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA==",
"dev": true "dev": true
}, },
"@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"@surma/rollup-plugin-off-main-thread": { "@surma/rollup-plugin-off-main-thread": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
@@ -11513,9 +11422,9 @@
"dev": true "dev": true
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001503", "version": "1.0.30001565",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001503.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz",
"integrity": "sha512-Sf9NiF+wZxPfzv8Z3iS0rXM1Do+iOy2Lxvib38glFX+08TCYYYGR5fRJXk4d77C4AYwhUjgYgMsMudbh2TqCKw==", "integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==",
"dev": true "dev": true
}, },
"chalk": { "chalk": {
@@ -11731,6 +11640,7 @@
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"requires": { "requires": {
"ms": "2.1.2" "ms": "2.1.2"
} }
@@ -11891,23 +11801,6 @@
"once": "^1.4.0" "once": "^1.4.0"
} }
}, },
"engine.io-client": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz",
"integrity": "sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.11.0",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"engine.io-parser": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
"integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ=="
},
"es-abstract": { "es-abstract": {
"version": "1.20.5", "version": "1.20.5",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.5.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.5.tgz",
@@ -13234,7 +13127,8 @@
"ms": { "ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
}, },
"muggle-string": { "muggle-string": {
"version": "0.3.1", "version": "0.3.1",
@@ -14051,26 +13945,6 @@
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true "dev": true
}, },
"socket.io-client": {
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz",
"integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.5.2",
"socket.io-parser": "~4.2.4"
}
},
"socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
}
},
"source-map": { "source-map": {
"version": "0.8.0-beta.0", "version": "0.8.0-beta.0",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz",
@@ -14503,9 +14377,9 @@
"dev": true "dev": true
}, },
"vite": { "vite": {
"version": "4.4.9", "version": "4.5.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz",
"integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==",
"dev": true, "dev": true,
"requires": { "requires": {
"esbuild": "^0.18.10", "esbuild": "^0.18.10",
@@ -14902,23 +14776,12 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true "dev": true
}, },
"ws": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"requires": {}
},
"xml-name-validator": { "xml-name-validator": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
"integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
"dev": true "dev": true
}, },
"xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="
},
"y18n": { "y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+8 -9
View File
@@ -1,6 +1,6 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.19.0", "version": "1.20.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -18,27 +18,26 @@
"howler": "^2.2.4", "howler": "^2.2.4",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"sass": "^1.67.0", "sass": "^1.67.0",
"socket.io-client": "^4.7.2",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-i18n": "^9.4.1", "vue-i18n": "^9.4.1",
"vue-router": "^4.2.4" "vue-router": "^4.2.4"
}, },
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.3.3",
"@types/node": "^20.6.2", "@types/node": "^20.6.2",
"@vite-pwa/assets-generator": "^0.0.10", "@vite-pwa/assets-generator": "^0.0.10",
"@vitejs/plugin-vue": "^4.3.4", "@vitejs/plugin-vue": "^4.3.4",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.4.0",
"axios": "^1.5.0", "axios": "^1.5.0",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^4.4.9", "vite": "^4.4.9",
"vite-plugin-pwa": "^0.16.5", "vite-plugin-pwa": "^0.16.5",
"vue-tsc": "^1.8.11", "vue-tsc": "^1.8.11"
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.4.0",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0",
"@rushstack/eslint-patch": "^1.3.3"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+39 -40
View File
@@ -10,7 +10,7 @@
<main class="app_main"> <main class="app_main">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<keep-alive exclude="JournalView,SceneryView"> <keep-alive exclude="SceneryView">
<component :is="Component" :key="$route.name" /> <component :is="Component" :key="$route.name" />
</keep-alive> </keep-alive>
</router-view> </router-view>
@@ -37,14 +37,15 @@ import { defineComponent, watch } from 'vue';
import Clock from './components/App/Clock.vue'; import Clock from './components/App/Clock.vue';
import packageInfo from '.././package.json'; import packageInfo from '.././package.json';
import { regions } from './data/options.json';
import { useStore } from './store/mainStore'; import { useMainStore } from './store/mainStore';
import StatusIndicator from './components/App/StatusIndicator.vue'; import StatusIndicator from './components/App/StatusIndicator.vue';
import TrainModal from './components/Global/TrainModal.vue'; import TrainModal from './components/Global/TrainModal.vue';
import AppHeader from './components/App/AppHeader.vue'; import AppHeader from './components/App/AppHeader.vue';
import axios from 'axios'; import axios from 'axios';
import StorageManager from './managers/storageManager'; import StorageManager from './managers/storageManager';
import { useApiStore } from './store/apiStore';
import { Status } from './typings/common';
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -56,7 +57,8 @@ export default defineComponent({
data: () => ({ data: () => ({
VERSION: packageInfo.version, VERSION: packageInfo.version,
store: useStore(), store: useMainStore(),
apiStore: useApiStore(),
currentLang: 'pl', currentLang: 'pl',
releaseURL: '', releaseURL: '',
@@ -64,29 +66,10 @@ export default defineComponent({
}), }),
created() { created() {
this.loadLang(); this.init();
this.store.connectToAPI();
this.store.isOffline = !window.navigator.onLine;
window.addEventListener('offline', () => {
this.store.isOffline = true;
this.store.activeData.activeSceneries = [];
this.store.activeData.trains = [];
this.store.activeData.connectedSocketCount = 0;
this.store.setStatuses();
});
window.addEventListener('online', () => {
this.store.isOffline = false;
});
}, },
async mounted() { async mounted() {
this.setReleaseURL();
watch( watch(
() => this.store.blockScroll, () => this.store.blockScroll,
(value) => { (value) => {
@@ -96,23 +79,39 @@ export default defineComponent({
); );
}, },
watch: {
'$route.query.region': {
immediate: true,
handler(regionQuery: string) {
if (regionQuery) {
this.store.region.id =
regions.find(
(reg) =>
reg.id == regionQuery.toLocaleLowerCase() ||
reg.value.toLocaleLowerCase() == regionQuery.toLocaleLowerCase()
)?.id || 'eu';
}
}
}
},
methods: { methods: {
init() {
this.loadLang();
this.setReleaseURL();
this.setupOfflineHandling();
this.apiStore.setupAPI();
},
setupOfflineHandling() {
this.store.isOffline = !window.navigator.onLine;
if (this.store.isOffline) this.handleOfflineMode();
window.addEventListener('offline', this.handleOfflineMode);
window.addEventListener('online', this.handleOnlineMode);
},
handleOfflineMode() {
this.store.isOffline = true;
this.apiStore.stopActiveDataScheduler();
this.apiStore.activeData = undefined;
this.apiStore.dataStatuses.connection = Status.Data.Offline;
},
handleOnlineMode() {
this.store.isOffline = false;
this.apiStore.setupAPI();
},
changeLang(lang: string) { changeLang(lang: string) {
this.$i18n.locale = lang; this.$i18n.locale = lang;
this.currentLang = lang; this.currentLang = lang;
+2 -2
View File
@@ -68,7 +68,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import StatusIndicator from './StatusIndicator.vue'; import StatusIndicator from './StatusIndicator.vue';
import Clock from './Clock.vue'; import Clock from './Clock.vue';
import RegionDropdown from '../Global/RegionDropdown.vue'; import RegionDropdown from '../Global/RegionDropdown.vue';
@@ -84,7 +84,7 @@ export default defineComponent({
setup() { setup() {
return { return {
store: useStore() store: useMainStore()
}; };
}, },
+10 -9
View File
@@ -194,9 +194,9 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { StoreState } from '../../store/typings'; import { useMainStore } from '../../store/mainStore';
import { useStore } from '../../store/mainStore';
import { Status } from '../../typings/common'; import { Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({ export default defineComponent({
data() { data() {
@@ -221,10 +221,11 @@ export default defineComponent({
}, },
setup() { setup() {
const store = useStore(); const store = useMainStore();
const apiStore = useApiStore();
return { return {
dataStatus: store.dataStatuses, dataStatus: apiStore.dataStatuses,
store store
}; };
}, },
@@ -233,15 +234,15 @@ export default defineComponent({
dataStatus: { dataStatus: {
deep: true, deep: true,
handler(statuses: StoreState['dataStatuses']) { handler(statuses: any) {
const connectionStatus = statuses.connection; const connectionStatus = statuses.connection;
const sceneryDataStatus = statuses.sceneries; const sceneryDataStatus = statuses.sceneries;
const trainsDataStatus = statuses.trains; const trainsDataStatus = statuses.trains;
const dispatcherDataStatus = statuses.dispatchers; const dispatcherDataStatus = statuses.dispatchers;
if (this.store.isOffline) { if (connectionStatus == Status.Data.Offline) {
this.setSignalStatus(Status.Data.Initialized); this.setSignalStatus(Status.Data.Offline);
this.indicator.status = Status.Data.Initialized; this.indicator.status = Status.Data.Offline;
this.indicator.message = 'data-status.S1-offline'; this.indicator.message = 'data-status.S1-offline';
return; return;
} }
@@ -292,7 +293,7 @@ export default defineComponent({
this.orangeLight = false; this.orangeLight = false;
this.redBottomLight = false; this.redBottomLight = false;
if (status == Status.Data.Initialized) { if (status == Status.Data.Initialized || status == Status.Data.Offline) {
this.redTopLight = true; this.redTopLight = true;
} }
+2 -2
View File
@@ -12,7 +12,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
export default defineComponent({ export default defineComponent({
emits: ['toggleModal'], emits: ['toggleModal'],
@@ -23,7 +23,7 @@ export default defineComponent({
data() { data() {
return { return {
store: useStore() store: useMainStore()
}; };
}, },
+17 -2
View File
@@ -30,7 +30,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, Ref, ref } from 'vue'; import { defineComponent, Ref, ref } from 'vue';
import { regions as regionsJSON } from '../../data/options.json'; import { regions as regionsJSON } from '../../data/options.json';
import { useStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
interface Item { interface Item {
id: string; id: string;
@@ -41,7 +41,7 @@ interface Item {
export default defineComponent({ export default defineComponent({
data() { data() {
return { return {
store: useStore(), store: useMainStore(),
selectedItemIndex: 0, selectedItemIndex: 0,
listOpen: false listOpen: false
}; };
@@ -59,6 +59,21 @@ export default defineComponent({
'store.region.id': { 'store.region.id': {
handler(regionId) { handler(regionId) {
this.selectedItemIndex = this.regionList.findIndex((reg) => reg.id == regionId); this.selectedItemIndex = this.regionList.findIndex((reg) => reg.id == regionId);
console.log('region id', regionId);
}
},
'$route.query.region': {
immediate: true,
handler(regionQuery: string) {
if (regionQuery) {
this.store.region.id =
regionsJSON.find(
(reg) =>
reg.id == regionQuery.toLocaleLowerCase() ||
reg.value.toLocaleLowerCase() == regionQuery.toLocaleLowerCase()
)?.id || 'eu';
}
} }
} }
}, },
+4 -4
View File
@@ -50,8 +50,8 @@
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import { useStore } from '../../store/mainStore';
import { API } from '../../typings/api'; import { API } from '../../typings/api';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -63,15 +63,15 @@ export default defineComponent({
data() { data() {
return { return {
store: useStore() apiStore: useApiStore()
}; };
}, },
methods: { methods: {
onImageError(event: Event, stockName: string) { onImageError(event: Event, stockName: string) {
const fallbackName = const fallbackName =
Object.keys(this.store.rollingStockData!.info).find((type) => { Object.keys(this.apiStore.rollingStockData!.info).find((type) => {
return this.store.rollingStockData!.info[type as keyof API.RollingStock.Info].find( return this.apiStore.rollingStockData!.info[type as keyof API.RollingStock.Info].find(
(v) => v[0] === stockName.split(':')[0] (v) => v[0] === stockName.split(':')[0]
); );
}) || 'vehicle-unknown'; }) || 'vehicle-unknown';
+5 -5
View File
@@ -16,8 +16,8 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useStore } from '../../store/mainStore';
import { API } from '../../typings/api'; import { API } from '../../typings/api';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -34,7 +34,7 @@ export default defineComponent({
data() { data() {
return { return {
store: useStore(), apiStore: useApiStore(),
isNotFound: false, isNotFound: false,
isLoaded: false isLoaded: false
}; };
@@ -50,11 +50,11 @@ export default defineComponent({
}, },
stockType() { stockType() {
if (!this.store.rollingStockData) return 'vehicle-unknown'; if (!this.apiStore.rollingStockData) return 'vehicle-unknown';
return ( return (
Object.keys(this.store.rollingStockData.info).find((type) => { Object.keys(this.apiStore.rollingStockData.info).find((type) => {
return this.store.rollingStockData?.info[type as keyof API.RollingStock.Info].find( return this.apiStore.rollingStockData?.info[type as keyof API.RollingStock.Info].find(
(v) => v[0] === this.name.split(':')[0] (v) => v[0] === this.name.split(':')[0]
); );
}) || 'vehicle-unknown' }) || 'vehicle-unknown'
-241
View File
@@ -1,241 +0,0 @@
<template>
<section class="daily-stats">
<span :data-active="statsStatus">
<b v-if="statsStatus == Status.Data.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">
&bull;
<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.maxTimetable">
&bull;
<i18n-t keypath="journal.timetable-stats-longest">
<template #id>
<router-link :to="`/journal/timetables?timetableId=${stats.maxTimetable.id}`">
<b>{{ stats.maxTimetable.id }}</b>
</router-link>
</template>
<template #author>
<router-link
:to="`/journal/dispatchers?dispatcherName=${stats.maxTimetable.authorName}`"
>
<b>{{ stats.maxTimetable.authorName }}</b>
</router-link>
</template>
<template #driver>
<b class="text--primary">{{ stats.maxTimetable.driverName }}</b>
</template>
<template #distance>
<b class="text--primary">{{ stats.maxTimetable.routeDistance }} km</b>
</template>
</i18n-t>
</div>
<div v-if="topDispatchers.length == 1">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active-dr">
<template #dispatcher>
<router-link :to="`/journal/dispatchers?dispatcherName=${topDispatchers[0].name}`">
<b>{{ topDispatchers[0].name }}</b>
</router-link>
</template>
<template #count>
<b class="text--primary">
{{ topDispatchers[0].count }}
{{ $t('journal.timetable-count', topDispatchers[0].count) }}
</b>
</template>
</i18n-t>
</div>
<div v-if="topDispatchers.length > 1">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active-dr-many">
<template #dispatchers>
<span v-for="(disp, i) in topDispatchers" :key="i">
<span v-if="i == topDispatchers.length - 1"> {{ $t('general.and') }} </span>
<router-link :to="`/journal/dispatchers?dispatcherName=${disp.name}`">
<b>{{ disp.name }}</b>
</router-link>
<span v-if="i < topDispatchers.length - 2">, </span>
</span>
</template>
<template #count>
<b class="text--primary">
{{ topDispatchers[0].count }}
{{ $t('journal.timetable-count', topDispatchers[0].count) }}
</b>
</template>
</i18n-t>
</div>
<div v-if="stats.longestDuties.length > 0">
&bull;
<i18n-t keypath="journal.timetable-stats-longest-duties">
<template #dispatcher>
<router-link
:to="`/journal/dispatchers?dispatcherName=${stats.longestDuties[0].name}`"
>
<b>{{ stats.longestDuties[0].name }}</b>
</router-link>
</template>
<template #station>{{ stats.longestDuties[0].station }}</template>
<template #duration>
{{ calculateDuration(stats.longestDuties[0].duration) }}
</template>
</i18n-t>
</div>
<div v-if="stats.mostActiveDrivers.length > 0">
&bull;
<i18n-t keypath="journal.timetable-stats-most-active-driver">
<template #driver>
<b class="text--primary">{{ stats.mostActiveDrivers[0].name }}</b>
</template>
<template #distance>
<b class="text--primary">{{ stats.mostActiveDrivers[0].distance.toFixed(2) }} km</b>
</template>
</i18n-t>
</div>
</span>
</span>
</section>
</template>
<script lang="ts">
import axios from 'axios';
import { defineComponent } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import { URLs } from '../../scripts/utils/apiURLs';
import { API } from '../../typings/api';
import { Status } from '../../typings/common';
export default defineComponent({
mixins: [dateMixin],
emits: ['toggleStatsOpen'],
data() {
return {
Status,
statsStatus: Status.Data.Loading,
intervalId: -1,
stats: {} as API.DailyStats.Response
};
},
activated() {
this.startFetchingDailyStats();
this.$emit('toggleStatsOpen', true);
},
deactivated() {
this.stopFetchingDailyStats();
},
computed: {
topDispatchers() {
if (this.stats.mostActiveDispatchers.length == 0) return [];
const maxCount = this.stats.mostActiveDispatchers[0].count;
return this.stats.mostActiveDispatchers.filter((disp) => disp.count === maxCount);
}
},
methods: {
async fetchDailyTimetableStats() {
try {
const res: API.DailyStats.Response = await (
await 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.stats = res;
this.statsStatus = Status.Data.Loaded;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...');
this.statsStatus = Status.Data.Error;
}
},
startFetchingDailyStats() {
this.fetchDailyTimetableStats();
if (this.intervalId != -1) return;
this.intervalId = 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 {
h3 {
text-align: center;
}
}
</style>
@@ -1,165 +0,0 @@
<template>
<div class="stats_container" v-click-outside="() => (cardVisible = false)">
<button class="stats_button" @click="toggleCard">
Statystyki dyżurnego {{ store.dispatcherStatsName }}
</button>
<div class="stats_card" v-if="store.dispatcherStatsName && cardVisible">
<div>
<Loading v-if="!store.dispatcherStatsData" />
<div class="loading" v-else-if="!store.dispatcherStatsData._count._all">
Ten dyżurny nie ma jeszcze szczegółowych statystyk!
</div>
<div v-else>
<h3>STATYSTYKI WYSTAWIONYCH ROZKŁADÓW</h3>
<div class="info-stats" v-if="store.dispatcherStatsData._count._all">
<span class="stat-badge">
<span>LICZBA</span>
<span>{{ store.dispatcherStatsData._count._all }}</span>
</span>
<span class="stat-badge">
<span>SUMA (KM)</span>
<span>{{ store.dispatcherStatsData._sum.routeDistance.toFixed(2) }}km</span>
</span>
<span class="stat-badge">
<span>NAJDŁUŻSZY</span>
<span>{{ store.dispatcherStatsData._max.routeDistance.toFixed(2) }}km</span>
</span>
<span class="stat-badge">
<span>ŚREDNIO</span>
<span>{{ store.dispatcherStatsData._avg.routeDistance.toFixed(2) }}km</span>
</span>
</div>
<h3>OSTATNIE WYSTAWIONE ROZKŁADY</h3>
<div class="last-timetables">
<div class="timetable-row" v-for="timetable in timetables" :key="timetable.id">
#{{ timetable.timetableId }} |
<b>{{ timetable.trainCategoryCode }} {{ timetable.trainNo }}</b> |
{{ timetable.driverName }} ({{ timetable.routeDistance }}km)
<div>{{ timetable.route.replace('|', ' > ') }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import axios from 'axios';
import { defineComponent } from 'vue';
import { URLs } from '../../scripts/utils/apiURLs';
import { useStore } from '../../store/mainStore';
import Loading from '../Global/Loading.vue';
import { API } from '../../typings/api';
export default defineComponent({
components: { Loading },
setup() {
const store = useStore();
return {
store
};
},
data() {
return {
cardVisible: false,
lastDispatcherName: '',
timetables: [] as API.TimetableHistory.Response
};
},
methods: {
toggleCard() {
if (!this.store.dispatcherStatsName) return;
this.cardVisible = !this.cardVisible;
if (this.cardVisible) this.fetchDispatcherStats();
},
async fetchDispatcherStats() {
if (this.lastDispatcherName != this.store.dispatcherStatsName) {
this.store.dispatcherStatsData = undefined;
}
const statsData: API.DispatcherStats.Response = await (
await axios.get(
`${URLs.stacjownikAPI}/api/getDispatcherInfo?name=${this.store.dispatcherStatsName}`
)
).data;
const timetables: API.TimetableHistory.Response = await (
await axios.get(
`${URLs.stacjownikAPI}/api/getTimetables?authorName=${this.store.dispatcherStatsName}`
)
).data;
this.timetables = timetables;
this.store.dispatcherStatsData = statsData;
this.lastDispatcherName = this.store.dispatcherStatsName;
}
}
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
@import '../../styles/variables.scss';
.stats_container {
position: relative;
}
.stats_card {
position: absolute;
z-index: 999;
top: 120%;
right: 0;
width: 500px;
max-width: 97vw;
min-height: 100px;
overflow: auto;
border-radius: 1em 0 1em 1em;
background-color: #222222f1;
box-shadow: 0 3px 10px 5px #131313;
padding: 1em 0.5em;
}
.last-timetables {
max-height: 400px;
margin: 0.5em 0;
}
.timetable-row {
width: 95%;
margin: 0.5em auto;
padding: 0.5em;
background-color: #4d4d4d;
}
h2.card-title {
font-size: 1.8em;
}
h3 {
margin-top: 1em;
}
h2,
h3 {
text-align: center;
}
.last-timetables {
overflow-y: auto;
}
</style>
@@ -0,0 +1,268 @@
<template>
<section class="daily-stats">
<span :data-active="statsStatus">
<span class="stats-list">
<h3>
{{ $t('journal.daily-stats.title') }}
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
</h3>
<hr class="header-separator" />
<b v-if="statsStatus == Status.Data.Loading">
{{ $t('app.loading') }}
</b>
<b class="text--error" v-else-if="statsStatus == Status.Data.Error">
{{ $t('journal.stats-error') }}
</b>
<b v-else-if="topDispatchers.length == 0">
{{ $t('journal.daily-stats.info') }}
</b>
<div v-else>
<div v-if="stats.totalTimetables">
&bull;
<i18n-t keypath="journal.daily-stats.total">
<template #count>
<b class="text--primary">
{{ stats.totalTimetables }}
{{ $t('journal.daily-stats.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.maxTimetable">
&bull;
<i18n-t keypath="journal.daily-stats.longest">
<template #id>
<router-link :to="`/journal/timetables?search-train=%23${stats.maxTimetable.id}`">
<b>{{ stats.maxTimetable.id }}</b>
</router-link>
</template>
<template #author>
<router-link
:to="`/journal/timetables?search-dispatcher=${stats.maxTimetable.authorName}`"
>
<b>{{ stats.maxTimetable.authorName }}</b>
</router-link>
</template>
<template #driver>
<b class="text--primary">{{ stats.maxTimetable.driverName }}</b>
</template>
<template #distance>
<b class="text--primary">{{ stats.maxTimetable.routeDistance }} km</b>
</template>
</i18n-t>
</div>
<div v-if="topDispatchers.length == 1">
&bull;
<i18n-t keypath="journal.daily-stats.most-active-dr">
<template #dispatcher>
<router-link
:to="`/journal/dispatchers?search-dispatcher=${topDispatchers[0].name}`"
>
<b>{{ topDispatchers[0].name }}</b>
</router-link>
</template>
<template #count>
<b class="text--primary">
{{ topDispatchers[0].count }}
{{ $t('journal.daily-stats.count', topDispatchers[0].count) }}
</b>
</template>
</i18n-t>
</div>
<div v-if="topDispatchers.length > 1">
&bull;
<i18n-t keypath="journal.daily-stats.most-active-dr-many">
<template #dispatchers>
<span v-for="(disp, i) in topDispatchers" :key="i">
<span v-if="i == topDispatchers.length - 1"> {{ $t('general.and') }} </span>
<router-link :to="`/journal/dispatchers?search-dispatcher=${disp.name}`">
<b>{{ disp.name }}</b>
</router-link>
<span v-if="i < topDispatchers.length - 2">, </span>
</span>
</template>
<template #count>
<b class="text--primary">
{{ topDispatchers[0].count }}
{{ $t('journal.daily-stats.count', topDispatchers[0].count) }}
</b>
</template>
</i18n-t>
</div>
<div v-if="stats.longestDuties.length > 0">
&bull;
<i18n-t keypath="journal.daily-stats.longest-duties">
<template #dispatcher>
<router-link
:to="`/journal/dispatchers?search-dispatcher=${stats.longestDuties[0].name}`"
>
<b>{{ stats.longestDuties[0].name }}</b>
</router-link>
</template>
<template #station>{{ stats.longestDuties[0].station }}</template>
<template #duration>
{{ calculateDuration(stats.longestDuties[0].duration) }}
</template>
</i18n-t>
</div>
<div v-if="stats.mostActiveDrivers.length > 0">
&bull;
<i18n-t keypath="journal.daily-stats.most-active-driver">
<template #driver>
<router-link
:to="`/journal/timetables?search-driver=${stats.mostActiveDrivers[0].name}`"
>
<b>{{ stats.mostActiveDrivers[0].name }}</b>
</router-link>
</template>
<template #distance>
<b class="text--primary">{{ stats.mostActiveDrivers[0].distance.toFixed(2) }} km</b>
</template>
</i18n-t>
</div>
<hr class="section-separator" />
<div class="stats-badges">
<span
class="stat-badge"
v-for="key in [
'rippedSwitches',
'derailments',
'skippedStopSignals',
'radioStops',
'kills'
]"
:key="key"
>
<span>{{ $t(`journal.daily-stats.${key}`) }}</span>
<span>{{
Object.entries(stats.globalDiff).find(([k, v]) => k == key)?.[1] || '--'
}}</span>
</span>
</div>
</div>
</span>
</span>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import { API } from '../../typings/api';
import { Status } from '../../typings/common';
import http from '../../http';
export default defineComponent({
name: 'journal-daily-stats',
mixins: [dateMixin],
// emits: ['toggleStatsOpen'],
data() {
return {
Status,
statsStatus: Status.Data.Loading,
intervalId: -1,
stats: {} as API.DailyStats.Response
};
},
activated() {
this.startFetchingDailyStats();
// this.$emit('toggleStatsOpen', true);
},
deactivated() {
this.stopFetchingDailyStats();
},
computed: {
topDispatchers() {
if (this.stats.mostActiveDispatchers.length == 0) return [];
const maxCount = this.stats.mostActiveDispatchers[0].count;
return this.stats.mostActiveDispatchers.filter((disp) => disp.count === maxCount);
}
},
methods: {
async fetchDailyTimetableStats() {
try {
const res: API.DailyStats.Response = await (await http.get('api/getDailyStats')).data;
this.stats = res;
this.statsStatus = Status.Data.Loaded;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...');
this.statsStatus = Status.Data.Error;
}
},
startFetchingDailyStats() {
this.fetchDailyTimetableStats();
if (this.intervalId != -1) return;
this.intervalId = window.setInterval(this.fetchDailyTimetableStats, 60000);
},
stopFetchingDailyStats() {
clearInterval(this.intervalId);
this.intervalId = -1;
}
}
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
@import '../../styles/JournalStats.scss';
@import '../../styles/badge.scss';
.daily-stats {
text-align: left;
}
.daily-stats > span[data-active='0'] {
opacity: 0.75;
}
.stats-list a {
text-decoration: underline;
}
.stats-badges {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
}
@include smallScreen {
h3 {
text-align: center;
}
}
</style>
@@ -0,0 +1,85 @@
<template>
<div class="journal-stats dispatcher" v-if="dispatcherName && stats">
<span class="loading" v-if="!stats.issuedTimetables && !stats.services">
{{ $t('journal.dispatcher-stats.empty') }}
</span>
<span v-else>
<h3>
<i18n-t keypath="journal.dispatcher-stats.title">
<template #name>
<span class="text--primary">{{ dispatcherName.toUpperCase() }}</span>
</template>
</i18n-t>
</h3>
<hr class="header-separator" />
<div class="info-stats">
<span class="stat-badge" v-if="stats.services">
<span>{{ $t('journal.dispatcher-stats.services-count') }}</span>
<span>{{ stats.services.count }}</span>
</span>
<span class="stat-badge" v-if="stats.services">
<span>{{ $t('journal.dispatcher-stats.service-max') }}</span>
<span>{{ calculateDuration(stats.services.durationMax) }}</span>
</span>
<span class="stat-badge" v-if="stats.services">
<span>{{ $t('journal.dispatcher-stats.service-avg') }}</span>
<span>{{ calculateDuration(stats.services.durationAvg) }}</span>
</span>
</div>
<hr class="section-separator" />
<div class="info-stats">
<span class="stat-badge" v-if="stats.issuedTimetables">
<span>{{ $t('journal.dispatcher-stats.timetables-count') }}</span>
<span>{{ stats.issuedTimetables.count }}</span>
</span>
<span class="stat-badge" v-if="stats.issuedTimetables">
<span>{{ $t('journal.dispatcher-stats.timetables-sum') }}</span>
<span>{{ stats.issuedTimetables.distanceSum.toFixed(2) }}km</span>
</span>
<span class="stat-badge" v-if="stats.issuedTimetables">
<span>{{ $t('journal.dispatcher-stats.timetables-max') }}</span>
<span>{{ stats.issuedTimetables.distanceMax.toFixed(2) }}km</span>
</span>
<span class="stat-badge" v-if="stats.issuedTimetables">
<span>{{ $t('journal.dispatcher-stats.timetables-avg') }}</span>
<span>{{ stats.issuedTimetables.distanceAvg.toFixed(2) }}km</span>
</span>
</div>
</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import dateMixin from '../../../mixins/dateMixin';
import { useMainStore } from '../../../store/mainStore';
export default defineComponent({
name: 'journal-dispatcher-stats',
mixins: [dateMixin],
setup() {
const store = useMainStore();
return {
stats: store.dispatcherStatsData,
dispatcherName: store.dispatcherStatsName
};
}
});
</script>
<style lang="scss" scoped>
@import '../../../styles/JournalStats.scss';
</style>
@@ -0,0 +1,256 @@
<template>
<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 == Status.Data.Loading" />
<div v-else-if="dataStatus == Status.Data.Error" class="journal_warning error">
{{ $t('app.error') }}
</div>
<div class="journal_warning" v-else-if="dispatcherHistory.length == 0">
{{ $t('app.no-result') }}
</div>
<div v-else>
<table class="dispatchers-table">
<thead>
<th>{{ $t('journal.history-name') }}</th>
<th>{{ $t('journal.history-hash') }}</th>
<th>{{ $t('journal.history-dispatcher') }}</th>
<th>{{ $t('journal.history-level') }}</th>
<th>{{ $t('journal.history-rate') }}</th>
<th>{{ $t('journal.history-region') }}</th>
<th>{{ $t('journal.history-date') }}</th>
</thead>
<tbody>
<transition-group name="list-anim">
<tr v-for="historyItem in dispatcherHistory" :key="historyItem.id">
<td>
<router-link
:to="`/journal/dispatchers?search-station=${historyItem.stationName}`"
>
<b>{{ historyItem.stationName }}</b>
</router-link>
</td>
<td>#{{ historyItem.stationHash }}</td>
<td>
<router-link
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
>
<b
v-if="isDonator(historyItem.dispatcherName)"
class="text--donator"
:title="$t('donations.dispatcher-message')"
>
{{ historyItem.dispatcherName }}
</b>
<b v-else>
{{ historyItem.dispatcherName }}
</b>
</router-link>
</td>
<td>
<b
v-if="historyItem.dispatcherLevel !== null"
class="level-badge dispatcher"
:style="
calculateExpStyle(
historyItem.dispatcherLevel,
historyItem.dispatcherIsSupporter
)
"
>
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
</b>
</td>
<td class="text--primary">
<b>{{ historyItem.dispatcherRate }}</b>
</td>
<td>
<b class="region-badge" :aria-describedby="historyItem.region">{{
regions.find((r) => r.id == historyItem.region)?.value || '???'
}}</b>
</td>
<td style="min-width: 200px" class="time">
<span v-if="historyItem.timestampTo" class="text--offline">
<b>{{ $d(historyItem.timestampFrom) }}</b>
{{ timestampToString(historyItem.timestampFrom) }}
- {{ timestampToString(historyItem.timestampTo) }} ({{
calculateDuration(historyItem.currentDuration)
}})
</span>
<span class="dispatcher-online" v-else>
<b class="text--online">
<router-link :to="`/scenery?station=${historyItem.stationName}`">{{
$t('journal.online-since')
}}</router-link>
{{ timestampToString(historyItem.timestampFrom) }}
</b>
({{ calculateDuration(historyItem.currentDuration) }})
</span>
</td>
</tr>
</transition-group>
</tbody>
</table>
<AddDataButton
:list="dispatcherHistory"
:scrollDataLoaded="scrollDataLoaded"
:scrollNoMoreData="scrollNoMoreData"
@addHistoryData="addHistoryData"
/>
</div>
<div class="journal_warning" v-if="scrollNoMoreData">
{{ $t('journal.no-further-data') }}
</div>
<div class="journal_warning" v-else-if="!scrollDataLoaded">
{{ $t('journal.loading-further-data') }}
</div>
</div>
</transition>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { regions } from '../../../data/options.json';
import { useMainStore } from '../../../store/mainStore';
import { API } from '../../../typings/api';
import { Status } from '../../../typings/common';
import Loading from '../../Global/Loading.vue';
import AddDataButton from '../../Global/AddDataButton.vue';
import dateMixin from '../../../mixins/dateMixin';
import donatorMixin from '../../../mixins/donatorMixin';
import styleMixin from '../../../mixins/styleMixin';
export default defineComponent({
components: { Loading, AddDataButton },
mixins: [dateMixin, styleMixin, donatorMixin],
props: {
dispatcherHistory: {
type: Array as PropType<API.DispatcherHistory.Response>,
required: true
},
scrollNoMoreData: {
type: Boolean
},
scrollDataLoaded: {
type: Boolean
},
addHistoryData: {
type: Function as PropType<() => void>
},
dataStatus: {
type: Number as PropType<Status.Data>
}
},
data() {
return {
Status,
store: useMainStore(),
regions
};
},
computed: {
computedDispatcherHistory() {
return this.dispatcherHistory.reduce(
(acc, historyItem, i) => {
if (this.isAnotherDay(i - 1, i))
acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL'));
acc.push(historyItem);
return acc;
},
[] as (API.DispatcherHistory.Data | string)[]
);
}
},
methods: {
navigateToScenery(name: string, isOnline: boolean) {
if (!isOnline) return;
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`);
},
isAnotherDay(prevIndex: number, currIndex: number) {
if (currIndex == 0) return true;
return (
new Date(this.dispatcherHistory[prevIndex].timestampFrom).getDate() !=
new Date(this.dispatcherHistory[currIndex].timestampFrom).getDate()
);
}
}
});
</script>
<style lang="scss" scoped>
@import '../../../styles/animations.scss';
@import '../../../styles/responsive.scss';
@import '../../../styles/badge.scss';
@import '../../../styles/variables.scss';
@import '../../../styles/JournalSection.scss';
table.dispatchers-table {
--_bg-table: #111;
--_bg-head: #101010;
--_bg-row: #2f2f2f;
width: 100%;
border-collapse: collapse;
position: relative;
text-align: center;
margin-bottom: 1em;
thead {
position: sticky;
top: 0;
background-color: var(--_bg-head);
}
th {
padding: 0.5em;
}
tr {
background-color: var(--_bg-row);
border-bottom: 2px solid black;
&:last-child {
border: none;
}
}
td {
padding: 0.75em;
.level-badge {
margin: 0 auto;
}
}
}
.text {
&--online {
color: springgreen;
}
&--offline {
color: #ddd;
}
}
</style>
@@ -1,260 +0,0 @@
<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 == Status.Data.Loading" />
<div v-else-if="dataStatus == Status.Data.Error" class="journal_warning error">
{{ $t('app.error') }}
</div>
<div class="journal_warning" v-else-if="dispatcherHistory.length == 0">
{{ $t('app.no-result') }}
</div>
<div 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
v-if="isDonator(historyItem.dispatcherName)"
class="text--donator"
:title="$t('donations.dispatcher-message')"
>
{{ historyItem.dispatcherName }}
</b>
<b v-else>
{{ historyItem.dispatcherName }}
</b>
</router-link>
</td>
<td>
<b
v-if="historyItem.dispatcherLevel !== null"
class="level-badge dispatcher"
:style="
calculateExpStyle(
historyItem.dispatcherLevel,
historyItem.dispatcherIsSupporter
)
"
>
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
</b>
</td>
<td class="text--primary">
<b>{{ historyItem.dispatcherRate }}</b>
</td>
<td>
<b class="region-badge" :aria-describedby="historyItem.region">{{
regions.find((r) => r.id == historyItem.region)?.value || '???'
}}</b>
</td>
<td style="min-width: 200px" class="time">
<span v-if="historyItem.timestampTo" class="text--offline">
<b>{{ $d(historyItem.timestampFrom) }}</b>
{{ timestampToString(historyItem.timestampFrom) }}
- {{ timestampToString(historyItem.timestampTo) }} ({{
calculateDuration(historyItem.currentDuration)
}})
</span>
<span class="dispatcher-online" v-else>
<b class="text--online">
<router-link :to="`/scenery?station=${historyItem.stationName}`">{{
$t('journal.online-since')
}}</router-link>
{{ timestampToString(historyItem.timestampFrom) }}
</b>
({{ calculateDuration(historyItem.currentDuration) }})
</span>
</td>
</tr>
</transition-group>
</tbody>
</table>
<AddDataButton
:list="dispatcherHistory"
: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 dateMixin from '../../mixins/dateMixin';
import styleMixin from '../../mixins/styleMixin';
import { useStore } from '../../store/mainStore';
import Loading from '../Global/Loading.vue';
import { regions } from '../../data/options.json';
import AddDataButton from '../Global/AddDataButton.vue';
import { API } from '../../typings/api';
import { Status } from '../../typings/common';
import donatorMixin from '../../mixins/donatorMixin';
export default defineComponent({
components: { Loading, AddDataButton },
mixins: [dateMixin, styleMixin, donatorMixin],
props: {
dispatcherHistory: {
type: Array as PropType<API.DispatcherHistory.Response>,
required: true
},
scrollNoMoreData: {
type: Boolean
},
scrollDataLoaded: {
type: Boolean
},
addHistoryData: {
type: Function as PropType<() => void>
},
dataStatus: {
type: Number as PropType<Status.Data>
}
},
data() {
return {
Status,
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);
return acc;
},
[] as (API.DispatcherHistory.Data | string)[]
);
}
},
methods: {
navigateToScenery(name: string, isOnline: boolean) {
if (!isOnline) return;
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`);
},
isAnotherDay(prevIndex: number, currIndex: number) {
if (currIndex == 0) return true;
return (
new Date(this.dispatcherHistory[prevIndex].timestampFrom).getDate() !=
new Date(this.dispatcherHistory[currIndex].timestampFrom).getDate()
);
}
}
});
</script>
<style lang="scss" scoped>
@import '../../styles/animations.scss';
@import '../../styles/responsive.scss';
@import '../../styles/badge.scss';
@import '../../styles/variables.scss';
@import '../../styles/JournalSection.scss';
table.scenery-history-table {
--_bg-table: #111;
--_bg-head: #101010;
--_bg-row: #2f2f2f;
width: 100%;
border-collapse: collapse;
position: relative;
text-align: center;
margin-bottom: 1em;
thead {
position: sticky;
top: 0;
background-color: var(--_bg-head);
}
th {
padding: 0.5em;
}
tr {
background-color: var(--_bg-row);
border-bottom: 2px solid black;
&:last-child {
border: none;
}
}
td {
padding: 0.75em;
.level-badge {
margin: 0 auto;
}
}
}
.text {
&--online {
color: springgreen;
}
&--offline {
color: #ddd;
}
}
</style>
+41 -41
View File
@@ -33,7 +33,7 @@
<h1 class="option-title">{{ $t('options.search-title') }}</h1> <h1 class="option-title">{{ $t('options.search-title') }}</h1>
<div class="search_content"> <div class="search_content">
<div class="search" v-for="(_, propName) in searchersValues" :key="propName"> <div class="search" v-for="(_, propName) in searchersValues" :key="propName">
<label v-if="propName == 'search-date'" for="date">{{ <label v-if="propName == 'search-date'" for="search-date">{{
$t(`options.search-${optionsType}-date`) $t(`options.search-${optionsType}-date`)
}}</label> }}</label>
@@ -41,12 +41,13 @@
<input <input
class="search-input" class="search-input"
v-model="searchersValues[propName]" v-model="searchersValues[propName]"
@keydown.enter="onSearchConfirm" @keydown.enter="searchConfirm"
@focus="preventKeyDown = true" @focus="preventKeyDown = true"
@blur="preventKeyDown = false" @blur="preventKeyDown = false"
:placeholder="$t(`options.${propName}`)" :placeholder="$t(`options.${propName}`)"
:type="propName == 'search-date' ? 'date' : 'text'" :type="propName == 'search-date' ? 'date' : 'text'"
:min="propName == 'search-date' ? '2022-02-01' : undefined" :min="propName == 'search-date' ? '2022-02-01' : undefined"
:id="`${propName}`"
:list="propName.toString()" :list="propName.toString()"
/> />
@@ -110,14 +111,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import axios from 'axios';
import { defineComponent, inject, PropType } from 'vue'; import { defineComponent, inject, PropType } from 'vue';
import keyMixin from '../../mixins/keyMixin'; import keyMixin from '../../mixins/keyMixin';
import { URLs } from '../../scripts/utils/apiURLs'; import { useMainStore } from '../../store/mainStore';
import { useStore } from '../../store/mainStore';
import { Journal } from './typings'; import { Journal } from './typings';
import { API } from '../../typings/api';
import { Status } from '../../typings/common'; import { Status } from '../../typings/common';
import http from '../../http';
export default defineComponent({ export default defineComponent({
emits: ['onSearchConfirm', 'onOptionsReset', 'onRefreshData'], emits: ['onSearchConfirm', 'onOptionsReset', 'onRefreshData'],
@@ -158,7 +157,7 @@ export default defineComponent({
dispatcherSuggestions: [] as string[], dispatcherSuggestions: [] as string[],
searchTimeout: 0, searchTimeout: 0,
store: useStore(), store: useMainStore(),
JournalFilterSection: Journal.FilterSection JournalFilterSection: Journal.FilterSection
}; };
@@ -182,12 +181,6 @@ export default defineComponent({
}, },
watch: { watch: {
async 'store.driverStatsName'() {
await this.fetchDriverStats();
// if (value) this.store.currentStatsTab = 'driver';
},
async 'searchersValues.search-driver'(value: string | undefined) { async 'searchersValues.search-driver'(value: string | undefined) {
clearTimeout(this.searchTimeout); clearTimeout(this.searchTimeout);
@@ -206,29 +199,34 @@ export default defineComponent({
}, },
methods: { methods: {
async fetchDriverStats() { // filters & sorters from URL params
this.store.driverStatsData = undefined; handleRouteParams() {
this.$router.push({
query: {
...this.$route.query,
'sorter-active':
this.sorterOptionIds.indexOf(`${this.sorterActive.id}`) != 0
? this.sorterActive.id
: undefined,
...Object.keys(this.searchersValues).reduce(
(acc, k) => {
const searchVal = this.searchersValues[k as Journal.TimetableSearchKey];
if (!this.store.driverStatsName) { acc[k] = searchVal || undefined;
this.store.driverStatsStatus = Status.Data.Initialized;
return;
}
try { return acc;
this.store.driverStatsStatus = Status.Data.Loading; },
{} as { [k: string]: string | undefined }
const statsData: API.DriverStats.Response = await ( ),
await axios.get( ...this.filterList?.reduce(
`${URLs.stacjownikAPI}/api/getDriverInfo?name=${this.store.driverStatsName}` (acc, f) => {
if (f.isActive) acc[f.filterSection] = f.default ? undefined : f.id;
return acc;
},
{} as { [k: string]: string | undefined }
) )
).data; }
});
this.store.driverStatsData = statsData;
this.store.driverStatsStatus = Status.Data.Loaded;
} catch (error) {
this.store.driverStatsStatus = Status.Data.Error;
console.error('Ups! Wystąpił błąd przy próbie pobrania statystyk maszynisty! :/');
}
}, },
refreshData() { refreshData() {
@@ -240,17 +238,17 @@ export default defineComponent({
window.clearTimeout(this.searchTimeout); window.clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(async () => { this.searchTimeout = window.setTimeout(async () => {
try { try {
const suggestions: string[] = await ( const suggestions: string[] = await (
await axios.get(`${URLs.stacjownikAPI}/api/get${type}Suggestions?name=${value}`) await http.get(`api/get${type}Suggestions?name=${value}`)
).data; ).data;
this[`${type}Suggestions`] = suggestions; this[`${type}Suggestions`] = suggestions;
} catch (error) { } catch (error) {
this[`${type}Suggestions`] = []; this[`${type}Suggestions`] = [];
} }
}, 450); }, 250);
}, },
// Override keyMixin function // Override keyMixin function
@@ -265,7 +263,7 @@ export default defineComponent({
onSorterChange(item: { id: string | number; value: string }) { onSorterChange(item: { id: string | number; value: string }) {
this.sorterActive.id = item.id; this.sorterActive.id = item.id;
this.sorterActive.dir = -1; this.sorterActive.dir = -1;
this.$emit('onSearchConfirm'); this.searchConfirm();
}, },
onFilterChange(filter: Journal.TimetableFilter) { onFilterChange(filter: Journal.TimetableFilter) {
@@ -275,25 +273,27 @@ export default defineComponent({
.forEach((f) => (f.isActive = false)); .forEach((f) => (f.isActive = false));
filter.isActive = true; filter.isActive = true;
this.$emit('onSearchConfirm'); this.searchConfirm();
}, },
onInputClear(id: any) { onInputClear(id: any) {
this.searchersValues[id] = ''; this.searchersValues[id] = '';
this.$emit('onSearchConfirm'); this.searchConfirm();
}, },
onSearchConfirm() { searchConfirm() {
this.$emit('onSearchConfirm'); this.$emit('onSearchConfirm');
this.handleRouteParams();
}, },
onSearchButtonConfirm() { onSearchButtonConfirm() {
this.showOptions = false; this.showOptions = false;
this.$emit('onSearchConfirm'); this.searchConfirm();
}, },
onResetButtonClick() { onResetButtonClick() {
this.$emit('onOptionsReset'); this.$emit('onOptionsReset');
this.handleRouteParams();
} }
} }
}); });
+62 -99
View File
@@ -1,122 +1,85 @@
<template> <template>
<div class="journal-stats" v-if="!store.isOffline"> <div
<div class="tabs"> class="journal-stats dropdown"
v-if="!mainStore.isOffline"
@keydown.esc="currentStatsTab = null"
>
<div
class="dropdown_background"
v-if="currentStatsTab !== null"
@click="currentStatsTab = null"
></div>
<div class="actions-bar">
<button <button
v-for="tab in data.tabs" v-for="button in statsButtons"
:key="tab.name" :key="button.tab"
class="btn--filled" class="btn--filled btn--image"
:data-selected="tab.name == store.currentStatsTab && areStatsOpen" :data-selected="button.tab == currentStatsTab"
:data-inactive="tab.inactive" :data-disabled="button.disabled"
:data-disabled="tab.inactive" :disabled="button.disabled"
:disabled="tab.inactive" @click="onTabButtonClick(button.tab)"
@click="onTabButtonClick(tab.name)"
> >
{{ $t(tab.titlePath) }} <img
v-if="button.iconName"
:src="`/images/icon-${button.iconName}.svg`"
:alt="button.iconName"
/>
{{ $t(button.localeKey) }}
</button> </button>
</div> </div>
<div class="stats-tab" v-show="areStatsOpen"> <transition name="dropdown-anim">
<keep-alive> <div class="dropdown_wrapper" v-if="currentStatsTab !== null">
<JournalDailyStats <keep-alive>
v-if="store.currentStatsTab == 'daily'" <component :is="currentStatsTab" :key="currentStatsTab"></component>
@toggleStatsOpen="toggleStatsOpen" </keep-alive>
/> </div>
<JournalDriverStats v-else-if="store.currentStatsTab == 'driver'" /> </transition>
</keep-alive>
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script lang="ts">
import { computed, onMounted, reactive, Ref, ref, watch } from 'vue'; import { defineComponent, PropType } from 'vue';
import { useStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import JournalDailyStats from './DailyStats.vue';
import JournalDriverStats from './JournalDriverStats.vue';
import StorageManager from '../../managers/storageManager'; import StorageManager from '../../managers/storageManager';
import { Journal } from './typings';
import JournalDailyStats from './JournalDailyStats.vue';
import JournalDispatcherStats from '../JournalView/JournalDispatchers/JournalDispatcherStats.vue';
import JournalDriverStats from '../JournalView/JournalTimetables/JournalDriverStats.vue';
// Types export default defineComponent({
type TStatTab = 'daily' | 'driver'; components: { JournalDailyStats, JournalDriverStats, JournalDispatcherStats },
props: {
// Variables statsButtons: {
const store = useStore(); type: Array as PropType<Journal.StatsButton[]>,
required: true
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 }[] },
}); data() {
return {
Journal,
mainStore: useMainStore(),
currentStatsTab: null as Journal.StatsTab | null
};
},
// Methods methods: {
function onTabButtonClick(tab: TStatTab) { onTabButtonClick(tab: Journal.StatsTab) {
if (lastClickedTab.value == tab || !lastClickedTab.value || !areStatsOpen.value) this.currentStatsTab = tab == this.currentStatsTab ? null : tab;
areStatsOpen.value = !areStatsOpen.value;
if (tab == 'daily') { StorageManager.setStringValue('journalStatsTab', this.currentStatsTab ?? '');
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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/JournalStats.scss'; @import '../../styles/dropdown.scss';
@import '../../styles/dropdown_filters.scss';
@import '../../styles/variables.scss'; @import '../../styles/variables.scss';
.tabs { .dropdown_wrapper {
position: relative; max-width: 100%;
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> </style>
@@ -1,14 +1,19 @@
<template> <template>
<div class="journal-stats"> <div class="journal-stats driver" v-if="store.driverStatsData">
<span v-if="store.driverStatsData"> <span>
<h3> <h3>
{{ $t('journal.stats-title') }} <i18n-t keypath="journal.driver-stats.title">
<span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span> <template #name>
<span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
</template>
</i18n-t>
</h3> </h3>
<hr class="header-separator" />
<div class="info-stats"> <div class="info-stats">
<span class="stat-badge"> <span class="stat-badge">
<span>{{ $t('journal.stats-timetables') }}</span> <span>{{ $t('journal.driver-stats.timetables') }}</span>
<span <span
>{{ store.driverStatsData._count.fulfilled }} / >{{ store.driverStatsData._count.fulfilled }} /
{{ store.driverStatsData._count._all }}</span {{ store.driverStatsData._count._all }}</span
@@ -16,17 +21,17 @@
</span> </span>
<span class="stat-badge"> <span class="stat-badge">
<span>{{ $t('journal.stats-longest-timetable') }}</span> <span>{{ $t('journal.driver-stats.longest-timetable') }}</span>
<span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span> <span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span>
</span> </span>
<span class="stat-badge"> <span class="stat-badge">
<span>{{ $t('journal.stats-avg-timetable') }}</span> <span>{{ $t('journal.driver-stats.avg-timetable') }}</span>
<span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span> <span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
</span> </span>
<span class="stat-badge"> <span class="stat-badge">
<span>{{ $t('journal.stats-distance') }}</span> <span>{{ $t('journal.driver-stats.distance') }}</span>
<span> <span>
{{ store.driverStatsData._sum.currentDistance.toFixed(2) }} / {{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km {{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km
@@ -34,7 +39,7 @@
</span> </span>
<span class="stat-badge"> <span class="stat-badge">
<span>{{ $t('journal.stats-stations') }}</span> <span>{{ $t('journal.driver-stats.stations') }}</span>
<span> <span>
{{ store.driverStatsData._sum.confirmedStopsCount }} / {{ store.driverStatsData._sum.confirmedStopsCount }} /
{{ store.driverStatsData._sum.allStopsCount }} {{ store.driverStatsData._sum.allStopsCount }}
@@ -42,26 +47,20 @@
</span> </span>
</div> </div>
</span> </span>
<b v-else-if="store.driverStatsStatus == Status.Data.Loading">{{
$t('journal.stats-loading')
}}</b>
<b v-else-if="store.driverStatsStatus == Status.Data.Error">
{{ $t('journal.stats-error ') }}
</b>
<b v-else>{{ $t('journal.driver-stats-info') }}</b>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useStore } from '../../store/mainStore'; import { useMainStore } from '../../../store/mainStore';
import { Status } from '../../typings/common'; import { Status } from '../../../typings/common';
export default defineComponent({ export default defineComponent({
name: 'journal-driver-stats',
data() { data() {
return { return {
store: useStore(), store: useMainStore(),
Status: Status Status: Status
}; };
} }
@@ -69,5 +68,5 @@ export default defineComponent({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/JournalStats.scss'; @import '../../../styles/JournalStats.scss';
</style> </style>
@@ -42,7 +42,7 @@ import { defineComponent, PropType } from 'vue';
import Loading from '../../Global/Loading.vue'; import Loading from '../../Global/Loading.vue';
import AddDataButton from '../../Global/AddDataButton.vue'; import AddDataButton from '../../Global/AddDataButton.vue';
import TimetableHistoryList from './TimetableHistoryList.vue'; import TimetableHistoryList from './TimetableHistoryList.vue';
import { useStore } from '../../../store/mainStore'; import { useMainStore } from '../../../store/mainStore';
import { Status } from '../../../typings/common'; import { Status } from '../../../typings/common';
import { API } from '../../../typings/api'; import { API } from '../../../typings/api';
@@ -71,7 +71,7 @@ export default defineComponent({
data() { data() {
return { return {
Status, Status,
store: useStore() store: useMainStore()
}; };
} }
}); });
@@ -111,16 +111,17 @@ export default defineComponent({
gap: 0.5em; gap: 0.5em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
@include smallScreen() {
justify-content: center;
}
} }
.info-date { .info-date {
margin-right: 0.5em; margin-right: 0.5em;
} }
.badges {
display: flex;
gap: 0.25em;
}
.info-badge { .info-badge {
padding: 0.05em 0.35em; padding: 0.05em 0.35em;
color: black; color: black;
@@ -142,7 +143,14 @@ export default defineComponent({
cursor: pointer; cursor: pointer;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center;
align-items: center; align-items: center;
gap: 0.25em; gap: 0.25em;
} }
@include smallScreen {
.item-general {
justify-content: center;
}
}
</style> </style>
+32 -14
View File
@@ -1,12 +1,5 @@
export namespace Journal { export namespace Journal {
export type DispatcherSearcher = { export type DispatcherSearchKey = 'search-dispatcher' | 'search-station' | 'search-date';
[key in 'search-dispatcher' | 'search-station' | 'search-date']: string;
};
export interface DispatcherSorter {
id: 'timestampFrom' | 'duration';
dir: -1 | 1;
}
export type TimetableSearchKey = export type TimetableSearchKey =
| 'search-driver' | 'search-driver'
@@ -19,11 +12,29 @@ export namespace Journal {
[key in TimetableSearchKey]: string; [key in TimetableSearchKey]: string;
}; };
export type DispatcherSearchType = {
[key in DispatcherSearchKey]: string;
};
export type TimetableSorterKey = 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
export type DispatcherSorterKey = 'timestampFrom' | 'duration';
export interface DispatcherSorter {
id: DispatcherSorterKey;
dir: -1 | 1;
}
export interface TimetableSorter {
id: TimetableSorterKey;
dir: 'asc' | 'desc';
}
export const enum TimetableFilterId { export const enum TimetableFilterId {
ALL_STATUSES = 'all-statuses',
ACTIVE = 'active', ACTIVE = 'active',
FULFILLED = 'fulfilled', FULFILLED = 'fulfilled',
ABANDONED = 'abandoned', ABANDONED = 'abandoned',
ALL = 'all', ALL_SPECIALS = 'all-specials',
TWR = 'twr', TWR = 'twr',
SKR = 'skr', SKR = 'skr',
TWR_SKR = 'twr-skr' TWR_SKR = 'twr-skr'
@@ -31,19 +42,26 @@ export namespace Journal {
export enum FilterSection { export enum FilterSection {
TIMETABLE_STATUS = 'timetable-status', TIMETABLE_STATUS = 'timetable-status',
TWRSKR = 'twrskr' SPECIAL = 'special'
} }
export interface TimetableFilter { export interface TimetableFilter {
id: TimetableFilterId; id: TimetableFilterId;
filterSection: string; filterSection: string;
isActive: boolean; isActive: boolean;
default: boolean;
} }
export type TimetableSorterKey = 'timetableId' | 'beginDate' | 'distance' | 'total-stops'; export enum StatsTab {
DRIVER_STATS = 'journal-driver-stats',
DISPATCHER_STATS = 'journal-dispatcher-stats',
DAILY_STATS = 'journal-daily-stats'
}
export interface TimetableSorter { export interface StatsButton {
id: TimetableSorterKey; tab: StatsTab;
dir: 'asc' | 'desc'; localeKey: string;
iconName: string;
disabled: boolean;
} }
} }
@@ -19,7 +19,9 @@
<tr v-for="historyItem in historyList" :key="historyItem.id"> <tr v-for="historyItem in historyList" :key="historyItem.id">
<td>#{{ historyItem.stationHash }}</td> <td>#{{ historyItem.stationHash }}</td>
<td> <td>
<router-link :to="`/journal/dispatchers?dispatcherName=${historyItem.dispatcherName}`"> <router-link
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
>
<b>{{ historyItem.dispatcherName }}</b> <b>{{ historyItem.dispatcherName }}</b>
</router-link> </router-link>
</td> </td>
@@ -33,6 +35,8 @@
> >
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }} {{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
</b> </b>
<b v-else>?</b>
</td> </td>
<td class="text--primary"> <td class="text--primary">
<b>{{ historyItem.dispatcherRate }}</b> <b>{{ historyItem.dispatcherRate }}</b>
@@ -66,17 +70,16 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import axios from 'axios';
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import Station from '../../scripts/interfaces/Station'; import Station from '../../scripts/interfaces/Station';
import { URLs } from '../../scripts/utils/apiURLs';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import styleMixin from '../../mixins/styleMixin'; import styleMixin from '../../mixins/styleMixin';
import listObserverMixin from '../../mixins/listObserverMixin'; import listObserverMixin from '../../mixins/listObserverMixin';
import { OnlineScenery } from '../../store/typings'; import { OnlineScenery } from '../../store/typings';
import { API } from '../../typings/api'; import { API } from '../../typings/api';
import { Status } from '../../typings/common'; import { Status } from '../../typings/common';
import http from '../../http';
export default defineComponent({ export default defineComponent({
name: 'SceneryDispatchersHistory', name: 'SceneryDispatchersHistory',
@@ -84,12 +87,10 @@ export default defineComponent({
components: { Loading }, components: { Loading },
props: { props: {
station: { station: {
type: Object as PropType<Station>, type: Object as PropType<Station>
required: true
}, },
onlineScenery: { onlineScenery: {
type: Object as PropType<OnlineScenery>, type: Object as PropType<OnlineScenery>
required: false
} }
}, },
@@ -113,12 +114,20 @@ export default defineComponent({
countFrom = 0, countFrom = 0,
countLimit = 30 countLimit = 30
): Promise<API.DispatcherHistory.Response | null> { ): Promise<API.DispatcherHistory.Response | null> {
if (!this.station && !this.onlineScenery) {
this.dataStatus = Status.Data.Loaded;
return null;
}
try { try {
this.dataStatus = Status.Data.Loading; this.dataStatus = Status.Data.Loading;
const requestString = `${URLs.stacjownikAPI}/api/getDispatchers?stationName=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`; const requestString = `api/getDispatchers?stationName=${
this.station?.name || this.onlineScenery?.name
}&countFrom=${countFrom}&countLimit=${countLimit}`;
const historyAPIData: API.DispatcherHistory.Response = await ( const historyAPIData: API.DispatcherHistory.Response = await (
await axios.get(requestString) await http.get(requestString)
).data; ).data;
this.dataStatus = Status.Data.Loaded; this.dataStatus = Status.Data.Loaded;
@@ -130,7 +139,9 @@ export default defineComponent({
} }
}, },
navigateToHistory() { navigateToHistory() {
this.$router.push(`/journal/dispatchers?sceneryName=${this.station.name}`); this.$router.push(
`/journal/dispatchers?search-station=${this.station?.name || this.onlineScenery?.name}`
);
} }
} }
}); });
+10 -8
View File
@@ -1,11 +1,11 @@
<template> <template>
<section class="info-header"> <section class="info-header">
<a class="scenery-name" :href="station.generalInfo?.url" target="_blank"> <a class="scenery-name" :href="station?.generalInfo?.url" target="_blank">
{{ station.name }} {{ stationName.replace(/_/g, ' ') }}
</a> </a>
<div class="scenery-abbrev"> <div class="scenery-abbrev" v-if="station?.generalInfo?.abbr">
{{ $t('scenery.abbrev') }} <b>{{ station.generalInfo?.abbr }}</b> {{ $t('scenery.abbrev') }} <b>{{ station.generalInfo.abbr }}</b>
</div> </div>
<div class="scenery-hash" v-if="onlineScenery?.hash">#{{ onlineScenery.hash }}</div> <div class="scenery-hash" v-if="onlineScenery?.hash">#{{ onlineScenery.hash }}</div>
@@ -20,13 +20,16 @@ import { OnlineScenery } from '../../store/typings';
export default defineComponent({ export default defineComponent({
props: { props: {
station: { station: {
type: Object as PropType<Station>, type: Object as PropType<Station>
},
stationName: {
type: String,
required: true required: true
}, },
onlineScenery: { onlineScenery: {
type: Object as PropType<OnlineScenery>, type: Object as PropType<OnlineScenery>
required: false
} }
} }
}); });
@@ -58,4 +61,3 @@ export default defineComponent({
font-size: 1.2em; font-size: 1.2em;
} }
</style> </style>
../../store/storeTypes
+6 -8
View File
@@ -1,10 +1,10 @@
<template> <template>
<div class="scenery-info"> <div class="scenery-info">
<section> <section>
<div class="scenery-info-general" v-if="station.generalInfo"> <div class="scenery-info-general">
<SceneryInfoIcons :station="station" /> <SceneryInfoIcons :station="station" />
<div class="scenery-general-list"> <div class="scenery-general-list" v-if="station?.generalInfo">
<span> <span>
<b>{{ $t('availability.title') }}:</b> <b>{{ $t('availability.title') }}:</b>
{{ $t(`availability.${station.generalInfo.availability}`) }} {{ $t(`availability.${station.generalInfo.availability}`) }}
@@ -46,11 +46,11 @@
</span> </span>
</div> </div>
<SceneryInfoRoutes :station="station" /> <SceneryInfoRoutes v-if="station" :station="station" />
<div <div
class="scenery-authors" class="scenery-authors"
v-if="station.generalInfo.authors && station.generalInfo.authors.length > 0" v-if="station?.generalInfo?.authors && station.generalInfo.authors.length > 0"
> >
<b> <b>
{{ {{
@@ -102,13 +102,11 @@ export default defineComponent({
}, },
props: { props: {
station: { station: {
type: Object as PropType<Station>, type: Object as PropType<Station>
required: true
}, },
onlineScenery: { onlineScenery: {
type: Object as PropType<OnlineScenery>, type: Object as PropType<OnlineScenery>
required: false
} }
} }
}); });
@@ -10,7 +10,7 @@
<router-link <router-link
class="dispatcher_name" class="dispatcher_name"
:to="`/journal/dispatchers?dispatcherName=${onlineScenery.dispatcherName}`" :to="`/journal/dispatchers?search-dispatcher=${onlineScenery.dispatcherName}`"
> >
<span <span
class="text--donator" class="text--donator"
@@ -1,24 +1,33 @@
<template> <template>
<section class="info-icons"> <section class="info-icons">
<span <span v-if="!station || !station.generalInfo">
v-if="station.generalInfo && station.generalInfo.reqLevel >= 0" <img
class="scenery-icon icon-info level" class="icon-info"
:style="calculateExpStyle(station.generalInfo.reqLevel)" src="/images/icon-unknown.svg"
> alt="icon-unknown"
{{ station.generalInfo.reqLevel >= 2 ? station.generalInfo.reqLevel : 'L' }} :title="$t('desc.unknown')"
/>
</span> </span>
<span <span
v-if="station.generalInfo" v-if="station?.generalInfo && station?.generalInfo.reqLevel >= 0"
class="scenery-icon icon-info level"
:style="calculateExpStyle(station?.generalInfo.reqLevel)"
>
{{ station?.generalInfo.reqLevel >= 2 ? station?.generalInfo.reqLevel : 'L' }}
</span>
<span
v-if="station?.generalInfo"
class="scenery-icon icon-info" class="scenery-icon icon-info"
:class="station.generalInfo.controlType.replace('+', '-')" :class="station?.generalInfo.controlType.replace('+', '-')"
:title="$t('desc.control-type') + $t(`controls.${station.generalInfo.controlType}`)" :title="$t('desc.control-type') + $t(`controls.${station?.generalInfo.controlType}`)"
v-html="getControlTypeAbbrev(station.generalInfo.controlType)" v-html="getControlTypeAbbrev(station?.generalInfo.controlType)"
> >
</span> </span>
<img <img
v-if="station.generalInfo?.SUP" v-if="station?.generalInfo?.SUP"
class="icon-info" class="icon-info"
src="/images/icon-SUP.svg" src="/images/icon-SUP.svg"
alt="SUP (RASP-UZK)" alt="SUP (RASP-UZK)"
@@ -26,7 +35,7 @@
/> />
<img <img
v-if="station.generalInfo?.signalType" v-if="station?.generalInfo?.signalType"
class="icon-info" class="icon-info"
:src="`/images/icon-${station.generalInfo.signalType}.svg`" :src="`/images/icon-${station.generalInfo.signalType}.svg`"
:alt="station.generalInfo.signalType" :alt="station.generalInfo.signalType"
@@ -34,7 +43,7 @@
/> />
<img <img
v-if="station.generalInfo?.availability == 'nonPublic'" v-if="station?.generalInfo?.availability == 'nonPublic'"
class="icon-info" class="icon-info"
src="/images/icon-lock.svg" src="/images/icon-lock.svg"
alt="Non-public scenery" alt="Non-public scenery"
@@ -42,7 +51,7 @@
/> />
<img <img
v-if="station.generalInfo?.availability == 'unavailable'" v-if="station?.generalInfo?.availability == 'unavailable'"
class="icon-info" class="icon-info"
src="/images/icon-unavailable.svg" src="/images/icon-unavailable.svg"
alt="Unavailable scenery" alt="Unavailable scenery"
@@ -50,7 +59,7 @@
/> />
<img <img
v-if="station.generalInfo?.availability == 'abandoned'" v-if="station?.generalInfo?.availability == 'abandoned'"
class="icon-info" class="icon-info"
src="/images/icon-abandoned.svg" src="/images/icon-abandoned.svg"
alt="Abandoned scenery" alt="Abandoned scenery"
@@ -58,20 +67,12 @@
/> />
<img <img
v-if="station.generalInfo?.lines" v-if="station?.generalInfo?.lines"
class="icon-info" class="icon-info"
src="/images/icon-real.svg" src="/images/icon-real.svg"
alt="real scenery" alt="real scenery"
:title="`${$t('desc.real')} ${station.generalInfo.lines}`" :title="`${$t('desc.real')} ${station.generalInfo.lines}`"
/> />
<img
v-if="!station.generalInfo"
class="icon-info"
src="/images/icon-unknown.svg"
alt="icon-unknown"
:title="$t('desc.unknown')"
/>
</section> </section>
</template> </template>
@@ -85,8 +86,7 @@ export default defineComponent({
mixins: [stationInfoMixin, styleMixin], mixins: [stationInfoMixin, styleMixin],
props: { props: {
station: { station: {
type: Object as PropType<Station>, type: Object as PropType<Station>
required: true
} }
} }
}); });
+19 -13
View File
@@ -13,14 +13,14 @@
</span> </span>
</span> </span>
<span class="header_links"> <span class="header_links" v-if="station">
<a <!-- <a
:href="`https://pragotron-td2.web.app/board?name=${station.name}`" :href="`https://pragotron-td2.web.app/board?name=${station.name}`"
target="_blank" target="_blank"
:title="$t('scenery.pragotron-link')" :title="$t('scenery.pragotron-link')"
> >
<img src="/images/icon-pragotron.svg" alt="icon-pragotron" /> <img src="/images/icon-pragotron.svg" alt="icon-pragotron" />
</a> </a> -->
<a :href="tabliceZbiorczeHref" target="_blank" :title="$t('scenery.tablice-link')"> <a :href="tabliceZbiorczeHref" target="_blank" :title="$t('scenery.tablice-link')">
<img src="/images/icon-tablice.ico" alt="icon-tablice" /> <img src="/images/icon-tablice.ico" alt="icon-tablice" />
@@ -48,7 +48,7 @@
<transition-group name="list-anim"> <transition-group name="list-anim">
<div <div
style="padding-bottom: 5em" style="padding-bottom: 5em"
v-if="store.dataStatuses.trains == 0 && computedScheduledTrains.length == 0" v-if="apiStore.dataStatuses.connection == 0 && computedScheduledTrains.length == 0"
key="list-loading" key="list-loading"
> >
<Loading /> <Loading />
@@ -187,10 +187,11 @@ import Loading from '../Global/Loading.vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import routerMixin from '../../mixins/routerMixin'; import routerMixin from '../../mixins/routerMixin';
import Station from '../../scripts/interfaces/Station'; import Station from '../../scripts/interfaces/Station';
import { useStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import modalTrainMixin from '../../mixins/modalTrainMixin'; import modalTrainMixin from '../../mixins/modalTrainMixin';
import ScheduledTrainStatus from './ScheduledTrainStatus.vue'; import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
import { OnlineScenery } from '../../store/typings'; import { OnlineScenery } from '../../store/typings';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({ export default defineComponent({
name: 'SceneryTimetable', name: 'SceneryTimetable',
@@ -201,12 +202,10 @@ export default defineComponent({
props: { props: {
station: { station: {
type: Object as PropType<Station>, type: Object as PropType<Station>
required: true
}, },
onlineScenery: { onlineScenery: {
type: Object as PropType<OnlineScenery>, type: Object as PropType<OnlineScenery>
required: false
} }
}, },
@@ -226,7 +225,8 @@ export default defineComponent({
const route = useRoute(); const route = useRoute();
const currentURL = computed(() => `${location.origin}${route.fullPath}`); const currentURL = computed(() => `${location.origin}${route.fullPath}`);
const store = useStore(); const apiStore = useApiStore();
const mainStore = useMainStore();
const chosenCheckpoint = ref( const chosenCheckpoint = ref(
props.station?.generalInfo?.checkpoints?.length == 0 props.station?.generalInfo?.checkpoints?.length == 0
@@ -237,25 +237,29 @@ export default defineComponent({
return { return {
currentURL, currentURL,
chosenCheckpoint, chosenCheckpoint,
store apiStore,
mainStore
}; };
}, },
computed: { computed: {
tabliceZbiorczeHref() { tabliceZbiorczeHref() {
let url = `https://tablice-td2.web.app/?station=${this.station.name}`; let url = `https://tablice-td2.web.app/?station=${this.station!.name}`;
if (this.chosenCheckpoint) url += `&checkpoint=${this.chosenCheckpoint}`; if (this.chosenCheckpoint) url += `&checkpoint=${this.chosenCheckpoint}`;
return url; return url;
}, },
computedScheduledTrains() { computedScheduledTrains() {
if (!this.station) return [];
return ( return (
this.onlineScenery?.scheduledTrains this.onlineScenery?.scheduledTrains
?.filter( ?.filter(
(train) => (train) =>
train.checkpointName.toLocaleLowerCase() == train.checkpointName.toLocaleLowerCase() ==
(this.chosenCheckpoint || this.station.name).toLocaleLowerCase() (this.chosenCheckpoint || this.station!.name).toLocaleLowerCase() &&
train.region == this.mainStore.region.id
) )
.sort((a, b) => { .sort((a, b) => {
if (a.stopStatusID > b.stopStatusID) return 1; if (a.stopStatusID > b.stopStatusID) return 1;
@@ -272,6 +276,8 @@ export default defineComponent({
methods: { methods: {
loadSelectedOption() { loadSelectedOption() {
if (!this.station) return;
this.chosenCheckpoint = this.chosenCheckpoint =
this.station.generalInfo?.checkpoints[0]?.checkpointName || this.station.name; this.station.generalInfo?.checkpoints[0]?.checkpointName || this.station.name;
}, },
@@ -28,7 +28,7 @@
<tbody> <tbody>
<tr v-for="historyItem in historyList" :key="historyItem.id"> <tr v-for="historyItem in historyList" :key="historyItem.id">
<td> <td>
<router-link :to="`/journal/timetables?timetableId=${historyItem.id}`"> <router-link :to="`/journal/timetables?search-train=%23${historyItem.id}`">
#{{ historyItem.id }} #{{ historyItem.id }}
</router-link> </router-link>
</td> </td>
@@ -37,11 +37,16 @@
{{ historyItem.trainNo }} {{ historyItem.trainNo }}
</td> </td>
<td>{{ historyItem.route.replace('|', ' -> ') }}</td> <td>{{ historyItem.route.replace('|', ' -> ') }}</td>
<td>{{ historyItem.driverName }}</td> <td>
<router-link :to="`/journal/timetables?search-driver=${historyItem.driverName}`">
{{ historyItem.driverName }}
</router-link>
</td>
<td> <td>
<router-link <router-link
v-if="historyItem.authorName" v-if="historyItem.authorName"
:to="`/journal/timetables?authorName=${historyItem.authorName}`" :to="`/journal/timetables?search-dispatcher=${historyItem.authorName}`"
>{{ historyItem.authorName }} >{{ historyItem.authorName }}
</router-link> </router-link>
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i> <i v-else>{{ $t('scenery.timetable-author-unknown') }}</i>
@@ -63,29 +68,26 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import axios from 'axios';
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import Station from '../../scripts/interfaces/Station'; import Station from '../../scripts/interfaces/Station';
import { URLs } from '../../scripts/utils/apiURLs';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import listObserverMixin from '../../mixins/listObserverMixin'; import listObserverMixin from '../../mixins/listObserverMixin';
import { OnlineScenery } from '../../store/typings'; import { OnlineScenery } from '../../store/typings';
import { API } from '../../typings/api'; import { API } from '../../typings/api';
import { Status } from '../../typings/common'; import { Status } from '../../typings/common';
import http from '../../http';
export default defineComponent({ export default defineComponent({
name: 'SceneryTimetablesHistory', name: 'SceneryTimetablesHistory',
mixins: [dateMixin, listObserverMixin], mixins: [dateMixin, listObserverMixin],
props: { props: {
station: { station: {
type: Object as PropType<Station>, type: Object as PropType<Station>
required: true
}, },
onlineScenery: { onlineScenery: {
type: Object as PropType<OnlineScenery>, type: Object as PropType<OnlineScenery>
required: false
} }
}, },
@@ -102,11 +104,20 @@ export default defineComponent({
}, },
methods: { methods: {
async fetchAPIData(countFrom = 0, countLimit = 15) { async fetchAPIData() {
try { if (!this.station && !this.onlineScenery) {
const requestString = `${URLs.stacjownikAPI}/api/getTimetables?issuedFrom=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`; this.dataStatus = Status.Data.Loaded;
return;
}
const response: API.TimetableHistory.Response = await (await axios.get(requestString)).data; try {
const response: API.TimetableHistory.Response = await (
await http.get('api/getTimetables', {
params: {
issuedFrom: this.station?.name
}
})
).data;
this.historyList = response; this.historyList = response;
@@ -117,7 +128,12 @@ export default defineComponent({
}, },
navigateToHistory() { navigateToHistory() {
this.$router.push(`/journal/timetables?issuedFrom=${this.station.name}`); this.$router.push({
path: '/journal/timetables',
query: {
'search-issuedFrom': this.station?.name || this.onlineScenery?.name
}
});
} }
}, },
components: { Loading } components: { Loading }
@@ -139,7 +139,7 @@ import { defineComponent, inject } from 'vue';
import keyMixin from '../../mixins/keyMixin'; import keyMixin from '../../mixins/keyMixin';
import routerMixin from '../../mixins/routerMixin'; import routerMixin from '../../mixins/routerMixin';
import { useStationFiltersStore } from '../../store/stationFiltersStore'; import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import FilterOption from './FilterOption.vue'; import FilterOption from './FilterOption.vue';
import StorageManager from '../../managers/storageManager'; import StorageManager from '../../managers/storageManager';
@@ -163,7 +163,7 @@ export default defineComponent({
setup() { setup() {
const isVisible = inject('isFilterCardVisible'); const isVisible = inject('isFilterCardVisible');
const store = useStore(); const store = useMainStore();
const filterStore = useStationFiltersStore(); const filterStore = useStationFiltersStore();
return { return {
@@ -447,7 +447,7 @@ export default defineComponent({
.section-inputs { .section-inputs {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, 1fr);
gap: 0.5em; gap: 0.5em;
margin: 1em 0; margin: 1em 0;
} }
+11 -12
View File
@@ -116,7 +116,7 @@
<td class="station_dispatcher-name"> <td class="station_dispatcher-name">
<span v-if="station.onlineInfo?.dispatcherName"> <span v-if="station.onlineInfo?.dispatcherName">
<b <b
v-if="store.donatorsData.includes(station.onlineInfo.dispatcherName)" v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
:title="$t('donations.dispatcher-message')" :title="$t('donations.dispatcher-message')"
@click.stop="openDonationModal" @click.stop="openDonationModal"
> >
@@ -279,7 +279,7 @@
</table> </table>
</div> </div>
<Loading v-if="!isDataLoaded && stations.length == 0" /> <Loading v-if="apiStore.dataStatuses.connection == Status.Loading" />
<div class="no-stations" v-else-if="stations.length == 0"> <div class="no-stations" v-else-if="stations.length == 0">
{{ $t('sceneries.no-stations') }} {{ $t('sceneries.no-stations') }}
@@ -288,17 +288,18 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, computed, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import stationInfoMixin from '../../mixins/stationInfoMixin'; import stationInfoMixin from '../../mixins/stationInfoMixin';
import styleMixin from '../../mixins/styleMixin'; import styleMixin from '../../mixins/styleMixin';
import Station from '../../scripts/interfaces/Station'; import Station from '../../scripts/interfaces/Station';
import { useStationFiltersStore } from '../../store/stationFiltersStore'; import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import { HeadIdsTypes, headIconsIds, headIds } from '../../scripts/data/stationHeaderNames'; import { HeadIdsTypes, headIconsIds, headIds } from '../../scripts/data/stationHeaderNames';
import StationStatusBadge from '../Global/StationStatusBadge.vue'; import StationStatusBadge from '../Global/StationStatusBadge.vue';
import { Status } from '../../typings/common'; import { Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -325,17 +326,15 @@ export default defineComponent({
}, },
setup() { setup() {
const store = useStore(); const mainStore = useMainStore();
const apiStore = useApiStore();
const stationFiltersStore = useStationFiltersStore(); const stationFiltersStore = useStationFiltersStore();
const isDataLoaded = computed(() => {
return store.dataStatuses.sceneries != Status.Data.Loading;
});
return { return {
isDataLoaded, Status: Status.Data,
stationFiltersStore, stationFiltersStore,
store mainStore,
apiStore
}; };
}, },
@@ -357,7 +356,7 @@ export default defineComponent({
openDonationModal(e: Event) { openDonationModal(e: Event) {
this.$emit('toggleDonationModal', true); this.$emit('toggleDonationModal', true);
this.store.modalLastClickedTarget = e.target; this.mainStore.modalLastClickedTarget = e.target;
}, },
openForumSite(e: Event, url: string | undefined) { openForumSite(e: Event, url: string | undefined) {
+5 -3
View File
@@ -35,7 +35,7 @@
<div class="train-driver"> <div class="train-driver">
<b <b
v-if="store.donatorsData.includes(train.driverName)" v-if="apiStore.donatorsData.includes(train.driverName)"
:title="$t('donations.driver-message')" :title="$t('donations.driver-message')"
> >
{{ train.driverName }} {{ train.driverName }}
@@ -126,7 +126,8 @@ import trainInfoMixin from '../../mixins/trainInfoMixin';
import Train from '../../scripts/interfaces/Train'; import Train from '../../scripts/interfaces/Train';
import ProgressBar from '../Global/ProgressBar.vue'; import ProgressBar from '../Global/ProgressBar.vue';
import TrainThumbnail from '../Global/TrainThumbnail.vue'; import TrainThumbnail from '../Global/TrainThumbnail.vue';
import { useStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({ export default defineComponent({
mixins: [trainInfoMixin, styleMixin], mixins: [trainInfoMixin, styleMixin],
@@ -145,7 +146,8 @@ export default defineComponent({
data() { data() {
return { return {
store: useStore() store: useMainStore(),
apiStore: useApiStore()
}; };
} }
}); });
+2 -2
View File
@@ -72,7 +72,7 @@
import { computed, defineComponent, PropType } from 'vue'; import { computed, defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import Train from '../../scripts/interfaces/Train'; import Train from '../../scripts/interfaces/Train';
import { useStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import StopDate from '../Global/StopDate.vue'; import StopDate from '../Global/StopDate.vue';
import StockList from '../Global/StockList.vue'; import StockList from '../Global/StockList.vue';
import { TrainStop } from '../../store/typings'; import { TrainStop } from '../../store/typings';
@@ -92,7 +92,7 @@ export default defineComponent({
setup(props) { setup(props) {
return { return {
store: useStore(), store: useMainStore(),
lastConfirmed: computed(() => { lastConfirmed: computed(() => {
return props.train.timetableData!.followingStops.findIndex( return props.train.timetableData!.followingStops.findIndex(
+6 -4
View File
@@ -16,7 +16,7 @@
<hr style="margin: 0.5em 0" /> <hr style="margin: 0.5em 0" />
<div v-if="store.dataStatuses.trains == Status.Loaded && regionTrains.length > 0"> <div v-if="apiStore.dataStatuses.connection == Status.Loaded && regionTrains.length > 0">
<div class="top-list general"> <div class="top-list general">
<transition-group tag="ul" name="stats-anim"> <transition-group tag="ul" name="stats-anim">
<li class="badge" key="timetable-count"> <li class="badge" key="timetable-count">
@@ -88,7 +88,7 @@
</div> </div>
</div> </div>
<div v-else-if="store.dataStatuses.trains != Status.Loaded"> <div v-else-if="apiStore.dataStatuses.connection != Status.Loaded">
{{ $t('train-stats.stats-loading') }} {{ $t('train-stats.stats-loading') }}
</div> </div>
@@ -102,8 +102,9 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import { Status } from '../../typings/common'; import { Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore';
interface ITop { interface ITop {
name: string; name: string;
@@ -127,7 +128,8 @@ export default defineComponent({
data() { data() {
return { return {
showOptions: false, showOptions: false,
store: useStore(), store: useMainStore(),
apiStore: useApiStore(),
Status: Status.Data Status: Status.Data
}; };
}, },
+10 -10
View File
@@ -1,17 +1,13 @@
<template> <template>
<transition name="status-anim" mode="out-in" tag="div" class="train-table"> <transition name="status-anim" mode="out-in" tag="div" class="train-table">
<div :key="store.dataStatuses.trains"> <div :key="apiStore.dataStatuses.connection">
<div class="table-info" key="offline" v-if="store.isOffline"> <div class="table-info" key="offline" v-if="store.isOffline">
{{ $t('app.offline') }} {{ $t('app.offline') }}
</div> </div>
<Loading v-else-if="trains.length == 0 && store.dataStatuses.trains == 0" key="loading" /> <Loading v-else-if="apiStore.dataStatuses.connection == Status.Loading" key="loading" />
<div <div class="table-info" key="no-trains" v-else-if="trains.length == 0">
class="table-info"
key="no-trains"
v-else-if="trains.length == 0 && store.dataStatuses.trains != 0"
>
{{ $t('trains.no-trains') }} {{ $t('trains.no-trains') }}
</div> </div>
@@ -35,10 +31,11 @@
import { defineComponent, inject, PropType, Ref } from 'vue'; import { defineComponent, inject, PropType, Ref } from 'vue';
import modalTrainMixin from '../../mixins/modalTrainMixin'; import modalTrainMixin from '../../mixins/modalTrainMixin';
import Train from '../../scripts/interfaces/Train'; import Train from '../../scripts/interfaces/Train';
import { useStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import TrainInfo from './TrainInfo.vue'; import TrainInfo from './TrainInfo.vue';
import { Status } from '../../typings/common'; import { Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({ export default defineComponent({
components: { Loading, TrainInfo }, components: { Loading, TrainInfo },
@@ -53,7 +50,8 @@ export default defineComponent({
mixins: [modalTrainMixin], mixins: [modalTrainMixin],
setup() { setup() {
const store = useStore(); const store = useMainStore();
const apiStore = useApiStore();
const searchedTrain = inject('searchedTrain') as Ref<string>; const searchedTrain = inject('searchedTrain') as Ref<string>;
const searchedDriver = inject('searchedDriver') as Ref<string>; const searchedDriver = inject('searchedDriver') as Ref<string>;
@@ -61,6 +59,8 @@ export default defineComponent({
searchedTrain, searchedTrain,
searchedDriver, searchedDriver,
store, store,
apiStore,
Status: Status.Data,
sorterActive: inject('sorterActive') as { sorterActive: inject('sorterActive') as {
id: string | number; id: string | number;
dir: number; dir: number;
@@ -72,7 +72,7 @@ export default defineComponent({
dataStatus() { dataStatus() {
if (this.store.isOffline) return Status.Data.Offline; if (this.store.isOffline) return Status.Data.Offline;
if (this.trains.length == 0 && this.store.dataStatuses.trains == Status.Data.Loading) if (this.trains.length == 0 && this.apiStore.dataStatuses.connection == Status.Data.Loading)
return Status.Data.Loading; return Status.Data.Loading;
return Status.Data.Loaded; return Status.Data.Loaded;
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
import axios from 'axios';
const http = axios.create({
baseURL:
import.meta.env.VITE_API_MODE === 'development'
? 'http://localhost:3001'
: 'https://stacjownik.spythere.eu'
});
export default http;
+46 -25
View File
@@ -144,7 +144,8 @@
"filter-withComments": "COMMENTS", "filter-withComments": "COMMENTS",
"filter-twr": "HIGH RISK CARGO", "filter-twr": "HIGH RISK CARGO",
"filter-skr": "EXCEEDED GAUGE", "filter-skr": "EXCEEDED GAUGE",
"filter-twr-skr": "ALL TYPES", "filter-twr-skr": "BOTH TYPES",
"filter-all-specials": "ALL",
"filter-common": "NO WARNINGS", "filter-common": "NO WARNINGS",
"filter-passenger": "PASSENGER", "filter-passenger": "PASSENGER",
"filter-freight": "FREIGHT", "filter-freight": "FREIGHT",
@@ -156,9 +157,9 @@
"filter-clear": "CLEAR FILTERS", "filter-clear": "CLEAR FILTERS",
"filter-section-timetable-status": "TIMETABLE STATUS", "filter-section-timetable-status": "TIMETABLE STATUS",
"filter-section-twrskr": "WARNINGS", "filter-section-special": "SPECIAL TYPE",
"filter-all": "ALL ENTRIES", "filter-all-statuses": "ALL",
"filter-abandoned": "ABANDONED", "filter-abandoned": "ABANDONED",
"filter-fulfilled": "FULFILLED", "filter-fulfilled": "FULFILLED",
"filter-active": "ACTIVE" "filter-active": "ACTIVE"
@@ -347,29 +348,49 @@
"last-seen-at": "Last seen at", "last-seen-at": "Last seen at",
"currently-at": "Currently at", "currently-at": "Currently at",
"stats-title": "DRIVING STATISTICS OF", "driver-stats": {
"button": "DRIVER STATS",
"title": "{name}'s DRIVER STATS",
"info": "Enter a proper nickname into filters [F] to see user's driving statistics!",
"timetables": "TIMETABLES",
"longest-timetable": "LONGEST TIMETABLE",
"avg-timetable": "AVERAGE TIMETABLE LENGTH",
"distance": "DISTANCE",
"stations": "STATIONS"
},
"stats-timetables": "TIMETABLES", "daily-stats": {
"stats-longest-timetable": "LONGEST TIMETABLE", "button": "DAILY STATS",
"stats-avg-timetable": "AVERAGE TIMETABLE LENGTH", "title": "STATS OF THE DAY",
"stats-distance": "DISTANCE", "info": "Today's statistics are unavailable yet!",
"stats-stations": "STATIONS", "total": "Issued timetables: {count} (total distance: {distance})",
"longest": "The longest timetable: #{id} (made by {author} for {driver}, distance: {distance})",
"most-active-dr": "The most active dispatcher: {dispatcher} (created {count})",
"most-active-dr-many": "The most active dispatchers: {dispatchers} (created {count} each)",
"most-active-driver": "The most active driver: {driver} (total driven distance: {distance})",
"longest-duties": "The longest service: {dispatcher} at {station} (duration: {duration})",
"count": "timetable | timetables",
"timetable-stats-title": "Daily stats on {date}", "rippedSwitches": "RIPPED SWITCHES",
"timetable-stats-total": "Issued timetables: {count} (total distance: {distance})", "derailments": "DERAILMENTS",
"timetable-stats-longest": "The longest timetable: #{id} (made by {author} for {driver}, distance: {distance})", "skippedStopSignals": "SKIPPED STOP SIGNALS",
"timetable-stats-most-active-dr": "The most active dispatcher: {dispatcher} (created {count})", "radioStops": "RADIOSTOPS",
"timetable-stats-most-active-dr-many": "The most active dispatchers: {dispatchers} (created {count} each)", "kills": "KILLS"
"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", "dispatcher-stats": {
"button": "DISPATCHER STATS",
"daily-stats-title": "DAILY STATS", "title": "{name}'s DISPATCHER STATS",
"daily-stats-info": "Today's statistics are unavailable yet!", "empty": "This user has no statistics saved yet!",
"info": "Enter a proper nickname into filters [F] to see user's dispatcher statistics!",
"driver-stats-title": "DRIVER STATS", "services-count": "SERVICES",
"driver-stats-info": "Enter a proper nickname into filters [F] to see user's driving statistics!", "service-max": "MAX SERVICE DURATION",
"service-avg": "AVG SERVICE DURATION",
"timetables-count": "ISSUED TIMETABLES",
"timetables-sum": "TIMETABLES DISTANCE SUM",
"timetables-max": "LONGEST TIMETABLE",
"timetables-avg": "AVG TIMETABLE DISTANCE"
},
"stats-loading": "Fetching statistics...", "stats-loading": "Fetching statistics...",
"stats-error": "Oops! An unexpected error occurred while trying to fetch statistics! :/", "stats-error": "Oops! An unexpected error occurred while trying to fetch statistics! :/",
@@ -405,8 +426,8 @@
"two-way-routes": "Two way routes", "two-way-routes": "Two way routes",
"option-active-timetables": "Active timetables", "option-active-timetables": "Active timetables",
"option-timetables-history": "Timetables history", "option-timetables-history": "Timetables history PL1",
"option-dispatchers-history": "Dispatchers history", "option-dispatchers-history": "Dispatchers history PL1",
"timetable-author-title": "Issued by", "timetable-author-title": "Issued by",
"timetable-author-unknown": "Author unknown", "timetable-author-unknown": "Author unknown",
+45 -24
View File
@@ -133,7 +133,8 @@
"filter-noComments": "BEZ UWAG", "filter-noComments": "BEZ UWAG",
"filter-twr": "WYS. RYZYKA", "filter-twr": "WYS. RYZYKA",
"filter-skr": "SKRAJNIA", "filter-skr": "SKRAJNIA",
"filter-twr-skr": "WSZYSTKIE", "filter-twr-skr": "TWR/SKR",
"filter-all-statuses": "WSZYSTKIE",
"filter-common": "ZWYKŁE", "filter-common": "ZWYKŁE",
"filter-passenger": "PASAŻERSKIE", "filter-passenger": "PASAŻERSKIE",
"filter-freight": "TOWAROWE", "filter-freight": "TOWAROWE",
@@ -145,9 +146,9 @@
"filter-clear": "WYŁĄCZ FILTRY", "filter-clear": "WYŁĄCZ FILTRY",
"filter-section-timetable-status": "STATUS ROZKŁADU JAZDY", "filter-section-timetable-status": "STATUS ROZKŁADU JAZDY",
"filter-section-twrskr": "UWAGI", "filter-section-special": "TYPY SPECJALNE",
"filter-all": "WSZYSTKIE", "filter-all-specials": "WSZYSTKIE",
"filter-abandoned": "PORZUCONE", "filter-abandoned": "PORZUCONE",
"filter-fulfilled": "WYPEŁNIONE", "filter-fulfilled": "WYPEŁNIONE",
"filter-active": "AKTYWNE" "filter-active": "AKTYWNE"
@@ -326,31 +327,51 @@
"load-data": "Pobierz dalszą historię...", "load-data": "Pobierz dalszą historię...",
"stats-title": "STATYSTYKI MASZYNISTY",
"last-seen-at": "Ostatnio widziany na: ", "last-seen-at": "Ostatnio widziany na: ",
"currently-at": "Obecnie na scenerii: ", "currently-at": "Obecnie na scenerii: ",
"stats-timetables": "ROZKŁADY JAZDY", "driver-stats": {
"stats-longest-timetable": "NAJDŁUŻSZY RJ", "button": "STAT. MASZYNISTY",
"stats-avg-timetable": "ŚREDNIA DŁUGOŚĆ RJ", "title": "STATYSTYKI MASZYNISTY {name}",
"stats-distance": "DYSTANS", "info": "Wpisz nazwę użytkownika w filtrach [F], aby zobaczyć jego statystyki maszynisty!",
"stats-stations": "STACJE", "timetables": "ROZKŁADY JAZDY",
"longest-timetable": "NAJDŁUŻSZY RJ",
"avg-timetable": "ŚREDNIA DŁUGOŚĆ RJ",
"distance": "DYSTANS",
"stations": "STACJE"
},
"timetable-stats-total": "Stworzone rozkłady jazdy: {count} (łączny dystans: {distance})", "daily-stats": {
"timetable-stats-longest": "Najdłuższy rozkład jazdy: #{id} (stworzony przez dyżurnego {author} dla maszynisty {driver} o dystansie {distance})", "button": "STATYSTYKI DNIA",
"timetable-stats-most-active-dr": "Najaktywniejszy dyżurny: {dispatcher} (stworzył {count})", "title": "STATYSTYKI DNIA",
"timetable-stats-most-active-dr-many": "Najaktywniejsi dyżurni: {dispatchers} (stworzyli po {count})", "info": "Dzisiejsze statystyki nie są jeszcze dostępne!",
"timetable-stats-most-active-driver": "Najaktywniejszy maszynista: {driver} (łączny przejechany dystans: {distance})", "total": "Stworzone rozkłady jazdy: {count} (łączny dystans: {distance})",
"timetable-stats-longest-duties": "Najdłuższa służba: {dispatcher} na scenerii {station} (czas trwania: {duration})", "longest": "Najdłuższy rozkład jazdy: #{id} (stworzony przez dyżurnego {author} dla maszynisty {driver} o dystansie {distance})",
"most-active-dr": "Najaktywniejszy dyżurny: {dispatcher} (stworzył {count})",
"most-active-dr-many": "Najaktywniejsi dyżurni: {dispatchers} (stworzyli po {count})",
"most-active-driver": "Najaktywniejszy maszynista: {driver} (łączny przejechany dystans: {distance})",
"longest-duties": "Najdłuższa służba: {dispatcher} na scenerii {station} (czas trwania: {duration})",
"count": "rozkład jazdy | rozkładów jazdy",
"timetable-count": "rozkład jazdy | rozkładów jazdy", "rippedSwitches": "ROZPRUTE ZWROTNICE",
"derailments": "WYKOLEJENIA",
"skippedStopSignals": "POMINIĘTE S1",
"radioStops": "RADIOSTOPY",
"kills": "POTRĄCENIA"
},
"daily-stats-title": "STATYSTYKI DNIA", "dispatcher-stats": {
"daily-stats-info": "Dzisiejsze statystyki nie są jeszcze dostępne!", "button": "STATYSTYKI DYŻURNEGO",
"title": "STATYSTYKI DYŻURNEGO {name}",
"driver-stats-title": "STATYSTYKI GRACZA", "info": "Wpisz nazwę użytkownika w filtrach [F], aby zobaczyć jego statystyki dyżurnego!",
"driver-stats-info": "Wpisz nazwę użytkownika w filtrach [F], aby zobaczyć jego statystyki maszynisty!", "services-count": "DYŻURY",
"service-max": "MAKS. CZAS DYŻURU",
"service-avg": "ŚREDNI CZAS DYŻURU",
"timetables-count": "WYSTAWIONE RJ",
"timetables-sum": "SUMA WYSTAWIONYCH RJ",
"timetables-max": "NAJDŁUŻSZY WYSTAWIONY RJ",
"timetables-avg": "ŚREDNIA WYSTAWIONYCH RJ"
},
"stats-loading": "Pobieranie statystyk...", "stats-loading": "Pobieranie statystyk...",
"stats-error": "Ups! Wystąpił błąd podczas próby pobrania statystyk!", "stats-error": "Ups! Wystąpił błąd podczas próby pobrania statystyk!",
@@ -386,8 +407,8 @@
"two-way-routes": "Szlaki dwutorowe", "two-way-routes": "Szlaki dwutorowe",
"option-active-timetables": "Aktywne rozkłady jazdy", "option-active-timetables": "Aktywne rozkłady jazdy",
"option-timetables-history": "Historia rozkładów", "option-timetables-history": "Historia rozkładów PL1",
"option-dispatchers-history": "Historia dyżurów", "option-dispatchers-history": "Historia dyżurów PL1",
"timetable-author-title": "Wydany przez", "timetable-author-title": "Wydany przez",
"timetable-author-unknown": "Autor nieznany", "timetable-author-unknown": "Autor nieznany",
+3 -3
View File
@@ -1,16 +1,16 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useStore } from '../store/mainStore'; import { useApiStore } from '../store/apiStore';
export default defineComponent({ export default defineComponent({
data() { data() {
return { return {
store: useStore() apiStore: useApiStore()
}; };
}, },
methods: { methods: {
isDonator(name: string) { isDonator(name: string) {
return this.store.donatorsData.includes(name); return this.apiStore.donatorsData.includes(name);
} }
} }
}); });
-2
View File
@@ -10,8 +10,6 @@ export default defineComponent({
mountObserver(actionFunction: () => void, target: Element) { mountObserver(actionFunction: () => void, target: Element) {
this.observer = new IntersectionObserver( this.observer = new IntersectionObserver(
(entries) => { (entries) => {
console.log(entries);
if (entries[0].intersectionRatio > 0.5) actionFunction(); if (entries[0].intersectionRatio > 0.5) actionFunction();
}, },
{ threshold: 0.2 } { threshold: 0.2 }
+2 -2
View File
@@ -1,10 +1,10 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useStore } from '../store/mainStore'; import { useMainStore } from '../store/mainStore';
export default defineComponent({ export default defineComponent({
data() { data() {
return { return {
store: useStore() store: useMainStore()
}; };
}, },
+5 -9
View File
@@ -18,7 +18,8 @@ const routes: Array<RouteRecordRaw> = [
props: (route) => ({ props: (route) => ({
train: route.query.train, train: route.query.train,
driver: route.query.driver, driver: route.query.driver,
trainId: route.query.trainId trainId: route.query.trainId,
region: route.query.region
}) })
}, },
{ {
@@ -39,9 +40,7 @@ const routes: Array<RouteRecordRaw> = [
name: 'JournalTimetables', name: 'JournalTimetables',
component: JournalTimetablesVue, component: JournalTimetablesVue,
props: (route) => ({ props: (route) => ({
trainNo: route.query.trainNo, region: route.query.region
driverName: route.query.driverName,
timetableId: route.query.timetableId
}) })
}, },
{ {
@@ -49,8 +48,7 @@ const routes: Array<RouteRecordRaw> = [
name: 'JournalDispatchers', name: 'JournalDispatchers',
component: JournalDispatchersVue, component: JournalDispatchersVue,
props: (route) => ({ props: (route) => ({
sceneryName: route.query.sceneryName, region: route.query.region
dispatcherName: route.query.dispatcherName
}) })
}, },
{ {
@@ -61,12 +59,10 @@ const routes: Array<RouteRecordRaw> = [
const router = createRouter({ const router = createRouter({
scrollBehavior(to, from, savedPosition) { scrollBehavior(to, from, savedPosition) {
if (to.name == 'SceneryView' && from.name && from.query['view'] === undefined) if (to.name == 'SceneryView' && from.name !== to.name && from.query['view'] === undefined)
return { el: `.app_main` }; return { el: `.app_main` };
if (savedPosition) return savedPosition; if (savedPosition) return savedPosition;
// if (from.name == 'SceneryView' && to.name == 'StationsView') return { el: `.last-selected`, top: 20 };
}, },
history: createWebHistory(), history: createWebHistory(),
routes routes
-7
View File
@@ -1,7 +0,0 @@
export const URLs = {
stacjownikAPI:
import.meta.env.VITE_APP_API_DEV === '1' && !import.meta.env.PROD
? 'http://localhost:3001'
: 'https://stacjownik.spythere.eu',
stacjownikAPIDev: 'localhost:3000'
};
+134
View File
@@ -0,0 +1,134 @@
import { defineStore } from 'pinia';
import http from '../http';
import { API } from '../typings/api';
import axios from 'axios';
import { Status } from '../typings/common';
import { StationJSONData } from './typings';
export const useApiStore = defineStore('apiStore', {
state: () => ({
dataStatuses: {
connection: Status.Data.Loading,
sceneries: Status.Data.Loading,
timetables: Status.Data.Loading,
dispatchers: Status.Data.Loading,
trains: Status.Data.Loading
},
activeData: undefined as API.ActiveData.Response | undefined,
rollingStockData: undefined as API.RollingStock.Response | undefined,
donatorsData: [] as API.Donators.Response,
sceneryData: [] as StationJSONData[],
activeDataTimeout: undefined as number | undefined
}),
actions: {
async setupAPI() {
// Static data
this.fetchStockInfoData();
this.fetchDonatorsData();
this.fetchStationsGeneralInfo();
if (this.activeDataTimeout === undefined) this.startActiveDataScheduler();
},
// async setDataStatuses() {
// if (!window.navigator.onLine) {
// this.dataStatuses.connection = Status.Data.Offline;
// this.dataStatuses.sceneries = Status.Data.Offline;
// this.dataStatuses.trains = Status.Data.Offline;
// this.dataStatuses.dispatchers = Status.Data.Offline;
// this.dataStatuses.timetables = Status.Data.Offline;
// }
// if (!this.activeData?.activeSceneries) {
// this.dataStatuses.connection = Status.Data.Loaded;
// this.dataStatuses.sceneries = Status.Data.Error;
// this.dataStatuses.trains = Status.Data.Error;
// this.dataStatuses.dispatchers = Status.Data.Error;
// return;
// }
// this.dataStatuses.connection = Status.Data.Loaded;
// this.dataStatuses.sceneries = Status.Data.Loaded;
// this.dataStatuses.trains = !this.activeData.trains ? Status.Data.Warning : Status.Data.Loaded;
// this.dataStatuses.dispatchers = Status.Data.Loaded;
// },
async fetchDonatorsData() {
try {
const response = await http.get<API.Donators.Response>('api/getDonators');
this.donatorsData = response.data;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania informacji o donatorach:', error);
}
},
async fetchStockInfoData() {
try {
this.rollingStockData = (
await axios.get<API.RollingStock.Response>(
'https://raw.githubusercontent.com/Spythere/api/main/td2/data/stockInfo.json'
)
).data;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania informacji o taborze z API:', error);
}
},
async startActiveDataScheduler() {
if (!window.navigator.onLine) {
this.dataStatuses.connection = Status.Data.Offline;
return;
}
if (import.meta.env.VITE_API_MODE === 'mock') {
const mockActiveData = await import('../data/mockActiveData.json');
this.dataStatuses.connection = Status.Data.Loaded;
this.activeData = mockActiveData;
console.warn('Stacjownik działa w trybie mockowania danych z WS');
return;
}
try {
const data = (await http.get<API.ActiveData.Response>('api/getActiveData')).data;
this.activeData = data;
this.dataStatuses.connection = Status.Data.Loaded;
} catch (error) {
this.dataStatuses.connection = Status.Data.Error;
console.error('Wystąpił błąd podczas pobierania danych online z API!');
} finally {
this.activeDataTimeout = window.setTimeout(
() => {
this.startActiveDataScheduler();
},
~~(1000 * (Math.random() * (25 - 20) + 25))
);
}
},
async stopActiveDataScheduler() {
window.clearTimeout(this.activeDataTimeout);
this.activeDataTimeout = undefined;
},
async fetchStationsGeneralInfo() {
const sceneryData: StationJSONData[] = (await http.get<StationJSONData[]>('api/getSceneries'))
.data;
if (!sceneryData) {
this.dataStatuses.sceneries = Status.Data.Error;
return;
}
this.dataStatuses.sceneries = Status.Data.Loaded;
this.sceneryData = sceneryData;
}
}
});
+96 -134
View File
@@ -1,42 +1,24 @@
import axios from 'axios';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { io } from 'socket.io-client';
import StationRoutes from '../scripts/interfaces/StationRoutes'; import StationRoutes from '../scripts/interfaces/StationRoutes';
import Train from '../scripts/interfaces/Train'; import Train from '../scripts/interfaces/Train';
import { URLs } from '../scripts/utils/apiURLs';
import { parseSpawns, getScheduledTrains, getStationTrains } from './utils'; import { parseSpawns, getScheduledTrains, getStationTrains } from './utils';
import { OnlineScenery, ScheduledTrain, StationJSONData, StoreState } from './typings'; import { OnlineScenery, ScheduledTrain, StoreState } from './typings';
import packageInfo from '../../package.json';
import { Websocket, API } from '../typings/api';
import { Status } from '../typings/common'; import { Status } from '../typings/common';
import Station from '../scripts/interfaces/Station';
import { useApiStore } from './apiStore';
import { API } from '../typings/api';
export const useStore = defineStore('store', { export const useMainStore = defineStore('store', {
state: () => state: () =>
({ ({
activeData: {} as unknown,
rollingStockData: undefined,
donatorsData: [],
stationList: [],
regionOnlineCounters: [],
routesList: [],
sceneryData: [],
lastDispatcherStatuses: [],
region: { id: 'eu', value: 'PL1' }, region: { id: 'eu', value: 'PL1' },
trainCount: 0,
stationCount: 0,
webSocket: undefined,
isOffline: false, isOffline: false,
dispatcherStatsName: '', dispatcherStatsName: '',
dispatcherStatsData: undefined, dispatcherStatsStatus: Status.Data.Initialized,
driverStatsName: '', driverStatsName: '',
driverStatsData: undefined, driverStatsData: undefined,
@@ -44,31 +26,15 @@ export const useStore = defineStore('store', {
chosenModalTrainId: undefined, chosenModalTrainId: undefined,
dataStatuses: {
connection: Status.Data.Loading,
sceneries: Status.Data.Loading,
timetables: Status.Data.Loading,
dispatchers: Status.Data.Loading,
trains: Status.Data.Loading
},
currentStatsTab: null,
blockScroll: false, blockScroll: false,
listenerLaunched: false, modalLastClickedTarget: null
modalLastClickedTarget: null,
tooltip: {
content: '',
visible: false,
x: 0,
y: 0
}
}) as StoreState, }) as StoreState,
getters: { getters: {
trainList(): Train[] { trainList(): Train[] {
return (this.activeData?.trains ?? []) const apiStore = useApiStore();
return (apiStore.activeData?.trains ?? [])
.filter((train) => train.timetable || train.online) .filter((train) => train.timetable || train.online)
.map((train) => { .map((train) => {
const stock = train.stockString.split(';'); const stock = train.stockString.split(';');
@@ -87,7 +53,7 @@ export const useStore = defineStore('store', {
distance: train.distance, distance: train.distance,
signal: train.signal, signal: train.signal,
online: train.online, online: Boolean(train.online),
driverId: train.driverId, driverId: train.driverId,
driverName: train.driverName, driverName: train.driverName,
currentStationName: train.currentStationName, currentStationName: train.currentStationName,
@@ -119,10 +85,12 @@ export const useStore = defineStore('store', {
}, },
onlineSceneryList(state): OnlineScenery[] { onlineSceneryList(state): OnlineScenery[] {
if (state.isOffline) return []; const apiStore = useApiStore();
if (!state.activeData?.activeSceneries) return [];
return state.activeData?.activeSceneries.reduce((list, scenery) => { if (state.isOffline) return [];
if (!apiStore.activeData?.activeSceneries) return [];
return apiStore.activeData?.activeSceneries.reduce((list, scenery) => {
if (scenery.isOnline !== 1 && Date.now() - scenery.lastSeen > 1000 * 60 * 2) return list; if (scenery.isOnline !== 1 && Date.now() - scenery.lastSeen > 1000 * 60 * 2) return list;
if (scenery.dispatcherStatus == Status.ActiveDispatcher.UNKNOWN) return list; if (scenery.dispatcherStatus == Status.ActiveDispatcher.UNKNOWN) return list;
@@ -181,20 +149,12 @@ export const useStore = defineStore('store', {
return list; return list;
}, [] as OnlineScenery[]); }, [] as OnlineScenery[]);
} },
},
actions: {
async fetchStationsGeneralInfo() {
const sceneryData: StationJSONData[] = await (
await axios.get(`${URLs.stacjownikAPI}/api/getSceneries`)
).data;
if (!sceneryData) { stationList(): Station[] {
this.dataStatuses.sceneries = Status.Data.Error; const apiStore = useApiStore();
return;
}
this.stationList = sceneryData.map((scenery) => { return apiStore.sceneryData.map((scenery) => {
return { return {
name: scenery.name, name: scenery.name,
@@ -243,95 +203,97 @@ export const useStore = defineStore('store', {
} }
}; };
}); });
}, }
},
actions: {
async processStationsOnlineInfo(activeData: API.ActiveData.Response) {
if (!activeData.activeSceneries) return;
async connectToWebsocket() { const onlineSceneries = activeData.activeSceneries.reduce((acc, scenery) => {
if (import.meta.env.VITE_APP_WS_DEV === '1') { const savedStation = this.stationList.find((st) => scenery.stationName === st.name);
const mockWebsocketData = await import('../data/mockWebsocketData.json');
this.dataStatuses.connection = Status.Data.Loaded;
this.activeData = mockWebsocketData as any;
this.setStatuses();
console.warn('Stacjownik działa w trybie mockowania danych z WS'); if (scenery.isOnline !== 1 && Date.now() - scenery.lastSeen > 1000 * 60 * 2) return acc;
if (scenery.dispatcherStatus == Status.ActiveDispatcher.UNKNOWN) return acc;
return; const station = this.stationList.find((s) => s.name === scenery.stationName);
}
const socket = io(URLs.stacjownikAPI, { const scheduledTrains = getScheduledTrains(this.trainList, scenery, station?.generalInfo);
transports: ['websocket', 'polling'],
rememberUpgrade: true,
reconnection: true
});
socket.emit('CONNECTION', { version: packageInfo.version }); const stationTrains = getStationTrains(
this.trainList,
scheduledTrains,
this.region.id,
scenery
);
socket.on('connect_error', () => { // Remove checkpoint duplicates
this.dataStatuses.connection = Status.Data.Error; const uniqueScheduledTrains = scheduledTrains.reduce(
}); (uniqueList, sTrain) =>
uniqueList.find((v) => v.trainId === sTrain.trainId)
? uniqueList
: [...uniqueList, sTrain],
[] as ScheduledTrain[]
);
socket.on('UPDATE', (data: Websocket.ActiveData) => { const dispatcherTimestamp =
this.activeData = data; scenery.dispatcherStatus == Status.ActiveDispatcher.NO_LIMIT
this.dataStatuses.connection = Status.Data.Loaded; ? Date.now() + 25500000
: scenery.dispatcherStatus > 5
? scenery.dispatcherStatus
: null;
this.setStatuses(); const onlineInfo = {
}); name: scenery.stationName,
hash: scenery.stationHash,
region: scenery.region,
maxUsers: scenery.maxUsers,
currentUsers: scenery.currentUsers,
spawns: parseSpawns(scenery.spawnString),
dispatcherName: scenery.dispatcherName,
dispatcherRate: scenery.dispatcherRate,
dispatcherId: scenery.dispatcherId,
dispatcherExp: scenery.dispatcherExp,
dispatcherIsSupporter: scenery.dispatcherIsSupporter,
scheduledTrains: scheduledTrains,
stationTrains: stationTrains,
dispatcherStatus: scenery.dispatcherStatus,
dispatcherTimestamp: dispatcherTimestamp,
socket.emit('FETCH_DATA', { version: packageInfo.version }, (data: Websocket.ActiveData) => { isOnline: scenery.isOnline == 1,
this.dataStatuses.connection = Status.Data.Loaded;
this.activeData = data;
this.setStatuses();
});
this.webSocket = socket; scheduledTrainCount: {
}, all: uniqueScheduledTrains.length,
confirmed: uniqueScheduledTrains.filter((train) => train.stopInfo.confirmed).length,
unconfirmed: uniqueScheduledTrains.filter((train) => !train.stopInfo.confirmed).length
}
};
async connectToAPI() { if (savedStation) savedStation.onlineInfo = onlineInfo;
this.connectToWebsocket(); else
this.fetchStockInfoData(); this.stationList.push({
this.fetchDonatorsData(); name: onlineInfo.name,
this.fetchStationsGeneralInfo(); onlineInfo: onlineInfo
});
acc.push(onlineInfo);
return acc;
}, [] as OnlineScenery[]);
// Reset online info of already offline sceneries
this.stationList
.filter(
(station) =>
station.onlineInfo &&
onlineSceneries.findIndex(
(os) => os.region == station.onlineInfo!.region && station.name == os.name
) != -1
)
.forEach((station) => (station.onlineInfo = undefined));
}, },
async changeRegion(region: StoreState['region']) { async changeRegion(region: StoreState['region']) {
this.region = region; this.region = region;
},
async fetchStockInfoData() {
try {
this.rollingStockData = (
await axios.get<API.RollingStock.Response>(
'https://raw.githubusercontent.com/Spythere/api/main/td2/data/stockInfo.json'
)
).data;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania informacji o taborze z API:', error);
}
},
async fetchDonatorsData() {
try {
const response = await axios.get<API.Donators.Response>(
`${URLs.stacjownikAPI}/api/getDonators`
);
if (response.data) this.donatorsData = response.data;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania informacji o donatorach:', error);
}
},
async setStatuses() {
if (!this.activeData.activeSceneries) {
this.dataStatuses.sceneries = Status.Data.Error;
this.dataStatuses.trains = Status.Data.Error;
this.dataStatuses.dispatchers = Status.Data.Error;
return;
}
this.dataStatuses.sceneries = Status.Data.Loaded;
this.dataStatuses.trains = !this.activeData.trains ? Status.Data.Warning : Status.Data.Loaded;
this.dataStatuses.dispatchers = Status.Data.Loaded;
} }
} }
}); });
+16 -4
View File
@@ -1,6 +1,6 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import inputData from '../data/options.json'; import inputData from '../data/options.json';
import { useStore } from './mainStore'; import { useMainStore } from './mainStore';
import { filterStations, sortStations } from '../scripts/utils/filterUtils'; import { filterStations, sortStations } from '../scripts/utils/filterUtils';
import { HeadIdsTypes } from '../scripts/data/stationHeaderNames'; import { HeadIdsTypes } from '../scripts/data/stationHeaderNames';
import StorageManager from '../managers/storageManager'; import StorageManager from '../managers/storageManager';
@@ -70,14 +70,26 @@ export const useStationFiltersStore = defineStore('stationFiltersStore', {
}, },
filteredStationList: (state) => { filteredStationList: (state) => {
const store = useStore(); const store = useMainStore();
return store.stationList const savedStationNames = store.stationList.map((s) => s.name);
.map((station) => ({
const onlineUnsavedStations = store.onlineSceneryList
.filter((os) => !savedStationNames.includes(os.name) && os.region == store.region.id)
.map((os) => ({
name: os.name,
generalInfo: undefined,
onlineInfo: os
}));
return [
...onlineUnsavedStations,
...store.stationList.map((station) => ({
...station, ...station,
onlineInfo: store.onlineSceneryList.find( onlineInfo: store.onlineSceneryList.find(
(os) => os.name == station.name && os.region == store.region.id (os) => os.name == station.name && os.region == store.region.id
) )
})) }))
]
.filter((station) => filterStations(station, state.filters)) .filter((station) => filterStations(station, state.filters))
.sort((a, b) => sortStations(a, b, state.sorterActive)); .sort((a, b) => sortStations(a, b, state.sorterActive));
} }
+3 -39
View File
@@ -1,6 +1,4 @@
import { Socket } from 'socket.io-client'; import { API } from '../typings/api';
import Station from '../scripts/interfaces/Station';
import { API, Websocket } from '../typings/api';
import { Status } from '../typings/common'; import { Status } from '../typings/common';
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault'; export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
@@ -12,26 +10,8 @@ export interface RegionCounters {
} }
export interface StoreState { export interface StoreState {
stationList: Station[];
activeData: Websocket.ActiveData;
rollingStockData?: API.RollingStock.Response;
donatorsData: API.Donators.Response;
regionOnlineCounters: RegionCounters[];
lastDispatcherStatuses: {
hash: string;
statusTimestamp: number;
statusID: Status.ActiveDispatcher;
}[];
sceneryData: any[][];
region: { id: string; value: string }; region: { id: string; value: string };
trainCount: number;
stationCount: number;
webSocket?: Socket;
isOffline: boolean; isOffline: boolean;
dispatcherStatsName: string; dispatcherStatsName: string;
@@ -43,26 +23,8 @@ export interface StoreState {
chosenModalTrainId?: string; chosenModalTrainId?: string;
currentStatsTab: 'daily' | 'driver' | null;
dataStatuses: {
connection: Status.Data;
sceneries: Status.Data;
timetables: Status.Data;
dispatchers: Status.Data;
trains: Status.Data;
};
listenerLaunched: boolean;
blockScroll: boolean; blockScroll: boolean;
modalLastClickedTarget: EventTarget | null; modalLastClickedTarget: EventTarget | null;
tooltip: {
visible: boolean;
x: number;
y: number;
content: string;
};
} }
export interface StationRoutesInfo { export interface StationRoutesInfo {
@@ -166,6 +128,8 @@ export interface ScheduledTrain {
stopLabel: string; stopLabel: string;
stopStatus: StopStatus; stopStatus: StopStatus;
stopStatusID: number; stopStatusID: number;
region: string;
} }
export enum StopStatus { export enum StopStatus {
+2
View File
@@ -175,6 +175,8 @@ export function getCheckpointTrain(
stopStatus: trainStopStatus.stopStatus, stopStatus: trainStopStatus.stopStatus,
stopStatusID: trainStopStatus.stopStatusID, stopStatusID: trainStopStatus.stopStatusID,
region: train.region,
arrivingLine, arrivingLine,
departureLine, departureLine,
+2 -3
View File
@@ -5,6 +5,7 @@
overflow-y: auto; overflow-y: auto;
height: 90vh; height: 90vh;
min-height: 550px; min-height: 550px;
margin-top: 0.5em;
padding-right: 0.2em; padding-right: 0.2em;
} }
@@ -24,7 +25,7 @@
text-align: end; text-align: end;
padding: 0.25em; padding: 0.25em;
margin: 0.5em 0; margin-top: 0.5em;
} }
.journal_warning { .journal_warning {
@@ -53,9 +54,7 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 0.5em; gap: 0.5em;
position: relative; position: relative;
margin-bottom: 0.5em;
} }
.btn--load-data { .btn--load-data {
+15 -4
View File
@@ -2,24 +2,35 @@
@import 'responsive.scss'; @import 'responsive.scss';
.stats-tab { .stats-tab {
position: absolute;
right: 0;
z-index: 99;
transform: translateY(1em);
width: 100%;
background-color: #1a1a1a; background-color: #1a1a1a;
box-shadow: 0 0 5px 1px $accentCol; box-shadow: 0 0 5px 1px $accentCol;
padding: 1em; padding: 1em;
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
}
margin-bottom: 0.5em; hr.header-separator {
margin-bottom: 1em;
}
width: 100%; hr.section-separator {
margin: 1em 0;
} }
.info-stats { .info-stats {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center;
gap: 0.5em; gap: 0.5em;
margin-top: 1em;
} }
.stat-badge { .stat-badge {
+2 -1
View File
@@ -30,7 +30,8 @@
top: calc(100% + 0.5em); top: calc(100% + 0.5em);
background-color: $bgCol; background-color: $bgCol;
box-shadow: 0 5px 10px 2px #0f0f0f; // box-shadow: 0 5px 10px 2px #0f0f0f;
box-shadow: 0 0 5px 1px $accentCol;
width: 100%; width: 100%;
max-width: 550px; max-width: 550px;
+1 -5
View File
@@ -5,11 +5,6 @@
.actions-bar { .actions-bar {
display: flex; display: flex;
gap: 0.5em; gap: 0.5em;
margin-bottom: 0.5em;
}
.filters-options {
position: relative;
} }
h1.option-title { h1.option-title {
@@ -57,6 +52,7 @@ h1.option-title {
.sort-option[data-selected='true'] { .sort-option[data-selected='true'] {
color: $accentCol; color: $accentCol;
font-weight: bold;
} }
.filter-option { .filter-option {
+49
View File
@@ -0,0 +1,49 @@
@font-face {
font-family: 'Quicksand';
src:
url('/fonts/Quicksand-Bold.woff2') format('woff2'),
url('/fonts/Quicksand-Bold.woff') format('woff');
font-weight: bold;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Quicksand';
src:
url('/fonts/Quicksand-SemiBold.woff2') format('woff2'),
url('/fonts/Quicksand-SemiBold.woff') format('woff');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Quicksand';
src:
url('/fonts/Quicksand-Medium.woff2') format('woff2'),
url('/fonts/Quicksand-Medium.woff') format('woff');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Quicksand';
src:
url('/fonts/Quicksand-Regular.woff2') format('woff2'),
url('/fonts/Quicksand-Regular.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Quicksand';
src:
url('/fonts/Quicksand-Light.woff2') format('woff2'),
url('/fonts/Quicksand-Light.woff') format('woff');
font-weight: 300;
font-style: normal;
font-display: swap;
}
+8 -2
View File
@@ -1,3 +1,5 @@
@import 'fonts.scss';
:root { :root {
--clr-primary: #ffc014; --clr-primary: #ffc014;
--clr-secondary: #2f2f2f; --clr-secondary: #2f2f2f;
@@ -11,7 +13,7 @@
--clr-skr: #ff5100; --clr-skr: #ff5100;
--clr-twr: #ffbb00; --clr-twr: #ffbb00;
--clr-error: #df3e3e; --clr-error: #fa3636;
--clr-warning: #c59429; --clr-warning: #c59429;
--clr-donator: #f7a4ff; --clr-donator: #f7a4ff;
@@ -158,6 +160,10 @@ ul {
color: #ccc; color: #ccc;
} }
&--error {
color: var(--clr-error);
}
&--donator { &--donator {
color: var(--clr-donator); color: var(--clr-donator);
text-shadow: var(--clr-donator) 0 0 10px; text-shadow: var(--clr-donator) 0 0 10px;
@@ -183,7 +189,7 @@ a.a-button {
&[data-disabled='true'] { &[data-disabled='true'] {
user-select: none; user-select: none;
pointer-events: none; pointer-events: none;
opacity: 0.85; opacity: 0.7;
} }
&.btn--filled { &.btn--filled {
+70 -35
View File
@@ -1,6 +1,12 @@
import { Status } from './common'; import { Status } from './common';
export namespace API { export namespace API {
export namespace ActiveData {
export interface Response {
activeSceneries?: API.ActiveSceneries.Response;
trains?: API.ActiveTrains.Response;
}
}
export namespace DispatcherHistory { export namespace DispatcherHistory {
export type Response = Data[]; export type Response = Data[];
@@ -25,7 +31,11 @@ export namespace API {
export namespace DispatcherStats { export namespace DispatcherStats {
export interface DistanceStat { export interface DistanceStat {
routeDistance: number; routeDistance: number | null;
}
export interface DurationStat {
currentDuration: number | null;
} }
export interface Count { export interface Count {
@@ -33,11 +43,18 @@ export namespace API {
} }
export interface Response { export interface Response {
_sum: DistanceStat; services: {
_max: DistanceStat; count: number;
_min: DistanceStat; durationMax: number;
_avg: DistanceStat; durationAvg: number;
_count: Count; } | null;
issuedTimetables: {
count: number;
distanceMax: number;
distanceAvg: number;
distanceSum: number;
} | null;
} }
} }
@@ -116,9 +133,9 @@ export namespace API {
driverLevel?: number; driverLevel?: number;
currentStationName: string; currentStationName: string;
currentStationHash: string; currentStationHash?: string;
online: boolean; online: number;
lastSeen: number; lastSeen: number;
region: string; region: string;
@@ -132,16 +149,16 @@ export namespace API {
stopNameRAW: string; stopNameRAW: string;
stopType: string; stopType: string;
stopDistance: number; stopDistance: number;
pointId: number; pointId: string;
mainStop: boolean; mainStop: boolean;
arrivalLine: string; arrivalLine: string | null;
arrivalTimestamp: number; arrivalTimestamp: number;
arrivalRealTimestamp: number; arrivalRealTimestamp: number;
arrivalDelay: number; arrivalDelay: number;
departureLine: string; departureLine: string | null;
departureTimestamp: number; departureTimestamp: number;
departureRealTimestamp: number; departureRealTimestamp: number;
departureDelay: number; departureDelay: number;
@@ -150,9 +167,9 @@ export namespace API {
beginsHere: boolean; beginsHere: boolean;
terminatesHere: boolean; terminatesHere: boolean;
confirmed: boolean; confirmed: number;
stopped: boolean; stopped: number;
stopTime: number; stopTime: number | null;
} }
export interface Timetable { export interface Timetable {
@@ -257,21 +274,47 @@ export namespace API {
distanceAvg: number; distanceAvg: number;
maxTimetable: API.TimetableHistory.Data | null; maxTimetable: API.TimetableHistory.Data | null;
mostActiveDispatchers: { globalDiff: GlobalDiff;
name: string; globalMax: GlobalMax;
count: number;
}[];
mostActiveDrivers: { mostActiveDispatchers: MostActiveDispatcher[];
name: string; mostActiveDrivers: MostActiveDriver[];
distance: number;
}[];
longestDuties: { longestDuties: LongestDuty[];
name: string; }
duration: number;
station: string; export interface MostActiveDispatcher {
}[]; name: string;
count: number;
}
export interface MostActiveDriver {
name: string;
distance: number;
}
export interface LongestDuty {
name: string;
duration: number;
station: string;
}
export interface GlobalDiff {
rippedSwitches: number;
derailments: number;
skippedStopSignals: number;
radioStops: number;
kills: number;
drivenKilometers: number;
routedTrains: number;
}
export interface GlobalMax {
_max: {
drivers: number;
dispatchers: number;
timetables: number;
};
} }
} }
@@ -280,14 +323,6 @@ export namespace API {
} }
} }
export namespace Websocket {
export interface ActiveData {
activeSceneries?: API.ActiveSceneries.Response;
trains?: API.ActiveTrains.Response;
connectedSocketCount: number;
}
}
export namespace GithubAPI { export namespace GithubAPI {
export namespace Release { export namespace Release {
export interface Author { export interface Author {
+1 -1
View File
@@ -11,7 +11,7 @@ export namespace Status {
} }
export enum Data { export enum Data {
Offline = 2, Offline = -2,
Initialized = -1, Initialized = -1,
Loading = 0, Loading = 0,
Error = 1, Error = 1,
+89 -38
View File
@@ -3,15 +3,19 @@
<JournalHeader /> <JournalHeader />
<div class="journal_wrapper"> <div class="journal_wrapper">
<JournalOptions <div class="journal_top-bar">
@on-search-confirm="fetchHistoryData" <JournalOptions
@on-options-reset="resetOptions" @on-search-confirm="fetchHistoryData"
@on-refresh-data="fetchHistoryData(true)" @on-options-reset="resetOptions"
:sorter-option-ids="['timestampFrom', 'duration']" @on-refresh-data="fetchHistoryData(true)"
:data-status="dataStatus" :sorter-option-ids="['timestampFrom', 'duration']"
:current-options-active="currentOptionsActive" :data-status="dataStatus"
optionsType="dispatchers" :current-options-active="currentOptionsActive"
/> optionsType="dispatchers"
/>
<JournalStats :statsButtons="statsButtons" />
</div>
<div class="journal_refreshed-date" v-if="dataRefreshedAt"> <div class="journal_refreshed-date" v-if="dataRefreshedAt">
{{ $t('journal.data-refreshed-at') }}: {{ dataRefreshedAt.toLocaleString($i18n.locale) }} {{ $t('journal.data-refreshed-at') }}: {{ dataRefreshedAt.toLocaleString($i18n.locale) }}
@@ -32,26 +36,34 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, provide, reactive, Ref, ref } from 'vue'; import { defineComponent, provide, reactive, Ref, ref } from 'vue';
import axios from 'axios';
import JournalOptions from '../components/JournalView/JournalOptions.vue'; import http from '../http';
import { URLs } from '../scripts/utils/apiURLs'; import { useMainStore } from '../store/mainStore';
import { useStore } from '../store/mainStore';
import JournalDispatchersList from '../components/JournalView/JournalDispatchersList.vue';
import JournalHeader from '../components/JournalView/JournalHeader.vue';
import { LocationQuery } from 'vue-router'; import { LocationQuery } from 'vue-router';
import { Journal } from '../components/JournalView/typings'; import { Journal } from '../components/JournalView/typings';
import { API } from '../typings/api'; import { API } from '../typings/api';
import { Status } from '../typings/common'; import { Status } from '../typings/common';
const DISPATCHERS_API_URL = `${URLs.stacjownikAPI}/api/getDispatchers`; import JournalDispatchersList from '../components/JournalView/JournalDispatchers/JournalDispatchersList.vue';
import JournalOptions from '../components/JournalView/JournalOptions.vue';
import JournalHeader from '../components/JournalView/JournalHeader.vue';
import JournalStats from '../components/JournalView/JournalStats.vue';
const statsButtons: Journal.StatsButton[] = [
{
tab: Journal.StatsTab.DISPATCHER_STATS,
localeKey: 'journal.dispatcher-stats.button',
iconName: 'user',
disabled: true
}
];
export default defineComponent({ export default defineComponent({
components: { components: {
JournalOptions, JournalOptions,
JournalDispatchersList, JournalHeader,
JournalHeader JournalStats,
JournalDispatchersList
}, },
name: 'JournalDispatchers', name: 'JournalDispatchers',
@@ -68,6 +80,8 @@ export default defineComponent({
}, },
data: () => ({ data: () => ({
statsButtons,
currentQuery: '', currentQuery: '',
currentQueryArray: [] as string[], currentQueryArray: [] as string[],
dataRefreshedAt: null as Date | null, dataRefreshedAt: null as Date | null,
@@ -92,7 +106,7 @@ export default defineComponent({
'search-dispatcher': '', 'search-dispatcher': '',
'search-station': '', 'search-station': '',
'search-date': '' 'search-date': ''
} as Journal.DispatcherSearcher); } as Journal.DispatcherSearchType);
const countFromIndex = ref(0); const countFromIndex = ref(0);
const countLimit = 15; const countLimit = 15;
@@ -105,7 +119,7 @@ export default defineComponent({
const scrollElement: Ref<HTMLElement | null> = ref(null); const scrollElement: Ref<HTMLElement | null> = ref(null);
return { return {
store: useStore(), mainStore: useMainStore(),
sorterActive, sorterActive,
searchersValues, searchersValues,
@@ -123,6 +137,15 @@ export default defineComponent({
this.currentOptionsActive = this.currentOptionsActive =
q.length > 2 || q.length > 2 ||
q.some((qv) => qv.startsWith('sortBy=') && qv.split('=')[1] != 'timestampFrom'); q.some((qv) => qv.startsWith('sortBy=') && qv.split('=')[1] != 'timestampFrom');
},
'mainStore.dispatcherStatsData'(stats) {
this.statsButtons.find((sb) => sb.tab == Journal.StatsTab.DISPATCHER_STATS)!.disabled =
stats === undefined;
},
async 'mainStore.dispatcherStatsName'() {
this.fetchDispatcherStats();
} }
}, },
@@ -145,6 +168,16 @@ export default defineComponent({
}, },
methods: { methods: {
handleRouteParams() {
this.$router.push({
query: {
'search-date': this.searchersValues['search-date'] || undefined,
'search-station': this.searchersValues['search-station'] || undefined,
'search-dispatcher': this.searchersValues['search-dispatcher'] || undefined
}
});
},
handleScroll(e: Event) { handleScroll(e: Event) {
const listElement = e.target as HTMLElement; const listElement = e.target as HTMLElement;
const scrollTop = listElement.scrollTop; const scrollTop = listElement.scrollTop;
@@ -157,24 +190,44 @@ export default defineComponent({
}, },
handleQueries(query: LocationQuery) { handleQueries(query: LocationQuery) {
const queryKeys = Object.keys(query); this.setOptions(query as any);
if (queryKeys.includes('sceneryName')) this.setSearchers('', `${query.sceneryName}`, '');
if (queryKeys.includes('dispatcherName'))
this.setSearchers('', '', `${query.dispatcherName}`);
}, },
setSearchers(date: string, station: string, dispatcher: string) { async fetchDispatcherStats() {
this.searchersValues['search-date'] = date; if (!this.mainStore.dispatcherStatsName) {
this.searchersValues['search-station'] = station; this.mainStore.dispatcherStatsData = undefined;
this.searchersValues['search-dispatcher'] = dispatcher; return;
}
try {
const statsData: API.DispatcherStats.Response = await (
await http.get('api/getDispatcherStats', {
params: {
name: this.mainStore.dispatcherStatsName
}
})
).data;
this.mainStore.dispatcherStatsData = statsData;
} catch (error) {
this.mainStore.dispatcherStatsData = undefined;
console.error('Ups! Wystąpił błąd przy próbie pobrania statystyk dyżurnego! :/');
}
},
setOptions(options: { [key: string]: string }) {
this.searchersValues['search-date'] = options['search-date'] ?? '';
this.searchersValues['search-station'] = options['search-station'] ?? '';
this.searchersValues['search-dispatcher'] = options['search-dispatcher'] ?? '';
this.sorterActive.id =
(options['sorter-active'] as Journal.DispatcherSorterKey) ?? 'timestampFrom';
}, },
resetOptions() { resetOptions() {
this.setSearchers('', '', ''); this.setOptions({});
this.sorterActive.id = 'timestampFrom'; this.sorterActive.id = 'timestampFrom';
this.fetchHistoryData();
}, },
async addHistoryData() { async addHistoryData() {
@@ -183,9 +236,7 @@ export default defineComponent({
this.countFromIndex = this.historyList.length; this.countFromIndex = this.historyList.length;
const responseData: API.DispatcherHistory.Response = await ( const responseData: API.DispatcherHistory.Response = await (
await axios.get( await http.get(`api/getDispatchers?${this.currentQuery}&countFrom=${this.countFromIndex}`)
`${DISPATCHERS_API_URL}?${this.currentQuery}&countFrom=${this.countFromIndex}`
)
).data; ).data;
if (!responseData) return; if (!responseData) return;
@@ -232,7 +283,7 @@ export default defineComponent({
if (reset) this.dataStatus = Status.Data.Loading; if (reset) this.dataStatus = Status.Data.Loading;
const responseData: API.DispatcherHistory.Response = await ( const responseData: API.DispatcherHistory.Response = await (
await axios.get(`${DISPATCHERS_API_URL}?${this.currentQuery}`) await http.get(`api/getDispatchers?${this.currentQuery}`)
).data; ).data;
if (!responseData) { if (!responseData) {
@@ -246,7 +297,7 @@ export default defineComponent({
this.historyList = responseData; this.historyList = responseData;
// Stats display // Stats display
this.store.dispatcherStatsName = this.mainStore.dispatcherStatsName =
this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim() this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim()
? this.historyList[0].dispatcherName ? this.historyList[0].dispatcherName
: ''; : '';
+139 -79
View File
@@ -3,18 +3,19 @@
<JournalHeader /> <JournalHeader />
<div class="journal_wrapper"> <div class="journal_wrapper">
<JournalOptions <div class="journal_top-bar">
@on-search-confirm="fetchHistoryData" <JournalOptions
@on-options-reset="resetOptions" @onOptionsReset="resetOptions"
@on-refresh-data="fetchHistoryData" @onRefreshData="fetchHistoryData"
:sorter-option-ids="['timetableId', 'beginDate', 'routeDistance', 'allStopsCount']" :sorter-option-ids="['timetableId', 'beginDate', 'routeDistance', 'allStopsCount']"
:filters="journalTimetableFilters" :filters="journalTimetableFilters"
:currentOptionsActive="currentOptionsActive" :currentOptionsActive="currentOptionsActive"
:data-status="dataStatus" :data-status="dataStatus"
optionsType="timetables" optionsType="timetables"
/> />
<JournalStats /> <JournalStats :statsButtons="statsButtons" />
</div>
<div class="journal_refreshed-date" v-if="dataRefreshedAt"> <div class="journal_refreshed-date" v-if="dataRefreshedAt">
{{ $t('journal.data-refreshed-at') }}: {{ dataRefreshedAt.toLocaleString($i18n.locale) }} {{ $t('journal.data-refreshed-at') }}: {{ dataRefreshedAt.toLocaleString($i18n.locale) }}
@@ -35,7 +36,6 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, provide, reactive, Ref, ref } from 'vue'; import { defineComponent, provide, reactive, Ref, ref } from 'vue';
import axios from 'axios';
import dateMixin from '../mixins/dateMixin'; import dateMixin from '../mixins/dateMixin';
import routerMixin from '../mixins/routerMixin'; import routerMixin from '../mixins/routerMixin';
@@ -45,8 +45,7 @@ import JournalOptions from '../components/JournalView/JournalOptions.vue';
import JournalStats from '../components/JournalView/JournalStats.vue'; import JournalStats from '../components/JournalView/JournalStats.vue';
import JournalHeader from '../components/JournalView/JournalHeader.vue'; import JournalHeader from '../components/JournalView/JournalHeader.vue';
import { URLs } from '../scripts/utils/apiURLs'; import { useMainStore } from '../store/mainStore';
import { useStore } from '../store/mainStore';
import { LocationQuery } from 'vue-router'; import { LocationQuery } from 'vue-router';
@@ -54,50 +53,62 @@ import JournalTimetablesList from '../components/JournalView/JournalTimetables/J
import { Journal } from '../components/JournalView/typings'; import { Journal } from '../components/JournalView/typings';
import { Status } from '../typings/common'; import { Status } from '../typings/common';
import { API } from '../typings/api'; import { API } from '../typings/api';
import http from '../http';
const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`;
export const journalTimetableFilters: Journal.TimetableFilter[] = [ export const journalTimetableFilters: Journal.TimetableFilter[] = [
{ {
id: Journal.TimetableFilterId.ALL, id: Journal.TimetableFilterId.ALL_STATUSES,
filterSection: Journal.FilterSection.TIMETABLE_STATUS, filterSection: Journal.FilterSection.TIMETABLE_STATUS,
isActive: true isActive: true,
default: true
}, },
{ {
id: Journal.TimetableFilterId.ACTIVE, id: Journal.TimetableFilterId.ACTIVE,
filterSection: Journal.FilterSection.TIMETABLE_STATUS, filterSection: Journal.FilterSection.TIMETABLE_STATUS,
isActive: false isActive: false,
default: false
}, },
{ {
id: Journal.TimetableFilterId.FULFILLED, id: Journal.TimetableFilterId.FULFILLED,
filterSection: Journal.FilterSection.TIMETABLE_STATUS, filterSection: Journal.FilterSection.TIMETABLE_STATUS,
isActive: false isActive: false,
default: false
}, },
{ {
id: Journal.TimetableFilterId.ABANDONED, id: Journal.TimetableFilterId.ABANDONED,
filterSection: Journal.FilterSection.TIMETABLE_STATUS, filterSection: Journal.FilterSection.TIMETABLE_STATUS,
isActive: false isActive: false,
default: false
}, },
{ {
id: Journal.TimetableFilterId.TWR_SKR, id: Journal.TimetableFilterId.ALL_SPECIALS,
filterSection: Journal.FilterSection.TWRSKR, filterSection: Journal.FilterSection.SPECIAL,
isActive: true isActive: true,
default: true
}, },
{ {
id: Journal.TimetableFilterId.TWR, id: Journal.TimetableFilterId.TWR,
filterSection: Journal.FilterSection.TWRSKR, filterSection: Journal.FilterSection.SPECIAL,
isActive: false isActive: false,
default: false
}, },
{ {
id: Journal.TimetableFilterId.SKR, id: Journal.TimetableFilterId.SKR,
filterSection: Journal.FilterSection.TWRSKR, filterSection: Journal.FilterSection.SPECIAL,
isActive: false isActive: false,
default: false
},
{
id: Journal.TimetableFilterId.TWR_SKR,
filterSection: Journal.FilterSection.SPECIAL,
isActive: false,
default: false
} }
]; ];
@@ -107,8 +118,12 @@ interface TimetablesQueryParams {
timetableId?: string; timetableId?: string;
authorName?: string; authorName?: string;
timestampFrom?: number; // timestampFrom?: number;
timestampTo?: number; // timestampTo?: number;
dateFrom?: string;
dateTo?: string;
issuedFrom?: string; issuedFrom?: string;
countFrom?: number; countFrom?: number;
@@ -141,6 +156,24 @@ export default defineComponent({
}, },
data: () => ({ data: () => ({
journalTimetableFilters,
mainStore: useMainStore(),
statsButtons: [
{
tab: Journal.StatsTab.DAILY_STATS,
localeKey: 'journal.daily-stats.button',
iconName: 'stats',
disabled: false
},
{
tab: Journal.StatsTab.DRIVER_STATS,
localeKey: 'journal.driver-stats.button',
iconName: 'train',
disabled: true
}
],
currentQueryParams: {} as TimetablesQueryParams, currentQueryParams: {} as TimetablesQueryParams,
dataRefreshedAt: null as Date | null, dataRefreshedAt: null as Date | null,
@@ -152,7 +185,6 @@ export default defineComponent({
currentOptionsActive: false, currentOptionsActive: false,
timetableHistory: [] as API.TimetableHistory.Response, timetableHistory: [] as API.TimetableHistory.Response,
journalTimetableFilters,
dataStatus: Status.Data.Loading, dataStatus: Status.Data.Loading,
dataErrorMessage: '' dataErrorMessage: ''
@@ -160,10 +192,11 @@ export default defineComponent({
setup() { setup() {
const sorterActive: Journal.TimetableSorter = reactive({ id: 'timetableId', dir: 'desc' }); const sorterActive: Journal.TimetableSorter = reactive({ id: 'timetableId', dir: 'desc' });
// const journalFilterActive = ref(journalTimetableFilters[0]);
const initFilters: readonly Journal.TimetableFilter[] = JSON.parse( const initFilters: readonly Journal.TimetableFilter[] = JSON.parse(
JSON.stringify(journalTimetableFilters) JSON.stringify(journalTimetableFilters)
); );
const filterList: Journal.TimetableFilter[] = reactive(JSON.parse(JSON.stringify(initFilters))); const filterList: Journal.TimetableFilter[] = reactive(JSON.parse(JSON.stringify(initFilters)));
const searchersValues = reactive({ const searchersValues = reactive({
@@ -192,15 +225,22 @@ export default defineComponent({
countFromIndex, countFromIndex,
countLimit, countLimit,
scrollElement, scrollElement
store: useStore()
}; };
}, },
watch: { watch: {
currentQueryParams(q: TimetablesQueryParams) { currentQueryParams(q: TimetablesQueryParams) {
this.currentOptionsActive = Object.values(q).some((v) => v !== undefined); this.currentOptionsActive = Object.values(q).some((v) => v !== undefined);
},
'mainStore.driverStatsData'(driverStats) {
this.statsButtons.find((sb) => sb.tab == Journal.StatsTab.DRIVER_STATS)!.disabled =
driverStats === undefined;
},
async 'mainStore.driverStatsName'() {
this.fetchDriverStats();
} }
}, },
@@ -228,42 +268,51 @@ export default defineComponent({
}, },
handleQueries(query: LocationQuery) { handleQueries(query: LocationQuery) {
const queryKeys = Object.keys(query); this.setOptions(query as any);
if (queryKeys.includes('timetableId'))
this.setSearchers('', '', `#${query.timetableId}`, '', '');
if (queryKeys.includes('issuedFrom'))
this.setSearchers('', '', '', '', `${query.issuedFrom}`);
if (queryKeys.includes('authorName'))
this.setSearchers('', '', '', `${query.authorName}`, '');
}, },
setSearchers( async fetchDriverStats() {
date: string, if (!this.mainStore.driverStatsName) {
driver: string, this.mainStore.driverStatsData = undefined;
train: string, this.mainStore.driverStatsStatus = Status.Data.Initialized;
dispatcher: string, return;
issuedFrom: string }
) {
this.searchersValues['search-date'] = date; try {
this.searchersValues['search-driver'] = driver; this.mainStore.driverStatsStatus = Status.Data.Loading;
this.searchersValues['search-train'] = train;
this.searchersValues['search-dispatcher'] = dispatcher; const statsData: API.DriverStats.Response = await (
this.searchersValues['search-issuedFrom'] = issuedFrom; await http.get(`api/getDriverInfo?name=${this.mainStore.driverStatsName}`)
).data;
this.mainStore.driverStatsData = statsData;
this.mainStore.driverStatsStatus = Status.Data.Loaded;
} catch (error) {
this.mainStore.driverStatsData = undefined;
this.mainStore.driverStatsStatus = Status.Data.Error;
console.error('Ups! Wystąpił błąd przy próbie pobrania statystyk maszynisty! :/');
}
},
setOptions(options: { [key: string]: string }) {
this.searchersValues['search-date'] = options['search-date'] ?? '';
this.searchersValues['search-driver'] = options['search-driver'] ?? '';
this.searchersValues['search-train'] = options['search-train'] ?? '';
this.searchersValues['search-dispatcher'] = options['search-dispatcher'] ?? '';
this.searchersValues['search-issuedFrom'] = options['search-issuedFrom'] ?? '';
this.sorterActive.id =
(options['sorter-active'] as Journal.TimetableSorterKey) ?? 'timetableId';
this.filterList.forEach((f) => {
f.isActive =
options[f.filterSection] === f.id ||
(options[f.filterSection] === undefined && f.default);
});
}, },
resetOptions() { resetOptions() {
this.setSearchers('', '', '', '', ''); this.setOptions({});
this.sorterActive.id = 'timetableId';
this.filterList.forEach(
(f) =>
(f.isActive =
this.initFilters.find((initFilter) => initFilter.id == f.id)?.isActive || false)
);
this.fetchHistoryData();
}, },
async addHistoryData() { async addHistoryData() {
@@ -272,7 +321,7 @@ export default defineComponent({
this.currentQueryParams['countFrom'] = this.timetableHistory.length; this.currentQueryParams['countFrom'] = this.timetableHistory.length;
const responseData: API.TimetableHistory.Response = await ( const responseData: API.TimetableHistory.Response = await (
await axios.get(`${TIMETABLES_API_URL}`, { await http.get('api/getTimetables', {
params: { ...this.currentQueryParams } params: { ...this.currentQueryParams }
}) })
).data; ).data;
@@ -292,13 +341,19 @@ export default defineComponent({
const driverName = this.searchersValues['search-driver'].trim() || undefined; const driverName = this.searchersValues['search-driver'].trim() || undefined;
const trainNo = this.searchersValues['search-train'].trim() || undefined; const trainNo = this.searchersValues['search-train'].trim() || undefined;
const authorName = this.searchersValues['search-dispatcher'].trim() || undefined; const authorName = this.searchersValues['search-dispatcher'].trim() || undefined;
const dateString = this.searchersValues['search-date'].trim() || undefined; const dateFrom = this.searchersValues['search-date'].trim() || undefined;
const issuedFrom = this.searchersValues['search-issuedFrom'].trim() || undefined; const issuedFrom = this.searchersValues['search-issuedFrom'].trim() || undefined;
const timestampFrom = dateString let dateTo: string | undefined = undefined;
? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000
: undefined; if (dateFrom) {
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined; const d = new Date(dateFrom);
d.setDate(d.getDate() + 1);
dateTo = d.toISOString().split('T')[0];
}
// const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) : undefined;
// const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
const queryParams: TimetablesQueryParams = {}; const queryParams: TimetablesQueryParams = {};
@@ -321,23 +376,28 @@ export default defineComponent({
queryParams['fulfilled'] = 1; queryParams['fulfilled'] = 1;
break; break;
case Journal.TimetableFilterId.ALL: case Journal.TimetableFilterId.ALL_STATUSES:
queryParams['terminated'] = undefined; queryParams['terminated'] = undefined;
queryParams['fulfilled'] = undefined; queryParams['fulfilled'] = undefined;
break; break;
case Journal.TimetableFilterId.TWR_SKR: case Journal.TimetableFilterId.ALL_SPECIALS:
queryParams['twr'] = undefined; queryParams['twr'] = undefined;
queryParams['skr'] = undefined; queryParams['skr'] = undefined;
break; break;
case Journal.TimetableFilterId.TWR: case Journal.TimetableFilterId.TWR:
queryParams['twr'] = 1; queryParams['twr'] = 1;
queryParams['skr'] = undefined; queryParams['skr'] = 0;
break; break;
case Journal.TimetableFilterId.SKR: case Journal.TimetableFilterId.SKR:
queryParams['twr'] = undefined; queryParams['twr'] = 0;
queryParams['skr'] = 1;
break;
case Journal.TimetableFilterId.TWR_SKR:
queryParams['twr'] = 1;
queryParams['skr'] = 1; queryParams['skr'] = 1;
break; break;
@@ -352,8 +412,8 @@ export default defineComponent({
queryParams['countLimit'] = undefined; queryParams['countLimit'] = undefined;
queryParams['authorName'] = authorName; queryParams['authorName'] = authorName;
queryParams['timestampFrom'] = timestampFrom; queryParams['dateFrom'] = dateFrom;
queryParams['timestampTo'] = timestampTo; queryParams['dateTo'] = dateTo;
queryParams['issuedFrom'] = issuedFrom; queryParams['issuedFrom'] = issuedFrom;
queryParams['sortBy'] = queryParams['sortBy'] =
this.sorterActive.id != 'timetableId' ? this.sorterActive.id : undefined; this.sorterActive.id != 'timetableId' ? this.sorterActive.id : undefined;
@@ -365,7 +425,7 @@ export default defineComponent({
try { try {
const responseData: API.TimetableHistory.Response = await ( const responseData: API.TimetableHistory.Response = await (
await axios.get(`${TIMETABLES_API_URL}`, { await http.get('api/getTimetables', {
params: this.currentQueryParams params: this.currentQueryParams
}) })
).data; ).data;
+23 -16
View File
@@ -1,14 +1,6 @@
<template> <template>
<div class="scenery-view"> <div class="scenery-view">
<div class="scenery-offline" v-if="!stationInfo && store.dataStatuses.sceneries == 2"> <div class="scenery-wrapper" ref="card-wrapper">
<div>{{ $t('scenery.no-scenery') }}</div>
<action-button>
<router-link to="/">{{ $t('scenery.return-btn') }}</router-link>
</action-button>
</div>
<div class="scenery-wrapper" v-if="stationInfo" ref="card-wrapper">
<div class="scenery-left"> <div class="scenery-left">
<div class="scenery-actions"> <div class="scenery-actions">
<button class="back-btn btn" :title="$t('scenery.return-btn')" @click="navigateTo('/')"> <button class="back-btn btn" :title="$t('scenery.return-btn')" @click="navigateTo('/')">
@@ -16,7 +8,11 @@
</button> </button>
</div> </div>
<SceneryHeader :station="stationInfo" :onlineScenery="onlineSceneryInfo" /> <SceneryHeader
:stationName="station"
:station="stationInfo"
:onlineScenery="onlineSceneryInfo"
/>
<SceneryInfo :station="stationInfo" :onlineScenery="onlineSceneryInfo" /> <SceneryInfo :station="stationInfo" :onlineScenery="onlineSceneryInfo" />
</div> </div>
@@ -33,7 +29,14 @@
</button> </button>
</div> </div>
<keep-alive> <div
v-if="
apiStore.dataStatuses.sceneries == Status.Loading ||
apiStore.dataStatuses.connection == Status.Loading
"
></div>
<keep-alive v-else>
<component <component
:is="currentMode" :is="currentMode"
:onlineScenery="onlineSceneryInfo" :onlineScenery="onlineSceneryInfo"
@@ -50,7 +53,7 @@
import { computed, defineComponent } from 'vue'; import { computed, defineComponent } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import routerMixin from '../mixins/routerMixin'; import routerMixin from '../mixins/routerMixin';
import { useStore } from '../store/mainStore'; import { useMainStore } from '../store/mainStore';
import SceneryInfo from '../components/SceneryView/SceneryInfo.vue'; import SceneryInfo from '../components/SceneryView/SceneryInfo.vue';
import SceneryHeader from '../components/SceneryView/SceneryHeader.vue'; import SceneryHeader from '../components/SceneryView/SceneryHeader.vue';
@@ -58,6 +61,8 @@ import SceneryTimetable from '../components/SceneryView/SceneryTimetable.vue';
import SceneryTimetablesHistory from '../components/SceneryView/SceneryTimetablesHistory.vue'; import SceneryTimetablesHistory from '../components/SceneryView/SceneryTimetablesHistory.vue';
import SceneryDispatchersHistory from '../components/SceneryView/SceneryDispatchersHistory.vue'; import SceneryDispatchersHistory from '../components/SceneryView/SceneryDispatchersHistory.vue';
import ActionButton from '../components/Global/ActionButton.vue'; import ActionButton from '../components/Global/ActionButton.vue';
import { Status } from '../typings/common';
import { useApiStore } from '../store/apiStore';
enum SceneryViewMode { enum SceneryViewMode {
'TIMETABLES_ACTIVE', 'TIMETABLES_ACTIVE',
@@ -92,7 +97,9 @@ export default defineComponent({
mixins: [routerMixin], mixins: [routerMixin],
data: () => ({ data: () => ({
store: useStore(), store: useMainStore(),
apiStore: useApiStore(),
viewModes: [ viewModes: [
{ {
id: 'scenery.option-active-timetables', id: 'scenery.option-active-timetables',
@@ -110,7 +117,8 @@ export default defineComponent({
sceneryViewMode: SceneryViewMode, sceneryViewMode: SceneryViewMode,
selectedCheckpoint: '', selectedCheckpoint: '',
currentViewCompontent: 'SceneryTimetable', currentViewCompontent: 'SceneryTimetable',
onlineFrom: -1 onlineFrom: -1,
Status: Status.Data
}), }),
// activated() { // activated() {
@@ -185,8 +193,6 @@ button.back-btn {
&-view { &-view {
display: flex; display: flex;
justify-content: center; justify-content: center;
min-height: 100vh;
} }
&-offline { &-offline {
@@ -215,6 +221,7 @@ button.back-btn {
width: 100%; width: 100%;
max-width: 1700px; max-width: 1700px;
min-height: 100vh;
margin: 1rem 0; margin: 1rem 0;
text-align: center; text-align: center;
+2 -2
View File
@@ -21,7 +21,7 @@ import { defineComponent } from 'vue';
import StationTable from '../components/StationsView/StationTable.vue'; import StationTable from '../components/StationsView/StationTable.vue';
import StationFilterCard from '../components/StationsView/StationFilterCard.vue'; import StationFilterCard from '../components/StationsView/StationFilterCard.vue';
import { useStationFiltersStore } from '../store/stationFiltersStore'; import { useStationFiltersStore } from '../store/stationFiltersStore';
import { useStore } from '../store/mainStore'; import { useMainStore } from '../store/mainStore';
import Donation from '../components/Global/Donation.vue'; import Donation from '../components/Global/Donation.vue';
export default defineComponent({ export default defineComponent({
@@ -37,7 +37,7 @@ export default defineComponent({
STORAGE_KEY: 'options_saved', STORAGE_KEY: 'options_saved',
focusedStationName: '', focusedStationName: '',
filterStore: useStationFiltersStore(), filterStore: useStationFiltersStore(),
store: useStore(), store: useMainStore(),
isDonationModalOpen: false isDonationModalOpen: false
}), }),
+2 -2
View File
@@ -21,7 +21,7 @@ import TrainOptions from '../components/TrainsView/TrainOptions.vue';
import TrainTable from '../components/TrainsView/TrainTable.vue'; import TrainTable from '../components/TrainsView/TrainTable.vue';
import modalTrainMixin from '../mixins/modalTrainMixin'; import modalTrainMixin from '../mixins/modalTrainMixin';
import Train from '../scripts/interfaces/Train'; import Train from '../scripts/interfaces/Train';
import { useStore } from '../store/mainStore'; import { useMainStore } from '../store/mainStore';
import { TrainFilter, trainFilters } from '../components/TrainsView/typings'; import { TrainFilter, trainFilters } from '../components/TrainsView/typings';
import { filteredTrainList } from '../managers/trainFilterManager'; import { filteredTrainList } from '../managers/trainFilterManager';
import TrainStats from '../components/TrainsView/TrainStats.vue'; import TrainStats from '../components/TrainsView/TrainStats.vue';
@@ -58,7 +58,7 @@ export default defineComponent({
}), }),
setup() { setup() {
const store = useStore(); const store = useMainStore();
const initTrainFilters = [...trainFilters.map((f) => ({ ...f }))]; const initTrainFilters = [...trainFilters.map((f) => ({ ...f }))];
const sorterActive = reactive({ id: 'routeDistance', dir: -1 }); const sorterActive = reactive({ id: 'routeDistance', dir: -1 });
+4 -2
View File
@@ -6,20 +6,22 @@ export default defineConfig({
server: { server: {
port: 5001 port: 5001
}, },
publicDir: 'public',
plugins: [ plugins: [
vue(), vue(),
VitePWA({ VitePWA({
registerType: 'autoUpdate', registerType: 'autoUpdate',
includeAssets: ['/images/*.png', '/fonts/*.woff', '/fonts/*.woff2'],
workbox: { workbox: {
disableDevLogs: true,
globPatterns: ['**/*.{js,css,html,png,svg,jpg}'], globPatterns: ['**/*.{js,css,html,png,svg,jpg}'],
runtimeCaching: [ runtimeCaching: [
{ {
urlPattern: new RegExp('^https://stacjownik.spythere.pl/api/getSceneries', 'i'), urlPattern: new RegExp('^https://stacjownik.spythere.eu/api/getSceneries', 'i'),
handler: 'NetworkFirst', handler: 'NetworkFirst',
options: { options: {
cacheName: 'sceneries-cache', cacheName: 'sceneries-cache',
cacheableResponse: { cacheableResponse: {
statuses: [0, 200] statuses: [0, 200]
} }
+7 -56
View File
@@ -1577,11 +1577,6 @@
resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.5.1.tgz" resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.5.1.tgz"
integrity sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA== integrity sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA==
"@socket.io/component-emitter@~3.1.0":
version "3.1.0"
resolved "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz"
integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
"@surma/rollup-plugin-off-main-thread@^2.2.3": "@surma/rollup-plugin-off-main-thread@^2.2.3":
version "2.2.3" version "2.2.3"
resolved "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz" resolved "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz"
@@ -2143,9 +2138,9 @@ callsites@^3.0.0:
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
caniuse-lite@^1.0.30001400: caniuse-lite@^1.0.30001400:
version "1.0.30001503" version "1.0.30001565"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001503.tgz" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz"
integrity sha512-Sf9NiF+wZxPfzv8Z3iS0rXM1Do+iOy2Lxvib38glFX+08TCYYYGR5fRJXk4d77C4AYwhUjgYgMsMudbh2TqCKw== integrity sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==
chalk@^2.4.2: chalk@^2.4.2:
version "2.4.2" version "2.4.2"
@@ -2319,7 +2314,7 @@ de-indent@^1.0.2:
resolved "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz" resolved "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz"
integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg== integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==
debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
version "4.3.4" version "4.3.4"
resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -2454,22 +2449,6 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1:
dependencies: dependencies:
once "^1.4.0" once "^1.4.0"
engine.io-client@~6.5.2:
version "6.5.2"
resolved "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz"
integrity sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1"
engine.io-parser "~5.2.1"
ws "~8.11.0"
xmlhttprequest-ssl "~2.0.0"
engine.io-parser@~5.2.1:
version "5.2.1"
resolved "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz"
integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==
es-abstract@^1.19.0, es-abstract@^1.20.4: es-abstract@^1.19.0, es-abstract@^1.20.4:
version "1.20.5" version "1.20.5"
resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.5.tgz" resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.5.tgz"
@@ -4215,24 +4194,6 @@ slash@^3.0.0:
resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
socket.io-client@^4.7.2:
version "4.7.2"
resolved "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz"
integrity sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.2"
engine.io-client "~6.5.2"
socket.io-parser "~4.2.4"
socket.io-parser@~4.2.4:
version "4.2.4"
resolved "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz"
integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1"
source-map-js@^1.0.2, "source-map-js@>=0.6.2 <2.0.0": source-map-js@^1.0.2, "source-map-js@>=0.6.2 <2.0.0":
version "1.0.2" version "1.0.2"
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz"
@@ -4631,9 +4592,9 @@ vite-plugin-pwa@^0.16.5:
workbox-window "^7.0.0" workbox-window "^7.0.0"
"vite@^3.1.0 || ^4.0.0", vite@^4.0.0, vite@^4.4.9: "vite@^3.1.0 || ^4.0.0", vite@^4.0.0, vite@^4.4.9:
version "4.4.9" version "4.5.1"
resolved "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz" resolved "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz"
integrity sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA== integrity sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==
dependencies: dependencies:
esbuild "^0.18.10" esbuild "^0.18.10"
postcss "^8.4.27" postcss "^8.4.27"
@@ -4934,21 +4895,11 @@ wrappy@1:
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
ws@~8.11.0:
version "8.11.0"
resolved "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz"
integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==
xml-name-validator@^4.0.0: xml-name-validator@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz" resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz"
integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==
xmlhttprequest-ssl@~2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz"
integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==
y18n@^5.0.5: y18n@^5.0.5:
version "5.0.8" version "5.0.8"
resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz"