Compare commits
147 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba4a068df7 | |||
| 9afbd3b1eb | |||
| d27b66cfd9 | |||
| 8fea27f5a6 | |||
| 1d6838ef53 | |||
| ce3546745b | |||
| 06e70afc29 | |||
| cd8c29f327 | |||
| cd22a23aef | |||
| 5c56c0d63f | |||
| eaa7771ca7 | |||
| 99fca9f65d | |||
| eb3c42de8f | |||
| 345c5764f9 | |||
| 4c8afe5018 | |||
| c15939d5d0 | |||
| fa71587ca7 | |||
| f76a5f8603 | |||
| 1a377ddfd8 | |||
| a164918d3a | |||
| 96a14778c8 | |||
| 712813aba7 | |||
| 874568d3e4 | |||
| 0fae903884 | |||
| 022e937a17 | |||
| 812c34ae31 | |||
| 0c3e9e7ae2 | |||
| 3cd7201883 | |||
| 3c8fad0759 | |||
| 7397e3fd87 | |||
| c767ee6d2b | |||
| 63262ccf4a | |||
| 62736d4b04 | |||
| 59d694b233 | |||
| 1df3510df1 | |||
| 87f173a645 | |||
| ceb0a49932 | |||
| 4c9a560a4a | |||
| b5edfb8d3e | |||
| 7aaf620d6a | |||
| c296ef6dcd | |||
| 696d196b05 | |||
| 05c04e4aa1 | |||
| 93acfdb780 | |||
| 4a96ed3852 | |||
| 35ded92a64 | |||
| 2f946a37b4 | |||
| 86185a8f98 | |||
| c4473673a7 | |||
| 2c660b556e | |||
| 5969b4202c | |||
| 2d1b573101 | |||
| 24875d674f | |||
| b3ee8bd119 | |||
| 26e348b0be | |||
| 4e8aabe05e | |||
| f4aa0b28a1 | |||
| dc7d0a7ccc | |||
| 4d3d2c68de | |||
| 9515c77620 | |||
| 17d5574d0c | |||
| 3b68056acc | |||
| ca393057e4 | |||
| be64e4b61a | |||
| fab96589c1 | |||
| e028905048 | |||
| 0c21a8cb65 | |||
| 0a41aa2828 | |||
| bf7d00e29c | |||
| 6bd928d18e | |||
| 5c46209fd3 | |||
| 4de4991ff6 | |||
| 0431153326 | |||
| 6a23821f9c | |||
| a34c8807de | |||
| 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-old
|
||||
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
|
||||
@@ -0,0 +1,17 @@
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
github-releases-to-discord:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Github Releases To Discord
|
||||
uses: SethCohen/github-releases-to-discord@v1.13.1
|
||||
with:
|
||||
webhook_url: '${{ secrets.WEBHOOK_URL }}'
|
||||
color: '10038562'
|
||||
footer_title: 'Changelog - Rozkładownik SRJP'
|
||||
footer_timestamp: true
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Build & Deploy to VPS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
PROJECT_NAME: srjp-td2
|
||||
|
||||
jobs:
|
||||
build_and_deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build the app
|
||||
run: yarn && yarn build
|
||||
- name: Setup SSH key for connection with the server
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.VPS_SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa && chmod 600 ~/.ssh/id_rsa
|
||||
- name: Send new files
|
||||
run: rsync -avP -e "ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa -p 2022" ./dist/ ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }}:/var/www/$PROJECT_NAME --delete
|
||||
@@ -11,6 +11,9 @@ node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.env
|
||||
.env.*
|
||||
/dev-dist
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
@@ -22,3 +25,7 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# api-mock
|
||||
/api-mock/endpoints/
|
||||
/api-mock/*.lock
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
@@ -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', 'getVehiclesData'].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/getVehiclesData', (_, res) => {
|
||||
res.sendFile(path.join(cwd(), 'endpoints', 'getVehiclesData.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>
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Logger Function
|
||||
log() {
|
||||
local message="$1"
|
||||
local type="$2"
|
||||
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local color
|
||||
local endcolor="\033[0m"
|
||||
|
||||
case "$type" in
|
||||
"info") color="\033[38;5;79m" ;;
|
||||
"success") color="\033[1;32m" ;;
|
||||
"error") color="\033[1;31m" ;;
|
||||
*) color="\033[1;34m" ;;
|
||||
esac
|
||||
|
||||
echo -e "${color}${timestamp} - ${message}${endcolor}"
|
||||
}
|
||||
|
||||
# Error handler function
|
||||
handle_error() {
|
||||
local exit_code=$1
|
||||
local error_message="$2"
|
||||
log "Error: $error_message (Exit Code: $exit_code)" "error"
|
||||
exit $exit_code
|
||||
}
|
||||
|
||||
# Function to check for command availability
|
||||
command_exists() {
|
||||
command -v "$1" &> /dev/null
|
||||
}
|
||||
|
||||
check_os() {
|
||||
if ! [ -f "/etc/debian_version" ]; then
|
||||
echo "Error: This script is only supported on Debian-based systems."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to Install the script pre-requisites
|
||||
install_pre_reqs() {
|
||||
log "Installing pre-requisites" "info"
|
||||
|
||||
# Run 'apt-get update'
|
||||
if ! apt-get update -y; then
|
||||
handle_error "$?" "Failed to run 'apt-get update'"
|
||||
fi
|
||||
|
||||
# Run 'apt-get install'
|
||||
if ! apt-get install -y apt-transport-https ca-certificates curl gnupg; then
|
||||
handle_error "$?" "Failed to install packages"
|
||||
fi
|
||||
|
||||
if ! mkdir -p /usr/share/keyrings; then
|
||||
handle_error "$?" "Makes sure the path /usr/share/keyrings exist or run ' mkdir -p /usr/share/keyrings' with sudo"
|
||||
fi
|
||||
|
||||
rm -f /usr/share/keyrings/nodesource.gpg || true
|
||||
rm -f /etc/apt/sources.list.d/nodesource.list || true
|
||||
|
||||
# Run 'curl' and 'gpg' to download and import the NodeSource signing key
|
||||
if ! curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /usr/share/keyrings/nodesource.gpg; then
|
||||
handle_error "$?" "Failed to download and import the NodeSource signing key"
|
||||
fi
|
||||
|
||||
# Explicitly set the permissions to ensure the file is readable by all
|
||||
if ! chmod 644 /usr/share/keyrings/nodesource.gpg; then
|
||||
handle_error "$?" "Failed to set correct permissions on /usr/share/keyrings/nodesource.gpg"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to configure the Repo
|
||||
configure_repo() {
|
||||
local node_version=$1
|
||||
|
||||
arch=$(dpkg --print-architecture)
|
||||
if [ "$arch" != "amd64" ] && [ "$arch" != "arm64" ] && [ "$arch" != "armhf" ]; then
|
||||
handle_error "1" "Unsupported architecture: $arch. Only amd64, arm64, and armhf are supported."
|
||||
fi
|
||||
|
||||
echo "deb [arch=$arch signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$node_version nodistro main" | tee /etc/apt/sources.list.d/nodesource.list > /dev/null
|
||||
|
||||
# N|solid Config
|
||||
echo "Package: nsolid" | tee /etc/apt/preferences.d/nsolid > /dev/null
|
||||
echo "Pin: origin deb.nodesource.com" | tee -a /etc/apt/preferences.d/nsolid > /dev/null
|
||||
echo "Pin-Priority: 600" | tee -a /etc/apt/preferences.d/nsolid > /dev/null
|
||||
|
||||
# Nodejs Config
|
||||
echo "Package: nodejs" | tee /etc/apt/preferences.d/nodejs > /dev/null
|
||||
echo "Pin: origin deb.nodesource.com" | tee -a /etc/apt/preferences.d/nodejs > /dev/null
|
||||
echo "Pin-Priority: 600" | tee -a /etc/apt/preferences.d/nodejs > /dev/null
|
||||
|
||||
# Run 'apt-get update'
|
||||
if ! apt-get update -y; then
|
||||
handle_error "$?" "Failed to run 'apt-get update'"
|
||||
else
|
||||
log "Repository configured successfully."
|
||||
log "To install Node.js, run: apt-get install nodejs -y" "info"
|
||||
log "You can use N|solid Runtime as a node.js alternative" "info"
|
||||
log "To install N|solid Runtime, run: apt-get install nsolid -y \n" "success"
|
||||
fi
|
||||
}
|
||||
|
||||
# Define Node.js version
|
||||
NODE_VERSION="23.x"
|
||||
|
||||
# Check OS
|
||||
check_os
|
||||
|
||||
# Main execution
|
||||
install_pre_reqs || handle_error $? "Failed installing pre-requisites"
|
||||
configure_repo "$NODE_VERSION" || handle_error $? "Failed configuring repository"
|
||||
@@ -1,26 +1,30 @@
|
||||
{
|
||||
"name": "srjp-td2",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "1.1.3",
|
||||
"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": {
|
||||
"axios": "^1.7.9",
|
||||
"pinia": "^2.3.1",
|
||||
"vue": "^3.5.13"
|
||||
"lucide-vue-next": "^0.577.0",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.30",
|
||||
"vue-i18n": "11.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"@vue/tsconfig": "^0.9.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.5.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.5",
|
||||
"vue-tsc": "^2.2.0"
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vue-tsc": "^3.2.5"
|
||||
}
|
||||
}
|
||||
|
||||
|
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 width="50" height="30" viewBox="0 0 50 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M25 30L0 0H50L25 30Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 149 B |
@@ -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,20 @@
|
||||
{
|
||||
"name": "Rozkładownik TD2",
|
||||
"short_name": "SRJP",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#151414",
|
||||
"display": "standalone",
|
||||
"start_url": "."
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -1,394 +1,137 @@
|
||||
<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>
|
||||
<div class="text-white min-h-screen bg-zinc-950 print:bg-white">
|
||||
<!-- PWA update prompt -->
|
||||
<transition name="slide-anim">
|
||||
<UpdatePrompt v-if="needRefresh" @onUpdateClick="updateApp()" />
|
||||
</transition>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- Content -->
|
||||
<Navbar v-if="!globalStore.fullscreenMode" />
|
||||
<MainContainer />
|
||||
|
||||
<!-- Migrate Info -->
|
||||
<transition name="slide-anim">
|
||||
<MigrateInfo v-if="globalStore.isMigrationInfoOpen" />
|
||||
</transition>
|
||||
</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 UpdatePrompt from './components/App/UpdatePrompt.vue';
|
||||
|
||||
import { onMounted } from 'vue';
|
||||
import { useApiStore } from './stores/api.store';
|
||||
import { useGlobalStore } from './stores/global.store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRegisterSW } from 'virtual:pwa-register/vue';
|
||||
import { DataStatus } from './types/api.types';
|
||||
import MigrateInfo from './components/App/MigrateInfo.vue';
|
||||
|
||||
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;
|
||||
const { needRefresh, updateServiceWorker } = useRegisterSW({ immediate: true });
|
||||
|
||||
arrivalKm: string;
|
||||
onMounted(async () => {
|
||||
setupLocale();
|
||||
setupDarkMode();
|
||||
handleMigrationInfo();
|
||||
setupOfflineMode();
|
||||
loadStorageTimetables();
|
||||
setupAfterPrintClose();
|
||||
|
||||
arrivalSpeed: number;
|
||||
arrivalTracks: number;
|
||||
|
||||
departureKm: string;
|
||||
|
||||
departureSpeed: number;
|
||||
departureTracks: number;
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
},
|
||||
await apiStore.setupAPIData();
|
||||
handleQueries();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-collapse: collapse;
|
||||
function updateApp() {
|
||||
updateServiceWorker(true);
|
||||
needRefresh.value = false;
|
||||
}
|
||||
|
||||
.srjp-table {
|
||||
min-width: 750px;
|
||||
}
|
||||
function loadStorageTimetables() {
|
||||
if (!window.localStorage.getItem('savedTimetables')) return;
|
||||
|
||||
.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')!;
|
||||
}
|
||||
}
|
||||
|
||||
function setupOfflineMode() {
|
||||
apiStore.connectionMode = !navigator.onLine ? 'offline' : 'online';
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
apiStore.connectionMode = 'offline';
|
||||
|
||||
apiStore.journalTimetablesData = null;
|
||||
apiStore.activeData = null;
|
||||
});
|
||||
|
||||
window.addEventListener('online', () => {
|
||||
apiStore.connectionMode = 'online';
|
||||
apiStore.journalDataStatus = DataStatus.SUCCESS;
|
||||
|
||||
apiStore.setupAPIData();
|
||||
});
|
||||
}
|
||||
|
||||
function handleQueries() {
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
|
||||
if (query.has('id')) {
|
||||
const id = query.get('id')!;
|
||||
|
||||
const queryTrain = apiStore.activeData?.trains.find((train) => train.id == id);
|
||||
|
||||
if (queryTrain) {
|
||||
globalStore.selectedTrainId = id;
|
||||
globalStore.selectedActiveTrain = queryTrain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleMigrationInfo() {
|
||||
// Show only on old domain
|
||||
if (location.hostname !== 'srjp-td2.web.app' && location.hostname != 'localhost') return;
|
||||
|
||||
const showInfo = localStorage.getItem('showMigrationInfo');
|
||||
|
||||
// Do not show if already acknowledged
|
||||
if (showInfo === 'false') return;
|
||||
|
||||
setTimeout(() => {
|
||||
globalStore.isMigrationInfoOpen = true;
|
||||
}, 2000);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 496 B |
@@ -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,22 @@
|
||||
<template>
|
||||
<main
|
||||
class="grid print:block print:bg-white p-3 mx-auto max-w-[800px] min-h-[300px] gap-1 relative"
|
||||
:class="{
|
||||
'grid-rows-[auto_auto_1fr] h-[calc(100vh-40px)]': !globalStore.fullscreenMode,
|
||||
'grid-rows-[1fr] h-screen': globalStore.fullscreenMode
|
||||
}"
|
||||
>
|
||||
<SearchContainer v-if="!globalStore.fullscreenMode" />
|
||||
<TimetableWarnings v-if="!globalStore.fullscreenMode" />
|
||||
<TimetableContainer />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
import TimetableContainer from '../Timetable/TimetableContainer.vue';
|
||||
import TimetableWarnings from '../Timetable/TimetableWarnings.vue';
|
||||
import SearchContainer from '../TimetableSearch/SearchContainer.vue';
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="fixed z-10 bottom-0 left-0 p-1 text-center w-full bg-yellow-400 text-black font-bold">
|
||||
<div class="flex justify-center items-center flex-wrap gap-2">
|
||||
<i18n-t keypath="migrate-info.line-1" for="migrate-info" tag="div">
|
||||
<a href="https://srjp-td2.spythere.eu/" target="_blank" class="underline">
|
||||
https://srjp-td2.spythere.eu/
|
||||
</a>
|
||||
</i18n-t>
|
||||
<button
|
||||
class="p-1 bg-zinc-700 text-white rounded-md hover:bg-zinc-500 focus-visible:bg-zinc-500"
|
||||
@click="onAcceptButtonClick"
|
||||
>
|
||||
{{ t('migrate-info.accept-btn') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
|
||||
const store = useGlobalStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
function onAcceptButtonClick() {
|
||||
store.isMigrationInfoOpen = false;
|
||||
localStorage.setItem('showMigrationInfo', 'false');
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,40 @@
|
||||
<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" alt="SRJP logo" />
|
||||
<b class="ml-2 text-lg">
|
||||
Rozkładownik TD2
|
||||
<sup class="font-semibold text-zinc-300">
|
||||
<a :href="releaseHref" target="_blank">v{{ version }}</a>
|
||||
</sup>
|
||||
</b>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
class="bg-slate-600 p-1 px-2 rounded-md hover:bg-slate-500 flex items-center"
|
||||
@click="changeLang()"
|
||||
>
|
||||
<GlobeIcon :size="18" class="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 { GlobeIcon } from 'lucide-vue-next';
|
||||
import { version } from '../../../package.json';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { computed } from 'vue';
|
||||
|
||||
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 releaseHref = computed(() => `https://github.com/Spythere/srjp-td2/releases/tag/v${version}`);
|
||||
// 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,28 @@
|
||||
<template>
|
||||
<div class="fixed z-50 bottom-0 right-0">
|
||||
<button
|
||||
@click="onUpdateClick"
|
||||
class="p-3 m-3 bg-cyan-600 rounded-md text-xl hover:scale-105 transition-transform"
|
||||
ref="updateBtnEl"
|
||||
>
|
||||
<div>{{ $t('update-prompt.line1') }}</div>
|
||||
<u>{{ $t('update-prompt.line2') }}</u>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const emit = defineEmits(['onUpdateClick']);
|
||||
const updateBtnEl = ref<HTMLElement | null>(null);
|
||||
|
||||
function onUpdateClick() {
|
||||
emit('onUpdateClick');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateBtnEl.value?.focus();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<!-- Button closing fullscreen mode, relative to MainContainer -->
|
||||
<button
|
||||
v-if="globalStore.fullscreenMode"
|
||||
class="absolute right-6 top-3 p-1 rounded-md bg-green-600 hover:bg-green-500 print:hidden z-50"
|
||||
@click="() => (globalStore.fullscreenMode = false)"
|
||||
>
|
||||
<Minimize2Icon :size="22" />
|
||||
</button>
|
||||
|
||||
<!-- If there is no timetable chosen -->
|
||||
<div
|
||||
class="overflow-auto text-center font-bold text-zinc-400 p-2 min-h-full"
|
||||
v-if="globalStore.currentTimetableData == null"
|
||||
>
|
||||
<component :is="viewModes[globalStore.viewMode]" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="overflow-auto text-center font-bold text-white p-2 min-h-full"
|
||||
v-else-if="apiStore.apiDataStatus == DataStatus.LOADING"
|
||||
>
|
||||
Pobieranie danych...
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="overflow-auto text-center font-bold text-red-500 p-2 min-h-full"
|
||||
v-else-if="apiStore.apiDataStatus == DataStatus.ERROR"
|
||||
>
|
||||
Ups! Coś poszło nie tak przy pobieraniu danych! :/
|
||||
</div>
|
||||
|
||||
<!-- Timetable render based on current view mode -->
|
||||
<CurrentTimetableView v-else />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
import LocalStorageView from '../TimetableViews/LocalStorageView.vue';
|
||||
import JournalStorageView from '../TimetableViews/JournalStorageView.vue';
|
||||
import ActiveDataView from '../TimetableViews/ActiveDataView.vue';
|
||||
import CurrentTimetableView from '../TimetableViews/CurrentTimetableView.vue';
|
||||
import { Minimize2Icon } from 'lucide-vue-next';
|
||||
import { useApiStore } from '../../stores/api.store';
|
||||
import { DataStatus } from '../../types/api.types';
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
const apiStore = useApiStore();
|
||||
|
||||
const viewModes: Record<typeof globalStore.viewMode, any> = {
|
||||
active: ActiveDataView,
|
||||
storage: LocalStorageView,
|
||||
journal: JournalStorageView
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,771 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="p-1 font-bold w-max">
|
||||
{{ globalStore.currentTimetableData!.category }}
|
||||
{{ globalStore.currentTimetableData!.trainNo }} {{ $t('headers.relation') }}
|
||||
{{ globalStore.currentTimetableData!.route.replace('|', ' - ') }}
|
||||
</h2>
|
||||
|
||||
<table
|
||||
class="table-fixed mt-2 w-full border-collapse overflow-hidden"
|
||||
v-if="computedTimetableRows.length > 0"
|
||||
>
|
||||
<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="42" class="border border-black dark:border-white">V<sub>P</sub></th>
|
||||
<th width="40" 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>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="(row, i) in computedTimetableRows">
|
||||
<!-- Line no. -->
|
||||
<td
|
||||
class="text-center align-top border-l border-l-black dark:border-l-white relative"
|
||||
:class="{
|
||||
'border-t border-t-black dark:border-t-white':
|
||||
row.lastRowRef != null && row.lastRowRef.departureLineNumber != row.arrivalLineNumber,
|
||||
'border-b border-b-black dark:border-b-white': i == computedTimetableRows.length - 1
|
||||
}"
|
||||
>
|
||||
<div class="absolute -top-[0.5px] left-0 w-full h-full">
|
||||
<table class="h-full w-full border-collapse">
|
||||
<tbody>
|
||||
<!-- Arrival Line -->
|
||||
<tr class="align-top">
|
||||
<td>
|
||||
{{
|
||||
row.lastRowRef == null ||
|
||||
row.lastRowRef.departureLineNumber != row.arrivalLineNumber
|
||||
? row.arrivalLineNumber
|
||||
: ' '
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Departure Line -->
|
||||
<tr class="align-top">
|
||||
<td v-if="row.departureLineNumber != row.arrivalLineNumber" class="border-t">
|
||||
{{ row.departureLineNumber }}
|
||||
</td>
|
||||
<td v-else> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Km -->
|
||||
<td
|
||||
class="border border-black dark:border-white border-t-0 border-b-1 relative p-0"
|
||||
:class="{
|
||||
'border-b-0': i != computedTimetableRows.length - 1
|
||||
}"
|
||||
>
|
||||
<div class="absolute -top-[0.5px] left-0 w-full h-full">
|
||||
<table class="h-full w-full border-collapse">
|
||||
<tbody>
|
||||
<!-- Arrival Km -->
|
||||
<tr>
|
||||
<td
|
||||
class="align-top border-t"
|
||||
v-if="
|
||||
row.lastRowRef &&
|
||||
(row.lastRowRef.departureSpeedL != row.arrivalSpeedL ||
|
||||
row.lastRowRef.departureSpeedP != row.arrivalSpeedP ||
|
||||
row.lastRowRef.departureTracks != row.arrivalTracks ||
|
||||
row.lastRowRef.departureLineNumber != row.arrivalLineNumber)
|
||||
"
|
||||
>
|
||||
{{ row.arrivalKm }}
|
||||
</td>
|
||||
|
||||
<td class="align-top" v-else-if="row.lastRowRef == null">
|
||||
{{ row.arrivalKm }}
|
||||
</td>
|
||||
|
||||
<td v-else> </td>
|
||||
</tr>
|
||||
|
||||
<!-- Departure Km -->
|
||||
<tr
|
||||
:class="{
|
||||
'border-black dark:border-white border-t align-top':
|
||||
row.arrivalTracks != row.departureTracks ||
|
||||
row.departureSpeedL != row.arrivalSpeedL ||
|
||||
row.departureSpeedP != row.arrivalSpeedP ||
|
||||
row.departureLineNumber != row.arrivalLineNumber,
|
||||
hidden:
|
||||
row.arrivalTracks == row.departureTracks &&
|
||||
row.departureSpeedL == row.arrivalSpeedL &&
|
||||
row.departureSpeedP == row.arrivalSpeedP &&
|
||||
row.departureLineNumber == row.arrivalLineNumber
|
||||
}"
|
||||
>
|
||||
<td> {{ row.departureKm }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Vp, Vl -->
|
||||
<td
|
||||
class="text-center align-top p-0 border-l-black dark:border-l-white relative"
|
||||
:class="{
|
||||
'border-b border-b-black dark:border-b-white': i == computedTimetableRows.length - 1
|
||||
}"
|
||||
colspan="2"
|
||||
>
|
||||
<!-- Direction line arrow -->
|
||||
<div
|
||||
class="absolute h-0 w-0 border-x-transparent border-x-[6px] -left-[4px] border-t-[25px] -bottom-[12px] z-30 border-t-black dark:border-t-white print:border-t-black"
|
||||
v-if="i == computedTimetableRows.length - 1"
|
||||
></div>
|
||||
|
||||
<div class="absolute -top-[0.5px] left-0 w-full h-full">
|
||||
<table class="h-full w-full border-collapse">
|
||||
<tbody>
|
||||
<tr class="align-top">
|
||||
<td
|
||||
class="font-bold border-l-4 border-l-black dark:border-l-white"
|
||||
:class="{
|
||||
'border-t border-t-black dark:border-t-white':
|
||||
row.lastRowRef != null &&
|
||||
row.lastRowRef.departureSpeedP != row.arrivalSpeedP
|
||||
}"
|
||||
:colspan="row.arrivalTracks == 2 ? '1' : '2'"
|
||||
width="35"
|
||||
>
|
||||
{{
|
||||
row.lastRowRef == null ||
|
||||
row.lastRowRef.departureSpeedP != row.arrivalSpeedP ||
|
||||
row.lastRowRef.departureTracks != row.arrivalTracks
|
||||
? row.arrivalSpeedP
|
||||
: ' '
|
||||
}}
|
||||
</td>
|
||||
|
||||
<td
|
||||
v-if="row.arrivalTracks == 2"
|
||||
class="border-l border-l-black dark:border-l-white"
|
||||
:class="{
|
||||
'border-t border-t-black dark:border-t-white':
|
||||
row.lastRowRef != null &&
|
||||
row.lastRowRef.departureSpeedL != row.arrivalSpeedL
|
||||
}"
|
||||
width="35"
|
||||
>
|
||||
{{
|
||||
row.lastRowRef == null ||
|
||||
row.lastRowRef.departureSpeedL != row.arrivalSpeedL ||
|
||||
row.lastRowRef.departureTracks != row.arrivalTracks
|
||||
? row.arrivalSpeedL
|
||||
: ' '
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr
|
||||
class="border-l-4 border-l-black dark:border-l-white"
|
||||
:class="{
|
||||
'border-t border-t-black dark:border-t-white align-top':
|
||||
row.arrivalTracks != row.departureTracks ||
|
||||
row.departureSpeedL != row.arrivalSpeedL ||
|
||||
row.departureSpeedP != row.arrivalSpeedP
|
||||
}"
|
||||
>
|
||||
<td
|
||||
:colspan="row.departureTracks == 2 ? '1' : '2'"
|
||||
class="font-bold"
|
||||
width="35"
|
||||
>
|
||||
{{
|
||||
row.departureSpeedP != row.arrivalSpeedP ||
|
||||
row.departureTracks != row.arrivalTracks
|
||||
? row.departureSpeedP
|
||||
: ' '
|
||||
}}
|
||||
</td>
|
||||
|
||||
<td
|
||||
v-if="row.departureTracks == 2"
|
||||
class="border-l border-l-black dark:border-l-white"
|
||||
width="35"
|
||||
>
|
||||
{{
|
||||
row.departureSpeedL != row.arrivalSpeedL ||
|
||||
row.departureTracks != row.arrivalTracks
|
||||
? row.departureSpeedL
|
||||
: ' '
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Station -->
|
||||
<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>
|
||||
|
||||
<!-- Time -->
|
||||
<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 }"
|
||||
>
|
||||
<span
|
||||
v-if="
|
||||
(row.scheduledArrivalDate?.getTime() || 0) !=
|
||||
(row.scheduledDepartureDate?.getTime() || 0)
|
||||
"
|
||||
>
|
||||
{{
|
||||
row.scheduledArrivalDate?.toLocaleTimeString('pl-PL', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}}<sup
|
||||
v-if="
|
||||
row.scheduledArrivalDate && row.scheduledArrivalDate.getSeconds() != 0
|
||||
"
|
||||
>{{ Math.floor((row.scheduledArrivalDate.getSeconds() / 60) * 10) }}</sup
|
||||
>
|
||||
</span>
|
||||
|
||||
<span v-else> | </span>
|
||||
</td>
|
||||
<td width="30">{{ row.driveTime > 0 ? 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 }"
|
||||
>
|
||||
<span>{{
|
||||
row.scheduledDepartureDate?.toLocaleTimeString('pl-PL', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}}</span>
|
||||
<sup
|
||||
v-if="
|
||||
row.scheduledDepartureDate && row.scheduledDepartureDate.getSeconds() != 0
|
||||
"
|
||||
>{{ Math.floor((row.scheduledDepartureDate.getSeconds() / 60) * 10) }}</sup
|
||||
>
|
||||
</td>
|
||||
<td width="30" class="font-bold">{{ row.stopTime || '' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Locos -->
|
||||
<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>
|
||||
|
||||
<!-- Load / Length -->
|
||||
<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>{{ Math.floor(row.stockMass / 1000) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ row.stockLength }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Vmax-->
|
||||
<td
|
||||
class="text-center border border-black dark:border-white"
|
||||
:class="{ 'text-stone-400 ': i > 0 }"
|
||||
>
|
||||
{{ row.stockVmax }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="mt-2">
|
||||
<b><u>Kursuje:</u></b>
|
||||
<div>
|
||||
- {{ parseTimetableRunDate(computedTimetableRows[0].scheduledDepartureDate!) }}
|
||||
<span
|
||||
v-if="
|
||||
computedTimetableRows[
|
||||
computedTimetableRows.length - 1
|
||||
].scheduledArrivalDate!.getDate() !=
|
||||
computedTimetableRows[0].scheduledDepartureDate!.getDate()
|
||||
"
|
||||
>
|
||||
-
|
||||
{{
|
||||
parseTimetableRunDate(
|
||||
computedTimetableRows[computedTimetableRows.length - 1].scheduledArrivalDate!
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="timetableWarnings.length != 0">
|
||||
<b><u>Uwagi do rozkładu:</u></b>
|
||||
<div>- {{ timetableWarnings }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useApiStore } from '../../stores/api.store';
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
import type { SceneryRoute, StopRow, TimetablePathData } from '../../types/common.types';
|
||||
import { parseTimetableRunDate } from '../../utils/dateUtils';
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
const apiStore = useApiStore();
|
||||
|
||||
// Tymczasowa tabelka z posterunkami APO
|
||||
const apoNames = ['Stary Kisielin, pe', 'Czerwony Dwór, pe', 'Szczejkowice, pe'];
|
||||
|
||||
const timetableWarnings = computed(() => {
|
||||
const timetableData = globalStore.currentTimetableData;
|
||||
|
||||
if (!timetableData) return '';
|
||||
|
||||
return timetableData.warningNotes;
|
||||
});
|
||||
|
||||
const computedTimetableRows = computed(() => {
|
||||
const timetableData = globalStore.currentTimetableData;
|
||||
|
||||
if (!timetableData) return [];
|
||||
|
||||
let timeFrom = Date.now();
|
||||
|
||||
const stockVmax = timetableData.trainMaxSpeed,
|
||||
stockMass = timetableData.mass,
|
||||
stockLength = timetableData.length;
|
||||
|
||||
const timetablePath = parseTimetablePath(timetableData.path);
|
||||
|
||||
const stopRows: StopRow[] = [];
|
||||
|
||||
let lastRowRef: StopRow | null = null;
|
||||
|
||||
let currentPathIndex = 0;
|
||||
let currentPath = timetablePath[0];
|
||||
|
||||
let lastDepartureTimestamp = 0;
|
||||
|
||||
let arrivalSpeedL = 0,
|
||||
arrivalSpeedP = 0;
|
||||
|
||||
let departureSpeedL = 0,
|
||||
departureSpeedP = 0;
|
||||
|
||||
let arrivalKm = 0,
|
||||
arrivalTracks = 0,
|
||||
departureTracks = 2,
|
||||
arrivalLineNumber = 0,
|
||||
departureLineNumber = 0,
|
||||
abbrevs = [] as string[];
|
||||
|
||||
if (currentPath.departureLineData) {
|
||||
departureSpeedL = Math.min(currentPath.departureLineData.routeSpeed, stockVmax);
|
||||
departureSpeedP = currentPath.departureLineData.routeSpeedExit
|
||||
? Math.min(currentPath.departureLineData.routeSpeedExit, stockVmax)
|
||||
: departureSpeedL;
|
||||
|
||||
departureTracks = currentPath.departureLineData.routeTracks;
|
||||
|
||||
arrivalSpeedL = departureSpeedL;
|
||||
arrivalSpeedP = departureSpeedP;
|
||||
|
||||
arrivalTracks = departureTracks;
|
||||
|
||||
departureLineNumber = currentPath.departureLineData?.realLineNo ?? 0;
|
||||
arrivalLineNumber = departureLineNumber;
|
||||
|
||||
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) {
|
||||
if (arrivalKm >= stop.stopDistance)
|
||||
arrivalKm =
|
||||
(Number(stopRows[stopRows.length - 1].departureKm ?? '0') + stop.stopDistance) / 2;
|
||||
|
||||
if (currentPath.arrivalLineData) {
|
||||
arrivalSpeedP = Math.min(currentPath.arrivalLineData.routeSpeed, stockVmax);
|
||||
arrivalSpeedL = currentPath.arrivalLineData.routeSpeedExit
|
||||
? Math.min(currentPath.arrivalLineData.routeSpeedExit, stockVmax)
|
||||
: arrivalSpeedP;
|
||||
|
||||
arrivalTracks = currentPath.arrivalLineData.routeTracks;
|
||||
arrivalLineNumber = currentPath.arrivalLineData.realLineNo ?? 0;
|
||||
abbrevs = getAbbrevs(currentPath.arrivalLineData);
|
||||
}
|
||||
|
||||
departureSpeedL = arrivalSpeedL;
|
||||
departureSpeedP = arrivalSpeedP;
|
||||
departureLineNumber = arrivalLineNumber;
|
||||
|
||||
departureTracks = arrivalTracks;
|
||||
}
|
||||
|
||||
if (
|
||||
stop.mainStop ||
|
||||
(/^podg|po|pe$/.test(stop.stopNameRAW) && !/^sbl/i.test(stop.stopNameRAW))
|
||||
) {
|
||||
let correctedDepartureSpeedL = 0,
|
||||
correctedDepartureSpeedP = 0,
|
||||
correctedDepartureTracks = 0;
|
||||
|
||||
const internalRouteInfo = stop.departureLine
|
||||
? currentPath.sceneryData?.routesInfo.find(
|
||||
(route) => route.isInternal && route.routeName == stop.departureLine
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (internalRouteInfo) {
|
||||
correctedDepartureSpeedL = Math.min(internalRouteInfo.routeSpeed, stockVmax);
|
||||
correctedDepartureSpeedP = internalRouteInfo.routeSpeedExit
|
||||
? Math.min(internalRouteInfo.routeSpeedExit, stockVmax)
|
||||
: correctedDepartureSpeedL;
|
||||
|
||||
departureSpeedL = correctedDepartureSpeedL;
|
||||
departureSpeedP = correctedDepartureSpeedP;
|
||||
|
||||
arrivalLineNumber = internalRouteInfo.realLineNo ?? arrivalLineNumber;
|
||||
departureLineNumber = internalRouteInfo.realLineNo ?? departureLineNumber;
|
||||
|
||||
abbrevs = getAbbrevs(internalRouteInfo);
|
||||
|
||||
correctedDepartureTracks = internalRouteInfo.routeTracks;
|
||||
departureTracks = internalRouteInfo.routeTracks;
|
||||
|
||||
if (stopRows.length == 0) {
|
||||
arrivalSpeedL = departureSpeedL;
|
||||
arrivalSpeedP = departureSpeedP;
|
||||
arrivalTracks = departureTracks;
|
||||
arrivalLineNumber = departureLineNumber;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
arrivalLineNumber: arrivalLineNumber == 0 ? '' : arrivalLineNumber.toString(),
|
||||
departureLineNumber: departureLineNumber == 0 ? '' : departureLineNumber.toString(),
|
||||
driveTime: lastDepartureTimestamp ? stop.arrivalTimestamp - lastDepartureTimestamp : 0,
|
||||
|
||||
abbrevs: [...pointAbbrevs, ...abbrevs],
|
||||
|
||||
arrivalKm: arrivalKm.toFixed(3),
|
||||
departureKm: stop.stopDistance.toFixed(3),
|
||||
|
||||
arrivalSpeedL,
|
||||
arrivalSpeedP,
|
||||
|
||||
arrivalTracks: arrivalTracks,
|
||||
|
||||
departureSpeedL,
|
||||
departureSpeedP,
|
||||
|
||||
departureTracks: departureTracks,
|
||||
|
||||
headUnits: timetableData.headUnits,
|
||||
stockVmax,
|
||||
stockLength,
|
||||
stockMass,
|
||||
|
||||
lastRowRef
|
||||
};
|
||||
|
||||
// console.debug(stop.stopNameRAW, stop.departureLine);
|
||||
|
||||
arrivalKm = stop.stopDistance;
|
||||
|
||||
arrivalSpeedL = correctedDepartureSpeedL || arrivalSpeedL;
|
||||
arrivalSpeedP = correctedDepartureSpeedP || arrivalSpeedP;
|
||||
arrivalTracks = correctedDepartureTracks || arrivalTracks;
|
||||
|
||||
if (stop.departureTimestamp) lastDepartureTimestamp = stop.departureTimestamp;
|
||||
lastRowRef = rowData;
|
||||
|
||||
stopRows.push(rowData);
|
||||
}
|
||||
|
||||
if (stop.departureLine && stop.departureLine == currentPath.departureLine) {
|
||||
arrivalKm = stop.stopDistance;
|
||||
|
||||
// Reverse search for last scenery checkpoint
|
||||
if (currentPath.departureLineData) {
|
||||
if (
|
||||
currentPath.departureLineData.routeLength != 0 &&
|
||||
!currentPath.departureLineData.isRouteSBL
|
||||
)
|
||||
arrivalKm = stop.stopDistance + currentPath.departureLineData.routeLength / 1000;
|
||||
|
||||
if (
|
||||
stopRows[stopRows.length - 1].isMain &&
|
||||
currentPath.departureLineData.isRouteSBL &&
|
||||
stop.departureLine == currentPath.departureLine
|
||||
)
|
||||
arrivalKm = stop.stopDistance + currentPath.departureLineData.routeLength / 1000;
|
||||
|
||||
for (let i = stopRows.length - 1; i >= 0; i--) {
|
||||
stopRows[i].departureTracks = currentPath.departureLineData.routeTracks;
|
||||
|
||||
stopRows[i].departureSpeedL = Math.min(
|
||||
currentPath.departureLineData.routeSpeed,
|
||||
stockVmax
|
||||
);
|
||||
|
||||
stopRows[i].departureSpeedP = currentPath.departureLineData.routeSpeedExit
|
||||
? Math.min(currentPath.departureLineData.routeSpeedExit, stockVmax)
|
||||
: stopRows[i].departureSpeedL;
|
||||
|
||||
// stopRows[i].arrivalLineNumber =
|
||||
// currentPath.departureLineData.realLineNo?.toString() ?? '';
|
||||
|
||||
stopRows[i].departureLineNumber =
|
||||
currentPath.departureLineData.realLineNo?.toString() ?? '';
|
||||
|
||||
if (stopRows[i].isMain || stopRows[i].pointName.endsWith(', podg')) {
|
||||
stopRows[i].departureSpeedL = Math.min(
|
||||
currentPath.departureLineData.routeSpeed,
|
||||
stockVmax
|
||||
);
|
||||
|
||||
stopRows[i].departureSpeedP = currentPath.departureLineData.routeSpeedExit
|
||||
? Math.min(currentPath.departureLineData.routeSpeedExit, stockVmax)
|
||||
: stopRows[i].departureSpeedL;
|
||||
|
||||
stopRows[i].departureTracks = currentPath.departureLineData.routeTracks;
|
||||
|
||||
// console.log(
|
||||
// stop.departureLine,
|
||||
// currentPath.sceneryName,
|
||||
// stop.stopDistance,
|
||||
// currentPath.departureLineData.routeLength,
|
||||
// currentPath.departureLineData.isRouteSBL,
|
||||
// currentPath.departureLineData.realLineNo
|
||||
// );
|
||||
|
||||
abbrevs = getAbbrevs(currentPath.departureLineData);
|
||||
stopRows[i].abbrevs = abbrevs;
|
||||
break;
|
||||
}
|
||||
|
||||
stopRows[i].arrivalSpeedP = Math.min(currentPath.departureLineData.routeSpeed, stockVmax);
|
||||
stopRows[i].arrivalSpeedL = currentPath.departureLineData.routeSpeedExit
|
||||
? Math.min(currentPath.departureLineData.routeSpeedExit, stockVmax)
|
||||
: stopRows[i].arrivalSpeedP;
|
||||
stopRows[i].arrivalTracks = currentPath.departureLineData.routeTracks;
|
||||
stopRows[i].arrivalLineNumber =
|
||||
currentPath.departureLineData.realLineNo?.toString() ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
@media print {
|
||||
th,
|
||||
tr,
|
||||
td {
|
||||
border-color: theme('colors.black');
|
||||
}
|
||||
|
||||
table {
|
||||
page-break-after: auto;
|
||||
}
|
||||
|
||||
tr {
|
||||
page-break-inside: avoid;
|
||||
page-break-after: auto;
|
||||
}
|
||||
|
||||
thead {
|
||||
display: table-header-group;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,394 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="font-semibold text-center text-3xl">
|
||||
{{ globalStore.currentTimetableData!.category }}
|
||||
{{ globalStore.currentTimetableData!.trainNo }}
|
||||
</h2>
|
||||
|
||||
<h3 class="font-medium text-center">
|
||||
{{ globalStore.currentTimetableData!.route.replace('|', ' - ') }}
|
||||
</h3>
|
||||
|
||||
<p class="mt-2 text-center">
|
||||
Kursuje: {{ timetableDate.toLocaleDateString('pl-PL', { day: '2-digit' }) }}.{{
|
||||
romanMonthDigits[timetableDate.getMonth()]
|
||||
}}.{{ timetableDate.toLocaleDateString('pl-PL', { year: 'numeric' }) }}
|
||||
</p>
|
||||
|
||||
<p class="mt-2">
|
||||
Lokomotywa elektryczna {{ globalStore.currentTimetableData!.headUnits[0] }}, waga:
|
||||
{{ (globalStore.currentTimetableData!.mass / 1000).toFixed(1) }} t, długość:
|
||||
{{ globalStore.currentTimetableData!.length }} m
|
||||
</p>
|
||||
|
||||
<p></p>
|
||||
|
||||
<table class="table-fixed w-full border-collapse h-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<!-- Name -->
|
||||
<th
|
||||
width="250"
|
||||
class="font-normal border border-black dark:border-white border-l-transparent"
|
||||
>
|
||||
<MapPinIcon :size="20" class="mx-auto" />
|
||||
</th>
|
||||
|
||||
<!-- Info -->
|
||||
<th width="50" class="font-normal border border-black dark:border-white">
|
||||
<CircleAlertIcon :size="20" class="mx-auto" />
|
||||
</th>
|
||||
|
||||
<!-- Drive time -->
|
||||
<th width="30" class="font-normal border border-black dark:border-white">
|
||||
<TimerIcon :size="20" class="mx-auto" />
|
||||
</th>
|
||||
|
||||
<!-- Arrival -->
|
||||
<th width="70" class="font-normal border border-black dark:border-white">
|
||||
<CalendarArrowUpIcon :size="20" class="mx-auto" />
|
||||
</th>
|
||||
|
||||
<!-- Stop time -->
|
||||
<th width="40" class="font-normal border border-black dark:border-white">
|
||||
<HandIcon :size="20" class="mx-auto" />
|
||||
</th>
|
||||
|
||||
<!-- Departure -->
|
||||
<th width="70" class="font-normal border border-black dark:border-white">
|
||||
<CalendarArrowDownIcon :size="20" class="mx-auto" />
|
||||
</th>
|
||||
|
||||
<!-- vMax -->
|
||||
<th
|
||||
width="80"
|
||||
class="font-normal border border-black dark:border-white border-r-transparent"
|
||||
>
|
||||
<CircleGaugeIcon :size="20" class="mx-auto" />
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(row, i) in computedTimetableRows"
|
||||
:class="{ 'bg-slate-100 dark:bg-zinc-900 print:bg-gray-300': i % 2 == 0 }"
|
||||
class="leading-none"
|
||||
>
|
||||
<td class="px-2 font-thin text-nowrap overflow-hidden overflow-ellipsis">
|
||||
<span :class="{ 'font-semibold': row.isMain, 'font-normal': !row.isMain }">
|
||||
{{ row.pointName }}</span
|
||||
><span
|
||||
>.............................................................................................
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="border border-black dark:border-white border-t-transparent border-b-transparent"
|
||||
></td>
|
||||
|
||||
<td
|
||||
class="border border-black dark:border-white border-t-transparent border-b-transparent text-center font-bold"
|
||||
>
|
||||
{{ row.driveTime ? Math.floor(row.driveTime / 60000) : '' }}
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="border border-black dark:border-white border-t-transparent border-b-transparent text-right font-bold px-2"
|
||||
>
|
||||
<span v-if="row.stopType == 'pt'">+ </span>
|
||||
<span> {{ row.arrivalDateStr }} </span>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="border border-black dark:border-white border-t-transparent border-b-transparent text-center font-semibold"
|
||||
>
|
||||
{{ row.stopTime || '' }}
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="border border-black dark:border-white border-t-transparent border-b-transparent text-right font-bold px-2 relative"
|
||||
>
|
||||
<span
|
||||
class="absolute right-[-3px] border-r-[5px] border-black"
|
||||
:class="{
|
||||
'top-0 h-[calc(100%+1px)]':
|
||||
row.arrivalTracks == row.departureTracks && row.arrivalTracks == 2,
|
||||
'top-0 h-[calc(50%+1px)]': row.arrivalTracks > row.departureTracks,
|
||||
'top-1/2 h-[calc(50%+1px)]': row.arrivalTracks < row.departureTracks
|
||||
}"
|
||||
></span>
|
||||
{{ row.departureDateStr }}
|
||||
</td>
|
||||
|
||||
<!-- v-if="
|
||||
i == 0 || (i > 0 && computedTimetableRows[i - 1].departureSpeed != row.arrivalSpeed)
|
||||
" -->
|
||||
<td class="text-center font-bold">
|
||||
<span v-if="i == 0 || computedTimetableRows[i - 1].departureSpeed != row.arrivalSpeed">
|
||||
{{ i == 0 ? row.departureSpeed : row.arrivalSpeed }}
|
||||
<span v-if="row.arrivalSpeed != row.departureSpeed">
|
||||
/
|
||||
{{ row.departureSpeed }}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
import type { StopRowCZ, TimetablePathData } from '../../types/common.types';
|
||||
import { useApiStore } from '../../stores/api.store';
|
||||
import {
|
||||
CalendarArrowDownIcon,
|
||||
CalendarArrowUpIcon,
|
||||
CircleAlertIcon,
|
||||
CircleGaugeIcon,
|
||||
HandIcon,
|
||||
MapPinIcon,
|
||||
TimerIcon
|
||||
} from 'lucide-vue-next';
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
const apiStore = useApiStore();
|
||||
|
||||
const timetableDate = ref(new Date());
|
||||
|
||||
const romanMonthDigits = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII'];
|
||||
|
||||
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: StopRowCZ[] = [];
|
||||
|
||||
let currentPathIndex = 0;
|
||||
let currentPath = timetablePath[0];
|
||||
|
||||
let lastDepartureTimestamp = 0;
|
||||
|
||||
let arrivalSpeed = 0,
|
||||
departureSpeed = 0,
|
||||
arrivalTracks = 0,
|
||||
departureTracks = 2;
|
||||
|
||||
if (currentPath.departureLineData) {
|
||||
departureSpeed = Math.min(currentPath.departureLineData.routeSpeed, stockVmax);
|
||||
arrivalSpeed = Math.min(currentPath.departureLineData.routeSpeed, stockVmax);
|
||||
|
||||
departureTracks = currentPath.departureLineData.routeTracks;
|
||||
arrivalTracks = currentPath.departureLineData.routeTracks;
|
||||
}
|
||||
|
||||
const stopList = parseStopListString(timetableData.stopListString);
|
||||
|
||||
timetableDate.value = new Date(stopList[0].departureTimestamp);
|
||||
|
||||
stopList.forEach((stop) => {
|
||||
if (stop.arrivalLine && stop.arrivalLine == currentPath.arrivalLine) {
|
||||
if (currentPath.arrivalLineData) {
|
||||
arrivalSpeed = Math.min(currentPath.arrivalLineData.routeSpeed, stockVmax);
|
||||
arrivalTracks = currentPath.arrivalLineData.routeTracks;
|
||||
}
|
||||
|
||||
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 = Math.min(internalRouteInfo.routeSpeed, stockVmax);
|
||||
departureSpeed = Math.min(internalRouteInfo.routeSpeed, stockVmax);
|
||||
correctedDepartureTracks = internalRouteInfo.routeTracks;
|
||||
departureTracks = internalRouteInfo.routeTracks;
|
||||
|
||||
if (stopRows.length == 0) {
|
||||
arrivalSpeed = departureSpeed;
|
||||
arrivalTracks = departureTracks;
|
||||
}
|
||||
}
|
||||
|
||||
const scheduledArrivalDate = stop.arrivalTimestamp ? new Date(stop.arrivalTimestamp) : null;
|
||||
|
||||
const scheduledDepartureDate = stop.departureTimestamp
|
||||
? new Date(stop.departureTimestamp)
|
||||
: null;
|
||||
|
||||
let arrivalDateStr =
|
||||
scheduledArrivalDate?.toLocaleTimeString('pl-PL', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
}) ?? '';
|
||||
|
||||
let departureDateStr =
|
||||
scheduledDepartureDate?.toLocaleTimeString('pl-PL', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
}) ?? '';
|
||||
|
||||
if (
|
||||
stopRows.length > 0 &&
|
||||
stopRows[stopRows.length - 1]?.scheduledArrivalDate?.getHours() ==
|
||||
scheduledArrivalDate?.getHours()
|
||||
) {
|
||||
arrivalDateStr = arrivalDateStr.split(':').slice(1).join(' ');
|
||||
}
|
||||
|
||||
if (
|
||||
stopRows[stopRows.length - 1]?.scheduledDepartureDate?.getHours() ==
|
||||
scheduledDepartureDate?.getHours()
|
||||
) {
|
||||
departureDateStr = departureDateStr.split(':').slice(1).join(' ');
|
||||
}
|
||||
|
||||
let rowData: StopRowCZ = {
|
||||
isMain: stop.mainStop,
|
||||
pointKm: stop.stopDistance.toFixed(3),
|
||||
pointName: stop.stopNameRAW,
|
||||
scheduledArrivalDate,
|
||||
scheduledDepartureDate,
|
||||
stopTime: stop.stopTime ? (stop.departureTimestamp - stop.arrivalTimestamp) / 60000 : 0,
|
||||
stopType: stop.stopType,
|
||||
sceneryName: currentPath.sceneryName,
|
||||
driveTime: lastDepartureTimestamp ? stop.arrivalTimestamp - lastDepartureTimestamp : 0,
|
||||
|
||||
arrivalSpeed: arrivalSpeed,
|
||||
departureSpeed: departureSpeed,
|
||||
|
||||
arrivalTracks,
|
||||
departureTracks,
|
||||
|
||||
headUnits: timetableData.headUnits,
|
||||
stockVmax,
|
||||
stockLength,
|
||||
stockMass,
|
||||
|
||||
arrivalDateStr,
|
||||
departureDateStr
|
||||
};
|
||||
|
||||
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
|
||||
if (currentPath.departureLineData) {
|
||||
for (let i = stopRows.length - 1; i >= 0; i--) {
|
||||
stopRows[i].departureTracks = currentPath.departureLineData.routeTracks;
|
||||
|
||||
stopRows[i].departureSpeed = Math.min(
|
||||
currentPath.departureLineData.routeSpeed,
|
||||
stockVmax
|
||||
);
|
||||
|
||||
if (stopRows[i].isMain || stopRows[i].pointName.endsWith(', podg')) {
|
||||
stopRows[i].departureSpeed = Math.min(
|
||||
currentPath.departureLineData.routeSpeed,
|
||||
stockVmax
|
||||
);
|
||||
|
||||
stopRows[i].departureTracks = currentPath.departureLineData.routeTracks;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
stopRows[i].arrivalSpeed = Math.min(currentPath.departureLineData.routeSpeed, stockVmax);
|
||||
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: Number(stopTime ?? 0),
|
||||
stopType: stopType ?? null,
|
||||
mainStop: isMainStop == 'true',
|
||||
stopDistance: parseFloat(stopDistance),
|
||||
departureTimestamp: parseInt(departureTimestamp),
|
||||
departureLine
|
||||
};
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<!-- Local -->
|
||||
<div class="print:hidden" v-if="globalStore.currentTimetableData?.savedTimestamp">
|
||||
<div class="flex gap-2 mt-1">
|
||||
<div class="flex items-center gap-2 bg-zinc-900 p-1 w-full">
|
||||
<div>
|
||||
<InfoIcon :size="20" />
|
||||
</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?.journalCreatedAt ??
|
||||
globalStore.currentTimetableData.savedTimestamp
|
||||
).toLocaleString()
|
||||
}}</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="font-bold bg-zinc-900 p-1 hover:bg-zinc-800 rounded-md"
|
||||
@click="removeTimetable(globalStore.currentTimetableData.timetableId)"
|
||||
>
|
||||
<Trash2Icon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="font-bold bg-zinc-900 p-1 hover:bg-zinc-800 rounded-md"
|
||||
@click="globalStore.selectedStorageTimetable = null"
|
||||
>
|
||||
<Undo2Icon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Journal -->
|
||||
<div class="print:hidden" v-else-if="globalStore.currentTimetableData?.journalCreatedAt">
|
||||
<div class="flex gap-2 mt-1">
|
||||
<div class="flex items-center gap-2 bg-zinc-900 p-1 w-full">
|
||||
<div>
|
||||
<InfoIcon :size="20" />
|
||||
</div>
|
||||
|
||||
<i18n-t keypath="journal-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.journalCreatedAt).toLocaleString()
|
||||
}}</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="font-bold bg-zinc-900 p-1 hover:bg-zinc-800 rounded-md"
|
||||
@click="globalStore.selectedJournalTimetable = null"
|
||||
>
|
||||
<Undo2Icon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { TimetableData } from '../../types/common.types';
|
||||
import { InfoIcon, Trash2Icon, Undo2Icon } from 'lucide-vue-next';
|
||||
|
||||
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,42 @@
|
||||
<template>
|
||||
<select
|
||||
v-model="globalStore.selectedTrainId"
|
||||
@change="selectTrain"
|
||||
name="trains"
|
||||
id="trains-select"
|
||||
class="bg-zinc-800 p-1 rounded-md print:hidden w-full"
|
||||
:disabled="apiStore.apiDataStatus != DataStatus.SUCCESS"
|
||||
aria-label="Active train select"
|
||||
>
|
||||
<option :value="null" disabled>
|
||||
{{
|
||||
apiStore.apiDataStatus == 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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useApiStore } from '../../stores/api.store';
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
import { DataStatus } from '../../types/api.types';
|
||||
import { getRegionNameById } from '../../utils/trainUtils';
|
||||
|
||||
const apiStore = useApiStore();
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
function selectTrain() {
|
||||
if (!apiStore.activeData) return;
|
||||
|
||||
globalStore.selectedActiveTrain =
|
||||
globalStore.activeTimetableTrains.find((train) => train.id == globalStore.selectedTrainId) ??
|
||||
null;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="flex gap-2 items-center" v-if="globalStore.currentTimetableData == null">
|
||||
<div class="flex gap-2 w-full">
|
||||
<input
|
||||
v-model="globalStore.journalTimetableSearch.driverName"
|
||||
type="text"
|
||||
@keydown.enter="fetchJournalTimetables"
|
||||
:class="`bg-zinc-800 p-1 rounded-md print:hidden w-full ${
|
||||
apiStore.connectionMode == 'offline' ? 'opacity-35' : ''
|
||||
}`"
|
||||
:disabled="
|
||||
apiStore.journalDataStatus == DataStatus.LOADING || apiStore.connectionMode == 'offline'
|
||||
"
|
||||
:placeholder="$t('journal-driver-search-placeholder')"
|
||||
/>
|
||||
|
||||
<input
|
||||
v-model="globalStore.journalTimetableSearch.route"
|
||||
type="text"
|
||||
@keydown.enter="fetchJournalTimetables"
|
||||
:class="`bg-zinc-800 p-1 rounded-md print:hidden w-full ${
|
||||
apiStore.connectionMode == 'offline' ? 'opacity-35' : ''
|
||||
}`"
|
||||
:disabled="
|
||||
apiStore.journalDataStatus == DataStatus.LOADING || apiStore.connectionMode == 'offline'
|
||||
"
|
||||
:placeholder="$t('journal-route-search-placeholder')"
|
||||
/>
|
||||
|
||||
<input
|
||||
v-model="globalStore.journalTimetableSearch.date"
|
||||
type="date"
|
||||
@keydown.enter="fetchJournalTimetables"
|
||||
:class="`bg-zinc-800 p-1 rounded-md print:hidden w-full ${
|
||||
apiStore.connectionMode == 'offline' ? 'opacity-35' : ''
|
||||
}`"
|
||||
:disabled="
|
||||
apiStore.journalDataStatus == DataStatus.LOADING || apiStore.connectionMode == 'offline'
|
||||
"
|
||||
:placeholder="$t('journal-date-search-placeholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="bg-zinc-800 hover:bg-zinc-700 p-1 rounded-md"
|
||||
v-if="globalStore.viewMode == 'journal'"
|
||||
@click="clearSearch"
|
||||
>
|
||||
<Trash2Icon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="bg-zinc-800 hover:bg-zinc-700 p-1 rounded-md"
|
||||
v-if="globalStore.viewMode == 'journal'"
|
||||
@click="fetchJournalTimetables"
|
||||
>
|
||||
<SearchIcon />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useApiStore } from '../../stores/api.store';
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
import { DataStatus, type JournalTimetablesShortResponse } from '../../types/api.types';
|
||||
import { SearchIcon, Trash2Icon } from 'lucide-vue-next';
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
const apiStore = useApiStore();
|
||||
|
||||
async function fetchJournalTimetables() {
|
||||
const searchValues = globalStore.journalTimetableSearch;
|
||||
|
||||
let fetchParams: Record<string, any> = {};
|
||||
|
||||
if (searchValues['driverName']) {
|
||||
fetchParams['driverName'] = searchValues['driverName'].trim();
|
||||
}
|
||||
|
||||
if (searchValues['date']) {
|
||||
let dateFromStr = new Date(searchValues['date']).toISOString();
|
||||
|
||||
let dateTo = new Date(dateFromStr);
|
||||
dateTo.setDate(dateTo.getDate() + 1);
|
||||
|
||||
fetchParams['dateFrom'] = dateFromStr;
|
||||
fetchParams['dateTo'] = dateTo.toISOString();
|
||||
}
|
||||
|
||||
if (searchValues['route']) {
|
||||
const [routeFrom, routeTo] = searchValues['route'].split('-');
|
||||
|
||||
if (routeFrom) {
|
||||
fetchParams['issuedFrom'] = routeFrom.trim();
|
||||
}
|
||||
|
||||
if (routeTo) {
|
||||
fetchParams['terminatingAt'] = routeTo.trim();
|
||||
}
|
||||
}
|
||||
|
||||
fetchParams['hasStopsDetails'] = 1;
|
||||
fetchParams['returnType'] = 'short';
|
||||
|
||||
try {
|
||||
apiStore.journalDataStatus = DataStatus.LOADING;
|
||||
|
||||
const response = await apiStore.client.get<JournalTimetablesShortResponse>(
|
||||
'api/getTimetables',
|
||||
fetchParams
|
||||
);
|
||||
|
||||
apiStore.journalDataStatus = DataStatus.SUCCESS;
|
||||
apiStore.journalTimetablesData = response;
|
||||
} catch (error) {
|
||||
apiStore.journalDataStatus = DataStatus.ERROR;
|
||||
apiStore.journalTimetablesData = null;
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
Object.keys(globalStore.journalTimetableSearch).forEach(
|
||||
(k) => ((globalStore.journalTimetableSearch as any)[k] = '')
|
||||
);
|
||||
|
||||
apiStore.journalTimetablesData = null;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="flex gap-2" v-if="globalStore.currentTimetableData == null">
|
||||
<div class="w-full">
|
||||
<input
|
||||
v-model="globalStore.localTimetableSearch"
|
||||
type="text"
|
||||
class="bg-zinc-800 p-1 rounded-md print:hidden w-full"
|
||||
:placeholder="$t('train-search-placeholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
class="bg-zinc-800 hover:bg-zinc-700 p-1 rounded-md"
|
||||
v-if="globalStore.viewMode == 'storage'"
|
||||
@click="clearSearch"
|
||||
>
|
||||
<Trash2Icon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Trash2Icon } from 'lucide-vue-next';
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
function clearSearch() {
|
||||
globalStore.localTimetableSearch = '';
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="flex gap-2 flex-col print:hidden">
|
||||
<!-- Top Actions & Modes -->
|
||||
<SearchModeActions />
|
||||
|
||||
<!-- Active Data Search -->
|
||||
<ActiveSearchInput v-if="globalStore.viewMode == 'active'" />
|
||||
|
||||
<!-- Local Storage Search -->
|
||||
<LocalSearchInput v-else-if="globalStore.viewMode == 'storage'" />
|
||||
|
||||
<!-- Journal Serach -->
|
||||
<JournalSearchInput v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
|
||||
import ActiveSearchInput from './ActiveSearchInput.vue';
|
||||
import JournalSearchInput from './JournalSearchInput.vue';
|
||||
import LocalSearchInput from './LocalSearchInput.vue';
|
||||
import SearchModeActions from './SearchModeActions.vue';
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
</script>
|
||||
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<div class="flex justify-between gap-2">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
:class="`p-1 rounded-md ${
|
||||
globalStore.viewMode == 'active'
|
||||
? 'bg-green-600 hover:bg-green-500'
|
||||
: 'bg-zinc-800 hover:bg-zinc-700'
|
||||
}`"
|
||||
@click="toggleViewMode('active')"
|
||||
aria-label="Active data view mode"
|
||||
>
|
||||
<WifiIcon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
:class="`p-1 rounded-md ${
|
||||
globalStore.viewMode == 'storage'
|
||||
? 'bg-green-600 hover:bg-green-500'
|
||||
: 'bg-zinc-800 hover:bg-zinc-700'
|
||||
}`"
|
||||
@click="toggleViewMode('storage')"
|
||||
aria-label="Storage view mode"
|
||||
>
|
||||
<ArchiveIcon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
:class="`p-1 rounded-md ${
|
||||
globalStore.viewMode == 'journal'
|
||||
? 'bg-green-600 hover:bg-green-500'
|
||||
: 'bg-zinc-800 hover:bg-zinc-700'
|
||||
}`"
|
||||
@click="toggleViewMode('journal')"
|
||||
aria-label="Journal view mode"
|
||||
>
|
||||
<HistoryIcon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700"
|
||||
@click="toggleDarkMode"
|
||||
aria-label="Dark mode toggle"
|
||||
>
|
||||
<MoonIcon v-if="globalStore.darkMode" />
|
||||
<SunIcon v-else />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700 disabled:opacity-60 disabled:hover:bg-zinc-800"
|
||||
@click="toggleFullscreenMode()"
|
||||
:disabled="globalStore.currentTimetableData == null"
|
||||
aria-label="Full screen toggle"
|
||||
>
|
||||
<FullscreenIcon :size="24" />
|
||||
</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"
|
||||
aria-label="Print mode"
|
||||
>
|
||||
<PrinterIcon />
|
||||
</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"
|
||||
aria-label="Save timetable to storage"
|
||||
>
|
||||
<FolderDownIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch } from 'vue';
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
import type { ViewMode, TimetableData } from '../../types/common.types';
|
||||
import {
|
||||
ArchiveIcon,
|
||||
FolderDownIcon,
|
||||
FullscreenIcon,
|
||||
HistoryIcon,
|
||||
MoonIcon,
|
||||
PrinterIcon,
|
||||
SunIcon,
|
||||
WifiIcon
|
||||
} from 'lucide-vue-next';
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
// Computed
|
||||
const isTimetableSaved = computed(() => {
|
||||
if (!globalStore.currentTimetableData) return false;
|
||||
|
||||
return Object.keys(globalStore.storageTimetables).includes(
|
||||
`${globalStore.currentTimetableData.timetableId}`
|
||||
);
|
||||
});
|
||||
|
||||
// Watchers
|
||||
watch(
|
||||
() => globalStore.selectedActiveTrain,
|
||||
(curr) => {
|
||||
if (curr != null) {
|
||||
globalStore.generatedDate = new Date();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Methods
|
||||
function toggleViewMode(viewMode: ViewMode) {
|
||||
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 toggleFullscreenMode() {
|
||||
globalStore.fullscreenMode = !globalStore.fullscreenMode;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="apiStore.connectionMode == 'online'">{{ $t('train-select-info') }}</div>
|
||||
<div v-else class="bg-red-500 text-white p-2">{{ $t('data-offline-mode') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useApiStore } from '../../stores/api.store';
|
||||
|
||||
const apiStore = useApiStore();
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div
|
||||
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 relative"
|
||||
:class="{ dark: globalStore.darkMode }"
|
||||
>
|
||||
<!-- <button
|
||||
v-if="globalStore.fullscreenMode"
|
||||
class="fixed right-0 top-0 bg-green-600 p-1 m-1 rounded-md hover:bg-green-500 print:hidden"
|
||||
@click="() => (globalStore.fullscreenMode = false)"
|
||||
>
|
||||
<Minimize2Icon :size="22" />
|
||||
</button> -->
|
||||
|
||||
<TimetableContent />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
import TimetableContent from '../Timetable/TimetableContent.vue';
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
</script>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div class="text-white">
|
||||
<h2
|
||||
class="font-bold p-2 bg-zinc-700 mb-3 text-2xl flex items-center gap-2 justify-center flex-wrap"
|
||||
>
|
||||
<HistoryIcon :size="25" />
|
||||
{{ $t('journal-preview-title') }}
|
||||
</h2>
|
||||
|
||||
<div v-if="apiStore.connectionMode == 'offline'" class="bg-red-500 p-2">
|
||||
{{ $t('data-offline-mode') }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="apiStore.journalDataStatus == DataStatus.LOADING" class="bg-zinc-900 p-2">
|
||||
{{ $t('data-loading-text') }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="apiStore.journalDataStatus == DataStatus.ERROR" class="bg-red-500 p-2">
|
||||
{{ $t('data-loading-error-text') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="!apiStore.journalTimetablesData"
|
||||
class="text-zinc-400 mt-2"
|
||||
v-html="$t('journal-empty-info')"
|
||||
></div>
|
||||
|
||||
<div v-else-if="apiStore.journalTimetablesData.length == 0">
|
||||
<p class="text-zinc-300 mb-2">
|
||||
{{ $t('journal-no-data') }}
|
||||
</p>
|
||||
|
||||
<b class="text-red-300">
|
||||
{{ $t('journal-reminder-text') }}
|
||||
</b>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<ul>
|
||||
<li
|
||||
v-for="timetable in apiStore.journalTimetablesData"
|
||||
class="flex gap-1 w-full my-2"
|
||||
@click="fetchTimetableDetails(timetable.id)"
|
||||
>
|
||||
<button class="bg-zinc-900 p-2 w-full cursor-pointer hover:bg-zinc-800 text-left">
|
||||
<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>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-if="apiStore.journalTimetablesData.length > 0">
|
||||
<hr class="border-t-2 border-t-gray-500" />
|
||||
|
||||
<p class="text-zinc-400 text-sm">
|
||||
{{ $t('journal-footer-text') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HistoryIcon } from 'lucide-vue-next';
|
||||
import { useApiStore } from '../../stores/api.store';
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
import { DataStatus } from '../../types/api.types';
|
||||
import type { JournalTimetableDetailed } from '../../types/common.types';
|
||||
|
||||
const apiStore = useApiStore();
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
async function fetchTimetableDetails(id: number) {
|
||||
try {
|
||||
const response = await apiStore.client.get<JournalTimetableDetailed[]>('api/getTimetables', {
|
||||
timetableId: id,
|
||||
returnType: 'detailed'
|
||||
});
|
||||
|
||||
if (response.length > 0) globalStore.selectedJournalTimetable = response[0];
|
||||
} catch (error) {
|
||||
globalStore.selectedJournalTimetable = null;
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="text-white">
|
||||
<div class="font-bold p-2 bg-zinc-700 mb-3">
|
||||
<div class="text-2xl flex items-center gap-2 justify-center flex-wrap">
|
||||
<ArchiveIcon :size="25" />
|
||||
<span>{{ $t('storage-preview-title') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
globalStore.selectedStorageTimetable == null &&
|
||||
Object.keys(globalStore.storageTimetables).length == 0
|
||||
"
|
||||
class="text-zinc-400"
|
||||
>
|
||||
{{ $t('storage-empty-info') }}
|
||||
</div>
|
||||
|
||||
<div class="font-bold p-2 bg-zinc-800 mb-3" v-else-if="filteredTimetables.length == 0">
|
||||
{{ $t('storage-preview-empty') }}
|
||||
</div>
|
||||
|
||||
<ul v-else>
|
||||
<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 flex gap-x-2 items-center flex-wrap">
|
||||
<span>#{{ timetable.timetableId }}</span>
|
||||
|
||||
<i class="flex items-center gap-1"><ArchiveIcon :size="18" :stroke-width="3" /> {{ new Date(timetable.savedTimestamp!).toLocaleString() }}</i>
|
||||
|
||||
<i
|
||||
v-if="timetable.journalCreatedAt"
|
||||
class="flex items-center gap-0.5"
|
||||
:title="
|
||||
$t('storage-journal-timetable-placeholder', {
|
||||
date: new Date(timetable.journalCreatedAt).toLocaleDateString('pl-PL')
|
||||
})
|
||||
"
|
||||
>
|
||||
<HistoryIcon :size="18" :stroke-width="3" />
|
||||
{{ new Date(timetable.journalCreatedAt).toLocaleDateString('pl-PL') }}
|
||||
</i>
|
||||
</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)"
|
||||
>
|
||||
<Trash2Icon />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useGlobalStore } from '../../stores/global.store';
|
||||
import type { TimetableData } from '../../types/common.types';
|
||||
import { ArchiveIcon, HistoryIcon, Trash2Icon } from 'lucide-vue-next';
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
const i18n = useI18n();
|
||||
|
||||
const filteredTimetables = computed(() => {
|
||||
let timetables = Object.values(globalStore.storageTimetables);
|
||||
|
||||
if (globalStore.localTimetableSearch.length != 0)
|
||||
timetables = timetables.filter((st) =>
|
||||
`${st.timetableId} ${st.driverName} ${st.route} ${st.category} ${st.trainNo}`
|
||||
.toLocaleLowerCase()
|
||||
.includes(globalStore.localTimetableSearch.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) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
export class HttpClient {
|
||||
constructor(private readonly baseURL: string) {}
|
||||
|
||||
async get<T>(url: string, params?: Record<string, any>): Promise<T> {
|
||||
const absoluteURL = new URL(this.baseURL + '/' + url);
|
||||
|
||||
if (params) {
|
||||
Object.keys(params).forEach((key) => {
|
||||
if (params[key] === undefined) return;
|
||||
|
||||
absoluteURL.searchParams.append(key, params[key]);
|
||||
});
|
||||
}
|
||||
|
||||
const data = await fetch(absoluteURL);
|
||||
|
||||
if (!data.ok) {
|
||||
throw new Error(`Cannot fetch ${absoluteURL}: ${data.status}`);
|
||||
}
|
||||
|
||||
return data.json();
|
||||
}
|
||||
}
|
||||
@@ -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,53 @@
|
||||
{
|
||||
"data-loading-text": "Loading data...",
|
||||
"data-loading-error-text": "Oops! An error occurent while loading data from the server!",
|
||||
"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)",
|
||||
|
||||
"update-prompt": {
|
||||
"line1": "New version of SRJP is available!",
|
||||
"line2": "Click here to update the app!"
|
||||
},
|
||||
|
||||
"data-offline-mode": "You're currently using the offline mode of the SRJP app - server data is unavailable!",
|
||||
|
||||
"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"
|
||||
},
|
||||
|
||||
"migrate-info": {
|
||||
"line-1": "Rozkładownik is being moved to a new domain - {0}! You can still use the current website, but it will no longer be updated and will be shut down in the nearest future!",
|
||||
"accept-btn": "ROGER THAT!"
|
||||
},
|
||||
|
||||
"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",
|
||||
"storage-journal-timetable-placeholder": "Saved historical timetable from day {date}",
|
||||
"delete-timetable-confirm": "Are you sure that you want to delete this timetable?",
|
||||
|
||||
"journal-preview-title": "TIMETABLES JOURNAL",
|
||||
"journal-empty-info": "Enter timetable details in the text fields above (use at least one field).<br>Up to 15 newest timetables will be shown.",
|
||||
"journal-driver-search-placeholder": "Driver",
|
||||
"journal-date-search-placeholder": "Date",
|
||||
"journal-route-search-placeholder": "Route",
|
||||
"journal-preview-info": "Historical timetable {id} for user {driverName} from: {date}",
|
||||
|
||||
"journal-no-data": "No data for the current search! Check if the data you entered is correct.",
|
||||
"journal-reminder-text": "Warning: detailed timetables data for SRJP purpose are collected since 1st February 2025 and only for users who support Stacjownik project!",
|
||||
"journal-footer-text": "Detailed timetables data for SRJP purpose are collected since 1st February 2025 and only for users who support Stacjownik project!"
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"data-loading-text": "Ładowanie danych...",
|
||||
"data-loading-error-text": "Ups! Wystąpił błąd podczas pobierania danych z serwera!",
|
||||
"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)",
|
||||
|
||||
"update-prompt": {
|
||||
"line1": "Nowa wersja SRJP jest dostępna!",
|
||||
"line2": "Kliknij, aby zaktualizować aplikację!"
|
||||
},
|
||||
|
||||
"data-offline-mode": "Korzystasz z trybu offline aplikacji SRJP - dane serwerowe są niedostępne!",
|
||||
|
||||
"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"
|
||||
},
|
||||
|
||||
"migrate-info": {
|
||||
"line-1": "Rozkładownik zostaje przeniesiony na nową domenę - {0}! Możesz korzystać z obecnej strony, jednak nie będzie ona otrzymywać już aktualizacji i w przyszłości zostanie wyłączona!",
|
||||
"accept-btn": "PRZYJĄŁEM!"
|
||||
},
|
||||
|
||||
"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óć",
|
||||
"storage-journal-timetable-placeholder": "Zapisany historyczny rozkład jazdy z dnia {date}",
|
||||
"delete-timetable-confirm": "Czy na pewno chcesz usunąć ten rozkład jazdy z archiwum?",
|
||||
|
||||
"journal-preview-title": "DZIENNIK ROZKŁADÓW JAZDY",
|
||||
"journal-empty-info": "Wpisz dane rozkładu korzystając z pól tekstowych powyżej (co najmniej jednego).<br>W przypadku wielu rozkładów jazdy wyświetli się maks. 15 najnowszych.",
|
||||
"journal-driver-search-placeholder": "Maszynista",
|
||||
"journal-date-search-placeholder": "Data",
|
||||
"journal-route-search-placeholder": "Relacja",
|
||||
"journal-preview-info": "Rozkład historyczny {id} maszynisty {driverName} z dnia {date}",
|
||||
|
||||
"journal-no-data": "Brak wyników dla obecnego wyszukiwania! Sprawdź czy wpisałeś poprawnie dane.",
|
||||
"journal-reminder-text": "Uwaga: szczegółowe rozkłady jazdy są zapisywane od 1 lutego 2025r. wyłącznie dla osób wspierających projekt Stacjownika!",
|
||||
"journal-footer-text": "Szczegółowe dane o rozkładach jazdy do wygenerowania SRJP są zbierane od 1 lutego 2025r. wyłącznie dla maszynistów wspierających projekt Stacjownika!"
|
||||
}
|
||||
@@ -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,27 @@
|
||||
import { useApiStore } from '../stores/api.store';
|
||||
|
||||
export function useVehicleMixin() {
|
||||
const apiStore = useApiStore();
|
||||
|
||||
/**
|
||||
* Gets loco load (obc. lok.) in tons - effectively train mass without locomotive or lone locomotive / unit mass
|
||||
*/
|
||||
function getLocoLoad(trainMass: number, stockString: string) {
|
||||
if (!apiStore.vehiclesData) return trainMass;
|
||||
|
||||
const stockArray = stockString.split(';');
|
||||
const headUnitsNames = stockArray.slice(0, 3).filter((v) => /-\d{3,}$/.test(v));
|
||||
|
||||
const headVehicleData = apiStore.vehiclesData.find((v) => v.name == headUnitsNames[0]);
|
||||
|
||||
if (!headVehicleData) return trainMass;
|
||||
|
||||
// 0t load for loco only
|
||||
if (headVehicleData.type.startsWith("loco") && stockArray.length == 1) return 0;
|
||||
else if (headVehicleData.type.startsWith("unit")) return trainMass;
|
||||
|
||||
return Math.min(trainMass, trainMass - headVehicleData.group.weight);
|
||||
}
|
||||
|
||||
return { getLocoLoad };
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import {
|
||||
DataStatus,
|
||||
type ActiveDataResponse,
|
||||
type SceneriesDataResponse,
|
||||
type VehiclesDataResponse
|
||||
} from '../types/api.types';
|
||||
import type {
|
||||
ActiveData,
|
||||
JournalTimetableShort,
|
||||
SceneryData,
|
||||
VehicleData
|
||||
} from '../types/common.types';
|
||||
import { HttpClient } from '../http';
|
||||
|
||||
let activeDataInterval = -1;
|
||||
|
||||
// Base API URL
|
||||
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;
|
||||
}
|
||||
|
||||
export const useApiStore = defineStore('api', {
|
||||
state() {
|
||||
return {
|
||||
client: new HttpClient(baseURL),
|
||||
|
||||
activeData: null as ActiveData | null,
|
||||
sceneryData: null as SceneryData[] | null,
|
||||
vehiclesData: null as VehicleData[] | null,
|
||||
journalTimetablesData: null as JournalTimetableShort[] | null,
|
||||
|
||||
outdatedTimerId: -1,
|
||||
isActiveDataOutdated: false,
|
||||
|
||||
apiDataStatus: DataStatus.LOADING,
|
||||
journalDataStatus: DataStatus.SUCCESS,
|
||||
|
||||
connectionMode: 'online' as 'online' | 'offline'
|
||||
};
|
||||
},
|
||||
|
||||
actions: {
|
||||
async setupAPIData() {
|
||||
clearInterval(activeDataInterval);
|
||||
|
||||
try {
|
||||
this.apiDataStatus = DataStatus.LOADING;
|
||||
|
||||
await Promise.all([
|
||||
this.fetchSceneriesData(),
|
||||
this.fetchVehiclesData(),
|
||||
this.fetchActiveData()
|
||||
]);
|
||||
|
||||
this.apiDataStatus = DataStatus.SUCCESS;
|
||||
} catch (error) {
|
||||
this.apiDataStatus = DataStatus.ERROR;
|
||||
console.log('Data fetching error: ', error);
|
||||
}
|
||||
|
||||
activeDataInterval = setInterval(() => {
|
||||
this.fetchActiveData();
|
||||
}, 25000);
|
||||
},
|
||||
|
||||
async fetchActiveData() {
|
||||
try {
|
||||
const response = await this.client.get<ActiveDataResponse>('api/getActiveData');
|
||||
|
||||
this.activeData = response;
|
||||
this.isActiveDataOutdated = false;
|
||||
|
||||
if (this.outdatedTimerId != -1) clearTimeout(this.outdatedTimerId);
|
||||
|
||||
this.outdatedTimerId = setTimeout(() => {
|
||||
this.isActiveDataOutdated = true;
|
||||
}, 60000);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchSceneriesData() {
|
||||
try {
|
||||
const response = await this.client.get<SceneriesDataResponse>('api/getSceneries');
|
||||
|
||||
this.sceneryData = response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchVehiclesData() {
|
||||
try {
|
||||
const response = await this.client.get<VehiclesDataResponse>('api/getVehiclesData');
|
||||
|
||||
this.vehiclesData = response.vehicles.map((v) => ({
|
||||
...v,
|
||||
group: response.vehicleGroups.find((g) => g.id == v.vehicleGroupsId)!
|
||||
}));
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,42 +1,119 @@
|
||||
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,
|
||||
JournalTimetableDetailed,
|
||||
TimetableData,
|
||||
ViewMode
|
||||
} from '../types/common.types';
|
||||
import { getHeadUnits } from '../utils/trainUtils';
|
||||
import { useVehicleMixin } from '../mixins/useVehicleMixin';
|
||||
|
||||
export const useGlobalStore = defineStore('global', {
|
||||
state: () => ({
|
||||
activeData: null as ActiveData | null,
|
||||
sceneryData: null as SceneryData[] | null,
|
||||
darkMode: false,
|
||||
fullscreenMode: false,
|
||||
viewMode: 'active' as ViewMode,
|
||||
|
||||
selectedTrainId: null as string | null,
|
||||
selectedActiveTrain: null as ActiveTrain | null,
|
||||
selectedStorageTimetable: null as TimetableData | null,
|
||||
selectedJournalTimetable: null as JournalTimetableDetailed | null,
|
||||
|
||||
storageTimetables: {} as Record<number, TimetableData>,
|
||||
|
||||
timetableWarnings: [] as string[],
|
||||
|
||||
generatedDate: null as Date | null,
|
||||
generatedMs: 0,
|
||||
|
||||
localTimetableSearch: '',
|
||||
|
||||
journalTimetableSearch: {
|
||||
driverName: '',
|
||||
date: '',
|
||||
route: ''
|
||||
},
|
||||
|
||||
showSettings: false,
|
||||
isMigrationInfoOpen: 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'));
|
||||
},
|
||||
|
||||
currentTimetableData(): TimetableData | null {
|
||||
const vehicleUtils = useVehicleMixin();
|
||||
|
||||
if (this.viewMode == 'active') {
|
||||
const selectedTrain = this.selectedActiveTrain;
|
||||
|
||||
if (!selectedTrain || !selectedTrain.timetable) return null;
|
||||
|
||||
return {
|
||||
trainNo: selectedTrain.trainNo,
|
||||
mass: vehicleUtils.getLocoLoad(selectedTrain.mass, selectedTrain.stockString),
|
||||
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: getHeadUnits(selectedTrain.stockString)
|
||||
};
|
||||
} else if (this.viewMode == 'journal') {
|
||||
const selectedTimetable = this.selectedJournalTimetable;
|
||||
|
||||
if (!selectedTimetable || !selectedTimetable.stopListString) return null;
|
||||
|
||||
return {
|
||||
journalCreatedAt: new Date(selectedTimetable.createdAt).getTime(),
|
||||
trainNo: selectedTimetable.trainNo,
|
||||
mass: vehicleUtils.getLocoLoad(
|
||||
selectedTimetable.stockMass,
|
||||
selectedTimetable.stockString
|
||||
),
|
||||
length: selectedTimetable.stockLength,
|
||||
driverId: selectedTimetable.driverId,
|
||||
driverName: selectedTimetable.driverName,
|
||||
category: selectedTimetable.trainCategoryCode,
|
||||
hasDangerousCargo: selectedTimetable.hasDangerousCargo,
|
||||
hasExtraDeliveries: selectedTimetable.hasExtraDeliveries,
|
||||
warningNotes: selectedTimetable.warningNotes,
|
||||
path: selectedTimetable.path,
|
||||
route: selectedTimetable.route,
|
||||
trainMaxSpeed: selectedTimetable.trainMaxSpeed,
|
||||
timetableId: selectedTimetable.id,
|
||||
stopListString: selectedTimetable.stopListString,
|
||||
headUnits: getHeadUnits(selectedTimetable.stockString)
|
||||
};
|
||||
} else {
|
||||
return this.selectedStorageTimetable;
|
||||
}
|
||||
},
|
||||
|
||||
async _fetchSceneriesData() {
|
||||
try {
|
||||
const response = (await httpClient.get<SceneriesDataResponse>('/api/getSceneries')).data;
|
||||
|
||||
this.sceneryData = response;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
|
||||
setupData() {
|
||||
this._fetchActiveData();
|
||||
this._fetchSceneriesData();
|
||||
|
||||
setInterval(() => {
|
||||
this._fetchActiveData();
|
||||
}, 20000);
|
||||
},
|
||||
}
|
||||
},
|
||||
actions: {}
|
||||
});
|
||||
|
||||
@@ -2,25 +2,98 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.slide-anim-enter-active,
|
||||
.slide-anim-leave-active {
|
||||
transition: all 250ms ease-in-out;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.slide-anim-enter-from,
|
||||
.slide-anim-leave-to {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,57 @@
|
||||
import type { ActiveData, SceneryData } from './common.types';
|
||||
import type {
|
||||
ActiveData,
|
||||
JournalTimetableShort,
|
||||
SceneryData,
|
||||
VehicleGroup,
|
||||
VehicleRestrictions
|
||||
} from './common.types';
|
||||
|
||||
/***
|
||||
* API Data Status
|
||||
* */
|
||||
|
||||
|
||||
export enum DataStatus {
|
||||
'INIT' = -1,
|
||||
'LOADING' = 0,
|
||||
'SUCCESS' = 1,
|
||||
'ERROR' = 2
|
||||
}
|
||||
|
||||
/***
|
||||
* Active Data API
|
||||
* */
|
||||
|
||||
export type ActiveDataResponse = ActiveData;
|
||||
|
||||
/***
|
||||
* Sceneries API
|
||||
* */
|
||||
|
||||
export type SceneriesDataResponse = SceneryData[];
|
||||
|
||||
/***
|
||||
* Journal API
|
||||
* */
|
||||
|
||||
export type JournalTimetablesShortResponse = JournalTimetableShort[];
|
||||
|
||||
/***
|
||||
* Vehicles API
|
||||
* */
|
||||
|
||||
export interface VehiclesDataResponse {
|
||||
vehicles: VehicleDataAPI[];
|
||||
vehicleGroups: VehicleGroupAPI[];
|
||||
}
|
||||
|
||||
export interface VehicleDataAPI {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
cabinName?: string;
|
||||
restrictions?: VehicleRestrictions;
|
||||
vehicleGroupsId: number;
|
||||
}
|
||||
|
||||
export interface VehicleGroupAPI extends VehicleGroup {}
|
||||
|
||||
@@ -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 {
|
||||
@@ -116,7 +119,217 @@ export interface SceneryRoute {
|
||||
isInternal: boolean;
|
||||
isRouteSBL: boolean;
|
||||
routeSpeed: number;
|
||||
routeSpeedExit?: number;
|
||||
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;
|
||||
arrivalLineNumber: string;
|
||||
departureLineNumber: string;
|
||||
driveTime: number;
|
||||
abbrevs: string[];
|
||||
sceneryName: string;
|
||||
arrivalKm: string;
|
||||
arrivalSpeedL: number;
|
||||
arrivalSpeedP: number;
|
||||
arrivalTracks: number;
|
||||
departureKm: string;
|
||||
departureSpeedL: number;
|
||||
departureSpeedP: number;
|
||||
departureTracks: number;
|
||||
headUnits: string[];
|
||||
stockVmax: number;
|
||||
stockLength: number;
|
||||
stockMass: number;
|
||||
|
||||
lastRowRef: StopRow | null;
|
||||
}
|
||||
|
||||
export interface StopRowCZ {
|
||||
pointName: string;
|
||||
pointKm: string;
|
||||
isMain: boolean;
|
||||
stopTime: number;
|
||||
stopType: string;
|
||||
scheduledArrivalDate: Date | null;
|
||||
scheduledDepartureDate: Date | null;
|
||||
driveTime: number;
|
||||
sceneryName: string;
|
||||
arrivalSpeed: number;
|
||||
departureSpeed: number;
|
||||
arrivalTracks: number;
|
||||
departureTracks: number;
|
||||
headUnits: string[];
|
||||
stockVmax: number;
|
||||
stockLength: number;
|
||||
stockMass: number;
|
||||
|
||||
arrivalDateStr: string;
|
||||
departureDateStr: string;
|
||||
}
|
||||
|
||||
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[];
|
||||
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;
|
||||
journalCreatedAt?: number;
|
||||
}
|
||||
|
||||
export interface VehicleData {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
group: VehicleGroup;
|
||||
cabinName?: string;
|
||||
restrictions?: VehicleRestrictions;
|
||||
}
|
||||
|
||||
export interface VehicleRestrictions {
|
||||
sponsorOnly?: number;
|
||||
teamOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface VehicleGroup {
|
||||
id: number;
|
||||
name: string;
|
||||
speed: number;
|
||||
speedLoaded?: number;
|
||||
speedLoco?: number;
|
||||
length: number;
|
||||
weight: number;
|
||||
cargoTypes?: VehicleCargoType[];
|
||||
locoProps?: VehicleLocoProps;
|
||||
massSpeeds?: VehicleMassSpeeds;
|
||||
}
|
||||
|
||||
export interface VehicleCargoType {
|
||||
id: string;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
export interface VehicleLocoProps {
|
||||
coldStart: boolean;
|
||||
doubleManned: boolean;
|
||||
}
|
||||
|
||||
export interface VehicleMassSpeeds {
|
||||
none: number;
|
||||
cargo?: Record<string, number>;
|
||||
passenger?: Record<string, number>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
const romanMonthDigits = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII'];
|
||||
|
||||
export function parseTimetableRunDate(date: Date) {
|
||||
return `${date.getDate()}.${romanMonthDigits[date.getMonth()]}.${date
|
||||
.getFullYear()
|
||||
.toString()
|
||||
.slice(2)}`;
|
||||
}
|
||||
@@ -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,45 @@
|
||||
const unitNameCorrections: Record<string, string[]> = {
|
||||
'2EN57': ['EN57', 'EN57'],
|
||||
'201E': ['ET22'],
|
||||
'4E': ['EU07'],
|
||||
M62: ['ST44'],
|
||||
CTLR4C: ['ST44']
|
||||
};
|
||||
|
||||
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 function getHeadUnits(stockString: string) {
|
||||
const stockList = stockString.split(';').slice(0, 3);
|
||||
|
||||
return stockList.reduce((acc, unitType, i) => {
|
||||
if (i != 0 && !/-\d{3,}$/.test(unitType)) return acc;
|
||||
|
||||
const unitName = unitType.slice(0, unitType.indexOf('-'));
|
||||
|
||||
const correctedNames = unitNameCorrections[unitName] ?? [unitName];
|
||||
|
||||
acc.push(...correctedNames);
|
||||
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
}
|
||||
@@ -4,7 +4,8 @@ export default {
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
darkMode: 'selector',
|
||||
plugins: [],
|
||||
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
|
||||
"types": ["vite/client", "vite-plugin-pwa/client"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"root":["./src/http.ts","./src/i18n.ts","./src/main.ts","./src/vite-env.d.ts","./src/mixins/useVehicleMixin.ts","./src/stores/api.store.ts","./src/stores/global.store.ts","./src/types/api.types.ts","./src/types/common.types.ts","./src/utils/dateUtils.ts","./src/utils/trainUtils.ts","./src/App.vue","./src/components/App/MainBottom.vue","./src/components/App/MainContainer.vue","./src/components/App/MigrateInfo.vue","./src/components/App/Navbar.vue","./src/components/App/SettingsCard.vue","./src/components/App/UpdatePrompt.vue","./src/components/Timetable/TimetableContainer.vue","./src/components/Timetable/TimetableContent.vue","./src/components/Timetable/TimetableContentCZ.vue","./src/components/Timetable/TimetableWarnings.vue","./src/components/TimetableSearch/ActiveSearchInput.vue","./src/components/TimetableSearch/JournalSearchInput.vue","./src/components/TimetableSearch/LocalSearchInput.vue","./src/components/TimetableSearch/SearchContainer.vue","./src/components/TimetableSearch/SearchModeActions.vue","./src/components/TimetableViews/ActiveDataView.vue","./src/components/TimetableViews/CurrentTimetableView.vue","./src/components/TimetableViews/JournalStorageView.vue","./src/components/TimetableViews/LocalStorageView.vue"],"version":"5.9.3"}
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
@@ -17,8 +16,7 @@
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"root":["./vite.config.ts"],"version":"5.9.3"}
|
||||
@@ -1,7 +1,35 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
})
|
||||
plugins: [
|
||||
vue(),
|
||||
VitePWA({
|
||||
registerType: 'prompt',
|
||||
workbox: {
|
||||
disableDevLogs: true,
|
||||
globPatterns: ['**/*.{js,css,html,png,svg,jpg,ico}'],
|
||||
cleanupOutdatedCaches: true,
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: /^https:\/\/stacjownik.spythere.eu\/api\/(getSceneries|getVehiclesData)/i,
|
||||
handler: 'StaleWhileRevalidate',
|
||||
options: {
|
||||
expiration: {
|
||||
maxAgeSeconds: 3600
|
||||
},
|
||||
cacheName: 'stacjownik-api-cache',
|
||||
cacheableResponse: { statuses: [0, 200] }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
devOptions: { enabled: false, suppressWarnings: true }
|
||||
})
|
||||
],
|
||||
server: {
|
||||
port: 5345
|
||||
}
|
||||
});
|
||||
|
||||