Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e888544c1 | |||
| 56dcca3d5b | |||
| 91cf7b955a | |||
| dacc0bc09d | |||
| 336530cff9 | |||
| 371e8085a9 | |||
| b8548b865b | |||
| c0bdee939d | |||
| ff14e362bb | |||
| 9094a0b784 | |||
| dc9389a7c7 | |||
| 2d7159c844 | |||
| 93311a130c | |||
| 5616fbd7cf | |||
| d4e365d311 | |||
| e43663c541 | |||
| ea99c68911 | |||
| fb9cff3a00 | |||
| 25fcd20e94 | |||
| 40213944e6 | |||
| 984bbccaf5 | |||
| 518b2da700 | |||
| a252140b4f | |||
| 2472814a03 | |||
| 5f264e8b63 | |||
| c334b5bfd3 | |||
| 0638bfff31 | |||
| eac4cad809 | |||
| 0d625a3192 | |||
| bf51ac34f1 | |||
| c8381d1222 | |||
| 795b10959f | |||
| 2a3f4ca1ef | |||
| a650a2f719 | |||
| 23e7d04dfa | |||
| b901176e8c | |||
| ddd8bcc462 | |||
| 519d5ec5fa | |||
| c862164f69 | |||
| 818144c894 | |||
| 0174ddb8ab | |||
| 8c7ffc7913 | |||
| 5c6910df63 | |||
| 745b769070 | |||
| 3add3db2f2 | |||
| f19a256153 | |||
| 48e8129902 | |||
| 4b420f6eec | |||
| 2556851f3f | |||
| ec1f0416c7 | |||
| c5e1f304d2 | |||
| 684a400e46 | |||
| bb26082358 | |||
| dcaf0d0ea3 | |||
| a2aea77768 | |||
| aea657d04d | |||
| 00608bc667 | |||
| b67635886d | |||
| a5275b7f25 | |||
| 958c8d3b65 | |||
| 684bbdac31 | |||
| d1adcd8287 | |||
| 051d6b22b8 | |||
| 2d47534333 | |||
| 2e5513b968 | |||
| af88628d15 | |||
| 255a294e40 | |||
| c97a525f24 | |||
| e3268a689c | |||
| 706f3ea9f8 | |||
| 34df7eede5 | |||
| 31c4e43762 | |||
| fb019a1e40 |
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"projects": {
|
||||
"default": "srjp-td2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# This file was auto-generated by the Firebase CLI
|
||||
# https://github.com/firebase/firebase-tools
|
||||
|
||||
name: Deploy to Firebase Hosting on merge
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
build_and_deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: yarn && yarn build
|
||||
- uses: FirebaseExtended/action-hosting-deploy@v0
|
||||
with:
|
||||
repoToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SRJP_TD2 }}
|
||||
channelId: live
|
||||
projectId: srjp-td2
|
||||
@@ -0,0 +1,21 @@
|
||||
# This file was auto-generated by the Firebase CLI
|
||||
# https://github.com/firebase/firebase-tools
|
||||
|
||||
name: Deploy to Firebase Hosting on PR
|
||||
on: pull_request
|
||||
permissions:
|
||||
checks: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
jobs:
|
||||
build_and_preview:
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: yarn && yarn build
|
||||
- uses: FirebaseExtended/action-hosting-deploy@v0
|
||||
with:
|
||||
repoToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SRJP_TD2 }}
|
||||
projectId: srjp-td2
|
||||
@@ -11,6 +11,8 @@ node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
@@ -22,3 +24,7 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# api-mock
|
||||
/api-mock/endpoints/
|
||||
/api-mock/*.lock
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Spythere
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,5 +1,7 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
# Rozkładownik TD2
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
### Aplikacja pozwalająca na wygenerowanie służbowego rozkładu jazdy dla pociągu w symulatorze [Train Driver 2](https://web.td2.info.pl/pl/)
|
||||
|
||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||
~ by Spythere
|
||||
|
||||

|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { existsSync } from 'fs';
|
||||
import { mkdir, writeFile } from 'fs/promises';
|
||||
|
||||
async function fetchJSONEndpointData(url, fileName) {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
await writeFile(`./endpoints/${fileName}`, JSON.stringify(data));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!existsSync('endpoints')) await mkdir('endpoints');
|
||||
|
||||
Promise.all(
|
||||
['getActiveData', 'getDonators', 'getSceneries', 'getVehicles'].map((endpointName) =>
|
||||
fetchJSONEndpointData(
|
||||
`https://stacjownik.spythere.eu/api/${endpointName}`,
|
||||
`${endpointName}.json`
|
||||
)
|
||||
)
|
||||
).then(() => {
|
||||
console.log('Endpoints downloaded!');
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,28 @@
|
||||
import express from 'express';
|
||||
import path from 'path';
|
||||
import { cwd } from 'process';
|
||||
import cors from 'cors';
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors());
|
||||
|
||||
app.get('/api/getActiveData', (_, res) => {
|
||||
res.sendFile(path.join(cwd(), 'endpoints', 'getActiveData.json'));
|
||||
});
|
||||
|
||||
app.get('/api/getSceneries', (_, res) => {
|
||||
res.sendFile(path.join(cwd(), 'endpoints', 'getSceneries.json'));
|
||||
});
|
||||
|
||||
app.get('/api/getVehicles', (_, res) => {
|
||||
res.sendFile(path.join(cwd(), 'endpoints', 'getVehicles.json'));
|
||||
});
|
||||
|
||||
app.get('/api/getDonators', (_, res) => {
|
||||
res.sendFile(path.join(cwd(), 'endpoints', 'getDonators.json'));
|
||||
});
|
||||
|
||||
app.listen(3123, () => {
|
||||
console.log('Mocking API server...');
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "api-mock",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node index.js",
|
||||
"fetch": "node fetchEndpoints.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"hosting": {
|
||||
"public": "dist",
|
||||
"ignore": [
|
||||
"firebase.json",
|
||||
"**/.*",
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,39 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Vue + TS</title>
|
||||
|
||||
<title>Rozkładownik TD2 - SRJP</title>
|
||||
|
||||
<meta name="keywords" content="Rozkładownik, TD2, Train Driver 2, srjp-td2, SRJP, rozkładownik, stacjownik, pojazdownik, td2.info.pl" />
|
||||
<meta name="description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
|
||||
|
||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<meta name="apple-mobile-web-app-title" content="SRJP" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
|
||||
<!-- Static OpenGraph meta -->
|
||||
<meta name="description" content="Generator rozkładów jazdy dla symulatora Train Driver 2" />
|
||||
<meta property="og:url" content="https://srjp-td2.web.app" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="Rozkładownik TD2 - SRJP" />
|
||||
<meta property="og:description" content="Generator rozkładów jazdy dla symulatora Train Driver 2" />
|
||||
<meta property="og:image" content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/srjp-banner2.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:site_name" content="Rozkładownik TD2 - SRJP" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="Rozkładownik TD2 - SRJP" />
|
||||
<meta name="twitter:description" content="Generator rozkładów jazdy dla symulatora Train Driver 2" />
|
||||
<meta property="og:image" content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/srjp-banner2.png" />
|
||||
</head>
|
||||
<body>
|
||||
<body >
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
{
|
||||
"name": "srjp-td2",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --mode staging",
|
||||
"dev:mock": "vite --mode development & yarn --cwd ./api-mock start",
|
||||
"mock:setup": "cd ./api-mock && yarn && yarn fetch",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/vue": "^2.2.0",
|
||||
"axios": "^1.7.9",
|
||||
"pinia": "^2.3.1",
|
||||
"vue": "^3.5.13"
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
|
||||
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="512" height="512"><svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="512" height="512" rx="256" fill="#151414"></rect>
|
||||
<path d="M72.4253 291.986V279.965H120.201C123.283 279.965 124.824 278.424 124.824 275.342V264.246C124.824 261.266 123.54 259.571 120.971 259.16L90.9189 252.995C78.5898 250.529 72.4253 242.259 72.4253 228.183V219.553C72.4253 202.292 81.0557 193.662 98.3164 193.662H133.608L143.934 201.675V213.696H99.2411C96.1588 213.696 94.6177 215.237 94.6177 218.32V228.337C94.6177 231.214 95.9019 232.909 98.4705 233.423L128.523 239.433C140.852 241.899 147.016 250.17 147.016 264.246V274.109C147.016 291.37 138.386 300 121.125 300H82.7509L72.4253 291.986ZM167.651 300V193.662H219.433C236.694 193.662 245.324 202.292 245.324 219.553V237.122C245.324 249.964 240.546 257.978 230.991 261.163L248.406 295.377L245.786 300H226.676L207.874 263.013H189.843V300H167.651ZM189.843 242.978H218.508C221.591 242.978 223.132 241.437 223.132 238.355V218.32C223.132 215.237 221.591 213.696 218.508 213.696H189.843V242.978ZM262.96 274.109V253.766H285.153V275.342C285.153 278.424 286.694 279.965 289.776 279.965H310.736C313.818 279.965 315.359 278.424 315.359 275.342V213.696H286.386V193.662H337.551V274.109C337.551 291.37 328.921 300 311.66 300H288.852C271.591 300 262.96 291.37 262.96 274.109ZM361.948 300V193.662H413.731C430.991 193.662 439.622 202.292 439.622 219.553V240.204C439.622 257.465 430.991 266.095 413.731 266.095H384.141V300H361.948ZM384.141 246.06H412.806C415.888 246.06 417.429 244.519 417.429 241.437V218.32C417.429 215.237 415.888 213.696 412.806 213.696H384.141V246.06Z" fill="white"></path>
|
||||
<path d="M304.958 332.848V322.831H348.418V332.848H332.236V376H321.14V332.848H304.958ZM356.61 376V322.831H376.799C391.285 322.831 398.529 330.074 398.529 344.561V354.27C398.529 368.757 391.285 376 376.799 376H356.61ZM367.706 365.983H377.415C384.093 365.983 387.432 362.643 387.432 355.965V342.866C387.432 336.187 384.093 332.848 377.415 332.848H367.706V365.983ZM407.35 376V358.662C407.35 351.624 410.432 347.489 416.597 346.256L430.852 343.405C432.136 343.148 432.779 342.3 432.779 340.862V335.16C432.779 333.619 432.008 332.848 430.467 332.848H408.891V326.838L414.054 322.831H430.929C439.56 322.831 443.875 327.146 443.875 335.776V340.785C443.875 347.823 440.792 351.958 434.628 353.191L420.372 356.042C419.088 356.299 418.446 357.147 418.446 358.585V365.983H443.875V376H407.35Z" fill="#E63E3E"></path>
|
||||
</svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
|
||||
@media (prefers-color-scheme: dark) { :root { filter: none; } }
|
||||
</style></svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5 fill-white">
|
||||
<path fill-rule="evenodd" d="M5 2.75C5 1.784 5.784 1 6.75 1h6.5c.966 0 1.75.784 1.75 1.75v3.552c.377.046.752.097 1.126.153A2.212 2.212 0 0 1 18 8.653v4.097A2.25 2.25 0 0 1 15.75 15h-.241l.305 1.984A1.75 1.75 0 0 1 14.084 19H5.915a1.75 1.75 0 0 1-1.73-2.016L4.492 15H4.25A2.25 2.25 0 0 1 2 12.75V8.653c0-1.082.775-2.034 1.874-2.198.374-.056.75-.107 1.127-.153L5 6.25v-3.5Zm8.5 3.397a41.533 41.533 0 0 0-7 0V2.75a.25.25 0 0 1 .25-.25h6.5a.25.25 0 0 1 .25.25v3.397ZM6.608 12.5a.25.25 0 0 0-.247.212l-.693 4.5a.25.25 0 0 0 .247.288h8.17a.25.25 0 0 0 .246-.288l-.692-4.5a.25.25 0 0 0-.247-.212H6.608Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 736 B |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Rozkładownik TD2",
|
||||
"short_name": "SRJP",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#151414",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -1,394 +1,74 @@
|
||||
<template>
|
||||
<div class="app">
|
||||
<select name="trains" id="trains-select" class="mb-2 bg-zinc-800 p-1 rounded-md" v-model="selectedTrainId">
|
||||
<option :value="train.id" v-for="train in timetableTrains">{{ train.driverName }} | {{ train.timetable?.category }} {{ train.trainNo }}</option>
|
||||
</select>
|
||||
|
||||
<table class="srjp-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="50" class="border border-white">Nr linii</th>
|
||||
<th width="100" class="border border-white">Km</th>
|
||||
<th width="40" class="border border-white" colspan="2">V<sub>D</sub></th>
|
||||
<th width="250" class="border border-white">Stacja</th>
|
||||
<th width="100" class="border border-white">Godzina</th>
|
||||
<th width="50" class="border border-white text-xs p-0">
|
||||
<table class="header-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Lok I</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Lok II</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Lok III</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</th>
|
||||
<th width="60" class="border border-white text-xs p-0">
|
||||
<table class="header-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Obc. lok.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dł. poc.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</th>
|
||||
<th width="50" class="border border-white">Vmax</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-if="computedTimetable">
|
||||
<tr v-for="(row, i) in computedTimetable">
|
||||
<td class="text-center align-top border border-white">{{ row.realLine }}</td>
|
||||
<td class="border border-white">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="align-top">{{ row.arrivalKm }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="align-bottom">{{ row.departureKm }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
<!-- :class="{ 'border-t-4': i > 0 && computedTimetable[i - 1].sceneryName != row.sceneryName }" -->
|
||||
<!-- :class="{ 'border-ts': i > 0 && computedTimetable[i - 1].sceneryName != row.sceneryName }" -->
|
||||
<td class="text-center align-top font-bold p-0 border-l-4" colspan="2">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr
|
||||
:class="{
|
||||
'align-top': i == 0 || computedTimetable[i - 1].departureTracks == row.arrivalTracks,
|
||||
'border-t': i != 0 && computedTimetable[i - 1].departureSpeed != row.arrivalSpeed,
|
||||
}"
|
||||
>
|
||||
<td :colspan="row.arrivalTracks == 2 ? '1' : '2'">
|
||||
{{
|
||||
i == 0 ||
|
||||
computedTimetable[i - 1].departureSpeed != row.arrivalSpeed ||
|
||||
computedTimetable[i - 1].departureTracks != row.arrivalTracks
|
||||
? row.arrivalSpeed
|
||||
: ' '
|
||||
}}
|
||||
<!-- {{ row.arrivalTracks }} -->
|
||||
</td>
|
||||
<td v-if="row.arrivalTracks == 2" class="border-l">
|
||||
{{
|
||||
i == 0 ||
|
||||
computedTimetable[i - 1].departureSpeed != row.arrivalSpeed ||
|
||||
computedTimetable[i - 1].departureTracks != row.arrivalTracks
|
||||
? row.arrivalSpeed
|
||||
: ' '
|
||||
}}
|
||||
<!-- {{ row.arrivalTracks }} -->
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class=""
|
||||
:class="{
|
||||
'border-b': row.departureSpeed != row.arrivalSpeed || i == computedTimetable.length - 1,
|
||||
'border-t': row.arrivalTracks != row.departureTracks,
|
||||
'align-bottom': row.arrivalTracks == row.departureTracks,
|
||||
}"
|
||||
>
|
||||
<td :colspan="row.departureTracks == 2 ? '1' : '2'">
|
||||
{{ row.departureSpeed != row.arrivalSpeed || row.departureTracks != row.arrivalTracks ? row.departureSpeed : ' ' }}
|
||||
<!-- {{ row.departureTracks }} -->
|
||||
</td>
|
||||
<td v-if="row.departureTracks == 2" class="border-l">
|
||||
{{ row.departureSpeed != row.arrivalSpeed || row.departureTracks != row.arrivalTracks ? row.departureSpeed : ' ' }}
|
||||
<!-- {{ row.departureTracks }} -->
|
||||
</td>
|
||||
</tr>
|
||||
<!-- <tr
|
||||
class="align-top"
|
||||
:class="{
|
||||
'border-b':
|
||||
row.arrivalSpeed && row.departureSpeed && (row.arrivalSpeed != row.departureSpeed || row.arrivalTracks != row.departureTracks),
|
||||
}"
|
||||
>
|
||||
<td :colspan="row.arrivalTracks == 2 ? '1' : '2'">{{ row.arrivalSpeed || ' ' }}</td>
|
||||
<td v-if="row.arrivalTracks == 2" class="border-l">{{ row.arrivalSpeed || ' ' }}</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="align-bottom"
|
||||
:class="{
|
||||
'border-b':
|
||||
i < computedTimetable.length - 1 &&
|
||||
(computedTimetable[i + 1].arrivalSpeed != row.departureSpeed || computedTimetable[i + 1].arrivalTracks != row.departureTracks),
|
||||
}"
|
||||
>
|
||||
<td :colspan="row.departureTracks == 2 ? '1' : '2'">{{ row.departureSpeed || ' ' }}</td>
|
||||
<td v-if="row.departureTracks == 2" class="border-l">{{ row.departureSpeed || ' ' }}</td>
|
||||
</tr> -->
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
<td class="p-1 border border-white">
|
||||
<div class="flex flex-col h-full justify-between">
|
||||
<div :class="{ 'font-bold': row.isMain }">
|
||||
{{ row.pointName }}
|
||||
<span v-if="row.stopType"> ; {{ row.stopType }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<span>{{ row.pointKm }}</span>
|
||||
<span>R1, PP</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="border border-white">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr class="text-center align-top">
|
||||
<td class="border-r-[1px] border-r-white" :class="{ 'font-bold': row.stopTime > 0 }">
|
||||
{{
|
||||
(row.scheduledArrivalDate?.getTime() || 0) != (row.scheduledDepartureDate?.getTime() || 0)
|
||||
? row.scheduledArrivalDate?.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' })
|
||||
: '|'
|
||||
}}
|
||||
</td>
|
||||
<td width="30">{{ row.driveTime ? Math.floor(row.driveTime / 60000) : '' }}</td>
|
||||
</tr>
|
||||
<tr class="text-center align-bottom">
|
||||
<td class="border-r-[1px] border-r-white" :class="{ 'font-bold': row.stopTime > 0 }">
|
||||
{{ row.scheduledDepartureDate?.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' }) }}
|
||||
</td>
|
||||
<td width="30" class="font-bold">{{ row.stopTime || '' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
<td class="p-0 text-center border border-white">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr class="border-b-[1px] border-b-white">
|
||||
<td>{{ selectedTrain!.stockString.split(';')[0].split('-')[0] }}</td>
|
||||
</tr>
|
||||
<tr class="border-b-[1px] border-b-white">
|
||||
<td> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
<td class="p-0 text-center border border-white">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr class="border-b-[1px] border-b-white">
|
||||
<td>{{ Math.floor(selectedTrain!.mass / 1000) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ selectedTrain!.length }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
<td class="text-center border border-white">70</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="text-white min-h-screen bg-zinc-950">
|
||||
<Navbar />
|
||||
<MainContainer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import Navbar from './components/App/Navbar.vue';
|
||||
import MainContainer from './components/App/MainContainer.vue';
|
||||
import { onMounted } from 'vue';
|
||||
import { useApiStore } from './stores/api.store';
|
||||
import { useGlobalStore } from './stores/global.store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const additionalData = {
|
||||
// Mijanki
|
||||
passings: ['Stolnica Wielka'],
|
||||
// SHP
|
||||
shpSystems: [],
|
||||
// 4-stawne SBL
|
||||
sbl4: [],
|
||||
};
|
||||
const originalDocumentTitle = document.title;
|
||||
|
||||
interface StopRow {
|
||||
pointName: string;
|
||||
pointKm: string;
|
||||
isMain: boolean;
|
||||
stopTime: number;
|
||||
stopType: string;
|
||||
scheduledArrivalDate: Date | null;
|
||||
scheduledDepartureDate: Date | null;
|
||||
realLine: string;
|
||||
driveTime: number;
|
||||
controlAbbrevs: string[];
|
||||
additionalAbbrevs: string[];
|
||||
const apiStore = useApiStore();
|
||||
const globalStore = useGlobalStore();
|
||||
const i18n = useI18n();
|
||||
|
||||
sceneryName: string;
|
||||
onMounted(async () => {
|
||||
setupLocale();
|
||||
setupDarkMode();
|
||||
loadStorageTimetables();
|
||||
setupAfterPrintClose();
|
||||
|
||||
arrivalKm: string;
|
||||
await apiStore.setupAPIData();
|
||||
|
||||
arrivalSpeed: number;
|
||||
arrivalTracks: number;
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
|
||||
departureKm: string;
|
||||
if (query.has('id')) {
|
||||
const id = query.get('id')!;
|
||||
|
||||
departureSpeed: number;
|
||||
departureTracks: number;
|
||||
}
|
||||
const queryTrain = apiStore.activeData?.trains.find((train) => train.id == id);
|
||||
|
||||
export default defineComponent({
|
||||
data: () => ({
|
||||
selectedTrainId: '',
|
||||
globalStore: useGlobalStore(),
|
||||
}),
|
||||
|
||||
mounted() {
|
||||
this.globalStore.setupData();
|
||||
},
|
||||
|
||||
computed: {
|
||||
timetableTrains() {
|
||||
return this.globalStore.activeData?.trains.filter((train) => train.timetable != undefined) ?? [];
|
||||
},
|
||||
|
||||
selectedTrain() {
|
||||
return this.timetableTrains.find((train) => train.id == this.selectedTrainId);
|
||||
},
|
||||
|
||||
computedTimetable() {
|
||||
if (!this.selectedTrain) return null;
|
||||
|
||||
const timetable = this.selectedTrain.timetable;
|
||||
|
||||
if (!timetable) return null;
|
||||
|
||||
const timetablePath = timetable.path.split(';').map((pathEl) => {
|
||||
const [arrivalLine, scenery, departureLine] = pathEl.split(',');
|
||||
const sceneryName = scenery.split(' ').slice(0, -1).join(' ');
|
||||
|
||||
const sceneryData = this.globalStore.sceneryData?.find((sc) => sc.name == sceneryName) ?? null;
|
||||
const arrivalLineData = arrivalLine ? sceneryData?.routesInfo.find((rt) => rt.routeName == arrivalLine) ?? null : null;
|
||||
const departureLineData = departureLine ? sceneryData?.routesInfo.find((rt) => rt.routeName == departureLine) ?? null : null;
|
||||
|
||||
return {
|
||||
sceneryName,
|
||||
arrivalLine: arrivalLine ?? '',
|
||||
arrivalLineData,
|
||||
departureLine: departureLine ?? '',
|
||||
departureLineData,
|
||||
};
|
||||
});
|
||||
|
||||
const stopRows: StopRow[] = [];
|
||||
|
||||
let currentPathIndex = 0;
|
||||
let currentPath = timetablePath[0];
|
||||
|
||||
let lastDepartureTimestamp = 0;
|
||||
|
||||
let arrivalKm = 0,
|
||||
arrivalSpeed = currentPath.departureLineData?.routeSpeed ?? 0,
|
||||
arrivalTracks = currentPath.departureLineData?.routeTracks ?? 0;
|
||||
|
||||
let departureSpeed = currentPath.departureLineData?.routeSpeed ?? 0,
|
||||
departureTracks = currentPath.departureLineData?.routeTracks ?? 2;
|
||||
|
||||
let checkEntryAsFirst = true;
|
||||
|
||||
for (const stop of timetable.stopList) {
|
||||
if (stop.arrivalLine && stop.arrivalLine == currentPath.arrivalLine) {
|
||||
arrivalKm = stop.stopDistance;
|
||||
arrivalSpeed = currentPath.arrivalLineData?.routeSpeed ?? 0;
|
||||
arrivalTracks = currentPath.arrivalLineData?.routeTracks ?? 2;
|
||||
|
||||
departureSpeed = arrivalSpeed;
|
||||
departureTracks = arrivalTracks;
|
||||
}
|
||||
|
||||
if (/^<strong>|, (podg|po)$|^(!_, pe)$/.test(stop.stopName)) {
|
||||
let rowData: StopRow = {
|
||||
isMain: /^<strong>/.test(stop.stopName),
|
||||
pointKm: stop.stopDistance.toFixed(3),
|
||||
pointName: stop.stopNameRAW,
|
||||
scheduledArrivalDate: stop.arrivalTimestamp ? new Date(stop.arrivalTimestamp) : null,
|
||||
scheduledDepartureDate: stop.departureTimestamp ? new Date(stop.departureTimestamp) : null,
|
||||
stopTime: stop.stopTime ?? 0,
|
||||
stopType: stop.stopType,
|
||||
sceneryName: currentPath.sceneryName,
|
||||
realLine: '-',
|
||||
driveTime: lastDepartureTimestamp ? stop.arrivalTimestamp - lastDepartureTimestamp : 0,
|
||||
additionalAbbrevs: [],
|
||||
controlAbbrevs: [],
|
||||
|
||||
arrivalKm: arrivalKm.toFixed(3),
|
||||
departureKm: stop.stopDistance.toFixed(3),
|
||||
|
||||
arrivalSpeed: arrivalSpeed,
|
||||
arrivalTracks: arrivalTracks,
|
||||
|
||||
departureSpeed: departureSpeed,
|
||||
departureTracks: departureTracks,
|
||||
};
|
||||
|
||||
arrivalKm = stop.stopDistance;
|
||||
checkEntryAsFirst = false;
|
||||
|
||||
if (stop.departureTimestamp) lastDepartureTimestamp = stop.departureTimestamp;
|
||||
|
||||
stopRows.push(rowData);
|
||||
}
|
||||
|
||||
if (stop.departureLine && stop.departureLine == currentPath.departureLine) {
|
||||
// Reverse search for last scenery checkpoint
|
||||
for (let i = stopRows.length - 1; i > 0; i--) {
|
||||
stopRows[i].departureTracks = currentPath.departureLineData?.routeTracks ?? 0;
|
||||
|
||||
if (stopRows[i].isMain || stopRows[i].pointName.endsWith(', podg')) {
|
||||
stopRows[i].departureSpeed = currentPath.departureLineData?.routeSpeed ?? 0;
|
||||
stopRows[i].departureTracks = currentPath.departureLineData?.routeTracks ?? 0;
|
||||
break;
|
||||
}
|
||||
|
||||
stopRows[i].arrivalTracks = currentPath.departureLineData?.routeTracks ?? 0;
|
||||
}
|
||||
|
||||
currentPath = timetablePath[++currentPathIndex];
|
||||
checkEntryAsFirst = true;
|
||||
}
|
||||
}
|
||||
|
||||
return stopRows;
|
||||
},
|
||||
},
|
||||
if (queryTrain) {
|
||||
globalStore.selectedTrainId = id;
|
||||
globalStore.selectedActiveTrain = queryTrain;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
function loadStorageTimetables() {
|
||||
if (!window.localStorage.getItem('savedTimetables')) return;
|
||||
|
||||
.srjp-table {
|
||||
min-width: 750px;
|
||||
}
|
||||
|
||||
.no-bottom-border {
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
@media print {
|
||||
table {
|
||||
page-break-inside: auto;
|
||||
try {
|
||||
globalStore.storageTimetables = JSON.parse(window.localStorage.getItem('savedTimetables')!);
|
||||
} catch (error) {
|
||||
alert('Ups! Coś poszło nie tak podczas pobierania zapisanych RJ!');
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
function setupDarkMode() {
|
||||
globalStore.darkMode =
|
||||
localStorage.currentTheme === 'dark' || (!('currentTheme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
}
|
||||
|
||||
function setupAfterPrintClose() {
|
||||
window.addEventListener('afterprint', () => {
|
||||
document.title = originalDocumentTitle;
|
||||
});
|
||||
}
|
||||
|
||||
function setupLocale() {
|
||||
if (window.localStorage.getItem('locale') == null) {
|
||||
const browserLang = window.navigator.language;
|
||||
if (browserLang == 'pl-PL') i18n.locale.value = browserLang == 'pl-PL' ? 'pl' : 'en';
|
||||
window.localStorage.setItem('locale', i18n.locale.value);
|
||||
} else {
|
||||
i18n.locale.value = window.localStorage.getItem('locale')!;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div v-if="globalStore.generatedDate" class="text-center text-sm text-zinc-400 mt-1 print:hidden">
|
||||
Rozkład wygenerowany w
|
||||
{{
|
||||
globalStore.generatedMs
|
||||
}}ms - aktualny dla godziny:
|
||||
{{
|
||||
globalStore.generatedDate.toLocaleTimeString()
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
</script>
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<main class="grid print:block p-3 mx-auto max-w-[800px] h-screen grid-rows-[auto_auto_1fr] gap-1">
|
||||
<TimetableSelect />
|
||||
<TimetableWarnings />
|
||||
<TrainTimetable />
|
||||
<!-- <MainBottom /> -->
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TimetableSelect from '../Timetable/TimetableSelect.vue';
|
||||
import TimetableWarnings from "../Timetable/TimetableWarnings.vue";
|
||||
import TrainTimetable from '../Timetable/TrainTimetable.vue';
|
||||
// import MainBottom from './MainBottom.vue';
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<nav class="bg-zinc-900 w-full p-1 print:hidden flex justify-between items-center relative">
|
||||
<div class="flex items-center">
|
||||
<img src="/favicon.svg" class="size-8 inline" />
|
||||
<b class="ml-2 text-lg"
|
||||
>Rozkładownik TD2 <sup class="font-semibold text-zinc-300">{{ version }}</sup></b
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="bg-slate-600 p-1 px-2 rounded-md hover:bg-slate-500 flex items-center" @click="changeLang()">
|
||||
<LanguageIcon class="size-5 inline-block align-middle mr-2" /> {{ i18n.locale.value == 'pl' ? 'POL' : 'ENG' }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- <div v-if="apiMode == 'mocking'"><ExclamationTriangleIcon class="size-6 inline mr-1 text-yellow-400" /> API mocking</div> -->
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LanguageIcon } from '@heroicons/vue/16/solid';
|
||||
import { version } from '../../../package.json';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
function changeLang(locale?: string) {
|
||||
i18n.locale.value = locale ?? i18n.locale.value == 'pl' ? 'en' : 'pl';
|
||||
window.localStorage.setItem('locale', i18n.locale.value);
|
||||
}
|
||||
// const apiMode = import.meta.env.VITE_API_MODE;
|
||||
</script>
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="fixed top-0 left-0 w-screen h-screen z-10 bg-black bg-opacity-70" v-if="globalStore.showSettings" @click="globalStore.showSettings = false">
|
||||
<!-- <div class="fixed top-0 left-0 w-screen h-screen z-10" v-show="showSettings" @click="showSettings = false"></div> -->
|
||||
|
||||
<!-- <div class="absolute mt-2 top-full right-0 p-1 rounded-md bg-slate-600 w-full z-20 max-w-[250px]" v-show="showSettings"></div> -->
|
||||
|
||||
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] bg-zinc-800 p-2 h-[500px] z-20">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
class="bg-slate-900 p-1 rounded-md hover:bg-slate-800"
|
||||
:class="{ 'font-bold text-yellow-400': i18n.locale.value == 'pl' }"
|
||||
@click="chooseLang('pl')"
|
||||
>
|
||||
POLSKI
|
||||
</button>
|
||||
<button
|
||||
class="bg-slate-900 p-1 rounded-md hover:bg-slate-800"
|
||||
:class="{ 'font-bold text-yellow-400': i18n.locale.value == 'en' }"
|
||||
@click="chooseLang('en')"
|
||||
>
|
||||
ENGLISH
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
|
||||
const i18n = useI18n();
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
function chooseLang(locale: string) {
|
||||
i18n.locale.value = locale;
|
||||
window.localStorage.setItem('locale', i18n.locale.value);
|
||||
globalStore.showSettings = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<tbody>
|
||||
<tr v-for="(row, i) in computedTimetable">
|
||||
<td
|
||||
class="text-center align-top border-l border-l-black dark:border-l-white"
|
||||
:class="{
|
||||
'border-t border-t-black dark:border-t-white': i != 0 && computedTimetable[i - 1].realLine != row.realLine,
|
||||
'border-b border-b-black dark:border-b-white': i == computedTimetable.length - 1,
|
||||
}"
|
||||
>
|
||||
{{ i == 0 || computedTimetable[i - 1].realLine != row.realLine ? row.realLine : ' ' }}
|
||||
</td>
|
||||
|
||||
<td class="border border-black dark:border-white relative">
|
||||
<div class="absolute top-0 left-0 w-full h-full p-0.5">
|
||||
<table class="h-full w-full border-collapse">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="align-top">{{ row.arrivalKm == '0.000' ? '' : row.arrivalKm }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="align-bottom">{{ row.departureKm == '0.000' ? '' : row.departureKm }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="text-center align-top p-0 border-l-black dark:border-l-white relative"
|
||||
:class="{
|
||||
'border-t border-t-black dark:border-t-white': i != 0 && computedTimetable[i - 1].departureSpeed != row.arrivalSpeed,
|
||||
'border-b border-b-black dark:border-b-white': i == computedTimetable.length - 1,
|
||||
}"
|
||||
colspan="2"
|
||||
>
|
||||
<div class="absolute top-0 left-0 w-full h-full">
|
||||
<table class="h-full w-full border-collapse">
|
||||
<tbody>
|
||||
<tr class="align-top">
|
||||
<td :colspan="row.arrivalTracks == 2 ? '1' : '2'" class="font-bold" width="35">
|
||||
{{
|
||||
i == 0 ||
|
||||
computedTimetable[i - 1].departureSpeed != row.arrivalSpeed ||
|
||||
computedTimetable[i - 1].departureTracks != row.arrivalTracks
|
||||
? row.arrivalSpeed
|
||||
: ' '
|
||||
}}
|
||||
</td>
|
||||
<td v-if="row.arrivalTracks == 2" class="border-l border-l-black dark:border-l-white" width="35">
|
||||
{{
|
||||
i == 0 ||
|
||||
computedTimetable[i - 1].departureSpeed != row.arrivalSpeed ||
|
||||
computedTimetable[i - 1].departureTracks != row.arrivalTracks
|
||||
? row.arrivalSpeed
|
||||
: ' '
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
:class="{
|
||||
'border-t border-t-black dark:border-t-white align-top':
|
||||
row.arrivalTracks != row.departureTracks || row.departureSpeed != row.arrivalSpeed,
|
||||
}"
|
||||
>
|
||||
<td :colspan="row.departureTracks == 2 ? '1' : '2'" class="font-bold" width="35">
|
||||
{{ row.departureSpeed != row.arrivalSpeed || row.departureTracks != row.arrivalTracks ? row.departureSpeed : ' ' }}
|
||||
</td>
|
||||
|
||||
<td v-if="row.departureTracks == 2" class="border-l border-l-black dark:border-l-white" width="35">
|
||||
{{ row.departureSpeed != row.arrivalSpeed || row.departureTracks != row.arrivalTracks ? row.departureSpeed : ' ' }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="border border-black dark:border-white relative">
|
||||
<div class="absolute top-0 left-0 w-full h-full">
|
||||
<div class="flex flex-col h-full justify-between p-1">
|
||||
<div :class="{ 'font-bold': row.isMain }">
|
||||
{{ row.pointName }}
|
||||
<span v-if="row.stopTime"> ; {{ row.stopType || 'pt' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<span>{{ row.pointKm }}</span>
|
||||
<span>{{ row.abbrevs.join(', ') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="p-0 border border-black dark:border-white relative">
|
||||
<div class="absolute top-0 left-0 w-full h-full">
|
||||
<table class="h-full w-full border-collapse">
|
||||
<tbody>
|
||||
<tr class="text-center align-top h-full">
|
||||
<td class="border-r-[1px] border-r-black dark:border-r-white" :class="{ 'font-bold': row.stopTime > 0 }">
|
||||
{{
|
||||
(row.scheduledArrivalDate?.getTime() || 0) != (row.scheduledDepartureDate?.getTime() || 0)
|
||||
? row.scheduledArrivalDate?.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' })
|
||||
: '|'
|
||||
}}
|
||||
</td>
|
||||
<td width="30">{{ row.driveTime ? Math.floor(row.driveTime / 60000) : '' }}</td>
|
||||
</tr>
|
||||
<tr class="text-center align-bottom h-full">
|
||||
<td class="border-r-[1px] border-r-black dark:border-r-white" :class="{ 'font-bold': row.stopTime > 0 }">
|
||||
{{ row.scheduledDepartureDate?.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' }) }}
|
||||
</td>
|
||||
<td width="30" class="font-bold">{{ row.stopTime || '' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="p-0 text-center border border-black dark:border-white relative h-24 text-sm" :class="{ 'text-stone-400 ': i > 0 }">
|
||||
<table class="h-full w-full border-collapse">
|
||||
<tbody>
|
||||
<tr class="border-b-[1px] border-b-black dark:border-b-white">
|
||||
<td>{{ row.headUnits[0] }}</td>
|
||||
</tr>
|
||||
<tr class="border-b-[1px] border-b-black dark:border-b-white">
|
||||
<td>{{ row.headUnits[1] ?? ' ' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ row.headUnits[2] ?? ' ' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
<td class="p-0 text-center border border-black dark:border-white relative" :class="{ 'text-stone-400 ': i > 0 }">
|
||||
<div class="absolute top-0 left-0 w-full h-full">
|
||||
<table class="h-full w-full border-collapse">
|
||||
<tbody>
|
||||
<tr class="border-b-[1px] border-b-black dark:border-b-white">
|
||||
<td>{{ row.stockMass }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ row.stockLength }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="text-center border border-black dark:border-white" :class="{ 'text-stone-400 ': i > 0 }">{{ row.stockVmax }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
import type { StopRow } from '../../types/common.types';
|
||||
|
||||
defineProps({
|
||||
computedTimetable: {
|
||||
type: Object as PropType<StopRow[]>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@media print {
|
||||
table {
|
||||
page-break-after: auto;
|
||||
}
|
||||
|
||||
tr {
|
||||
page-break-inside: avoid;
|
||||
page-break-after: auto;
|
||||
}
|
||||
|
||||
thead {
|
||||
display: table-header-group;
|
||||
}
|
||||
|
||||
tr,
|
||||
td {
|
||||
border-color: theme('colors.black');
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="40" class="border border-black dark:border-white">{{ $t('headers.line_no') }}</th>
|
||||
<th width="100" class="border border-black dark:border-white">{{ $t('headers.line_km') }}</th>
|
||||
<th width="35" class="border border-black dark:border-white">V<sub>P</sub></th>
|
||||
<th width="35" class="border border-black dark:border-white">V<sub>L</sub></th>
|
||||
<th width="200" class="border border-black dark:border-white">{{ $t('headers.station') }}</th>
|
||||
<th width="100" class="border border-black dark:border-white">{{ $t('headers.time') }}</th>
|
||||
<th width="50" class="border border-black dark:border-white text-xs p-0">
|
||||
<table class="h-full w-full border-collapse">
|
||||
<tbody>
|
||||
<tr class="border-b border-b-black dark:border-b-white">
|
||||
<td class="">{{ $t('headers.loco_1') }}</td>
|
||||
</tr>
|
||||
<tr class="border-b border-b-black dark:border-b-white">
|
||||
<td>{{ $t('headers.loco_2') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('headers.loco_3') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</th>
|
||||
<th width="55" class="border border-black dark:border-white text-xs relative">
|
||||
<div class="absolute top-0 left-0 w-full h-full">
|
||||
<table class="h-full w-full border-collapse">
|
||||
<tbody>
|
||||
<tr class="border-b border-b-black dark:border-b-white">
|
||||
<td>{{ $t('headers.mass') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('headers.length') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</th>
|
||||
<th width="50" class="border border-black dark:border-white">{{ $t('headers.vmax') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@media print {
|
||||
th, tr {
|
||||
border-color: theme('colors.black');
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="text-white">
|
||||
<!-- <div v-if="apiStore.journalTimetables == null">
|
||||
<div class="font-bold text-xl">{{ $t('storage-empty-header') }}</div>
|
||||
<div>{{ $t('storage-empty-info') }}</div>
|
||||
</div> -->
|
||||
|
||||
<div class="font-bold text-2xl p-2 bg-zinc-800">DZIENNIK SRJP</div>
|
||||
|
||||
<div v-if="apiStore.journalTimetables == null" class="text-md mt-2 p-2 bg-zinc-900">
|
||||
Wyszukaj gracza w polu powyżej, aby pokazać jego najnowsze SRJP.
|
||||
<!-- <div class="text-sm text-red-400 mt-2 flex flex-wrap md:flex-nowrap justify-center items-center gap-1">
|
||||
<ExclamationCircleIcon class="size-8 min-w-8" />
|
||||
<span>
|
||||
Rozkłady z danymi do wygenerowania SRJP są dostępne tylko dla wspierających twórczość
|
||||
<a class="underline hover:text-red-200 transition-colors" href="https://td2.info.pl/profile/?u=20777" target="_blank">@Spythere</a> dla
|
||||
symulatora TD2!
|
||||
</span>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- <div class="font-bold text-xl p-2 bg-zinc-700 mb-3">{{ $t('storage-preview-title') }}</div> -->
|
||||
<!-- <div class="font-bold p-2 bg-zinc-800 mb-3" v-if="filteredTimetables.length == 0">{{ $t('storage-preview-empty') }}</div> -->
|
||||
|
||||
<li v-for="timetable in apiStore.journalTimetables" class="flex gap-1 w-full my-2">
|
||||
<button class="bg-zinc-900 p-2 w-full cursor-pointer hover:bg-zinc-800 text-left" @click="selectTimetable(timetable.id)">
|
||||
<div class="text-zinc-300">#{{ timetable.id }} • {{ new Date(timetable.createdAt).toLocaleString() }}</div>
|
||||
<b>{{ timetable.driverName }} | {{ timetable.trainCategoryCode }} {{ timetable.trainNo }}</b> {{ timetable.route.replace('|', ' > ') }}
|
||||
</button>
|
||||
|
||||
<!-- <button class="bg-zinc-900 p-2 hover:bg-zinc-800" @click="removeTimetable(timetable.timetableId)">
|
||||
<TrashIcon class="size-5 text-white" />
|
||||
</button> -->
|
||||
</li>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
import { useApiStore } from '../../stores/api.store';
|
||||
import { onMounted } from 'vue';
|
||||
import { ExclamationCircleIcon } from '@heroicons/vue/16/solid';
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
const apiStore = useApiStore();
|
||||
const i18n = useI18n();
|
||||
|
||||
onMounted(() => {
|
||||
// apiStore.fetchTimetableHistoryList();
|
||||
});
|
||||
|
||||
function selectTimetable(timetableId: number) {}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<div class="flex gap-2 mb-2">
|
||||
<div class="relative" @focusin="isMenuOpen = true" @keydown.esc="isMenuOpen = false">
|
||||
<button
|
||||
class="p-1 rounded-md flex gap-2"
|
||||
:class="{
|
||||
'bg-zinc-800 hover:bg-zinc-700': isMenuOpen == false,
|
||||
'bg-green-600 hover:bg-green-500': isMenuOpen == true,
|
||||
}"
|
||||
@click="isMenuOpen = !isMenuOpen"
|
||||
>
|
||||
<Bars3Icon class="size-6" />
|
||||
</button>
|
||||
|
||||
<div class="fixed z-20 left-0 top-0 w-screen h-screen" v-if="isMenuOpen" @click="isMenuOpen = false"></div>
|
||||
|
||||
<div class="absolute z-30 top-full left-0 w-36 p-1 mt-2 flex flex-col gap-1 bg-zinc-600 rounded-md" v-if="isMenuOpen">
|
||||
<button
|
||||
class="p-1 rounded-md flex gap-2"
|
||||
:class="{
|
||||
'bg-zinc-950 hover:bg-zinc-800': globalStore.viewMode != 'active',
|
||||
'bg-green-600 hover:bg-green-500': globalStore.viewMode == 'active',
|
||||
}"
|
||||
@click="toggleViewMode('active')"
|
||||
>
|
||||
<WifiIcon class="size-6" /> <span> Aktywne RJ</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="p-1 rounded-md flex gap-2"
|
||||
:class="{
|
||||
'bg-zinc-950 hover:bg-zinc-800': globalStore.viewMode != 'storage',
|
||||
'bg-green-600 hover:bg-green-500': globalStore.viewMode == 'storage',
|
||||
}"
|
||||
@click="toggleViewMode('storage')"
|
||||
>
|
||||
<ArchiveBoxArrowDownIcon class="size-6" /> Zapisane RJ
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="p-1 rounded-md flex gap-2"
|
||||
:class="{
|
||||
'bg-zinc-950 hover:bg-zinc-800': globalStore.viewMode != 'journal',
|
||||
'bg-green-600 hover:bg-green-500': globalStore.viewMode == 'journal',
|
||||
}"
|
||||
@click="toggleViewMode('journal')"
|
||||
>
|
||||
<CloudArrowDownIcon class="size-6" /> Dziennik RJ
|
||||
</button>
|
||||
|
||||
<!-- <button class="m-0 p-0" @focus="isMenuOpen = false"></button> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<select
|
||||
name="trains"
|
||||
id="trains-select"
|
||||
class="bg-zinc-800 p-1 rounded-md print:hidden w-full"
|
||||
:disabled="apiStore.activeDataStatus != DataStatus.SUCCESS"
|
||||
v-model="globalStore.selectedTrainId"
|
||||
v-if="globalStore.viewMode == 'active'"
|
||||
@change="selectTrain"
|
||||
>
|
||||
<option :value="null" disabled>
|
||||
{{ apiStore.activeDataStatus == DataStatus.LOADING ? $t('data-loading-text') : $t('train-select-placeholder') }}
|
||||
</option>
|
||||
<option :value="train.id" v-for="train in globalStore.activeTimetableTrains">
|
||||
{{ train.driverName }} | {{ train.timetable?.category }} {{ train.trainNo }} [{{ getRegionNameById(train.region) }}]
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
v-if="globalStore.viewMode == 'storage'"
|
||||
v-model="globalStore.storageTimetableSearch"
|
||||
class="bg-zinc-800 p-1 rounded-md print:hidden w-full"
|
||||
:placeholder="$t('train-search-placeholder')"
|
||||
/>
|
||||
|
||||
<div v-if="globalStore.viewMode == 'journal'" class="w-full relative">
|
||||
<input
|
||||
type="text"
|
||||
v-model="globalStore.journalTimetableSearch"
|
||||
class="bg-zinc-800 p-1 rounded-md print:hidden w-full"
|
||||
:placeholder="$t('journal-search-placeholder')"
|
||||
@keydown.enter="fetchJournalTrain"
|
||||
/>
|
||||
|
||||
<div class="absolute top-0 right-0">
|
||||
<button class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700">
|
||||
<MagnifyingGlassIcon class="text-white size-6" />
|
||||
</button>
|
||||
|
||||
<button class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700">
|
||||
<XMarkIcon class="text-white size-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700" @click="toggleDarkMode">
|
||||
<MoonIcon v-if="globalStore.darkMode" class="text-white size-6" />
|
||||
<SunIcon v-else class="text-white size-6" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700 disabled:opacity-60 disabled:hover:bg-zinc-800"
|
||||
:disabled="globalStore.currentTimetableData == null"
|
||||
@click="openPrintingWindow"
|
||||
>
|
||||
<PrinterIcon class="text-white size-6" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="p-1 rounded-md disabled:opacity-60 disabled:hover:bg-zinc-800"
|
||||
:disabled="globalStore.currentTimetableData == null"
|
||||
:class="{
|
||||
'bg-green-600 hover:bg-green-700': isTimetableSaved,
|
||||
'bg-zinc-800 hover:bg-zinc-700': !isTimetableSaved,
|
||||
}"
|
||||
@click="saveToStorage"
|
||||
>
|
||||
<ArrowDownTrayIcon class="text-white size-6" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useApiStore } from '../../stores/api.store';
|
||||
import { DataStatus } from '../../types/api.types';
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
import {
|
||||
PrinterIcon,
|
||||
MoonIcon,
|
||||
SunIcon,
|
||||
ArchiveBoxArrowDownIcon,
|
||||
ArrowDownTrayIcon,
|
||||
CloudArrowDownIcon,
|
||||
WifiIcon,
|
||||
XMarkIcon,
|
||||
MagnifyingGlassIcon,
|
||||
Bars3Icon,
|
||||
} from '@heroicons/vue/16/solid';
|
||||
import { getRegionNameById } from '../../utils/trainUtils';
|
||||
import type { TimetableData, ViewMode } from '../../types/common.types';
|
||||
|
||||
// Stores
|
||||
const apiStore = useApiStore();
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
const isMenuOpen = ref(false);
|
||||
|
||||
// Computed
|
||||
const isTimetableSaved = computed(() => {
|
||||
if (!globalStore.currentTimetableData) return false;
|
||||
|
||||
return Object.keys(globalStore.storageTimetables).includes(`${globalStore.currentTimetableData.timetableId}`);
|
||||
});
|
||||
|
||||
// Methods
|
||||
function selectTrain() {
|
||||
if (!apiStore.activeData) return;
|
||||
|
||||
globalStore.selectedActiveTrain = globalStore.activeTimetableTrains.find((train) => train.id == globalStore.selectedTrainId) ?? null;
|
||||
|
||||
if (globalStore.selectedActiveTrain != null) {
|
||||
globalStore.generatedDate = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
function fetchJournalTrain(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
console.log(globalStore.journalTimetableSearch);
|
||||
}
|
||||
|
||||
function toggleViewMode(viewMode: ViewMode) {
|
||||
if (viewMode == globalStore.viewMode) {
|
||||
switch (viewMode) {
|
||||
case 'active':
|
||||
globalStore.selectedActiveTrain = null;
|
||||
break;
|
||||
|
||||
case 'storage':
|
||||
globalStore.selectedStorageTimetable = null;
|
||||
break;
|
||||
|
||||
case 'journal':
|
||||
globalStore.selectedJournalTimetable = null;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
isMenuOpen.value = false;
|
||||
globalStore.viewMode = viewMode;
|
||||
}
|
||||
|
||||
function toggleDarkMode() {
|
||||
globalStore.darkMode = !globalStore.darkMode;
|
||||
|
||||
window.localStorage.setItem('currentTheme', globalStore.darkMode ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
function saveToStorage() {
|
||||
if (globalStore.currentTimetableData == null) return;
|
||||
|
||||
try {
|
||||
const savedTimetablesStorage = localStorage.getItem('savedTimetables');
|
||||
let savedTimetablesJSON: Record<number, TimetableData> = savedTimetablesStorage ? JSON.parse(savedTimetablesStorage) : {};
|
||||
|
||||
if (savedTimetablesJSON[globalStore.currentTimetableData.timetableId] !== undefined) {
|
||||
globalStore.selectedStorageTimetable = savedTimetablesJSON[globalStore.currentTimetableData.timetableId];
|
||||
globalStore.viewMode = 'storage';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
savedTimetablesJSON[globalStore.currentTimetableData.timetableId] = { ...globalStore.currentTimetableData, savedTimestamp: Date.now() };
|
||||
|
||||
localStorage.setItem('savedTimetables', JSON.stringify(savedTimetablesJSON));
|
||||
globalStore.storageTimetables = savedTimetablesJSON;
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
function openPrintingWindow() {
|
||||
if (globalStore.selectedActiveTrain != null) {
|
||||
const date = `${globalStore.generatedDate!.toLocaleDateString('pl-PL').replace(/\./g, '-')}--${globalStore
|
||||
.generatedDate!.toLocaleTimeString('pl-PL')
|
||||
.replace(/:/g, '-')}`;
|
||||
|
||||
document.title = `${globalStore.selectedActiveTrain.driverName} ; ${globalStore.selectedActiveTrain.timetable!.category} ${
|
||||
globalStore.selectedActiveTrain.trainNo
|
||||
}
|
||||
${globalStore.selectedActiveTrain.timetable?.route.replace('|', ' - ')} ; ${date}`;
|
||||
}
|
||||
|
||||
window.print();
|
||||
}
|
||||
|
||||
// function refreshData() {
|
||||
// apiStore.fetchActiveData();
|
||||
// selectTrain();
|
||||
// }
|
||||
</script>
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="text-white">
|
||||
<div v-if="globalStore.selectedStorageTimetable == null && Object.keys(globalStore.storageTimetables).length == 0">
|
||||
<div class="font-bold text-2xl p-1 bg-zinc-800">{{ $t('storage-empty-header') }}</div>
|
||||
<div class="text-md mt-2 p-2 bg-zinc-900">{{ $t('storage-empty-info') }}</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="font-bold text-2xl p-2 bg-zinc-800 mb-3">{{ $t('storage-preview-title') }}</div>
|
||||
<div class="font-bold p-2 bg-zinc-800 mb-3" v-if="filteredTimetables.length == 0">{{ $t('storage-preview-empty') }}</div>
|
||||
|
||||
<li v-for="timetable in filteredTimetables" class="flex gap-1 w-full my-2">
|
||||
<button class="bg-zinc-900 p-2 w-full cursor-pointer hover:bg-zinc-800 text-left" @click="selectTimetable(timetable)">
|
||||
<div class="text-zinc-300">#{{ timetable.timetableId }} • {{ new Date(timetable.savedTimestamp!).toLocaleString() }}</div>
|
||||
<b>{{ timetable.driverName }} | {{ timetable.category }} {{ timetable.trainNo }}</b> {{ timetable.route.replace('|', ' > ') }}
|
||||
</button>
|
||||
|
||||
<button class="bg-zinc-900 p-2 hover:bg-zinc-800" @click="removeTimetable(timetable.timetableId)">
|
||||
<TrashIcon class="size-5 text-white" />
|
||||
</button>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TrashIcon } from '@heroicons/vue/16/solid';
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
import type { TimetableData } from '../../types/common.types';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
const i18n = useI18n();
|
||||
|
||||
const filteredTimetables = computed(() => {
|
||||
let timetables = Object.values(globalStore.storageTimetables);
|
||||
|
||||
if (globalStore.storageTimetableSearch.length != 0)
|
||||
timetables = timetables.filter((st) =>
|
||||
`${st.timetableId} ${st.driverName} ${st.route} ${st.category} ${st.trainNo}`
|
||||
.toLocaleLowerCase()
|
||||
.includes(globalStore.storageTimetableSearch.toLocaleLowerCase())
|
||||
);
|
||||
|
||||
timetables.sort((a, b) => {
|
||||
return (b.savedTimestamp ?? 0) - (a.savedTimestamp ?? 0);
|
||||
});
|
||||
|
||||
return timetables;
|
||||
});
|
||||
|
||||
function selectTimetable(timetable: TimetableData) {
|
||||
globalStore.selectedStorageTimetable = timetable;
|
||||
}
|
||||
|
||||
function removeTimetable(timetableId: number) {
|
||||
const isConfirmed = confirm(i18n.t('delete-timetable-confirm'));
|
||||
|
||||
if (!isConfirmed) return;
|
||||
|
||||
try {
|
||||
const savedTimetablesStorage = localStorage.getItem('savedTimetables');
|
||||
let savedTimetablesJSON: Record<number, TimetableData> = savedTimetablesStorage ? JSON.parse(savedTimetablesStorage) : {};
|
||||
delete savedTimetablesJSON[timetableId];
|
||||
|
||||
localStorage.setItem('savedTimetables', JSON.stringify(savedTimetablesJSON));
|
||||
globalStore.storageTimetables = savedTimetablesJSON;
|
||||
} catch (error) {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="my-2 print:hidden" v-if="globalStore.currentTimetableData?.savedTimestamp">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex items-center gap-2 bg-zinc-900 p-1 w-full">
|
||||
<div>
|
||||
<InformationCircleIcon class="size-5" />
|
||||
</div>
|
||||
<i18n-t keypath="storage-preview-info" tag="span">
|
||||
<template v-slot:id>
|
||||
<b>#{{ globalStore.currentTimetableData.timetableId }}</b>
|
||||
</template>
|
||||
<template v-slot:driverName>
|
||||
<b>{{ globalStore.currentTimetableData.driverName }}</b>
|
||||
</template>
|
||||
<template v-slot:date>
|
||||
<b>{{ new Date(globalStore.currentTimetableData.savedTimestamp).toLocaleString() }}</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<button class="font-bold bg-zinc-900 p-1 hover:bg-zinc-800" @click="removeTimetable(globalStore.currentTimetableData.timetableId)">
|
||||
<TrashIcon class="text-white size-6" />
|
||||
</button>
|
||||
|
||||
<button class="font-bold bg-zinc-900 p-1 hover:bg-zinc-800" @click="globalStore.selectedStorageTimetable = null">
|
||||
<ArrowUturnLeftIcon class="text-white size-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ArrowUturnLeftIcon, InformationCircleIcon, TrashIcon } from '@heroicons/vue/16/solid';
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { TimetableData } from '../../types/common.types';
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
const i18n = useI18n();
|
||||
|
||||
function removeTimetable(timetableId: number) {
|
||||
const isConfirmed = confirm(i18n.t('delete-timetable-confirm'));
|
||||
|
||||
if (!isConfirmed) return;
|
||||
|
||||
try {
|
||||
const savedTimetablesStorage = localStorage.getItem('savedTimetables');
|
||||
let savedTimetablesJSON: Record<number, TimetableData> = savedTimetablesStorage ? JSON.parse(savedTimetablesStorage) : {};
|
||||
delete savedTimetablesJSON[timetableId];
|
||||
|
||||
localStorage.setItem('savedTimetables', JSON.stringify(savedTimetablesJSON));
|
||||
globalStore.storageTimetables = savedTimetablesJSON;
|
||||
|
||||
globalStore.selectedStorageTimetable = null;
|
||||
} catch (error) {}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<div
|
||||
:class="{ dark: globalStore.darkMode }"
|
||||
v-if="globalStore.currentTimetableData != null"
|
||||
class="overflow-auto p-1 bg-white print:bg-white dark:bg-zinc-950 print:text-black text-black dark:text-white min-h-full"
|
||||
>
|
||||
<div>
|
||||
<div class="p-1 font-bold w-max">
|
||||
{{ globalStore.currentTimetableData.category }} {{ globalStore.currentTimetableData.trainNo }} {{ $t('headers.relation') }}
|
||||
{{ globalStore.currentTimetableData.route.replace('|', ' - ') }}
|
||||
</div>
|
||||
|
||||
<table class="table-fixed mt-2 w-full border-collapse" v-if="computedTimetableRows.length > 0">
|
||||
<TimetableHeader />
|
||||
<TimetableBody :computed-timetable="computedTimetableRows" />
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-auto text-center font-bold text-zinc-400 p-1 min-h-full" v-else>
|
||||
<div v-if="globalStore.viewMode == 'active'">
|
||||
<div class="text-xl p-2 bg-zinc-900">{{ $t('train-select-info') }}</div>
|
||||
</div>
|
||||
|
||||
<TimetableStorage v-else-if="globalStore.viewMode == 'storage'" />
|
||||
<TimetableJournal v-else-if="globalStore.viewMode == 'journal'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
``;
|
||||
import { useApiStore } from '../../stores/api.store';
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
import TimetableBody from './TimetableBody.vue';
|
||||
import TimetableHeader from './TimetableHeader.vue';
|
||||
import type { SceneryRoute, StopRow, TimetablePathData } from '../../types/common.types';
|
||||
import TimetableStorage from './TimetableStorage.vue';
|
||||
import TimetableJournal from './TimetableJournal.vue';
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
const apiStore = useApiStore();
|
||||
|
||||
// Tymczasowa tabelka z posterunkami APO
|
||||
const apoNames = ['Stary Kisielin, pe', 'Czerwony Dwór, pe', 'Szczejkowice, pe'];
|
||||
|
||||
const computedTimetableRows = computed(() => {
|
||||
const timetableData = globalStore.currentTimetableData;
|
||||
|
||||
if (!timetableData) return [];
|
||||
|
||||
let timeFrom = Date.now();
|
||||
|
||||
const stockVmax = timetableData.trainMaxSpeed,
|
||||
stockMass = Math.floor(timetableData.mass / 1000),
|
||||
stockLength = timetableData.length;
|
||||
|
||||
const timetablePath = parseTimetablePath(timetableData.path);
|
||||
|
||||
const stopRows: StopRow[] = [];
|
||||
|
||||
let currentPathIndex = 0;
|
||||
let currentPath = timetablePath[0];
|
||||
|
||||
let lastDepartureTimestamp = 0;
|
||||
|
||||
let arrivalKm = 0,
|
||||
arrivalSpeed = 0,
|
||||
arrivalTracks = 0,
|
||||
departureSpeed = 0,
|
||||
departureTracks = 2,
|
||||
realLineNo = 0,
|
||||
abbrevs = [] as string[];
|
||||
|
||||
if (currentPath.departureLineData) {
|
||||
departureSpeed = currentPath.departureLineData.routeSpeed;
|
||||
departureTracks = currentPath.departureLineData.routeTracks;
|
||||
|
||||
arrivalSpeed = currentPath.departureLineData.routeSpeed;
|
||||
arrivalTracks = currentPath.departureLineData.routeTracks;
|
||||
|
||||
realLineNo = currentPath.departureLineData?.realLineNo ?? 0;
|
||||
abbrevs = getAbbrevs(currentPath.departureLineData);
|
||||
}
|
||||
|
||||
// console.debug('=========== ' + timetableData.trainNo + ' ===========');
|
||||
|
||||
const stopList = parseStopListString(timetableData.stopListString);
|
||||
|
||||
for (const stop of stopList) {
|
||||
if (stop.arrivalLine && stop.arrivalLine == currentPath.arrivalLine) {
|
||||
arrivalKm = stop.stopDistance;
|
||||
|
||||
if (currentPath.arrivalLineData) {
|
||||
arrivalSpeed = currentPath.arrivalLineData.routeSpeed;
|
||||
arrivalTracks = currentPath.arrivalLineData.routeTracks;
|
||||
realLineNo = currentPath.arrivalLineData.realLineNo ?? 0;
|
||||
abbrevs = getAbbrevs(currentPath.arrivalLineData);
|
||||
}
|
||||
|
||||
departureSpeed = arrivalSpeed;
|
||||
departureTracks = arrivalTracks;
|
||||
}
|
||||
|
||||
if (stop.mainStop || (/^podg|po|pe$/.test(stop.stopNameRAW) && !/^sbl/i.test(stop.stopNameRAW))) {
|
||||
let correctedDepartureSpeed = 0,
|
||||
correctedDepartureTracks = 0;
|
||||
|
||||
const internalRouteInfo = stop.departureLine
|
||||
? currentPath.sceneryData?.routesInfo.find((route) => route.isInternal && route.routeName == stop.departureLine)
|
||||
: undefined;
|
||||
|
||||
if (internalRouteInfo) {
|
||||
correctedDepartureSpeed = internalRouteInfo.routeSpeed;
|
||||
departureSpeed = internalRouteInfo.routeSpeed;
|
||||
realLineNo = internalRouteInfo.realLineNo ?? realLineNo;
|
||||
abbrevs = getAbbrevs(internalRouteInfo);
|
||||
|
||||
correctedDepartureTracks = internalRouteInfo.routeTracks;
|
||||
departureTracks = internalRouteInfo.routeTracks;
|
||||
|
||||
if (stopRows.length == 0) {
|
||||
arrivalSpeed = departureSpeed;
|
||||
arrivalTracks = departureTracks;
|
||||
}
|
||||
}
|
||||
|
||||
let pointAbbrevs = [];
|
||||
if (apoNames.includes(stop.stopNameRAW)) pointAbbrevs.unshift(`APO ${currentPath.sceneryData?.abbr}`);
|
||||
|
||||
let rowData: StopRow = {
|
||||
isMain: stop.mainStop,
|
||||
pointKm: stop.stopDistance.toFixed(3),
|
||||
pointName: stop.stopNameRAW,
|
||||
scheduledArrivalDate: stop.arrivalTimestamp ? new Date(stop.arrivalTimestamp) : null,
|
||||
scheduledDepartureDate: stop.departureTimestamp ? new Date(stop.departureTimestamp) : null,
|
||||
stopTime: stop.stopTime ? (stop.departureTimestamp - stop.arrivalTimestamp) / 60000 : 0,
|
||||
stopType: stop.stopType,
|
||||
sceneryName: currentPath.sceneryName,
|
||||
realLine: realLineNo == 0 ? '' : realLineNo.toString(),
|
||||
driveTime: lastDepartureTimestamp ? stop.arrivalTimestamp - lastDepartureTimestamp : 0,
|
||||
|
||||
abbrevs: [...pointAbbrevs, ...abbrevs],
|
||||
|
||||
arrivalKm: arrivalKm.toFixed(3),
|
||||
departureKm: stop.stopDistance.toFixed(3),
|
||||
|
||||
arrivalSpeed: arrivalSpeed,
|
||||
arrivalTracks: arrivalTracks,
|
||||
|
||||
departureSpeed: departureSpeed,
|
||||
departureTracks: departureTracks,
|
||||
|
||||
headUnits: timetableData.headUnits,
|
||||
stockVmax,
|
||||
stockLength,
|
||||
stockMass,
|
||||
};
|
||||
|
||||
// console.debug(stop.stopNameRAW, stop.departureLine);
|
||||
|
||||
arrivalKm = stop.stopDistance;
|
||||
arrivalSpeed = correctedDepartureSpeed || arrivalSpeed;
|
||||
arrivalTracks = correctedDepartureTracks || arrivalTracks;
|
||||
|
||||
if (stop.departureTimestamp) lastDepartureTimestamp = stop.departureTimestamp;
|
||||
|
||||
stopRows.push(rowData);
|
||||
}
|
||||
|
||||
if (stop.departureLine && stop.departureLine == currentPath.departureLine) {
|
||||
// Reverse search for last scenery checkpoint
|
||||
for (let i = stopRows.length - 1; i > 0; i--) {
|
||||
if (currentPath.departureLineData) {
|
||||
stopRows[i].departureTracks = currentPath.departureLineData.routeTracks;
|
||||
stopRows[i].departureSpeed = currentPath.departureLineData.routeSpeed;
|
||||
stopRows[i].realLine = currentPath.departureLineData.realLineNo?.toString() ?? '';
|
||||
|
||||
if (stopRows[i].isMain || stopRows[i].pointName.endsWith(', podg')) {
|
||||
stopRows[i].departureSpeed = currentPath.departureLineData.routeSpeed;
|
||||
stopRows[i].departureTracks = currentPath.departureLineData.routeTracks;
|
||||
|
||||
abbrevs = getAbbrevs(currentPath.departureLineData);
|
||||
stopRows[i].abbrevs = abbrevs;
|
||||
break;
|
||||
}
|
||||
|
||||
stopRows[i].arrivalSpeed = currentPath.departureLineData.routeSpeed;
|
||||
stopRows[i].arrivalTracks = currentPath.departureLineData.routeTracks;
|
||||
}
|
||||
}
|
||||
|
||||
currentPath = timetablePath[++currentPathIndex];
|
||||
}
|
||||
}
|
||||
|
||||
let timeTo = Date.now();
|
||||
|
||||
globalStore.generatedMs = timeTo - timeFrom;
|
||||
|
||||
return stopRows;
|
||||
});
|
||||
|
||||
function parseTimetablePath(path: string): TimetablePathData[] {
|
||||
return path.split(';').map((pathEl) => {
|
||||
const [arrivalLine, scenery, departureLine] = pathEl.split(',');
|
||||
const sceneryName = scenery.split(' ').slice(0, -1).join(' ');
|
||||
|
||||
const sceneryData = apiStore.sceneryData?.find((sc) => sc.name == sceneryName) ?? null;
|
||||
const arrivalLineData = arrivalLine ? sceneryData?.routesInfo.find((rt) => rt.routeName == arrivalLine) ?? null : null;
|
||||
const departureLineData = departureLine ? sceneryData?.routesInfo.find((rt) => rt.routeName == departureLine) ?? null : null;
|
||||
|
||||
return {
|
||||
sceneryName,
|
||||
sceneryData: sceneryData ?? null,
|
||||
arrivalLine: arrivalLine ?? '',
|
||||
departureLine: departureLine ?? '',
|
||||
arrivalLineData,
|
||||
departureLineData,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function parseStopListString(stopsString: string) {
|
||||
//${stop.arrivalLine ?? ''};${stop.arrivalTimestamp};${stop.stopNameRAW};${stop.stopTime ? stop.stopTime + '_' + stop.stopType : ''};${stop.mainStop};${stop.stopDistance};${stop.departureTimestamp};${stop.departureLine ?? ''}
|
||||
return stopsString.split('~~').map((stop) => {
|
||||
const [arrivalLine, arrivalTimestamp, stopNameRAW, stopDetails, isMainStop, stopDistance, departureTimestamp, departureLine] = stop.split(';');
|
||||
const [stopTime, stopType] = stopDetails.split('_');
|
||||
|
||||
return {
|
||||
arrivalLine,
|
||||
arrivalTimestamp: parseInt(arrivalTimestamp),
|
||||
stopNameRAW,
|
||||
stopTime: stopTime ?? 0,
|
||||
stopType: stopType ?? null,
|
||||
mainStop: isMainStop == 'true',
|
||||
stopDistance: parseFloat(stopDistance),
|
||||
departureTimestamp: parseInt(departureTimestamp),
|
||||
departureLine,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getAbbrevs(routeData: SceneryRoute) {
|
||||
const abbrevs = [];
|
||||
|
||||
if (routeData.isRouteSBL == true) abbrevs.push(`${routeData.routeSpeed > 130 ? '4' : ''}S${routeData.routeTracks == 2 ? 'S' : ''}`);
|
||||
else if (routeData.routeTracks == 2) abbrevs.push('PP');
|
||||
|
||||
return abbrevs;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createI18n } from 'vue-i18n';
|
||||
|
||||
import localePL from './locales/pl.json';
|
||||
import localeEN from './locales/en.json';
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: 'en',
|
||||
fallbackLocale: 'pl',
|
||||
legacy: false,
|
||||
|
||||
messages: {
|
||||
pl: localePL,
|
||||
en: localeEN,
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"data-loading-text": "Loading data...",
|
||||
"train-select-placeholder": "Choose active train from the list",
|
||||
"train-select-info": "Choose active train to generate SRJP timetable",
|
||||
"train-search-placeholder": "Enter TT details (number, route, user)",
|
||||
"headers": {
|
||||
"line_no": "Line\nno.",
|
||||
"line_km": "Km",
|
||||
"station": "Station",
|
||||
"time": "Time",
|
||||
"loco_1": "Loco I",
|
||||
"loco_2": "Loco II",
|
||||
"loco_3": "Loco III",
|
||||
"mass": "Loco load",
|
||||
"length": "Train len.",
|
||||
"vmax": "Vmax",
|
||||
"relation": "Route"
|
||||
},
|
||||
"storage-empty-header": "ARCHIVED TIMETABLES SEARCH MODE",
|
||||
"storage-empty-info": "Timetables will be shown here after their archiving.",
|
||||
"storage-preview-title": "ARCHIVED TIMETABLES",
|
||||
"storage-preview-empty": "No entries found for given parameters",
|
||||
"storage-preview-info": "Archived timetable {id} for user {driverName} from: {date}",
|
||||
"storage-preview-button-text": "Return",
|
||||
"delete-timetable-confirm": "Are you sure that you want to delete this timetable?",
|
||||
"journal-search-placeholder": "Driver nickname"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"data-loading-text": "Ładowanie danych...",
|
||||
"train-select-placeholder": "Wybierz pociąg z listy",
|
||||
"train-select-info": "Wybierz aktywny pociąg, aby wygenerować SRJP",
|
||||
"train-search-placeholder": "Wpisz szczegóły RJ (nr, relacja, gracz)",
|
||||
"headers": {
|
||||
"line_no": "Nr\nlinii",
|
||||
"line_km": "Km",
|
||||
"station": "Stacja",
|
||||
"time": "Godzina",
|
||||
"loco_1": "Lok I",
|
||||
"loco_2": "Lok II",
|
||||
"loco_3": "Lok III",
|
||||
"mass": "Obc. lok.",
|
||||
"length": "Dł. poc.",
|
||||
"vmax": "Vmax",
|
||||
"relation": "Relacja"
|
||||
},
|
||||
"storage-empty-header": "TRYB WYSZUKIWANA ZAPISANYCH ROZKŁADÓW JAZDY",
|
||||
"storage-empty-info": "Użyj funkcji zapisu rozkładu jazdy, aby go tutaj wyświetlić.",
|
||||
"storage-preview-title": "ZAPISANE ROZKŁADY JAZDY",
|
||||
"storage-preview-empty": "Nie znaleziono żadnych wpisów dla podanych parametrów",
|
||||
"storage-preview-info": "Rozkład archiwalny {id} maszynisty {driverName} z dnia {date}",
|
||||
"storage-preview-button-text": "Powróć",
|
||||
"delete-timetable-confirm": "Czy na pewno chcesz usunąć ten rozkład jazdy z archiwum?",
|
||||
"journal-search-placeholder": "Nick maszynisty"
|
||||
}
|
||||
@@ -1,8 +1,22 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createApp, type Directive } from 'vue';
|
||||
import App from './App.vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import './style.css'
|
||||
|
||||
import './style.css';
|
||||
import i18n from './i18n';
|
||||
|
||||
const pinia = createPinia();
|
||||
|
||||
createApp(App).use(pinia).mount('#app');
|
||||
const clickOutsideDirective: Directive = {
|
||||
mounted(el, binding) {
|
||||
el.clickOutsideEvent = (event: Event) => {
|
||||
if (!(el == event.target || el.contains(event.target))) {
|
||||
binding.value();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', el.clickOutsideEvent);
|
||||
},
|
||||
};
|
||||
|
||||
createApp(App).use(i18n).use(pinia).directive('click-outside', clickOutsideDirective).mount('#app');
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import type { AxiosInstance } from 'axios';
|
||||
import axios from 'axios';
|
||||
import { defineStore } from 'pinia';
|
||||
import { DataStatus, type ActiveDataResponse, type JournalTimetableShortResponse, type SceneriesDataResponse } from '../types/api.types';
|
||||
import type { ActiveData, JournalTimetableShort, SceneryData } from '../types/common.types';
|
||||
|
||||
export const useApiStore = defineStore('api', {
|
||||
state() {
|
||||
return {
|
||||
client: null as AxiosInstance | null,
|
||||
|
||||
activeData: null as ActiveData | null,
|
||||
sceneryData: null as SceneryData[] | null,
|
||||
|
||||
journalTimetables: null as JournalTimetableShort[] | null,
|
||||
|
||||
outdatedTimerId: -1,
|
||||
isActiveDataOutdated: false,
|
||||
|
||||
activeDataStatus: DataStatus.LOADING,
|
||||
};
|
||||
},
|
||||
|
||||
actions: {
|
||||
async setupAPIData() {
|
||||
if (this.client != null) return;
|
||||
|
||||
let baseURL = 'https://stacjownik.spythere.eu';
|
||||
|
||||
switch (import.meta.env.VITE_API_MODE) {
|
||||
case 'development':
|
||||
baseURL = 'http://localhost:3001';
|
||||
break;
|
||||
case 'mocking':
|
||||
baseURL = 'http://localhost:3123';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL,
|
||||
});
|
||||
|
||||
this.fetchSceneriesData();
|
||||
await this.fetchActiveData();
|
||||
|
||||
setInterval(() => {
|
||||
this.fetchActiveData();
|
||||
}, 25000);
|
||||
},
|
||||
|
||||
async fetchActiveData() {
|
||||
try {
|
||||
const response = (await this.client!.get<ActiveDataResponse>('/api/getActiveData')).data;
|
||||
|
||||
this.activeData = response;
|
||||
this.activeDataStatus = DataStatus.SUCCESS;
|
||||
this.isActiveDataOutdated = false;
|
||||
|
||||
if (this.outdatedTimerId != -1) clearTimeout(this.outdatedTimerId);
|
||||
|
||||
this.outdatedTimerId = setTimeout(() => {
|
||||
this.isActiveDataOutdated = true;
|
||||
}, 60000);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
|
||||
async fetchSceneriesData() {
|
||||
try {
|
||||
const response = (await this.client!.get<SceneriesDataResponse>('/api/getSceneries')).data;
|
||||
|
||||
this.sceneryData = response;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
|
||||
async fetchTimetableHistoryList() {
|
||||
try {
|
||||
const response = (
|
||||
await this.client!.get<JournalTimetableShortResponse>('/api/getTimetables', {
|
||||
params: {
|
||||
driverName: 'Spythere',
|
||||
returnType: 'short',
|
||||
hasStopsDetails: 1
|
||||
},
|
||||
})
|
||||
).data;
|
||||
|
||||
this.journalTimetables = response;
|
||||
|
||||
// this.sceneryData = response;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,42 +1,83 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import httpClient from '../utils/http';
|
||||
import type { ActiveDataResponse, SceneriesDataResponse } from '../types/api.types';
|
||||
import type { ActiveData, SceneryData } from '../types/common.types';
|
||||
import { useApiStore } from './api.store';
|
||||
import type { ActiveTrain, TimetableData, ViewMode } from '../types/common.types';
|
||||
import { unitNameCorrections } from '../utils/trainUtils';
|
||||
|
||||
export const useGlobalStore = defineStore('global', {
|
||||
state: () => ({
|
||||
activeData: null as ActiveData | null,
|
||||
sceneryData: null as SceneryData[] | null,
|
||||
darkMode: false,
|
||||
viewMode: 'active' as ViewMode,
|
||||
|
||||
selectedTrainId: null as string | null,
|
||||
selectedActiveTrain: null as ActiveTrain | null,
|
||||
selectedStorageTimetable: null as TimetableData | null,
|
||||
selectedJournalTimetable: null,
|
||||
|
||||
storageTimetables: {} as Record<number, TimetableData>,
|
||||
|
||||
timetableWarnings: [] as string[],
|
||||
|
||||
generatedDate: null as Date | null,
|
||||
generatedMs: 0,
|
||||
|
||||
storageTimetableSearch: '',
|
||||
journalTimetableSearch: '',
|
||||
|
||||
showSettings: false,
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
async _fetchActiveData() {
|
||||
try {
|
||||
const response = (await httpClient.get<ActiveDataResponse>('/api/getActiveData')).data;
|
||||
getters: {
|
||||
activeTimetableTrains() {
|
||||
const apiStore = useApiStore();
|
||||
|
||||
this.activeData = response;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
if (!apiStore.activeData) return [];
|
||||
|
||||
return apiStore.activeData.trains.filter((train) => train.timetable).sort((t1, t2) => t1.driverName.localeCompare(t2.driverName, 'pl-PL'));
|
||||
},
|
||||
|
||||
async _fetchSceneriesData() {
|
||||
try {
|
||||
const response = (await httpClient.get<SceneriesDataResponse>('/api/getSceneries')).data;
|
||||
currentTimetableData(): TimetableData | null {
|
||||
if (this.viewMode == 'active') {
|
||||
const selectedTrain = this.selectedActiveTrain;
|
||||
|
||||
this.sceneryData = response;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
if (!selectedTrain || !selectedTrain.timetable) return null;
|
||||
|
||||
setupData() {
|
||||
this._fetchActiveData();
|
||||
this._fetchSceneriesData();
|
||||
|
||||
setInterval(() => {
|
||||
this._fetchActiveData();
|
||||
}, 20000);
|
||||
return {
|
||||
trainNo: selectedTrain.trainNo,
|
||||
mass: selectedTrain.mass,
|
||||
length: selectedTrain.length,
|
||||
driverId: selectedTrain.driverId,
|
||||
driverName: selectedTrain.driverName,
|
||||
category: selectedTrain.timetable.category,
|
||||
hasDangerousCargo: selectedTrain.timetable.hasDangerousCargo,
|
||||
hasExtraDeliveries: selectedTrain.timetable.hasExtraDeliveries,
|
||||
warningNotes: selectedTrain.timetable.warningNotes,
|
||||
path: selectedTrain.timetable.path,
|
||||
route: selectedTrain.timetable.route,
|
||||
trainMaxSpeed: selectedTrain.timetable.trainMaxSpeed,
|
||||
timetableId: selectedTrain.timetable.timetableId,
|
||||
stopListString: selectedTrain.timetable.stopList
|
||||
.filter((stop) => stop.mainStop || /^podg|po|pe$/.test(stop.stopNameRAW))
|
||||
.map(
|
||||
(stop) =>
|
||||
`${stop.arrivalLine ?? ''};${stop.arrivalTimestamp};${stop.stopNameRAW};${stop.stopTime ? stop.stopTime + '_' + stop.stopType : ''};${
|
||||
stop.mainStop
|
||||
};${stop.stopDistance};${stop.departureTimestamp};${stop.departureLine ?? ''}`
|
||||
)
|
||||
.join('~~'),
|
||||
headUnits: selectedTrain.stockString
|
||||
.split(';')
|
||||
.slice(0, 3)
|
||||
.filter((s, i) => i == 0 || /-\d+$/.test(s))
|
||||
.map((s) => {
|
||||
const unitName = s.slice(0, s.indexOf('-'));
|
||||
|
||||
return unitNameCorrections[unitName] ?? unitName;
|
||||
}),
|
||||
};
|
||||
} else if (this.viewMode == 'storage') {
|
||||
const selectedStorageTimetable = this.selectedStorageTimetable;
|
||||
return selectedStorageTimetable;
|
||||
} else return null;
|
||||
},
|
||||
},
|
||||
actions: {},
|
||||
});
|
||||
|
||||
@@ -2,23 +2,85 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
width: theme('size.2');
|
||||
height: theme('size.2');
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
background: theme('colors.stone.900');
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: theme('colors.stone.400');
|
||||
border-radius: theme('borderRadius.md');
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: theme('colors.stone.900');
|
||||
border-radius: 0 0 theme('borderRadius.md') 0;
|
||||
|
||||
}
|
||||
|
||||
/* Tooltips */
|
||||
[data-tooltip] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-tooltip] > .tooltip-content {
|
||||
visibility: hidden;
|
||||
background-color: theme('colors.inherit');
|
||||
min-width: 125px;
|
||||
color: inherit;
|
||||
text-align: center;
|
||||
border-radius: theme('borderRadius.md');
|
||||
padding: 0.25rem 0.5rem;
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
top: calc(100% + 0.5rem);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
[data-tooltip]:hover > .tooltip-content,
|
||||
[data-tooltip]:focus-visible > .tooltip-content {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.list-move,
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
.list-leave-active {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Print mode */
|
||||
@media print {
|
||||
:root {
|
||||
color-scheme: light;
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import type { ActiveData, SceneryData } from './common.types';
|
||||
import type { ActiveData, JournalTimetableDetailed, JournalTimetableShort, SceneryData } from './common.types';
|
||||
|
||||
export type ActiveDataResponse = ActiveData;
|
||||
|
||||
export type SceneriesDataResponse = SceneryData[];
|
||||
|
||||
export type JournalTimetableShortResponse = JournalTimetableShort[];
|
||||
export type JournalTimetableDetailedResponse = JournalTimetableDetailed[];
|
||||
|
||||
export enum DataStatus {
|
||||
'LOADING' = 0,
|
||||
'SUCCESS' = 1,
|
||||
'ERROR' = 2,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export type ViewMode = 'active' | 'storage' | 'journal';
|
||||
|
||||
export interface ActiveData {
|
||||
trains: ActiveTrain[];
|
||||
activeSceneries: ActiveScenery[];
|
||||
@@ -38,6 +40,7 @@ export interface ActiveTrainTimetable {
|
||||
timetableId: number;
|
||||
sceneries: string[];
|
||||
path: string;
|
||||
trainMaxSpeed: number;
|
||||
}
|
||||
|
||||
export interface TimetableStop {
|
||||
@@ -119,4 +122,142 @@ export interface SceneryRoute {
|
||||
routeLength: number;
|
||||
routeTracks: number;
|
||||
hidden?: boolean;
|
||||
realLineNo?: number;
|
||||
}
|
||||
|
||||
export interface StopRow {
|
||||
pointName: string;
|
||||
pointKm: string;
|
||||
isMain: boolean;
|
||||
stopTime: number;
|
||||
stopType: string;
|
||||
scheduledArrivalDate: Date | null;
|
||||
scheduledDepartureDate: Date | null;
|
||||
realLine: string;
|
||||
driveTime: number;
|
||||
abbrevs: string[];
|
||||
sceneryName: string;
|
||||
arrivalKm: string;
|
||||
arrivalSpeed: number;
|
||||
arrivalTracks: number;
|
||||
departureKm: string;
|
||||
departureSpeed: number;
|
||||
departureTracks: number;
|
||||
headUnits: string[];
|
||||
stockVmax: number;
|
||||
stockLength: number;
|
||||
stockMass: number;
|
||||
}
|
||||
|
||||
export interface TimetablePathData {
|
||||
sceneryName: string;
|
||||
sceneryData: SceneryData | null;
|
||||
arrivalLine: string;
|
||||
departureLine: string;
|
||||
arrivalLineData: SceneryRoute | null;
|
||||
departureLineData: SceneryRoute | null;
|
||||
}
|
||||
|
||||
export interface JournalTimetableShort {
|
||||
id: number;
|
||||
allStopsCount: number;
|
||||
confirmedStopsCount: number;
|
||||
createdAt: string;
|
||||
beginDate: string;
|
||||
driverId: number;
|
||||
driverName: string;
|
||||
route: string;
|
||||
routeDistance: number;
|
||||
currentDistance: number;
|
||||
currentLocation: string[];
|
||||
currentSceneryName: string;
|
||||
currentSceneryHash: string;
|
||||
driverLevel: number;
|
||||
fulfilled: boolean;
|
||||
terminated: boolean;
|
||||
driverIsSupporter: boolean;
|
||||
trainCategoryCode: string;
|
||||
trainNo: number;
|
||||
region: string;
|
||||
hasDangerousCargo: boolean;
|
||||
hasExtraDeliveries: boolean;
|
||||
twr: boolean;
|
||||
skr: boolean;
|
||||
}
|
||||
|
||||
export interface JournalTimetableDetailed extends JournalTimetableShort {
|
||||
id: number;
|
||||
schemaVersion: string;
|
||||
allStopsCount: number;
|
||||
authorId: number;
|
||||
authorName: string;
|
||||
beginDate: string;
|
||||
confirmedStopsCount: number;
|
||||
currentDistance: number;
|
||||
driverId: number;
|
||||
driverName: string;
|
||||
endDate: string;
|
||||
fulfilled: boolean;
|
||||
route: string;
|
||||
routeDistance: number;
|
||||
region: string;
|
||||
sceneriesString: string;
|
||||
scheduledBeginDate: string;
|
||||
scheduledEndDate: string;
|
||||
terminated: boolean;
|
||||
timetableId: number;
|
||||
trainCategoryCode: string;
|
||||
trainNo: number;
|
||||
twr: boolean;
|
||||
skr: boolean;
|
||||
stockString: string;
|
||||
stockMass: number;
|
||||
stockLength: number;
|
||||
maxSpeed: number;
|
||||
trainMaxSpeed: number;
|
||||
hashesString: string;
|
||||
currentSceneryName: string;
|
||||
currentSceneryHash: any;
|
||||
driverIsSupporter: boolean;
|
||||
driverLevel: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
stockHistory: string[];
|
||||
hidden: boolean;
|
||||
routeSceneries: string;
|
||||
checkpointArrivals: any[];
|
||||
checkpointDepartures: any[];
|
||||
checkpointArrivalsScheduled: string[];
|
||||
checkpointDeparturesScheduled: string[];
|
||||
checkpointStopTypes: string[];
|
||||
currentLocation: string[];
|
||||
visitedSceneries: string[];
|
||||
sceneryHashes: string[];
|
||||
sceneryNames: string[];
|
||||
checkpointComments: string[];
|
||||
checkpointNames: string[];
|
||||
path: string;
|
||||
warningNotes: string;
|
||||
hasDangerousCargo: boolean;
|
||||
hasExtraDeliveries: boolean;
|
||||
stopListString?: string;
|
||||
}
|
||||
|
||||
export interface TimetableData {
|
||||
trainNo: number;
|
||||
mass: number;
|
||||
length: number;
|
||||
driverName: string;
|
||||
driverId: number;
|
||||
hasDangerousCargo: boolean;
|
||||
hasExtraDeliveries: boolean;
|
||||
warningNotes: string;
|
||||
category: string;
|
||||
route: string;
|
||||
timetableId: number;
|
||||
path: string;
|
||||
trainMaxSpeed: number;
|
||||
stopListString: string;
|
||||
headUnits: string[];
|
||||
savedTimestamp?: number;
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const httpClient = axios.create({
|
||||
baseURL: 'https://stacjownik.spythere.eu',
|
||||
timeout: 3000,
|
||||
});
|
||||
|
||||
export default httpClient;
|
||||
@@ -0,0 +1,29 @@
|
||||
export const getRegionNameById = (id: string) => {
|
||||
switch (id) {
|
||||
case 'eu':
|
||||
return 'PL1';
|
||||
|
||||
case 'cae':
|
||||
return 'PL2';
|
||||
|
||||
case 'us':
|
||||
return 'CZE';
|
||||
|
||||
case 'usw':
|
||||
return 'DE';
|
||||
|
||||
case 'ru':
|
||||
return 'ENG';
|
||||
|
||||
default:
|
||||
return 'PL1';
|
||||
}
|
||||
};
|
||||
|
||||
export const unitNameCorrections: Record<string, string> = {
|
||||
'2EN57': 'EN57',
|
||||
'201E': 'ET22',
|
||||
'4E': 'EU07',
|
||||
M62: 'ST44',
|
||||
CTLR4C: 'ST44',
|
||||
};
|
||||
@@ -4,6 +4,7 @@ export default {
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
darkMode: 'selector',
|
||||
plugins: [],
|
||||
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||
}
|
||||
|
||||
@@ -4,4 +4,7 @@ import vue from '@vitejs/plugin-vue'
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 5345
|
||||
}
|
||||
})
|
||||
|
||||
@@ -157,6 +157,32 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz#34aa0b52d0fbb1a654b596acfa595f0c7b77a77b"
|
||||
integrity sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==
|
||||
|
||||
"@heroicons/vue@^2.2.0":
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@heroicons/vue/-/vue-2.2.0.tgz#d81f14eed448eec9859849ed63facd3f29bca2b3"
|
||||
integrity sha512-G3dbSxoeEKqbi/DFalhRxJU4mTXJn7GwZ7ae8NuEQzd1bqdd0jAbdaBZlHPcvPD2xI1iGzNVB4k20Un2AguYPw==
|
||||
|
||||
"@intlify/core-base@10.0.5":
|
||||
version "10.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-10.0.5.tgz#c4d992381f8c3a50c79faf67be3404b399c3be28"
|
||||
integrity sha512-F3snDTQs0MdvnnyzTDTVkOYVAZOE/MHwRvF7mn7Jw1yuih4NrFYLNYIymGlLmq4HU2iIdzYsZ7f47bOcwY73XQ==
|
||||
dependencies:
|
||||
"@intlify/message-compiler" "10.0.5"
|
||||
"@intlify/shared" "10.0.5"
|
||||
|
||||
"@intlify/message-compiler@10.0.5":
|
||||
version "10.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-10.0.5.tgz#4eeace9f4560020d5e5d77f32bed7755e71d8efd"
|
||||
integrity sha512-6GT1BJ852gZ0gItNZN2krX5QAmea+cmdjMvsWohArAZ3GmHdnNANEcF9JjPXAMRtQ6Ux5E269ymamg/+WU6tQA==
|
||||
dependencies:
|
||||
"@intlify/shared" "10.0.5"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
"@intlify/shared@10.0.5":
|
||||
version "10.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-10.0.5.tgz#1b46ca8b541f03508fe28da8f34e4bb85506d6bc"
|
||||
integrity sha512-bmsP4L2HqBF6i6uaMqJMcFBONVjKt+siGluRq4Ca4C0q7W2eMaVZr8iCgF9dKbcVXutftkC7D6z2SaSMmLiDyA==
|
||||
|
||||
"@isaacs/cliui@^8.0.2":
|
||||
version "8.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
|
||||
@@ -403,7 +429,7 @@
|
||||
de-indent "^1.0.2"
|
||||
he "^1.2.0"
|
||||
|
||||
"@vue/devtools-api@^6.6.3":
|
||||
"@vue/devtools-api@^6.5.0", "@vue/devtools-api@^6.6.3":
|
||||
version "6.6.4"
|
||||
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
|
||||
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
|
||||
@@ -1181,7 +1207,7 @@ signal-exit@^4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
|
||||
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
|
||||
|
||||
source-map-js@^1.2.0, source-map-js@^1.2.1:
|
||||
source-map-js@^1.0.2, source-map-js@^1.2.0, source-map-js@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
@@ -1345,6 +1371,15 @@ vue-demi@^0.14.10:
|
||||
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
|
||||
integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==
|
||||
|
||||
vue-i18n@10:
|
||||
version "10.0.5"
|
||||
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-10.0.5.tgz#fdf4e6c7b669e80cfa3a12ed9625e2b46671cdf0"
|
||||
integrity sha512-9/gmDlCblz3i8ypu/afiIc/SUIfTTE1mr0mZhb9pk70xo2csHAM9mp2gdQ3KD2O0AM3Hz/5ypb+FycTj/lHlPQ==
|
||||
dependencies:
|
||||
"@intlify/core-base" "10.0.5"
|
||||
"@intlify/shared" "10.0.5"
|
||||
"@vue/devtools-api" "^6.5.0"
|
||||
|
||||
vue-tsc@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.2.0.tgz#dd06c56636f760d7534b7a7a0f6669ba93c217b8"
|
||||
|
||||