60 Commits

Author SHA1 Message Date
Spythere 3713289339 chore: restored location autofill 2025-10-12 00:25:15 +02:00
Spythere cafdbb9653 chore: changed panel navbar style 2025-10-12 00:18:39 +02:00
Spythere 7934a83cfd chore: added alert about removal of deprecated orders 2025-10-11 23:42:27 +02:00
Spythere 33f59db5f3 chore: changed to new order's version in local storage 2025-10-11 00:39:29 +02:00
Spythere 201c49845f chore: added instruction conflicts checking 2025-10-11 00:26:53 +02:00
Spythere a1d0c47910 fix: header colspan 2025-10-10 15:14:14 +02:00
Spythere fb82ac9fb2 chore: completed english order translation; added footer locale placeholders 2025-10-10 15:07:39 +02:00
Spythere 76a31c345d chore: updated plural messages based on the input fields 2025-10-09 14:09:32 +02:00
Spythere 4181119bd2 chore: added optional fields support in messages 2025-10-08 14:42:54 +02:00
Spythere 53029d37a5 fix: message format in simulator's chat 2025-10-07 14:12:50 +02:00
Spythere f5747db62d chore: added new ID format; restored options of incrementing ID o save/copy 2025-10-07 14:00:17 +02:00
Spythere 35bc44b969 refactor: moved order utils 2025-10-06 14:35:42 +02:00
Spythere 1085170004 fix: accessibility issues with form ids 2025-10-06 14:32:02 +02:00
Spythere d762d42344 chore: removed deprecated types and files 2025-10-06 14:29:06 +02:00
Spythere 31c241b3b7 chore: added helper placeholders to header & footer fields 2025-10-06 14:22:31 +02:00
Spythere e4a5c7babb refactor: fonts optimization 2025-10-06 01:28:48 +02:00
Spythere b4c673ea51 chore: improved responsiveness 2025-10-03 14:11:35 +02:00
Spythere e53cf6faba chore: added yarn.lock to gitignore 2025-10-03 01:09:49 +02:00
Spythere 3a3ba61454 refactor: components restructure; removed deprecated files 2025-10-03 00:58:07 +02:00
Spythere 6dcc96a677 chore: order date format 2025-10-02 16:30:39 +02:00
Spythere 897091468c chore: en locale update 2025-10-02 01:52:45 +02:00
Spythere 0160a27bf4 fix: dark mode checkmarks 2025-10-02 01:52:02 +02:00
Spythere 802da68576 fix: dark mode for lighter rows background 2025-10-02 01:50:10 +02:00
Spythere 78e82df63d chore: x mark for checked inputs 2025-10-02 01:38:29 +02:00
Spythere b5a4ba9c0a refactor: order train picker setup 2025-10-01 15:58:14 +02:00
Spythere b93a65007e chore: order fields verification on update 2025-10-01 15:00:55 +02:00
Spythere 70d29284a5 chore: checking whether new order is the same as the last one saved 2025-10-01 14:50:44 +02:00
Spythere 5fb235c9a7 chore: improved order verification loops 2025-10-01 14:40:04 +02:00
Spythere 40c7e47632 chore: added order fields verification for copying 2025-10-01 01:57:46 +02:00
Spythere eac9b47e10 chore: update prompt visibility 2025-09-30 16:18:43 +02:00
Spythere 6af0e9b822 chore: added resetting order button, changed buttons layout 2025-09-30 16:11:31 +02:00
Spythere 6306a07562 refactor: auto update values 2025-09-30 15:50:59 +02:00
Spythere a0a5e72701 refactor: order list & order message logic for new data 2025-09-29 19:30:30 +02:00
Spythere 7784e08f03 refactor: new order typings 2025-09-29 14:14:07 +02:00
Spythere 98fda8e849 refactor: app component 2025-09-27 22:24:47 +02:00
Spythere 81f484793f refactor: home component setup 2025-09-27 22:19:48 +02:00
Spythere 5a2be7b25e refactor: updated order message component 2025-09-27 22:16:26 +02:00
Spythere 73c397a1bc chore: added dark mode and language settings to the navbar 2025-09-27 21:51:59 +02:00
Spythere f54eada94d chore: added app navbar; adjusted colors and global styles 2025-09-27 15:40:02 +02:00
Spythere fa4504fec7 chore: added generating message text for custom instructions 2025-09-27 14:49:23 +02:00
Spythere 9118c186cf chore: added footer and missing html messages 2025-09-26 13:48:54 +02:00
Spythere 14f730f8ca chore: completed unfinished instruction rows, added row exceptions 2025-09-26 02:48:01 +02:00
Spythere 7afef587cc refactor: new order structure, added new order instructions (wip) 2025-09-24 02:31:59 +02:00
Spythere 35a883d608 chore: code cleanup 2025-07-11 02:30:06 +02:00
Spythere 2efa4a4f9a hotfix: checkpoint abbreviation 2025-07-11 02:29:41 +02:00
Spythere 92586fb880 hotfix(train picker): filling station name instead of dispatcher name 2025-07-07 22:59:19 +02:00
Spythere 3c9cdac832 chore: added github releases webhook 2025-07-07 18:57:33 +02:00
Spythere 4162f5e137 chore: added github releases webhook 2025-07-07 18:56:31 +02:00
Spythere b2b5716cc6 fix: footer typo 2025-07-07 18:38:05 +02:00
Spythere 26b0556bfa fix: translations 2025-07-07 18:34:15 +02:00
Spythere d59152fccd chore: nav buttons transition 2025-07-07 18:31:40 +02:00
Spythere 864967a77a fix: order "O" rows generation 2025-07-07 18:23:15 +02:00
Spythere 37c2650841 chore: changed lang button layout 2025-07-07 18:15:39 +02:00
Spythere 5b9b86248f chore: added changelog for update modal 2025-07-07 18:00:07 +02:00
Spythere 43b0dc9fa0 chore: packages upgrade 2025-07-01 18:25:11 +02:00
Spythere 972c73281d bump: v1.6.0 2025-07-01 18:24:16 +02:00
Spythere 8538072a60 refactor: added translations 2025-07-01 18:24:04 +02:00
Spythere e298a17ab7 chore: added loading locale from browser data 2025-07-01 16:20:25 +02:00
Spythere 519665697b chore: added i18n 2025-07-01 16:15:50 +02:00
Spythere c5221e337b chore: added multiline support 2025-07-01 16:12:53 +02:00
56 changed files with 3419 additions and 7907 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
VITE_APP_API_URL=https://stacjownik.spythere.eu/api VITE_APP_API_URL=https://stacjownik.spythere.eu/api
VITE_APP_SWDR_URL=https://api.td2.info.pl VITE_APP_SWDR_URL=https://api.td2.info.pl
VITE_APP_ORDER_VERSION=2 VITE_APP_ORDER_VERSION=3 # 3 - NEW 2025 ORDER INSTRUCTIONS
#VITE_UPDATE_TEST='test'
@@ -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: '9936031'
footer_title: 'Changelog - GeneraTOR'
footer_timestamp: true
+3
View File
@@ -25,3 +25,6 @@ pnpm-debug.log*
# Firebase # Firebase
.firebase .firebase
#Yarn lock files
yarn.lock
+31 -1
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="pl"> <html lang="pl">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
@@ -24,6 +24,36 @@
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png" /> <meta name="msapplication-TileImage" content="/ms-icon-144x144.png" />
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#ffffff" />
<!-- Preloads -->
<link
rel="preload"
href="/fonts/libre-franklin-500.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link
rel="preload"
href="/fonts/libre-franklin-700.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link
rel="preload"
href="/fonts/libre-franklin-800.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link
rel="preload"
href="/fonts/libre-franklin-regular.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<title>GeneraTOR</title> <title>GeneraTOR</title>
<meta name="description" content="Generator rozkazów pisemnych online" /> <meta name="description" content="Generator rozkazów pisemnych online" />
</head> </head>
+5 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "genera-tor", "name": "genera-tor",
"version": "1.5.2", "version": "1.7.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -13,14 +13,17 @@
}, },
"dependencies": { "dependencies": {
"axios": "^1.6.2", "axios": "^1.6.2",
"lucide-vue-next": "^0.525.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"showdown": "^2.1.0",
"vue": "^3.3.11", "vue": "^3.3.11",
"vue-i18n": "9.8.0", "vue-i18n": "11",
"vue-router": "^4.2.5", "vue-router": "^4.2.5",
"vue-tsc": "^2.2.8" "vue-tsc": "^2.2.8"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.13.10", "@types/node": "^22.13.10",
"@types/showdown": "^2.0.6",
"@vitejs/plugin-vue": "^4.5.2", "@vitejs/plugin-vue": "^4.5.2",
"@vue/eslint-config-prettier": "^8.0.0", "@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0", "@vue/eslint-config-typescript": "^12.0.0",
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+77 -52
View File
@@ -1,53 +1,101 @@
<template> <template>
<div id="app_wrapper"> <div>
<router-view />
<transition name="slide-anim"> <transition name="slide-anim">
<div v-if="needRefresh" class="update-prompt" @click="updateServiceWorker(true)"> <UpdateCard />
Nowa wersja GeneraTORa dostępna!
<u>Kliknij, aby odświeżyć aplikację!</u>
</div>
</transition> </transition>
<footer> <transition name="slide-anim">
&copy; <a href="https://td2.info.pl/profile/?u=20777">Spythere</a> <UpdatePrompt />
{{ new Date().getUTCFullYear() }} | v.{{ appVersion }} </transition>
</footer>
<div class="app-body">
<AppNavbar />
<main>
<RouterView />
</main>
</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { useRegisterSW } from 'virtual:pwa-register/vue';
import { defineComponent } from 'vue';
import packageInfo from '../package.json'; import packageInfo from '../package.json';
import axios from 'axios';
import { useStore } from './store/store'; import { useStore } from './store/store';
import orderStorageMixin from './mixins/orderStorageMixin';
export default defineComponent({ import UpdateCard from './components/Global/UpdateCard.vue';
mixins: [orderStorageMixin], import UpdatePrompt from './components/Global/UpdatePrompt.vue';
import AppNavbar from './components/App/AppNavbar.vue';
import StorageManager from './managers/storageManager';
import { onMounted } from 'vue';
setup() { const STORAGE_VERSION_KEY = 'app_version';
const { offlineReady, needRefresh, updateServiceWorker } = useRegisterSW({ immediate: true });
return { offlineReady, needRefresh, updateServiceWorker }; const store = useStore();
}, const appVersion = packageInfo.version;
data() {
return { appVersion: packageInfo.version, store: useStore() };
},
created() { onMounted(() => {
document.title = `GeneraTOR ${this.appVersion}`; loadLang();
this.store.orderDarkMode = this.getOrderSetting('dark-mode') === 'true'; loadSettings();
checkAppVersion();
handleQueries();
});
function loadSettings() {
document.title = `GeneraTOR ${appVersion}`;
store.orderDarkMode = StorageManager.getBooleanValue('dark-mode');
}
function handleQueries() {
const query = new URLSearchParams(window.location.search); const query = new URLSearchParams(window.location.search);
const id = query.get('sceneryId'); const id = query.get('sceneryId');
if (id != null) { if (id != null) {
this.store.orderMode = 'OrderTrainPicker'; store.panelMode = 'OrderTrainPickerPanel';
}
}
async function checkAppVersion() {
const storageVersion = StorageManager.getStringValue(STORAGE_VERSION_KEY);
try {
const releaseData = await (
await axios.get('https://api.github.com/repos/Spythere/genera-tor/releases/latest')
).data;
if (!releaseData) return;
store.appUpdateData.version = appVersion;
store.appUpdateData.changelog = releaseData.body;
store.appUpdateData.releaseURL = releaseData.html_url;
store.updateCardOpen =
(storageVersion != '' && storageVersion != appVersion) ||
import.meta.env.VITE_UPDATE_TEST === 'test';
} catch (error) {
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
}
StorageManager.setStringValue(STORAGE_VERSION_KEY, appVersion);
}
function loadLang() {
const storageLang = StorageManager.getStringValue('lang');
if (storageLang) {
store.changeLang(storageLang);
return;
}
if (!window.navigator.language) return;
const naviLanguage = window.navigator.language.toString();
if (!naviLanguage.startsWith('pl')) {
store.changeLang('en');
} }
} }
});
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -56,34 +104,11 @@ export default defineComponent({
#app { #app {
color: white; color: white;
min-height: 100vh; min-height: 100vh;
} }
.update-prompt {
position: fixed;
bottom: 0;
left: 0;
padding: 0.5em;
font-weight: bold;
text-align: center;
width: 100%;
background-color: colors.$accentCol;
cursor: pointer;
}
footer { footer {
text-align: center; text-align: center;
padding: 0.5em 0; padding: 0.5em 0;
} }
@media screen and (max-width: 500px) {
#app {
font-size: calc(1vw + 0.65rem);
}
}
</style> </style>
+22
View File
@@ -0,0 +1,22 @@
<template>
<footer>
&copy; <a href="https://td2.info.pl/profile/?u=20777">Spythere</a>
{{ new Date().getUTCFullYear() }} |
<button class="g-button text" @click="store.updateCardOpen = true">v{{ props.version }}</button>
</footer>
</template>
<script setup lang="ts">
import { useStore } from '../../store/store';
const store = useStore();
const props = defineProps({
version: {
type: String,
required: true
}
});
</script>
<style scoped></style>
+74
View File
@@ -0,0 +1,74 @@
<template>
<nav class="app-navbar">
<div class="navbar-brand">
<img src="/favicon.ico" alt="generator logo" width="30" />
<b>
Genera<span class="text--accent">TOR</span> <sup class="text--grayed">v{{ version }}</sup>
</b>
</div>
<div class="navbar-actions">
<button class="g-button action icon" @click="switchDarkMode">
<LucideMoon :size="20" v-if="store.orderDarkMode" />
<LucideSun :size="20" v-else />
</button>
<button class="g-button action icon" @click="switchLang">
<LucideGlobe :size="20" />
<span>{{ store.currentAppLocale == 'pl' ? 'POL' : 'ENG' }}</span>
</button>
</div>
</nav>
</template>
<script setup lang="ts">
import { LucideGlobe, LucideMoon, LucideSun } from 'lucide-vue-next';
import { version } from '../../../package.json';
import { useStore } from '../../store/store';
const store = useStore();
function switchDarkMode() {
store.orderDarkMode = !store.orderDarkMode;
window.localStorage.setItem('dark-mode', `${store.orderDarkMode}`);
}
function switchLang() {
store.changeLang(store.currentAppLocale == 'pl' ? 'en' : 'pl');
}
</script>
<style lang="scss" scoped>
.app-navbar {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 40px;
padding: 0.25em;
background-color: #1c1c1c;
}
.navbar-brand {
display: flex;
align-items: center;
gap: 0.5em;
sup {
font-size: 0.75em;
}
}
.navbar-actions {
display: flex;
align-items: center;
gap: 0.5em;
button {
padding: 0.5em;
gap: 0.25em;
border-radius: 0.5em;
}
}
</style>
+143
View File
@@ -0,0 +1,143 @@
<template>
<div class="update-card" v-if="store.updateCardOpen" @toggle-card="toggleCard(false)">
<div class="card-background"></div>
<div class="card-content">
<h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1>
<div class="changelog" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div>
<div class="no-features" v-else>{{ $t('update.no-data') }}</div>
<button class="g-button action btn-confirm" ref="confirmButtonEl" @click="toggleCard(false)">
{{ $t('update.confirm') }}
</button>
<p class="bottom-info">
{{ $t('update.info-1') }}
<br />
<span v-html="$t('update.info-2')"></span>
</p>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { Converter } from 'showdown';
import { useStore } from '../../store/store';
const converter = new Converter();
const store = useStore();
const confirmButtonEl = ref<HTMLButtonElement | null>(null);
watch(
computed(() => store.updateCardOpen),
(val) => {
if (val) {
confirmButtonEl.value?.focus();
}
}
);
const htmlChangelog = computed(() => {
if (store.appUpdateData.changelog == '') return '';
return converter.makeHtml(store.appUpdateData.changelog);
});
function toggleCard(value: boolean) {
store.updateCardOpen = value;
}
</script>
<style lang="scss" scoped>
// Converter styles
::v-deep(h1) {
text-align: center;
color: var(--clr-primary);
}
::v-deep(h2) {
padding: 0.25em 0;
border-bottom: 1px solid #aaa;
}
::v-deep(ul) {
list-style: disc;
padding: 1em;
line-height: 1.5em;
text-align: justify;
}
.update-card {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 200;
display: flex;
justify-content: center;
align-items: center;
}
.card-background {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 250;
cursor: pointer;
background-color: rgba(0, 0, 0, 0.55);
}
.card-content {
display: grid;
grid-template-rows: auto 1fr auto;
gap: 0.5em;
margin: 1em;
max-height: 95vh;
max-height: 95dvh;
background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #0e0e0e;
border-radius: 1em;
overflow: auto;
padding: 1em;
min-height: 700px;
overflow: auto;
max-width: 700px;
z-index: 300;
}
.no-features {
text-align: center;
}
.changelog {
text-align: left;
}
button.btn-confirm {
padding: 0.5em 0.75em;
font-size: 1.1em;
}
p.bottom-info {
text-align: center;
color: #ccc;
}
a {
text-decoration: underline;
}
</style>
+34
View File
@@ -0,0 +1,34 @@
<template>
<div v-if="needRefresh" class="update-prompt" @click="updateServiceWorker(true)">
{{ $t('update.update-available-text') }}
<u>{{ $t('update.update-available-underline') }}</u>
</div>
</template>
<script setup lang="ts">
import { useRegisterSW } from 'virtual:pwa-register/vue';
const { offlineReady, needRefresh, updateServiceWorker } = useRegisterSW({ immediate: true });
</script>
<style lang="scss" scoped>
@use '../../styles/colors';
.update-prompt {
position: fixed;
bottom: 0;
left: 0;
padding: 0.5em;
z-index: 200;
font-weight: bold;
text-align: center;
width: 100%;
background-color: colors.$accentCol;
cursor: pointer;
}
</style>
-232
View File
@@ -1,232 +0,0 @@
<template>
<div class="order" :class="{ dark: store.orderDarkMode }">
<div class="order_content">
<transition name="order-anim" mode="out-in">
<keep-alive>
<component :is="chosenOrderComponent" :key="chosenOrderComponent.name"></component>
</keep-alive>
</transition>
<OrderFooter />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../store/store';
import OrderNVue from './OrderN.vue';
import OrderSVue from './OrderS.vue';
import OrderFooter from './OrderFooter.vue';
import OrderOVue from './OrderO.vue';
const orderComponents = { orderN: OrderNVue, orderS: OrderSVue, orderO: OrderOVue };
export default defineComponent({
components: { OrderNVue, OrderSVue, OrderFooter },
setup() {
const store = useStore();
return { store };
},
computed: {
chosenOrderComponent() {
return orderComponents[this.store.chosenOrderType];
}
}
});
</script>
<style lang="scss">
@use '../styles/colors';
$darkModeTextCol: #eee;
.order {
background-color: white;
color: black;
&.dark {
background-color: colors.$bgColDarker;
color: $darkModeTextCol;
}
max-height: 95vh;
overflow: auto;
font-size: 15px;
h2 {
margin: 0;
padding: 0;
}
textarea:focus-visible {
outline: 2px solid colors.$accentCol;
}
input[type='checkbox']:focus-visible,
input[type='radio']:focus-visible {
outline: 2px solid colors.$accentCol;
}
input[type='checkbox'],
input[type='radio'],
select {
margin-top: 0.5em;
margin-right: 0.5em;
font-size: 0.8em;
color: black;
}
textarea.others {
width: 100%;
min-height: 200px;
resize: vertical;
}
@media screen and (max-width: 550px) {
font-size: 3vw;
}
}
.order_header {
padding: 0.5em;
border: 2px solid black;
border-bottom: none;
}
.order_content {
padding: 0.5em;
}
.flex-row {
display: flex;
justify-content: space-around;
align-items: flex-end;
margin-top: 0.5em;
input {
max-width: 10em;
}
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.horizontal-bar {
width: 100%;
height: 2px;
background-color: black;
margin: 0.5em 0;
}
.order input {
max-width: 100px;
background-color: transparent;
outline: none;
border: none;
font-size: 1em;
text-align: center;
border-bottom: 2px dotted black;
&:focus-visible {
border-bottom: 2px solid colors.$accentCol;
}
&.row-checkbox + input::placeholder {
color: red;
}
}
/* Dark mode */
.order.dark {
input {
border-color: $darkModeTextCol !important;
color: $darkModeTextCol !important;
&:focus-visible {
border-bottom: 2px solid colors.$accentCol !important;
}
&::placeholder {
color: #ccc !important;
}
}
select {
color: $darkModeTextCol !important;
border-color: $darkModeTextCol;
&:focus-visible {
border-color: colors.$accentCol;
}
}
option,
textarea {
color: $darkModeTextCol !important;
border-color: $darkModeTextCol !important;
background-color: colors.$bgColDarker !important;
}
.horizontal-bar {
background-color: white;
}
.order_header,
.order_other,
table,
tr,
td {
border-color: $darkModeTextCol !important;
}
}
.order_table-container {
table {
width: 100%;
td:first-child {
width: 10%;
text-align: center;
font-weight: bold;
}
td {
padding: 0.35em;
text-align: justify;
vertical-align: top;
line-height: 1.5em;
}
}
table,
td {
border: 2px solid black;
border-collapse: collapse;
}
}
.order-anim {
&-enter-active,
&-leave-active {
transition: opacity 150ms ease-in-out;
}
&-enter-from,
&-leave-to {
opacity: 0;
}
}
</style>
+119
View File
@@ -0,0 +1,119 @@
<template>
<div class="order" :class="{ dark: store.orderDarkMode }">
<OrderHeader />
<OrderMainContent />
<OrderFooter />
</div>
</template>
<script setup lang="ts">
import { useStore } from '../../store/store';
import OrderHeader from './OrderHeader.vue';
import OrderMainContent from './OrderMainContent.vue';
import OrderFooter from './OrderFooter.vue';
import { computed, onMounted, watch } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const store = useStore();
onMounted(() => {
generateMessage();
});
watch(
store.orderData,
() => {
generateMessage();
},
{ deep: true }
);
watch(
computed(() => store.currentAppLocale),
() => {
generateMessage();
}
);
function generateMessage() {
let messageHtml = `<b>${t('order.title')}</b><br />`;
messageHtml += '-------------<br />';
const headerData = store.orderData['header'];
const headerDateString = headerData['B']
? new Date(headerData['B']).toLocaleDateString('pl-PL', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})
: '';
messageHtml += `${t('order.header.A')}: ${headerData['A'] || '---'}<br />`;
messageHtml += `${t('order.header.B')}: ${headerDateString || '---'}<br />`;
messageHtml += `${t('order.header.C')}: ${headerData['C'] || '---'}<br />`;
messageHtml += `${t('order.header.D')}: ${headerData['D'] || '---'}<br />`;
const instructions = store.orderData['instructions'];
Object.entries(instructions).forEach(([i, value]) => {
if (value.active) {
if (value.inputFields) {
const localeKey = `order.${value.key}`;
const messageValues = Object.values(value.inputFields).map((fieldKey: string) => {
if (fieldKey.startsWith('select')) return t(`order.${value.key}.${fieldKey}`);
return fieldKey || '---';
});
messageHtml += '-------------<br />';
messageHtml += `<b>[${value.name}]</b> ${t(
localeKey + '.message-html',
messageValues,
Object.keys(value.inputFields).filter(
(k) => value.optionalFieldNames.includes(k) && value.inputFields[k].trim() != ''
).length
)}<br />`;
if (value.key == '2310' && value.listFields) {
messageHtml += '<br />';
value.listFields.forEach((listItem, i) => {
if (!listItem.active) return;
const listItemValues = Object.values(listItem.values).map((itemFieldKey) => {
return itemFieldKey || '---';
});
messageHtml += t(
`${localeKey}.message-html-list`,
[i + 1, ...listItemValues],
Object.keys(listItem.values).filter(
(k) => listItem.values[k].trim() != '' && k.startsWith('signalbox')
).length
);
messageHtml += '<br />';
});
}
} else {
messageHtml += `<b>[${i}]</b> ${t('order.' + i + '.message-html')}<br />`;
}
}
});
const footerData = store.orderData['footer'];
messageHtml += '-------------<br />';
messageHtml += `${t('order.footer.V')}: ${footerData['V'] || '---'}<br />`;
messageHtml += `${t('order.footer.W')}: ${footerData['W'] || '---'}<br />`;
messageHtml += `${t('order.footer.Y')}: ${footerData['Y'] || '---'}<br />`;
messageHtml += `${t('order.footer.Z')}: ${footerData['Z'] || '---'}<br />`;
store.orderMessage = messageHtml;
}
</script>
<style lang="scss">
@use '../../styles/order';
</style>
+81
View File
@@ -0,0 +1,81 @@
<template>
<table class="order-table">
<tbody>
<tr>
<td width="50%">
<div>
<input
type="text"
class="order-input"
id="footer-V"
v-model="store.orderData.footer.V"
:placeholder="t('order.footer.V-placeholder')"
/>
<label class="order-input-label" for="footer-V">{{ t('order.footer.V') }}</label>
</div>
</td>
<td>
<div>
<input
type="text"
class="order-input"
id="footer-W"
v-model="store.orderData.footer.W"
:placeholder="t('order.footer.W-placeholder')"
/>
<label class="order-input-label" for="footer-W">{{ t('order.footer.W') }}</label>
</div>
</td>
</tr>
<tr>
<td>
<div>
<input
type="text"
class="order-input"
id="footer-Y"
v-model="store.orderData.footer.Y"
:placeholder="t('order.footer.Y-placeholder')"
/>
<label class="order-input-label" for="footer-Y">{{ t('order.footer.Y') }}</label>
</div>
</td>
<td>
<div>
<input
type="text"
class="order-input"
id="footer-Z"
v-model="store.orderData.footer.Z"
:placeholder="t('order.footer.Z-placeholder')"
/>
<label class="order-input-label" for="footer-Z">{{ t('order.footer.Z') }}</label>
</div>
</td>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { useStore } from '../../store/store';
const { t } = useI18n();
const store = useStore();
</script>
<style scoped>
.order-input {
max-width: 100%;
width: 100%;
text-align: left;
}
.order-table > tbody > tr > td > div {
width: 100%;
padding: 0.5em 1rem;
}
</style>
+90
View File
@@ -0,0 +1,90 @@
<template>
<table class="order-table">
<tbody>
<tr>
<td style="padding: 0.25em" colspan="2">
<b>{{ t('order.title') }}</b>
</td>
</tr>
<tr>
<td width="50%">
<div class="header-input-box">
<input
type="text"
class="order-input"
id="header-A"
v-model="store.orderData.header.A"
:placeholder="t('order.header.A-placeholder')"
/>
<label class="order-input-label" for="header-A">{{ t('order.header.A') }}</label>
</div>
</td>
<td>
<div class="header-input-box">
<input
type="date"
class="order-input"
id="header-B"
v-model="store.orderData.header.B"
/>
<label class="order-input-label" for="header-B">{{ t('order.header.B') }}</label>
</div>
</td>
</tr>
<tr>
<td>
<div class="header-input-box">
<input
type="text"
class="order-input"
id="header-C"
v-model="store.orderData.header.C"
:placeholder="t('order.header.C-placeholder')"
/>
<label class="order-input-label" for="header-C">{{ t('order.header.C') }}</label>
</div>
</td>
<td>
<div class="header-input-box">
<input
type="text"
class="order-input"
id="header-D"
v-model="store.orderData.header.D"
:placeholder="t('order.header.D-placeholder')"
/>
<label class="order-input-label" for="header-D">{{ t('order.header.D') }}</label>
</div>
</td>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { useStore } from '../../store/store';
const { t } = useI18n();
const store = useStore();
</script>
<style scoped>
.header-input-box {
width: 100%;
padding: 0.5em 1em;
}
.order-input {
max-width: 100%;
width: 100%;
text-align: left;
}
.order-table,
.order-table tr:last-child td {
border-bottom: none;
}
</style>
+204
View File
@@ -0,0 +1,204 @@
<template>
<table class="order-table">
<tbody>
<!-- First row - 22 & 99 instructions -->
<tr>
<td width="10%" class="order-instruction-number">
<OrderRowCheckbox :row-index="0" />
</td>
<td>
<b>
{{ t('order.22.text') }}
</b>
</td>
<td width="5%" class="order-instruction-number">
<OrderRowCheckbox :row-index="1" />
</td>
<td width="45%">
<div>
<b>{{ t('order.99.text') }}</b>
</div>
<input
type="text"
class="order-input"
id="input-99-x1"
v-model="store.orderData.instructions[1].inputFields!.x1"
/>
<label for="input-99-x1" class="order-input-label">{{ t('order.99.x1') }}</label>
</td>
</tr>
<!-- From 21.10 -->
<tr
v-for="(instruction, i) in store.orderData.instructions.slice(2)"
:class="{
'bg-lighter': instruction.key.startsWith('218'),
dark: store.orderDarkMode
}"
>
<td width="10%" class="order-instruction-number">
<OrderRowCheckbox :row-index="i + 2" />
</td>
<td colspan="3">
<i18n-t :keypath="`order.${instruction.key}.text`" tag="div" scope="global">
<!-- For text directives (<b>, <u>, <br> etc.) -->
<template v-slot:[directive] v-for="directive in instruction.textDirectives">
<b v-if="directive.startsWith('bold')">
{{ t(`order.${instruction.key}.${directive}`) }}
</b>
<u v-else-if="directive.startsWith('underline')">
{{ t(`order.${instruction.key}.${directive}`) }}
</u>
<u v-else-if="directive.startsWith('highlight')">
<b>{{ t(`order.${instruction.key}.${directive}`) }}</b>
</u>
<br v-if="directive.startsWith('br')" />
</template>
<!-- For all instructions with input fields -->
<template v-slot:[fieldKey] v-for="(_, fieldKey) in instruction.inputFields">
<textarea
v-if="fieldKey == 'other2320'"
v-model="instruction.inputFields[fieldKey]"
class="order-textarea"
:id="`order-${instruction.key}-${fieldKey}`"
:placeholder="t(`order.${instruction.key}.${fieldKey}`)"
autocomplete="off"
></textarea>
<label class="order-input-box" v-else>
<input
v-model="instruction.inputFields[fieldKey]"
class="order-input"
:id="`order-${instruction.key}-${fieldKey}`"
:style="{ width: calculateInputWidthByFieldName(fieldKey) }"
autocomplete="off"
/>
<span>{{ t(`order.${instruction.key}.${fieldKey}`) }}</span>
</label>
</template>
<!-- For all instructions with select fields -->
<template v-for="(selectField, fieldKey) in instruction.selectFields" v-slot:[fieldKey]>
<select
class="order-select"
:id="`order-${instruction.key}-${fieldKey}`"
v-model="instruction.inputFields[fieldKey]"
>
<option :value="value" v-for="value in selectField.options">
{{ t(`order.${instruction.key}.${value}`) }}
</option>
</select>
</template>
<!-- For 23.10 only -->
<template v-slot:text-list v-if="instruction.key == '2310'">
<i18n-t
v-for="(listItem, i) in instruction.listFields"
:keypath="`order.${instruction.key}.text-list`"
tag="div"
scope="global"
>
<template v-slot:bold>
<label>
<input
v-model="listItem.active"
type="checkbox"
:id="`order-${instruction.key}-checkbox-${i}`"
/>
&nbsp;
<b>{{ t(`order.${instruction.key}.bold`, [i + 1]) }}</b>
</label>
</template>
<template v-slot:v>
<br />
<span style="font-size: 1.5em">v</span>
</template>
<template v-slot:[fieldKey] v-for="(_, fieldKey, j) in listItem.values">
<label class="order-input-box">
<input
v-model="instruction.listFields![i]['values'][fieldKey]"
class="order-input"
:id="`order-${instruction.key}-${fieldKey}-${i}`"
:style="{ width: calculateInputWidthByFieldName(fieldKey) }"
autocomplete="off"
/>
<span>{{
t(`order.${instruction.key}.${fieldKey}`, [j + 1 + 6 * i, 91 + i])
}}</span>
</label>
</template>
</i18n-t>
</template>
</i18n-t>
</td>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { useStore } from '../../store/store';
import OrderRowCheckbox from './OrderRowCheckbox.vue';
const { t } = useI18n();
const store = useStore();
function calculateInputWidthByFieldName(fieldName: string) {
if (fieldName.startsWith('track')) return '90px';
else if (fieldName.startsWith('signalbox')) return '130px';
else if (fieldName.startsWith('signal')) return '150px';
else if (fieldName.startsWith('train')) return '150px';
else if (fieldName.startsWith('other2320')) return '100%';
else if (fieldName.startsWith('other')) return '200px';
return '100px';
}
</script>
<style lang="scss" scoped>
.order-table {
border-top: none;
border-bottom: none;
td {
padding: 0.25em;
}
tr:not(:first-child) td {
padding: 0.25em 1em;
line-height: 2em;
text-align: justify;
}
tr.bg-lighter {
background-color: #eeece1;
}
tr.bg-lighter.dark {
background-color: #111;
}
}
.order-instruction-number {
position: relative;
height: 60px;
& > label {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 0.5em;
}
}
</style>
+90
View File
@@ -0,0 +1,90 @@
<template>
<label class="order-instruction-checkbox">
<input
type="checkbox"
v-model="instructionObject.active"
:id="`instruction-checkbox-${instructionObject.name}`"
/>
<div class="checkmark" :class="{ dark: store.orderDarkMode }"></div>
<div class="text">{{ instructionObject.name }}</div>
</label>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useStore } from '../../store/store';
const store = useStore();
const props = defineProps({
rowIndex: {
type: Number,
required: true
}
});
const instructionObject = computed(() => store.orderData.instructions[props.rowIndex]);
</script>
<style lang="scss" scoped>
.order-instruction-checkbox {
display: block;
position: relative;
text-align: center;
cursor: pointer;
font-weight: bold;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
& > input {
position: absolute;
opacity: 0;
height: 0;
width: 0;
cursor: pointer;
&:checked ~ .checkmark:after {
content: '\d7';
}
&:focus-visible ~ .text {
text-decoration: underline;
outline: 1px solid black;
}
}
.checkmark {
display: block;
position: relative;
margin: 0 auto;
height: 20px;
width: 20px;
background-color: #eee;
border: 2px solid black;
background-color: gold;
&.dark {
color: black;
}
&:after {
content: '';
position: absolute;
top: 50%;
left: 50%;
font-size: 20px;
transform: translate(-50%, -50%);
}
}
&:hover input ~ .checkmark {
background-color: #ffe44b;
}
}
</style>
-101
View File
@@ -1,101 +0,0 @@
<template>
<section class="order_info">
<table>
<tbody>
<tr>
<td colspan="4">
<input type="text" v-model="footerInfo.stationName" placeholder="nazwa stacji" />
<br />
stacja
</td>
<td colspan="3">
<input type="text" v-model="footerInfo.checkpointName" placeholder="skrót posterunku" />
<br />
posterunek
</td>
<td colspan="2">
<input type="text" v-model="footerInfo.hour" placeholder="godzina" />
<br />
godz.
</td>
<td colspan="1">
<input type="text" v-model="footerInfo.minutes" placeholder="minuta" />
<br />
min.
</td>
</tr>
<tr>
<td colspan="5">
<input type="text" v-model="footerInfo.dispatcherName" placeholder="dyżurny" />
<br />
dyżurny ruchu
</td>
<td colspan="5">
<input
type="text"
v-model="footerInfo.secondaryDispatcherName"
placeholder="dyżurny (wypełnić jedno)"
/>
<br />
z polecenia dyżurnego ruchu
</td>
</tr>
</tbody>
</table>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import orderFooterMixin from '../mixins/orderFooterMixin';
import { useStore } from '../store/store';
export default defineComponent({
mixins: [orderFooterMixin],
setup() {
const store = useStore();
return {
store,
footerInfo: store.orderFooter
};
},
watch: {
footerInfo: {
deep: true,
handler() {
this.generateFooter();
}
}
}
});
</script>
<style lang="scss" scoped>
.order_info {
table {
border-collapse: collapse;
width: 100%;
table-layout: fixed;
td {
border: 2px solid black;
border-collapse: collapse;
padding: 0.35em;
}
input {
max-width: 95%;
}
text-align: center;
}
}
</style>
-69
View File
@@ -1,69 +0,0 @@
<template>
<div class="order-helper g-modal">
<div class="modal-bg" @click="store.helperModalOpen = false"></div>
<div class="content modal-content">
<h2>Rozkazy pisemne - najczęstsze przypadki zastosowań w TD2</h2>
<hr />
<ul>
<li>
<b>Przetaczanie (manewr) taboru poza wskaźnik W 5 (granicy przetaczania)</b>
<p>
Rozkazu <u>nie stosujemy</u> w przypadku wyjazdu taboru na szlak dwutorowy, na którym
odbywa się ruch w kierunku zasadniczym, tj. "prawostronnym". Dla wszystkich szlaków
jednotorowych lub dwutorowych w sytuacji innej niż wymieniona wykorzystujemy działkę 4.
rozkazu pisemnego "S". <br /><br />
<i
>Szczegółowe informacje: instrukcja Ir-1 (R-1) § 12 pkt 4 "Manewry na torach
głównych"</i
>
</p>
<button class="g-button action">Wygeneruj treść rozkazu</button>
</li>
</ul>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../store/store';
import orderHelperData from '../data/orderHelperData.json';
export default defineComponent({
setup() {
return { store: useStore(), orderHelperData };
}
});
</script>
<style lang="scss" scoped>
@use '../styles/colors';
.content {
width: 100%;
max-width: 800px;
height: 100vh;
max-height: 800px;
margin: 1em;
padding: 1em;
background-color: #2b2b2b;
}
h2 {
text-align: center;
}
ul {
text-align: left;
li b {
font-size: 1.1em;
color: colors.$accentCol;
}
}
</style>
-195
View File
@@ -1,195 +0,0 @@
<template>
<section class="order-list">
<h3>Zapisane rozkazy pisemne ({{ localOrderList.length }})</h3>
<transition-group name="list" tag="ul">
<li class="no-orders-warning" v-if="sortedOrderList.length == 0" :key="-1">
Brak zapisanych rozkazów!
</li>
<li
v-for="order in sortedOrderList"
:selected="order.id == store.chosenLocalOrderId"
:key="order.id"
>
<b class="text--accent">#{{ order.id.split('-')[1] }}&nbsp;</b>
<b>
{{ getOrderName(order.orderType) }} nr {{ order.orderBody['header']['orderNo'] }} dla
pociągu nr {{ order.orderBody['header']['trainNo'] }}
</b>
<span
v-if="!order.orderVersion || order.orderVersion != ORDER_VERSION"
class="wrong-order-indicator"
tabindex="0"
data-tooltip="Przestarzała wersja rozkazu! Może generować złe informacje!"
>&#9888;
</span>
<br />
{{ order.createdAt ? 'Dodano: ' : 'Zaktualizowano: ' }}
{{ new Date(order.createdAt || order.updatedAt || 0).toLocaleString('pl-PL') }}
<hr />
<div class="buttons">
<button class="g-button" @click="selectLocalOrder(order)">Wybierz</button>
<button class="g-button" @click="removeOrder(order)">Usuń</button>
</div>
</li>
</transition-group>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import orderStorageMixin from '../mixins/orderStorageMixin';
import { useStore } from '../store/store';
import { LocalStorageOrder } from '../types/orderTypes';
export default defineComponent({
name: 'OrderList',
mixins: [orderStorageMixin],
data() {
return {
localOrderList: [] as LocalStorageOrder[],
ORDER_VERSION: import.meta.env['VITE_APP_ORDER_VERSION']
};
},
setup() {
return {
store: useStore(),
localStorage: window.localStorage
};
},
methods: {
getOrderName(orderType: string) {
return `Rozkaz "${orderType.split('order')[1]}"`;
},
removeOrder(order: LocalStorageOrder) {
if (!order) return;
this.removeLocalOrder(order);
this.localOrderList = this.localOrderList.filter((o) => o.id != order.id);
if (this.localOrderList.length == 0) this.saveOrderSetting('orderCount', 0);
}
},
computed: {
sortedOrderList() {
return this.localOrderList
.slice()
.sort((a, b) => (b.createdAt || b.updatedAt!) - (a.createdAt || a.updatedAt!));
}
},
activated() {
const localStorage = window.localStorage;
const orderList = [];
for (let key in localStorage) {
if (!/^order-/g.test(key)) continue;
const orderObj: LocalStorageOrder = JSON.parse(localStorage[key]);
if (!orderObj) continue;
orderList.push(orderObj);
}
this.localOrderList = orderList;
}
});
</script>
<style lang="scss" scoped>
@use '../styles/colors';
.list {
&-move,
&-enter-active,
&-leave-active {
transition: all 250ms ease;
}
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateY(30px);
}
&-leave-active {
position: absolute;
}
}
.order-list {
overflow: auto;
}
hr {
border: 1px solid #aaa;
height: 0;
}
ul {
overflow: hidden;
}
h3 {
position: sticky;
top: 0;
z-index: 100;
margin: 0;
margin-bottom: 1em;
text-align: center;
background-color: colors.$bgColDarker;
padding: 1em;
margin-bottom: 0.5em;
}
li {
text-align: left;
padding: 1em;
margin: 0.5em;
background-color: colors.$bgColDarker;
cursor: pointer;
&[selected='true'] {
outline: 1px solid colors.$accentCol;
}
&.no-orders-warning {
text-align: center;
font-size: 1.2em;
cursor: default;
}
}
.wrong-order-indicator {
color: colors.$accentCol;
padding: 0 0.25em;
}
.buttons {
display: flex;
gap: 0.5em;
button {
padding: 0.5em;
background-color: colors.$bgColLighter;
&:hover {
background-color: #666;
}
&:focus-visible {
outline: 2px solid colors.$accentCol;
}
}
}
</style>
-331
View File
@@ -1,331 +0,0 @@
<template>
<section class="order-message">
<h3>Wiadomość do wyświetlenia na czacie symulatora:</h3>
<div class="message_body" v-html="fullOrderMessage"></div>
<p class="message_info">
Po wygenerowaniu rozkazu skopiuj jego treść lub zapisz w pamięci przeglądarki za pomocą
przycisków poniżej
</p>
<div class="message_actions">
<button class="g-button action" @click="saveOrder">Zapisz nowy rozkaz</button>
<button class="g-button action" @click="copyMessage">Kopiuj treść rozkazu</button>
<button
class="g-button action"
:data-disabled="!store.chosenLocalOrderId"
@click="updateOrder"
>
Zaktualizuj rozkaz
<span class="text--accent"
>{{ store.chosenLocalOrderId && `#${store.chosenLocalOrderId.split('-')[1]}` }}
</span>
</button>
</div>
<div class="message_checkboxes">
<label for="dark-mode" class="g-checkbox">
<input
type="checkbox"
name="dark-mode"
id="dark-mode"
v-model="store.orderDarkMode"
@change="onCheckboxChange"
/>
<span>Ciemny motyw rozkazu</span>
</label>
<label for="copy-increment" class="g-checkbox">
<input
type="checkbox"
name="copy-increment"
id="copy-increment"
v-model="incrementOnCopy"
@change="onCheckboxChange"
/>
<span>Aktualizuj numer rozkazu po skopiowaniu</span>
</label>
<label for="save-increment" class="g-checkbox">
<input
type="checkbox"
name="save-increment"
id="save-increment"
v-model="incrementOnSave"
@change="onCheckboxChange"
/>
<span>Aktualizuj numer rozkazu po zapisaniu</span>
</label>
<label for="update-date" class="g-checkbox">
<input
type="checkbox"
name="update-date"
id="update-date"
v-model="updateDate"
@change="onCheckboxChange"
/>
<span>Aktualizuj godziny przy edycji</span>
</label>
</div>
<transition name="monit-anim">
<div class="action_monit" v-if="actionMonit" v-html="actionMonit"></div>
</transition>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../store/store';
import saveIcon from '../assets/icon-save.svg';
import orderStorageMixin from '../mixins/orderStorageMixin';
import orderValidationMixin from '../mixins/orderValidationMixin';
import { currentFormattedHours, currentFormattedMinutes } from '../utils/dateUtils';
export default defineComponent({
name: 'OrderMessage',
mixins: [orderStorageMixin, orderValidationMixin],
data() {
return {
saveIcon,
actionMonit: '',
monitTimeout: undefined as number | undefined,
incrementOnSave: true,
incrementOnCopy: true,
updateDate: true
};
},
setup() {
return {
store: useStore()
};
},
mounted() {
this.incrementOnSave = this.getOrderSetting('save-increment') === 'true';
this.incrementOnCopy = this.getOrderSetting('copy-increment') === 'true';
this.updateDate = this.getOrderSetting('update-date') === 'true';
},
computed: {
fullOrderMessage() {
return this.store.orderMessage + this.store.footerMessage;
}
},
watch: {
fullOrderMessage() {
if (this.updateDate) {
this.store.orderFooter['hour'] = currentFormattedHours();
this.store.orderFooter['minutes'] = currentFormattedMinutes();
}
}
},
methods: {
onCheckboxChange(e: Event) {
const checkbox = e.target as HTMLInputElement;
this.saveOrderSetting(checkbox.id, checkbox.checked);
},
showActionMonit(text: string) {
if (this.monitTimeout) {
this.actionMonit = '';
clearTimeout(this.monitTimeout);
setTimeout(() => {
this.actionMonit = text;
this.monitTimeout = window.setTimeout(() => {
this.actionMonit = '';
}, 5000);
}, 300);
return;
}
this.actionMonit = text;
this.monitTimeout = window.setTimeout(() => {
this.actionMonit = '';
}, 5000);
},
incrementOrderNo() {
const order = this.store[this.store.chosenOrderType];
order.header.orderNo = (Number(order.header.orderNo) + 1).toString();
},
copyMessage() {
if (!navigator.clipboard)
return this.showActionMonit(
'Ups! Twoja przeglądarka musi być dosyć przestarzała, ponieważ nie obsługuje zapisu do schowka! :/'
);
const hasAtLeastOneRow = /(\[ \d \])/g.test(this.fullOrderMessage);
const hasAllInputsFilled = !/_/g.test(this.store.orderMessage);
if (!hasAllInputsFilled)
return this.showActionMonit(
`<span class="text--warn">Wypełnij puste rubryki rozkazu przed jego skopiowaniem!</span>`
);
if (!hasAtLeastOneRow)
return this.showActionMonit(
`<span class="text--warn">Dodaj co najmniej jedną działkę rozkazu przed jego skopiowaniem!</span>`
);
const fieldsToCorrect = this.verifyOrderFields();
if (fieldsToCorrect.length > 0)
return this.showActionMonit(
`<span class="text--warn">Uzupełnij następujące rubryki na dole rozkazu przed jego skopiowaniem: ${fieldsToCorrect.join(
', '
)}</span>`
);
navigator.clipboard.writeText(this.fullOrderMessage);
if (this.incrementOnCopy) this.incrementOrderNo();
this.showActionMonit(
'<b class="text--accent">Skopiowano!</b> Możesz teraz wkleić treść rozkazu na czacie symulatora!'
);
},
saveOrder() {
const savedOrderStatus = this.saveLocalOrder();
switch (savedOrderStatus) {
case -1:
this.showActionMonit(
'<span class="text--warn">Wypełnij numer rozkazu, numer pociągu i datę zanim dodasz rozkaz!</span>'
);
break;
case 0:
this.showActionMonit(
'<span class="text--warn">Ostatni zapisany rozkaz jest identyczny z obecnym!</span>'
);
break;
case 1:
this.showActionMonit(
'Zapisano treść <b class="text--accent">rozkazu</b> w pamięci przeglądarki!'
);
if (this.incrementOnSave) this.incrementOrderNo();
break;
default:
break;
}
},
updateOrder() {
const updatedOrderStatus = this.updateLocalOrder();
switch (updatedOrderStatus) {
case -1:
this.showActionMonit(
'<span class="text--warn">Wystąpił błąd podczas aktualizowania tego rozkazu! :/</span>'
);
break;
case 0:
this.showActionMonit(
'<span class="text--warn">Nie wybrałeś żadnego zapisanego rozkazu!</span>'
);
break;
case 1:
this.showActionMonit('Zaktualizowano treść <b class="text--accent">rozkazu</b>!');
break;
}
}
}
});
</script>
<style lang="scss" scoped>
@use '../styles/colors';
.order-message {
h3 {
margin: 0;
margin-bottom: 1em;
text-align: center;
}
button {
margin: 0 0.5em;
}
}
.message_body {
height: 250px;
overflow: auto;
background-color: colors.$bgColLighter;
color: white;
text-align: justify;
border-radius: 0.5em;
padding: 0.5em;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
}
.message_info {
text-align: center;
color: #ccc;
}
.message_actions {
display: flex;
align-items: center;
justify-content: center;
margin-top: 1em;
button img {
height: 2ch;
vertical-align: text-bottom;
margin-right: 0.5em;
}
button[data-disabled='true'] {
user-select: none;
color: #aaa;
}
}
.message_checkboxes {
display: flex;
flex-direction: column;
margin-top: 1em;
}
.action_monit {
text-align: center;
padding: 1.5em;
font-size: 1.15em;
}
.monit-anim {
&-enter-active,
&-leave-active {
transition: all 100ms ease-in-out;
}
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateY(-20px);
}
}
</style>
-518
View File
@@ -1,518 +0,0 @@
<template>
<section class="order-n">
<section class="order_header" ref="header">
<h2 class="flex-center">
Rozkaz pisemny "N" nr &nbsp;
<input type="number" v-model="order.header.orderNo" placeholder="nr rozkazu" min="1" />
</h2>
<div class="flex-row">
dla pociągu nr
<input type="text" v-model="order.header.trainNo" placeholder="nr pociągu" />
dnia
<input type="text" v-model="order.header.date" placeholder="data" />
</div>
</section>
<section class="order_table-container">
<table>
<tbody>
<tr>
<td>
<label for="row-enabled-1">1</label>
<div>
<input type="checkbox" id="row-enabled-1" v-model="order.rows[0].enabled" />
</div>
</td>
<td ref="row-1">
Od
<input type="text" v-model="order.rows[0].from" holder="stacja / post." />
do
<input type="text" v-model="order.rows[0].to" holder="stacja / post." />
tor nr
<input type="text" v-model="order.rows[0].trackNo" holder="nr toru" />
jest zamknięty, ruch jednotorowy dwukierunkowy wprowadzono po torze nr
<input type="text" v-model="order.rows[0].trackNo2" holder="nr toru" />
</td>
</tr>
<tr>
<td>
<label for="row-enabled-2">2</label>
<div>
<input type="checkbox" id="row-enabled-2" v-model="order.rows[1].enabled" />
</div>
</td>
<td ref="row-2">
<strong>ZEZWALAM</strong> po otrzymaniu
<select id="select-2a" v-model="order.rows[1].option1">
<option :value="`sygnału &quot;Nakaz Jazdy&quot;`">sygnału "Nakaz Jazdy"</option>
<option value="tylko tego rozkazu pisemnego">tylko tego rozkazu pisemnego</option>
</select>
<div style="margin-top: 0.5rem">
<input
type="radio"
name="section-2"
id="checkbox-2a"
value="checkbox-2a"
v-model="order.rows[1].checkbox"
:checked="order.rows[1].checkbox == 'checkbox-2a'"
@change="handleRowCheckboxChange(2)"
/>
<label for="checkbox-2a">
przejechać obok wskazującego sygnał "Stój" semafora
<div style="margin-left: 1rem">
<input
type="radio"
name="section-2a"
id="radio-2a-1"
value="wyjazdowego"
v-model="order.rows[1].signalType"
@change="handleRowCheckboxChange(2)"
/>
<label for="radio-2a-1">
wyjazdowego
<input
type="text"
v-model="order.rows[1].signal1"
holder="nazwa sem."
:radio-checked="
order.rows[1].checkbox == 'checkbox-2a' &&
order.rows[1].signalType == 'wyjazdowego'
"
/>
</label>
<br />
<input
type="radio"
name="section-2a"
id="radio-2a-2"
value="drogowskazowego"
v-model="order.rows[1].signalType"
@change="handleRowCheckboxChange(2)"
/>
<label for="radio-2a-2">
drogowskazowego
<input
type="text"
v-model="order.rows[1].signal2"
holder="nazwa sem."
:radio-checked="
order.rows[1].checkbox == 'checkbox-2a' &&
order.rows[1].signalType == 'drogowskazowego'
"
/>
(odnoszącego się do wyjazdu pociągu)
</label>
<br />
<input
type="radio"
name="section-2a"
id="radio-2a-3"
value="wjazdowego"
v-model="order.rows[1].signalType"
@change="handleRowCheckboxChange(2)"
/>
<label for="radio-2a-3">
wjazdowego
<input
type="text"
v-model="order.rows[1].signal3"
holder="nazwa sem."
:radio-checked="
order.rows[1].checkbox == 'checkbox-2a' &&
order.rows[1].signalType == 'wjazdowego'
"
/>
na post. odg. bez sem. wyjazdowego
</label>
</div>
i wyjechać w kierunku
<input
type="text"
v-model="order.rows[1].direction1"
holder="stacja / post."
:radio-checked="order.rows[1].checkbox == 'checkbox-2a'"
/>
na tor szlakowy
<select v-model="order.rows[1].option2">
<option value="lewy">lewy</option>
<option value="prawy">prawy</option>
</select>
nr
<input
type="text"
v-model="order.rows[1].trackNoTo1"
holder="nr toru"
:radio-checked="order.rows[1].checkbox == 'checkbox-2a'"
/>
</label>
</div>
<div style="margin-top: 0.5rem">
<input
type="radio"
name="section-2"
id="checkbox-2b"
value="checkbox-2b"
v-model="order.rows[1].checkbox"
:checked="order.rows[1].checkbox == 'checkbox-2b'"
@change="handleRowCheckboxChange(2)"
/>
<label for="checkbox-2b">
z toru nr
<input
type="text"
v-model="order.rows[1].trackNoFrom"
holder="nr toru"
:radio-checked="order.rows[1].checkbox == 'checkbox-2b'"
/>
nie posiadającego semafora wyjazdowego wyjechać w kierunku
<input
type="text"
v-model="order.rows[1].direction2"
holder="stacja / post."
:radio-checked="order.rows[1].checkbox == 'checkbox-2b'"
/>
na tor szlakowy
<select v-model="order.rows[1].option3">
<option value="lewy">lewy</option>
<option value="prawy">prawy</option>
</select>
nr
<input
type="text"
v-model="order.rows[1].trackNoTo2"
holder="nr toru"
:radio-checked="order.rows[1].checkbox == 'checkbox-2b'"
/>
</label>
</div>
</td>
</tr>
<tr>
<td>
<label for="row-enabled-3">3</label>
<div>
<input type="checkbox" id="row-enabled-3" v-model="order.rows[2].enabled" />
</div>
</td>
<td ref="row-3">
<select v-model="order.rows[2].option1">
<option value="Jazda">Jazda</option>
<option value="Popychanie">Popychanie</option>
</select>
pociągu odbędzie się w kierunku:
<input type="text" v-model="order.rows[2].direction" holder="stacja / post." />
do km
<input type="text" v-model="order.rows[2].toKilometer" holder="kilometry" />
skąd
<select v-model="order.rows[2].option2">
<option value="pociąg">pociąg</option>
<option value="popychacz">popychacz</option>
</select>
ma wrócić po torze lewym nr
<input type="text" v-model="order.rows[2].trackNo" holder="nr toru" />
najpóźniej o godz.
<input type="text" v-model="order.rows[2].untilHour" holder="godzina" />
min.
<input type="text" v-model="order.rows[2].untilMin" holder="minuta" />
</td>
</tr>
<tr>
<td>
<label for="row-enabled-4">4</label>
<div>
<input type="checkbox" id="row-enabled-4" v-model="order.rows[3].enabled" />
</div>
</td>
<td ref="row-4">
<strong>WJAZD</strong> z toru szlakowego nr
<input type="text" v-model="order.rows[3].trackNo" holder="nr toru" />
na
<select v-model="order.rows[3].optionStation">
<option value="stację">stację</option>
<option value="posterunek odgałęźny">posterunek odgałęźny</option>
</select>
<input type="text" v-model="order.rows[3].stationName" holder="stacja / post." />
odbędzie się po otrzymaniu:
<div style="margin-top: 0.5rem">
<input
type="radio"
name="section-4"
id="checkbox-4a"
value="checkbox-4a"
v-model="order.rows[3].checkbox"
/>
<label for="checkbox-4a">
sygnału zastępczego "Sz" na osobnym urządzeniu ustawionym z
<select v-model="order.rows[3].side">
<option value="lewej">lewej</option>
<option value="prawej">prawej</option>
</select>
strony toru
</label>
</div>
<div style="margin-top: 0.5rem">
<input
type="radio"
name="section-4"
id="checkbox-4b"
value="checkbox-4b"
v-model="order.rows[3].checkbox"
/>
<label for="checkbox-4b">
rozkazu pisemnego "N" (doręczonego lub przekazanego przez urządzenia łączności)
</label>
</div>
</td>
</tr>
<tr>
<td>
<label for="row-enabled-5">5</label>
<div>
<input type="checkbox" id="row-enabled-5" v-model="order.rows[4].enabled" />
</div>
</td>
<td ref="row-5">
<strong>ZEZWALAM</strong> wjechać z toru szlakowego nr
<input type="text" v-model="order.rows[4].trackNo" holder="nr toru" />
z kierunku
<input type="text" v-model="order.rows[4].direction" holder="stacja / post." />
na
<select v-model="order.rows[4].stationType">
<option value="stację">stację</option>
<option value="posterunek odgałęźny">posterunek odgałęźny</option>
</select>
<input type="text" v-model="order.rows[4].stationName" holder="stacja / post." />
i przejechać obok sygnału "Stój" na
<input type="text" v-model="order.rows[4].on" holder="nazwa sygnału" />
</td>
</tr>
<tr style="height: 270px">
<td>
<label for="row-enabled-6">6</label>
<div>
<input type="checkbox" id="row-enabled-6" v-model="order.rows[5].enabled" />
</div>
</td>
<td ref="row-6">
<button
class="g-button text"
@click="order.rows[5].twoWay.enabled = !order.rows[5].twoWay.enabled"
>
&gt;
<span v-if="!order.rows[5].twoWay.enabled">
Wygeneruj treść na wprowadzenie ruchu dwukierunkowego
</span>
<span v-else>Wpisz treść własnoręcznie</span>
</button>
<div>Inne:</div>
<div v-if="order.rows[5].twoWay.enabled">
od
<input type="text" v-model="order.rows[5].twoWay.from" holder="stacja / post." />
do
<input type="text" v-model="order.rows[5].twoWay.to" holder="stacja / post." />
po torze nr
<input type="text" v-model="order.rows[5].twoWay.trackNo" holder="nr toru" />
wprowadzono ruch dwukierunkowy.
</div>
<textarea
v-else
class="others"
cols="30"
rows="10"
v-model="order.rows[5].content"
></textarea>
</td>
</tr>
</tbody>
</table>
</section>
</section>
</template>
<script lang="ts">
import { defineComponent, reactive } from 'vue';
import { useStore } from '../store/store';
import { handleOrderPlaceholders } from '../handlers/orderPlaceholderHandler';
type TOrderRows = 1 | 2 | 3 | 4 | 5;
export default defineComponent({
name: 'OrderN',
setup() {
const store = useStore();
const order = reactive(store.orderN);
const rowMethods = [
() => {
const { header } = order;
const message = `<i>Rozkaz pisemny "N" nr ${header.orderNo || '_'} dla pociągu nr ${
header.trainNo || '_'
} dnia ${header.date}</i>`;
return message;
},
() => {
const row = order.rows[0];
const message = `Od ${row.from || '_'} do ${row.to || '_'} tor nr ${
row.trackNo || '_'
} jest zamknięty, ruch jednotorowy dwukierunkowy wprowadzono po torze nr ${
row.trackNo2 || '_'
}`;
return message;
},
() => {
const row = order.rows[1];
let message = `ZEZWALAM po otrzymaniu ${row.option1 || '_'}`;
if (row.checkbox == 'checkbox-2a') {
message += ` przejechać obok wskazującego sygnał "Stój" semafora ${
row.signalType || '_'
} `;
if (row.signalType == 'wyjazdowego') message += row.signal1 || '_';
if (row.signalType == 'drogowskazowego')
message += `${row.signal2 || '_'} (odnoszącego się do wyjazdu pociągu)`;
if (row.signalType == 'wjazdowego')
message += `${row.signal3 || '_'} na post. odg. bez sem. wyjazdowego`;
message += ` i wyjechać w kierunku ${row.direction1 || '_'} na tor szlakowy ${
row.option2 || '_'
} nr ${row.trackNoTo1 || '_'}`;
}
if (row.checkbox == 'checkbox-2b') {
message += ` z toru nr ${
row.trackNoFrom || '_'
} nie posiadającego semafora wyjazdowego wyjechać w kierunku ${
row.direction2 || '_'
} na tor szlakowy ${row.option3 || '_'} nr ${row.trackNoTo2 || '_'}`;
}
return message;
},
() => {
const row = order.rows[2];
let message = `${row.option1 || '_'} pociągu odbędzie się w kierunku: ${
row.direction || '_'
} do km ${row.toKilometer || '_'} skąd ${row.option2 || '_'} ma wrócić po torze lewym nr ${
row.trackNo || '_'
} najpóźniej o godz. ${row.untilHour || '_'} min. ${row.untilMin || '_'}`;
return message;
},
() => {
const row = order.rows[3];
let message = `WJAZD z toru szlakowego nr ${row.trackNo || '_'} na ${
row.optionStation || '_'
} ${row.stationName || '_'} odbędzie się po otrzymaniu: `;
if (row.checkbox == 'checkbox-4a')
message += `sygnału zastępczego "Sz" na osobnym urządzeniu ustawionym z ${
row.side || '_'
} strony toru`;
if (row.checkbox == 'checkbox-4b')
message +=
'rozkazu pisemnego "N" (doręczonego lub przekazanego przez urządzenia łączności)';
return message;
},
() => {
const row = order.rows[4];
const message = `ZEZWALAM wjechać z toru szlakowego nr ${row.trackNo || '_'} z kierunku ${
row.direction || '_'
} na ${row.stationType || '_'} ${
row.stationName || '_'
} i przejechać obok sygnału "Stój" na ${row.on || '_'} `;
return message;
},
() => {
const row = order.rows[5];
if (row.twoWay.enabled)
return `Inne: od ${row.twoWay.from || '_'} do ${row.twoWay.to || '_'} po torze nr ${
row.twoWay.trackNo || '_'
} wprowadzono ruch dwukierunkowy.`;
return 'Inne: ' + row.content;
}
];
return {
store,
order,
rowMethods
};
},
watch: {
order: {
deep: true,
handler() {
this.generateMessage();
}
},
'order.rows': {
deep: true,
handler() {
this.updatePlaceholders();
}
}
},
mounted() {
this.updatePlaceholders();
},
activated() {
this.generateMessage();
},
methods: {
updatePlaceholders() {
this.order.rows.forEach((_, i) => {
this.handleRowCheckboxChange((i + 1) as TOrderRows);
});
},
handleRowCheckboxChange(rowIndex: TOrderRows) {
const isRowEnabled = this.order.rows[rowIndex - 1].enabled;
const rowRef = this.$refs[`row-${rowIndex}`] as HTMLTableElement;
this.$nextTick(() => {
handleOrderPlaceholders(isRowEnabled, rowRef);
});
},
generateMessage() {
let message = this.rowMethods[0]();
for (let i = 0; i < this.order.rows.length; i++) {
if (!this.order.rows[i].enabled) continue;
message += ` <b> [ ${i + 1} ] </b> ${this.rowMethods[i + 1]()}`;
}
this.store.orderMessage = message;
}
}
});
</script>
-201
View File
@@ -1,201 +0,0 @@
<template>
<section class="order-o">
<section class="order_header">
<h2 class="flex-center" style="padding: 0 0.5em">
Rozkaz pisemny "O" nr &nbsp;
<input type="number" v-model="order.header.orderNo" placeholder="nr rozkazu" min="1" />
</h2>
<div class="flex-row" style="padding: 0 0.5em">
dla pociągu nr
<input type="text" v-model="order.header.trainNo" placeholder="nr pociągu" />
dnia
<input type="text" v-model="order.header.date" />
</div>
<div class="horizontal-bar"></div>
<div style="display: flex; padding: 0 0.5em">
<b>1.</b>
<div style="margin-left: 1.5em">
1) zmniejszyć prędkość jazdy i zachować ostrożność <br />2) jechać ostrożnie (j.o.)
</div>
</div>
</section>
<section class="order_table">
<table cellborder="1">
<tbody>
<tr class="tr-header">
<td rowspan="2" width="35%">
Na posterunku, <br />
na szlaku
</td>
<td width="20%">od</td>
<td width="20%">do</td>
<td rowspan="2">1) prędkość najwyżej km/h</td>
<td rowspan="2">2) j.o.</td>
<td rowspan="2" width="35%">z powodu</td>
</tr>
<tr class="tr-header">
<td colspan="2">kilometra</td>
</tr>
<tr v-for="(row, i) in order.orderList" :key="i" class="tr-data">
<td>
<textarea v-model="row.name"></textarea>
</td>
<td>
<textarea v-model="row.from"></textarea>
</td>
<td>
<textarea v-model="row.to"></textarea>
</td>
<td>
<input type="text" v-model="row.vmax" />
</td>
<td>
<input type="checkbox" v-model="row.jo" />
</td>
<td>
<textarea v-model="row.reason"></textarea>
</td>
</tr>
</tbody>
</table>
</section>
<div class="order_other">
<span><b>2.</b> Inne:</span>
<br />
<textarea class="others" cols="30" rows="10" v-model="order.other"></textarea>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent, reactive } from 'vue';
import { useStore } from '../store/store';
export default defineComponent({
name: 'OrderO',
setup() {
const store = useStore();
const order = reactive(store.orderO);
const rowMethods = [
() => {
const { header } = order;
return `<i>Rozkaz pisemny "O" nr ${header.orderNo || '_'} dla pociągu nr ${
header.trainNo || '_'
} dnia ${header.date || '_'}</i>`;
}
];
return {
store,
order,
rowMethods
};
},
activated() {
this.generateMessage();
},
watch: {
order: {
deep: true,
handler() {
this.generateMessage();
}
}
},
methods: {
generateMessage() {
let message = this.rowMethods[0]();
if (this.order.orderList.some((row) => row.name)) message += `<b> [ 1 ] </b>`;
const rowsMessageList = [];
for (let i = 0; i < this.order.orderList.length; i++) {
const row = this.order.orderList[i];
if (!row.name) continue;
let rowMessage = '';
rowMessage += ` ${row.name || '_'} od ${row.from || '_'} do ${row.to || '_'} kilometra`;
if (row.vmax) rowMessage += ` prędkość najwyżej ${row.vmax} km/h`;
if (row.jo) rowMessage += ` jechać ostrożnie`;
rowMessage += ` z powodu: ${row.reason || '_'}`;
rowsMessageList.push(rowMessage);
}
message += rowsMessageList.join('; ');
if (this.order.other) message += ` <b> [ 2 ] </b> Inne: ${this.order.other}`;
this.store.orderMessage = message;
}
}
});
</script>
<style lang="scss" scoped>
table,
td,
th {
border: 2px solid black;
border-collapse: collapse;
}
.order_header {
padding: 0.5em 0;
}
.order_table {
.tr-header td {
padding: 1em 0.5em;
}
.tr-data td {
padding: 0.5em 0;
}
tbody {
font-weight: normal;
text-align: center;
vertical-align: middle;
}
input {
width: 80%;
}
}
.order_table {
textarea {
width: 90%;
min-height: 50px;
resize: vertical;
}
}
.order_other {
border-left: 2px solid black;
border-right: 2px solid black;
display: flex;
flex-direction: column;
padding: 0.5em;
}
</style>
-448
View File
@@ -1,448 +0,0 @@
<template>
<section class="order-s">
<section class="order_header">
<h2 class="flex-center">
Rozkaz pisemny "S" nr &nbsp;
<input type="number" v-model="order.header.orderNo" placeholder="nr rozkazu" min="1" />
</h2>
<div class="flex-row">
dla
<select id="select-header" v-model="order.header.for">
<option value="pociągu">pociągu</option>
<option value="manewru">manewru</option>
</select>
nr
<input type="text" v-model="order.header.trainNo" :placeholder="`nr ${order.header.for}`" />
dnia
<input type="text" v-model="order.header.date" placeholder="data" />
</div>
</section>
<section class="order_table-container">
<table>
<tbody>
<tr>
<td>
<label for="row-enabled-1">1</label>
<div>
<input
type="checkbox"
id="row-enabled-1"
v-model="order.rows[0].enabled"
@change="handleRowCheckboxChange(1)"
/>
</div>
</td>
<td ref="row-1">
zezwalam po otrzymaniu
<select id="select-1a" v-model="order.rows[0].option1">
<option :value="`sygnału &quot;nakaz jazdy&quot;`">sygnału "nakaz jazdy"</option>
<option value="tylko tego rozkazu pisemnego">tylko tego rozkazu pisemnego</option>
</select>
<div style="margin-top: 0.5rem">
<input
type="radio"
name="section-1a"
id="radio-1a-1"
value="radio-1a-1"
v-model="order.rows[0].radio1"
@change="handleRowCheckboxChange(1)"
/>
<label for="radio-1a-1">
przejechać obok wskazującego sygnał "Stój" semafora
<select id="select-signal" v-model="order.rows[0].optionSignal">
<option value="wyjazdowego">wyjazdowego</option>
<option value="drogowskazowego">drogowskazowego</option>
</select>
<input
type="text"
v-model="order.rows[0].signal1"
holder="nazwa sem."
:radio-checked="order.rows[0].radio1 == 'radio-1a-1'"
/>
<span v-if="order.rows[0].optionSignal == 'drogowskazowego'"> (odnoszącego się do wyjazdu pociągu)</span>
<br />
</label>
<hr />
<input
type="radio"
name="section-1a"
id="radio-1a-2"
value="radio-1a-2"
v-model="order.rows[0].radio1"
@change="handleRowCheckboxChange(1)"
/>
<label for="radio-1a-2">
wyjechać z toru nr
<input
type="text"
v-model="order.rows[0].trackNo"
holder="nr toru"
:radio-checked="order.rows[0].radio1 == 'radio-1a-2'"
/>
nie posiadającego semafora wyjazdowego
</label>
</div>
</td>
</tr>
<tr>
<td>
<label for="row-enabled-2">2</label>
<div>
<input
type="checkbox"
id="row-enabled-2"
v-model="order.rows[1].enabled"
@change="handleRowCheckboxChange(2)"
/>
</div>
</td>
<td ref="row-2">
zezwalam przejechać obok wskazującego sygnał "Stój" semafora:
<div>
<input
type="radio"
name="section-2a"
id="radio-2a-1"
value="wyjazdowego"
@change="handleRowCheckboxChange(2)"
v-model="order.rows[1].signalType"
/>
<label for="radio-2a-1">
wjazdowego
<input
type="text"
v-model="order.rows[1].signal1"
holder="nazwa sem."
:radio-checked="order.rows[1].signalType == 'wyjazdowego'"
/>
</label>
<br />
<input
type="radio"
name="section-2a"
id="radio-2a-2"
value="drogowskazowego"
@change="handleRowCheckboxChange(2)"
v-model="order.rows[1].signalType"
/>
<label for="radio-2a-2">
drogowskazowego
<input
type="text"
v-model="order.rows[1].signal2"
holder="nazwa sem."
:radio-checked="order.rows[1].signalType == 'drogowskazowego'"
/>
(odnoszącego się do wjazdu pociągu)
</label>
<br />
<input
type="radio"
name="section-2a"
id="radio-2a-3"
value="odstępowego"
@change="handleRowCheckboxChange(2)"
v-model="order.rows[1].signalType"
/>
<label for="radio-2a-3">
odstępowego
<input
type="text"
v-model="order.rows[1].signal3"
holder="nazwa sem."
:radio-checked="order.rows[1].signalType == 'odstępowego'"
/></label>
<br />
<input
type="radio"
name="section-2a"
id="radio-2a-4"
value="toru"
v-model="order.rows[1].signalType"
@change="handleRowCheckboxChange(2)"
/>
<label for="radio-2a-4">
wjechać z zamkniętego toru nr
<input
type="text"
v-model="order.rows[1].trackNo"
holder="nr toru"
:radio-checked="order.rows[1].signalType == 'toru'"
/>
nie posiadającego semafora wjazdowego
</label>
</div>
</td>
</tr>
<tr>
<td>
<label for="row-enabled-3">3</label>
<div>
<input
type="checkbox"
id="row-enabled-3"
v-model="order.rows[2].enabled"
@change="handleRowCheckboxChange(3)"
/>
</div>
</td>
<td ref="row-3">
Od
<input type="text" v-model="order.rows[2].from" holder="stacja / post." />
do
<input type="text" v-model="order.rows[2].to" holder="stacja / post." />
po torze nr
<input type="text" v-model="order.rows[2].trackNo" holder="nr toru" />
ruch pociągów prowadzony jest w odstępie posterunków następczych. Wskazania semaforów
sbl nieważne. Zachować ostrożność od ostatniego semafora ze wskaźnikiem "W18".
Szlak wolny, ostatni pociąg nr
<input type="text" v-model="order.rows[2].trainNo" holder="nr pociągu" />
przybył do
<input type="text" v-model="order.rows[2].arrivedTo" holder="stacja / post." />
o godzinie
<input type="text" v-model="order.rows[2].hour" holder="godzina" />
</td>
</tr>
<tr style="height: 270px">
<td>
<label for="row-enabled-4">4</label>
<div>
<input
type="checkbox"
id="row-enabled-4"
v-model="order.rows[3].enabled"
@change="handleRowCheckboxChange(4)"
/>
</div>
</td>
<td ref="row-4">
<button
class="g-button text"
@click="order.rows[3].w5.enabled = !order.rows[3].w5.enabled"
>
&gt;
<span v-if="!order.rows[3].w5.enabled"
>Wygeneruj treść na pominięcie wskaźnika W5</span
>
<span v-else>Wpisz treść własnoręcznie</span>
</button>
<div>Inne:</div>
<div v-if="order.rows[3].w5.enabled">
zezwalam na wyjazd poza
<select id="select-borderType" v-model="order.rows[3].w5.borderType">
<option value="wskaźnik przetaczania W5">wskaźnik przetaczania W5</option>
<option value="granicę przetaczania">granicę przetaczania</option>
</select>
po torze szlakowym nr
<input type="text" v-model="order.rows[3].w5.trackNo" holder="nr szlaku" />
do kilometra
<input type="text" v-model="order.rows[3].w5.maxKm" holder="km szlaku" />. Powrót
odbędzie się na
<select
id="select-returnWay"
v-model="order.rows[3].w5.returnWay"
style="width: 250px"
>
<option :value="`sygnał ręczny &quot;Do mnie&quot;`">
sygnał ręczny "Do mnie"
</option>
<option
:value="`sygnał &quot;Do mnie&quot; przekazany przez urządzenia radiołączności`"
>
sygnał "Do mnie" przekazany przez urządzenia radiołączności
</option>
<option value="sygnał Ms2 podany na tarczy manewrowej">
sygnał Ms2 podany na tarczy manewrowej
</option>
</select>
<input
type="text"
v-model="order.rows[3].w5.tmName"
holder="nazwa tarczy"
v-if="order.rows[3].w5.returnWay.includes('tarczy')"
/>
do godziny
<input type="text" v-model="order.rows[3].w5.maxHour" holder="godzina" />
</div>
<textarea
v-else
class="others"
cols="30"
rows="10"
v-model="order.rows[3].content"
></textarea>
</td>
</tr>
</tbody>
</table>
</section>
</section>
</template>
<script lang="ts">
import { defineComponent, reactive } from 'vue';
import { handleOrderPlaceholders } from '../handlers/orderPlaceholderHandler';
import { useStore } from '../store/store';
type TOrderRows = 1 | 2 | 3 | 4;
export default defineComponent({
name: 'OrderS',
setup() {
const store = useStore();
const order = reactive(store.orderS);
const rowMethods = [
() => {
const { header } = order;
return `<i>Rozkaz pisemny "S" nr ${header.orderNo || '_'} dla ${header.for || '_'} nr ${
header.trainNo || '_'
} dnia ${header.date || '_'}</i>`;
},
() => {
const row = order.rows[0];
let message = `zezwalam po otrzymaniu ${row.option1 || '_'}`;
if (row.radio1 == 'radio-1a-1')
message += ` przejechać obok wskazującego sygnał "Stój" semafora ${
row.optionSignal || '_'
} ${row.signal1 || '_'}${row.optionSignal == 'drogowskazowego' ? ' (odnoszącego się do wyjazdu pociągu)' : ''}`;
else
message += ` wyjechać z toru nr ${
row.trackNo || '_'
} nie posiadającego semafora wyjazdowego`;
return message;
},
() => {
const row = order.rows[1];
let message = `zezwalam przejechać obok wskazującego sygnał "Stój" semafora `;
switch (row.signalType) {
case 'wyjazdowego':
message += `wjazdowego ${row.signal1 || '_'}`;
break;
case 'drogowskazowego':
message += `drogowskazowego ${row.signal2 || '_'} (odnoszącego się do wjazdu pociągu)`;
break;
case 'odstępowego':
message += `odstępowego ${row.signal3 || '_'}`;
break;
case 'toru':
message += `wjechać z zamkniętego toru nr ${
row.trackNo || '_'
} nie posiadającego semafora wjazdowego`;
break;
default:
break;
}
return message;
},
() => {
const row = order.rows[2];
return `Od ${row.from || '_'} do ${row.to || '_'} po torze nr ${
row.trackNo || '_'
} ruch pociągów prowadzony jest w odstępie posterunków następczych. Wskazania semaforów sbl są nieważne. Zachować ostrożność od ostatniego semafora ze wskaźnikiem "W18". Szlak wolny, ostatni pociąg nr ${
row.trainNo || '_'
} przybył do ${row.arrivedTo || '_'} o godzinie ${row.hour || '_'}`;
},
() => {
const row = order.rows[3];
if (row.w5.enabled) {
const { borderType, trackNo, maxHour, maxKm, returnWay, tmName } = row.w5;
const textArray = [];
textArray.push(
'Inne: zezwalam na wyjazd poza',
borderType || '_',
'po torze szlakowym nr',
trackNo || '_'
);
if (maxKm) textArray.push(`do kilometra ${maxKm}`);
textArray.push('.');
textArray.push('Powrót odbędzie się na', returnWay || '_');
if (returnWay.includes('tarczy')) textArray.push(tmName || '_');
textArray.push(`do godziny ${maxHour || '_'}`);
return textArray.join(' ').replace(/ \./, '.');
}
return `Inne: ${row.content}`;
}
];
return {
store,
order,
rowMethods
};
},
activated() {
this.generateMessage();
},
mounted() {
this.updatePlaceholders();
},
watch: {
order: {
deep: true,
handler() {
this.generateMessage();
}
},
'order.rows': {
deep: true,
handler() {
this.updatePlaceholders();
}
}
},
methods: {
updatePlaceholders() {
this.order.rows.forEach((_, i) => {
this.handleRowCheckboxChange((i + 1) as TOrderRows);
});
},
generateMessage() {
let message = this.rowMethods[0]();
for (let i = 0; i < 4; i++) {
if (!this.order.rows[i].enabled) continue;
message += ` <b> [ ${i + 1} ] </b> ${this.rowMethods[i + 1]()}`;
}
this.store.orderMessage = message;
},
handleRowCheckboxChange(rowIndex: TOrderRows) {
const isRowEnabled = this.order.rows[rowIndex - 1].enabled;
const rowRef = this.$refs[`row-${rowIndex}`] as HTMLTableElement;
this.$nextTick(() => {
handleOrderPlaceholders(isRowEnabled, rowRef);
});
}
}
});
</script>
-365
View File
@@ -1,365 +0,0 @@
<template>
<div class="order-train-picker">
<div class="options">
<div class="options-top">
<select
name="dispatcher-select"
id="dispatcher-select"
v-model="selectedSceneryId"
@change="selectOption"
>
<option :value="null" disabled>Sceneria</option>
<option
v-for="scenery in filteredSceneries"
:value="`${scenery.stationName}|${scenery.stationHash}|${scenery.dispatcherName}|${scenery.region}`"
:key="scenery.dispatcherName + scenery.stationName"
>
{{ scenery.stationName }} &bull; {{ scenery.dispatcherName }}
</option>
</select>
<select
name="region-select"
id="region-select"
v-model="selectedRegion"
@change="selectOption"
>
<option :value="null" disabled>Region</option>
<option v-for="region in regions" :value="region" :key="region">
{{ getRegionNameById(region) }}
</option>
<!-- <option
v-for="scenery in filteredSceneries"
:value="`${scenery.stationName}|${scenery.stationHash}|${scenery.dispatcherName}|${scenery.region}`"
:key="scenery.dispatcherName + scenery.stationName"
>
</option> -->
</select>
</div>
<select
name="checkpoint-select"
id="checkpoint-select"
v-model="selectedCheckpointName"
:disabled="!selectedScenery"
>
<option :value="null" disabled>Posterunek</option>
<option :value="cp" v-for="cp in checkpointNameList" :key="cp">
{{ cp }}
</option>
</select>
<label for="fill-checkpoint" class="g-checkbox">
<input
type="checkbox"
name="fill-checkpoint"
id="fill-checkpoint"
v-model="fillCheckpointName"
/>
<span> Uzupełniaj skrót wybranego posterunku</span>
</label>
</div>
<div class="content">
<b v-if="!selectedSceneryId" class="text--accent">
Wybierz dyżurnego oraz scenerię, aby zobaczyć pociągi
</b>
<div v-else>
<div style="margin-bottom: 0.5em">
<h3 style="margin-bottom: 0.5em">Aktywne RJ i gracze na scenerii</h3>
<b class="text--accent">Kliknij na gracza, aby wypełnić obecny rozkaz jego danymi</b>
</div>
<ul class="train-list">
<li
v-for="train in sceneryTrains"
:key="train.trainNo + train.driverName"
@click="fillOrder(train.trainNo)"
>
<button class="g-button">
<span
v-if="train.currentStationName == selectedScenery?.stationName"
class="online-indicator"
></span>
<b>
{{ train.driverName }} &bull;
<span v-if="train.timetable" style="color: gold">{{
train.timetable.category
}}</span>
{{ train.trainNo }}
</b>
</button>
</li>
<li class="no-trains" v-if="sceneryTrains?.length == 0 && selectedSceneryId">
Brak graczy
</li>
</ul>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../store/store';
import {
currentFormattedDate,
currentFormattedHours,
currentFormattedMinutes
} from '../utils/dateUtils';
import http from '../http';
import { ISceneryData } from '../types/dataTypes';
import { API } from '../types/apiTypes';
import { getRegionNameById } from '../utils/sceneryUtils';
export default defineComponent({
name: 'order-train-picker',
data() {
return {
sceneriesData: undefined as ISceneryData[] | undefined,
activeData: undefined as API.ActiveData.Response | undefined,
selectedSceneryId: null as string | null,
selectedCheckpointName: null as string | null,
selectedRegion: 'eu',
fillCheckpointName: false,
refreshInterval: -1,
store: useStore(),
regions: ['eu', 'cae', 'usw', 'us', 'ru']
};
},
created() {
this.fillCheckpointName = window.localStorage.getItem('fill-checkpoint') == 'true';
this.fetchSceneriesData();
},
async activated() {
await this.fetchActiveData();
this.handleQueries();
this.refreshInterval = window.setInterval(() => {
this.fetchActiveData();
}, 25 * 1000);
},
deactivated() {
window.clearInterval(this.refreshInterval);
},
watch: {
fillCheckpointName(val: boolean) {
window.localStorage.setItem('fill-checkpoint', `${val}`);
}
},
computed: {
selectedScenery() {
return this.activeData?.activeSceneries?.find(
(scenery) =>
this.selectedSceneryId ==
`${scenery.stationName}|${scenery.stationHash}|${scenery.dispatcherName}|${scenery.region}` &&
this.selectedRegion == scenery.region
);
},
filteredSceneries() {
return this.activeData?.activeSceneries
?.filter((s) => s.isOnline && s.region == this.selectedRegion)
.sort((s1, s2) => s1.stationName.localeCompare(s2.stationName));
},
checkpointNameList() {
if (!this.selectedScenery) return [];
const checkpoints =
this.sceneriesData?.find((s) => s.name == this.selectedScenery?.stationName)?.checkpoints ??
'';
if (checkpoints.length == 0) return [this.selectedScenery.stationName];
return checkpoints.split(';');
},
sceneryTrains() {
if (!this.selectedScenery || !this.activeData?.trains) return [];
const scenery = this.selectedScenery;
return this.activeData.trains
?.filter(
(t) =>
(t.currentStationName == scenery.stationName &&
t.region == scenery.region &&
(t.online || t.lastSeen >= Date.now() - 60000)) ||
t.timetable?.path.includes(`${scenery.stationName} ${scenery.stationHash}.sc`)
)
.sort((t1, t2) => {
return (
(t2.currentStationName == scenery.stationName ? 1 : -1) -
(t1.currentStationName == scenery.stationName ? 1 : -1) ||
t1.driverName.localeCompare(t2.driverName)
);
});
}
},
methods: {
getRegionNameById,
async fetchSceneriesData() {
const data: ISceneryData[] = (await http.get<ISceneryData[]>('api/getSceneries')).data;
this.sceneriesData = data;
},
async fetchActiveData() {
const data: API.ActiveData.Response = await (await http.get('api/getActiveData')).data;
this.activeData = data;
},
selectOption() {
this.selectedCheckpointName =
this.checkpointNameList.length == 0 ? null : this.checkpointNameList[0];
},
fillOrder(trainNo: number) {
if (!this.selectedScenery) return;
const chosenOrder = this.store[this.store.chosenOrderType];
chosenOrder.header.trainNo = trainNo.toString();
chosenOrder.header.date = currentFormattedDate();
this.store.orderFooter.dispatcherName = this.selectedScenery.stationName;
this.store.orderFooter.stationName =
this.selectedCheckpointName?.split(',')[0] || this.selectedScenery.stationName;
this.store.orderFooter.hour = currentFormattedHours();
this.store.orderFooter.minutes = currentFormattedMinutes();
if (this.fillCheckpointName) {
const sceneryAbbrev = this.sceneriesData?.find(
({ name }) => name === this.selectedSceneryId
)?.abbr;
this.store.orderFooter.checkpointName =
sceneryAbbrev || this.store.orderFooter.stationName.slice(0, 2);
}
this.store.orderMode = 'OrderMessage';
},
handleQueries() {
const query = new URLSearchParams(window.location.search);
const id = query.get('sceneryId');
if (id) {
const [sceneryName, sceneryRegion] = id.split('|');
this.selectedRegion = sceneryRegion;
const queryScenery = this.activeData?.activeSceneries?.find(
(sc) => sc.stationName == sceneryName && sc.region == sceneryRegion && sc.isOnline
);
if (queryScenery) {
this.selectedSceneryId = `${queryScenery.stationName}|${queryScenery.stationHash}|${queryScenery.dispatcherName}|${queryScenery.region}`;
console.log(this.selectedRegion);
this.selectOption();
this.store.orderMode = 'OrderTrainPicker';
}
}
}
}
});
</script>
<style lang="scss" scoped>
@use '../styles/colors';
.order-train-picker {
display: flex;
flex-direction: column;
align-items: center;
overflow: auto;
padding: 0 0.5em;
}
.options {
display: flex;
flex-wrap: wrap;
width: 100%;
gap: 0.5em;
select {
background-color: colors.$bgColDarker;
font-size: 1em;
width: 100%;
&[disabled] {
color: gray;
}
}
}
.options-top {
display: grid;
grid-template-columns: 3fr auto;
gap: 0.5em;
width: 100%;
}
.content {
margin-top: 1em;
width: 100%;
text-align: center;
}
ul.train-list {
padding: 1px;
li.no-trains {
font-weight: bold;
background-color: colors.$bgColDarker;
padding: 0.5em;
margin-top: 0.5em;
}
li > button {
width: 100%;
background-color: colors.$bgColDarker;
padding: 0.5em;
margin-top: 0.5em;
&:hover {
background-color: colors.$bgColLighter;
}
&:focus-visible {
outline: 1px solid colors.$accentCol;
}
}
}
.online-indicator {
display: inline-block;
width: 9px;
height: 9px;
vertical-align: middle;
background-color: greenyellow;
border-radius: 100%;
margin: 0 5px;
}
</style>
+239
View File
@@ -0,0 +1,239 @@
<template>
<section class="order-list">
<h3>{{ t('order-list.title') }} ({{ storageOrderList.length }})</h3>
<transition-group name="list" tag="ul">
<li class="no-orders-warning" v-if="sortedOrderList.length == 0" :key="-1">
{{ t('order-list.no-saved-orders') }}
</li>
<li
v-for="order in sortedOrderList"
:selected="order.id == store.chosenLocalOrderId"
:key="order.id"
>
<b class="text--accent">#{{ order.id.split('-')[2] }}&nbsp;</b>
<b>
{{
t('order-list.order-title', {
trainNo: order.orderData.header.A
})
}}
</b>
<span
v-if="!order.orderVersion || order.orderVersion != ORDER_VERSION"
class="wrong-order-indicator"
tabindex="0"
:data-tooltip="t('order-list.warning-deprecated-version')"
>&#9888;
</span>
<div>
{{
t('order-list.order-subtitle', [
order.orderData.instructions
.filter((v) => v.active)
.map((v) => v.name)
.join(', ')
])
}}
</div>
<div>
{{ t(`order-list.order-${order.createdAt ? 'added' : 'updated'}`) }}
{{ new Date(order.createdAt || order.updatedAt || 0).toLocaleString('pl-PL') }}
</div>
<hr />
<div class="buttons">
<button class="g-button" @click="selectLocalOrder(order)">
{{ t('order-list.button-order-select') }}
</button>
<button class="g-button" @click="removeOrder(order.id)">
{{ t('order-list.button-order-remove') }}
</button>
</div>
</li>
</transition-group>
</section>
</template>
<script lang="ts" setup>
import { computed, onActivated, onMounted, Reactive, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from '../../store/store';
import { IStorageOrderData, LocalStorageOrderLegacy } from '../../types/orderTypes';
import StorageManager from '../../managers/storageManager';
const { t } = useI18n();
const store = useStore();
const storageOrderList = reactive<Reactive<IStorageOrderData[]>>([]);
const ORDER_VERSION = import.meta.env['VITE_APP_ORDER_VERSION'];
function removeOrder(orderId: string) {
StorageManager.removeValue(orderId);
if (store.chosenLocalOrderId == orderId) store.chosenLocalOrderId = '';
const orderIndex = storageOrderList.findIndex((o) => o.id == orderId);
if (orderIndex != -1) storageOrderList.splice(orderIndex, 1);
if (storageOrderList.length == 0) StorageManager.setNumericValue('orderCountV3', 0);
}
function selectLocalOrder(order: IStorageOrderData) {
Object.entries(order.orderData.header).forEach(([k, v]) => {
(store.orderData['header'] as any)[k] = v;
});
Object.entries(order.orderData.footer).forEach(([k, v]) => {
(store.orderData['footer'] as any)[k] = v;
});
Object.entries(order.orderData.instructions).forEach(([k, v]) => {
(store.orderData['instructions'] as any)[k] = v;
});
store.panelMode = 'OrderMessagePanel';
store.chosenLocalOrderId = order.id;
}
function isOrderDeprecated(
order: IStorageOrderData | LocalStorageOrderLegacy
): order is LocalStorageOrderLegacy {
return 'orderType' in order;
}
const sortedOrderList = computed(() => {
return storageOrderList
.slice()
.sort((a, b) => (b.createdAt || b.updatedAt || 0) - (a.createdAt || a.updatedAt || 0));
});
onActivated(() => {
const localStorage = window.localStorage;
const orderList: IStorageOrderData[] = [];
let deprecatedOrders: string[] = [];
for (let key in localStorage) {
if (!/^order-/g.test(key)) continue;
const orderObj: IStorageOrderData | LocalStorageOrderLegacy = JSON.parse(localStorage[key]);
if (!orderObj) continue;
if (isOrderDeprecated(orderObj)) {
console.warn(`Deprecated order found with ID: ${orderObj.id}`);
deprecatedOrders.push(key);
continue;
}
orderList.push(orderObj);
}
storageOrderList.length = 0;
storageOrderList.push(...orderList);
if (deprecatedOrders.length > 0) {
window.alert(
t('order-list.warning-removed-deprecated-orders', { count: deprecatedOrders.length })
);
deprecatedOrders.forEach((orderKey) => StorageManager.removeValue(orderKey));
StorageManager.removeValue('orderCount');
}
});
</script>
<style lang="scss" scoped>
@use '../../styles/colors';
.list {
&-move,
&-enter-active,
&-leave-active {
transition: all 250ms ease;
}
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateY(30px);
}
&-leave-active {
position: absolute;
width: 100%;
}
}
.order-list {
overflow: auto;
}
hr {
border: 1px solid #aaa;
height: 0;
}
ul {
overflow: hidden;
position: relative;
}
h3 {
position: sticky;
top: 0;
z-index: 100;
margin: 0;
margin-bottom: 1em;
text-align: center;
background-color: colors.$bgColDarker;
padding: 1em;
margin-bottom: 0.5em;
}
li {
text-align: left;
padding: 1em;
margin: 0.5em;
background-color: colors.$bgColDarker;
cursor: pointer;
&[selected='true'] {
outline: 1px solid colors.$accentCol;
}
&.no-orders-warning {
text-align: center;
font-size: 1.2em;
cursor: default;
}
}
.wrong-order-indicator {
color: colors.$accentCol;
padding: 0 0.25em;
}
.buttons {
display: flex;
gap: 0.5em;
button {
padding: 0.5em;
background-color: colors.$bgColLighter;
&:hover {
background-color: #666;
}
&:focus-visible {
outline: 2px solid colors.$accentCol;
}
}
}
</style>
+491
View File
@@ -0,0 +1,491 @@
<template>
<section class="order-message">
<h3>{{ $t('order-message.title') }}</h3>
<div class="message_body" v-html="orderMessagePreview"></div>
<p class="message_info">
{{ $t('order-message.info') }}
</p>
<div class="message_actions">
<button class="g-button action icon" @click="saveOrder">
<LucideSave />
{{ $t('order-message.button-save') }}
</button>
<button class="g-button action icon" @click="copyMessage">
<LucideCopy />
{{ $t('order-message.button-copy') }}
</button>
<button
class="g-button action icon"
:data-disabled="!store.chosenLocalOrderId"
@click="updateOrder"
>
<LucidePencil />
{{ $t('order-message.button-update') }}
<span class="text--accent">
{{ store.chosenLocalOrderId && `#${store.chosenLocalOrderId.split('-')[2]}` }}
</span>
</button>
<button class="g-button action icon" @click="resetOrder">
<LucideRotateCcw />
{{ $t('order-message.button-reset') }}
</button>
</div>
<div class="message_checkboxes">
<label for="copy-increment" class="g-checkbox">
<input
type="checkbox"
name="copy-increment"
id="copy-increment"
v-model="incrementOnCopy"
@change="onCheckboxChange"
/>
<span>{{ $t('order-options.update-number-on-copy') }}</span>
</label>
<label for="save-increment" class="g-checkbox">
<input
type="checkbox"
name="save-increment"
id="save-increment"
v-model="incrementOnSave"
@change="onCheckboxChange"
/>
<span>{{ $t('order-options.update-number-on-save') }}</span>
</label>
<label for="update-date" class="g-checkbox">
<input
type="checkbox"
name="update-date"
id="update-date"
v-model="updateDate"
@change="onCheckboxChange"
/>
<span>{{ $t('order-options.update-hours') }}</span>
</label>
</div>
<transition name="monit-anim">
<div
class="action_monit"
v-if="actionMonit.content"
v-html="actionMonit.content"
:class="{
'text--warn': actionMonit.type == 'warning'
}"
></div>
</transition>
</section>
</template>
<script lang="ts" setup>
import { computed, onMounted, Reactive, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { LucideCopy, LucidePencil, LucideRotateCcw, LucideSave } from 'lucide-vue-next';
import { useStore } from '../../store/store';
import { IOrderHeader, IOrderFooter, IStorageOrderData } from '../../types/orderTypes';
import StorageManager from '../../managers/storageManager';
import { getOrderFullId } from '../../utils/orderUtils';
type TActionMonitType = 'warning' | 'info' | 'success';
interface IActionMonit {
type: TActionMonitType;
content: string;
timeoutId: number | null;
}
const { t } = useI18n();
const store = useStore();
const actionMonit: Reactive<IActionMonit> = reactive({
visible: false,
type: 'info',
content: '',
timeoutId: null
});
const incrementOnSave = ref(true);
const incrementOnCopy = ref(true);
const updateDate = ref(true);
onMounted(() => {
incrementOnSave.value = StorageManager.getBooleanValue('save-increment');
incrementOnCopy.value = StorageManager.getBooleanValue('copy-increment');
updateDate.value = StorageManager.getBooleanValue('update-date');
});
const orderMessagePreview = computed(() => store.orderMessage);
watch(orderMessagePreview, () => {
if (updateDate.value == true) {
store.orderData.header.B = new Date().toISOString().split('T')[0];
store.orderData.footer.Y = new Date().toLocaleTimeString('pl-PL', {
hour: 'numeric',
minute: '2-digit'
});
}
});
function onCheckboxChange(e: Event) {
const checkbox = e.target as HTMLInputElement;
StorageManager.setBooleanValue(checkbox.id, checkbox.checked);
}
function showActionMonit(content: string, type: TActionMonitType) {
if (actionMonit.timeoutId != null) {
actionMonit.content = '';
clearTimeout(actionMonit.timeoutId);
setTimeout(() => {
actionMonit.content = content;
actionMonit.type = type;
actionMonit.timeoutId = window.setTimeout(() => {
actionMonit.content = '';
actionMonit.timeoutId = null;
}, 5000);
}, 100);
return;
}
actionMonit.content = content;
actionMonit.type = type;
actionMonit.timeoutId = window.setTimeout(() => {
actionMonit.content = '';
actionMonit.timeoutId = null;
}, 5000);
}
function checkConflicts() {
if (
store.orderData.instructions
.filter((i) => i.key == '2110' || i.key == '2115')
.every((i) => i.active)
) {
showActionMonit(
t('order-message.warning-conflicting-instructions', ['21.10', '21.15']),
'warning'
);
return true;
}
return false;
}
function areOrderFieldsCorrect() {
const fieldsToCorrect: string[] = [];
for (let headerKey in store.orderData.header) {
if (store.orderData.header[headerKey as keyof IOrderHeader].trim() == '') {
fieldsToCorrect.push(headerKey);
}
}
for (let footerKey in store.orderData.footer) {
if (store.orderData.footer[footerKey as keyof IOrderFooter].trim() == '') {
fieldsToCorrect.push(footerKey);
}
}
const areConflicting = checkConflicts();
if (areConflicting) return false;
// Header & footer fields check
if (fieldsToCorrect.length > 0) {
showActionMonit(t('order-message.warning-fill-missing'), 'warning');
return false;
}
// Active instructions' fields check
let hasAllInputsFilled = true,
hasNoActiveInstructions = true;
for (const instructionKey in store.orderData.instructions) {
const instruction = store.orderData.instructions[instructionKey];
if (!instruction.active) continue;
hasNoActiveInstructions = false;
for (const fieldKey in instruction.inputFields) {
const fieldValue = instruction.inputFields[fieldKey];
if (fieldValue.trim() == '' && !instruction.optionalFieldNames.includes(fieldKey)) {
hasAllInputsFilled = false;
break;
}
}
if (instruction.listFields) {
let hasAtLeastOneActive = false;
for (const listFieldKey in instruction.listFields) {
const listField = instruction.listFields[listFieldKey];
if (listField.active == false) continue;
hasAtLeastOneActive = true;
for (const fieldKey in listField.values) {
const fieldValue = listField.values[fieldKey];
if (fieldValue.trim() == '') {
hasAllInputsFilled = false;
break;
}
}
}
if (!hasAtLeastOneActive) {
hasAllInputsFilled = false;
break;
}
}
}
// Active instructions check
if (hasNoActiveInstructions) {
showActionMonit(t('order-message.warning-add-instruction'), 'warning');
return false;
}
if (!hasAllInputsFilled) {
showActionMonit(t('order-message.warning-fill-inputs'), 'warning');
return false;
}
return true;
}
function hasHeaderFieldsComplete() {
return Object.values(store.orderData.header).every((v) => {
return v.trim().length != 0;
});
}
function incrementOrderNo() {
const idData = store.orderData.footer.Z.split('-');
if (idData.length == 4) {
const sceneryHash = idData[2];
let orderNumber = Number(idData[1]) || 0;
store.orderData.footer.Z = getOrderFullId(++orderNumber, sceneryHash);
}
}
function copyMessage() {
if (!navigator.clipboard)
return showActionMonit(t('order-message.warning-outdated-clipboard'), 'warning');
const areFieldsCorrect = areOrderFieldsCorrect();
if (!areFieldsCorrect) return;
const simulatorChatMessage = '\n' + orderMessagePreview.value.replace(/<br \/>/g, '\n');
navigator.clipboard.writeText(simulatorChatMessage);
if (incrementOnCopy.value) incrementOrderNo();
showActionMonit(t('order-message.success-copy-html'), 'success');
}
function saveOrder() {
if (!hasHeaderFieldsComplete()) {
showActionMonit(`${t('order-message.warning-fill-top-save')}`, 'warning');
return;
}
const orderDataToSave: IStorageOrderData = {
id: '',
createdAt: Date.now(),
orderVersion: import.meta.env['VITE_APP_ORDER_VERSION'] || '3',
orderData: store.orderData
};
const localOrderCount = StorageManager.getNumericValue('orderCountV3') || 0;
if (localOrderCount == 0) StorageManager.setNumericValue('orderCountV3', 0);
const prevLocalOrder = StorageManager.getValue(`order-v3-${localOrderCount}`);
if (prevLocalOrder) {
try {
const prevOrderObj = JSON.parse(prevLocalOrder) as IStorageOrderData;
if (JSON.stringify(prevOrderObj.orderData) == JSON.stringify(orderDataToSave.orderData)) {
showActionMonit(t('order-message.warning-order-identical'), 'warning');
return;
}
} catch (error) {
console.error(
`Ups! An error occured when trying to parse previous local order (count: ${localOrderCount})`
);
}
}
const nextOrderCount = localOrderCount + 1;
const nextOrderId = `order-v3-${nextOrderCount}`;
orderDataToSave['id'] = nextOrderId;
StorageManager.setNumericValue('orderCountV3', nextOrderCount);
StorageManager.setValue(nextOrderId, JSON.stringify(orderDataToSave));
store.chosenLocalOrderId = nextOrderId;
showActionMonit(t('order-message.success-save-html'), 'success');
if (incrementOnSave.value) incrementOrderNo();
}
function updateOrder() {
if (!store.chosenLocalOrderId) {
showActionMonit(t('order-message.warning-no-order-selected'), 'warning');
return;
}
if (!hasHeaderFieldsComplete()) {
showActionMonit(t('order-message.warning-fill-top-update'), 'warning');
return;
}
const localOrder = window.localStorage.getItem(store.chosenLocalOrderId);
if (!localOrder) {
showActionMonit(t('order-message.error-update'), 'warning');
return;
}
const orderDataToUpdate: IStorageOrderData = {
id: store.chosenLocalOrderId,
orderData: store.orderData,
updatedAt: Date.now(),
orderVersion: import.meta.env['VITE_APP_ORDER_VERSION'] || '3'
};
window.localStorage.setItem(store.chosenLocalOrderId, JSON.stringify(orderDataToUpdate));
showActionMonit(t('order-message.success-update-html'), 'success');
}
function resetOrder() {
Object.keys(store.orderData.header).forEach((k) => {
store.orderData['header'][k as keyof IOrderHeader] = '';
});
Object.keys(store.orderData.footer).forEach((k) => {
store.orderData['footer'][k as keyof IOrderFooter] = '';
});
store.orderData.instructions.forEach((instruction) => {
instruction.active = false;
Object.keys(instruction.inputFields).forEach((k) => {
instruction.inputFields[k] = '';
});
if (instruction.listFields) {
instruction.listFields.forEach((field) => {
Object.keys(field.values).forEach((k) => {
field.active = false;
field.values[k] = '';
});
});
}
});
}
</script>
<style lang="scss" scoped>
@use '../../styles/colors';
.order-message {
h3 {
margin: 0;
margin-bottom: 1em;
text-align: center;
}
button {
margin: 0 0.5em;
}
}
.message_body {
height: 350px;
overflow: auto;
background-color: colors.$bgColLighter;
color: white;
text-align: justify;
border-radius: 0.5em;
padding: 0.5em;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
}
.message_info {
text-align: center;
color: #ccc;
}
.message_actions {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5em;
button.icon {
gap: 0.5em;
}
button img {
height: 2ch;
vertical-align: text-bottom;
margin-right: 0.5em;
}
button[data-disabled='true'] {
user-select: none;
color: #aaa;
}
}
.message_checkboxes {
display: flex;
flex-direction: column;
margin-top: 1em;
}
.action_monit {
text-align: center;
padding: 1.5em;
font-size: 1.15em;
}
.monit-anim {
&-enter-active,
&-leave-active {
transition: all 100ms ease-in-out;
}
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateY(-20px);
}
}
</style>
@@ -0,0 +1,337 @@
<template>
<div class="order-train-picker">
<div class="options">
<div class="options-top">
<select
name="dispatcher-select"
id="dispatcher-select"
v-model="selectedSceneryId"
@change="selectCheckpointOption"
>
<option :value="null" disabled>
{{ $t('order-train-picker.placeholder-scenery-name') }}
</option>
<option
v-for="scenery in filteredSceneries"
:value="`${scenery.stationName}|${scenery.stationHash}|${scenery.dispatcherName}|${scenery.region}`"
:key="scenery.dispatcherName + scenery.stationName"
>
{{ scenery.stationName }} &bull; {{ scenery.dispatcherName }}
</option>
</select>
<select
name="region-select"
id="region-select"
v-model="selectedRegion"
@change="selectCheckpointOption"
>
<option :value="null" disabled>
{{ $t('order-train-picker.placeholder-region-name') }}
</option>
<option v-for="region in regions" :value="region" :key="region">
{{ getRegionNameById(region) }}
</option>
<!-- <option
v-for="scenery in filteredSceneries"
:value="`${scenery.stationName}|${scenery.stationHash}|${scenery.dispatcherName}|${scenery.region}`"
:key="scenery.dispatcherName + scenery.stationName"
>
</option> -->
</select>
</div>
<select
name="checkpoint-select"
id="checkpoint-select"
v-model="selectedCheckpointName"
:disabled="!selectedScenery"
>
<option :value="null" disabled>
{{ $t('order-train-picker.placeholder-checkpoint-name') }}
</option>
<option :value="cp" v-for="cp in checkpointNameList" :key="cp">
{{ cp }}
</option>
</select>
</div>
<div class="content">
<b v-if="!selectedSceneryId" class="text--accent">
{{ $t('order-train-picker.info') }}
</b>
<div v-else>
<div style="margin-bottom: 0.5em">
<h3 style="margin-bottom: 0.5em">{{ $t('order-train-picker.title') }}</h3>
<b class="text--accent">{{ $t('order-train-picker.subtitle') }}</b>
</div>
<ul class="train-list">
<li
v-for="train in sceneryTrains"
:key="train.trainNo + train.driverName"
@click="fillOrderData(train)"
>
<button class="g-button">
<span
v-if="train.currentStationName == selectedScenery?.stationName"
class="online-indicator"
></span>
<span>
{{ train.driverName }} &bull;
<span v-if="train.timetable" style="color: gold">{{
train.timetable.category
}}</span>
{{ train.trainNo }}
</span>
</button>
</li>
<li class="no-trains" v-if="sceneryTrains?.length == 0 && selectedSceneryId">
{{ $t('order-train-picker.no-trains') }}
</li>
</ul>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onActivated, onDeactivated, computed } from 'vue';
import http from '../../http';
import { useStore } from '../../store/store';
import { API } from '../../types/apiTypes';
import { ISceneryData } from '../../types/dataTypes';
import StorageManager from '../../managers/storageManager';
import { getRegionNameById } from '../../utils/sceneryUtils';
import { getOrderFullId } from '../../utils/orderUtils';
const store = useStore();
const regions = ['eu', 'cae', 'usw', 'us', 'ru'];
const refreshInterval = ref(-1);
let sceneriesData = ref<ISceneryData[] | null>(null);
let activeData = ref<API.ActiveData.Response | null>(null);
const selectedSceneryId = ref<string | null>(null);
const selectedCheckpointName = ref<string | null>(null);
const selectedRegion = ref('eu');
onMounted(() => {
fetchSceneriesData();
});
onActivated(async () => {
await fetchActiveData();
handleQueries();
window.clearInterval(refreshInterval.value);
refreshInterval.value = window.setInterval(() => {
fetchActiveData();
}, 25000);
});
onDeactivated(() => {
window.clearInterval(refreshInterval.value);
});
const selectedScenery = computed(() => {
if (activeData.value == null) return null;
return (
activeData.value.activeSceneries?.find(
(scenery) =>
selectedSceneryId.value ==
`${scenery.stationName}|${scenery.stationHash}|${scenery.dispatcherName}|${scenery.region}` &&
selectedRegion.value == scenery.region
) ?? null
);
});
const filteredSceneries = computed(() => {
return activeData.value?.activeSceneries
?.filter((s) => s.isOnline && s.region == selectedRegion.value)
.sort((s1, s2) => s1.stationName.localeCompare(s2.stationName));
});
const checkpointNameList = computed(() => {
if (!selectedScenery.value) return [];
const checkpoints =
sceneriesData.value?.find((s) => s.name == selectedScenery.value?.stationName)?.checkpoints ??
'';
if (checkpoints.length == 0) return [selectedScenery.value.stationName];
return checkpoints.split(';');
});
const sceneryTrains = computed(() => {
if (!selectedScenery.value || !activeData.value?.trains) return [];
const scenery = selectedScenery.value;
return activeData.value.trains
?.filter(
(t) =>
(t.currentStationName == scenery.stationName &&
t.region == scenery.region &&
(t.online || t.lastSeen >= Date.now() - 60000)) ||
t.timetable?.path.includes(`${scenery.stationName} ${scenery.stationHash}.sc`)
)
.sort((t1, t2) => {
return (
(t2.currentStationName == scenery.stationName ? 1 : -1) -
(t1.currentStationName == scenery.stationName ? 1 : -1) ||
t1.driverName.localeCompare(t2.driverName)
);
});
});
async function fetchSceneriesData() {
const data = (await http.get<ISceneryData[]>('api/getSceneries')).data;
sceneriesData.value = data ?? null;
}
async function fetchActiveData() {
const data = (await http.get<API.ActiveData.Response>('api/getActiveData')).data;
activeData.value = data ?? null;
}
function selectCheckpointOption() {
selectedCheckpointName.value =
checkpointNameList.value.length == 0 ? null : checkpointNameList.value[0];
}
function fillOrderData(train: API.ActiveTrains.Data) {
if (!selectedScenery.value) return;
const scenery = selectedScenery.value;
store.orderData.header.A = train.trainNo.toString();
store.orderData.header.C = train.currentStationName;
store.orderData.header.D = selectedCheckpointName.value || scenery.stationName;
store.orderData.footer.V = train.driverName;
store.orderData.footer.W = scenery.dispatcherName;
const idData = store.orderData.footer.Z.split('-');
if (idData.length != 4) {
store.orderData.footer.Z = getOrderFullId(0, scenery.stationHash);
} else {
store.orderData.footer.Z = getOrderFullId(Number(idData[1]) || 0, scenery.stationHash);
}
store.panelMode = 'OrderMessagePanel';
}
function handleQueries() {
const query = new URLSearchParams(window.location.search);
const id = query.get('sceneryId');
if (id) {
const [sceneryName, sceneryRegion] = id.split('|');
selectedRegion.value = sceneryRegion;
const queryScenery = activeData.value?.activeSceneries?.find(
(sc) => sc.stationName == sceneryName && sc.region == sceneryRegion && sc.isOnline
);
if (queryScenery) {
selectedSceneryId.value = `${queryScenery.stationName}|${queryScenery.stationHash}|${queryScenery.dispatcherName}|${queryScenery.region}`;
selectCheckpointOption();
store.panelMode = 'OrderTrainPickerPanel';
}
}
}
</script>
<style lang="scss" scoped>
@use '../../styles/colors';
.order-train-picker {
display: flex;
flex-direction: column;
align-items: center;
overflow: auto;
padding: 0.5em;
}
.options {
display: flex;
flex-wrap: wrap;
width: 100%;
gap: 0.5em;
select {
background-color: colors.$bgColDarker;
font-size: 1em;
width: 100%;
&[disabled] {
color: gray;
}
}
}
.options-top {
display: grid;
grid-template-columns: 3fr auto;
gap: 0.5em;
width: 100%;
}
.content {
margin-top: 1em;
width: 100%;
text-align: center;
}
ul.train-list {
padding: 1px;
li.no-trains {
font-weight: bold;
background-color: colors.$bgColDarker;
padding: 0.5em;
margin-top: 0.5em;
}
li > button {
width: 100%;
background-color: colors.$bgColDarker;
padding: 0.5em;
margin-top: 0.5em;
&:hover {
background-color: colors.$bgColLighter;
}
&:focus-visible {
outline: 1px solid colors.$accentCol;
}
}
}
.online-indicator {
display: inline-block;
width: 9px;
height: 9px;
vertical-align: middle;
background-color: greenyellow;
border-radius: 100%;
margin: 0 5px;
}
</style>
-145
View File
@@ -1,145 +0,0 @@
<template>
<section class="sidebar">
<div class="sidebar_content">
<!-- <button class="option-save" @click="toggleOrderMode" :data-selected="store.orderMode == 'OrderList'">
<img :src="saveIcon" alt="save icon" />
</button> -->
<!-- <button @click="store.helperModalOpen = true">?</button> -->
<button
v-for="orderType in orderTypeList"
:key="orderType.id"
@click="selectOrderType(orderType.id)"
:data-selected="store.chosenOrderType == orderType.id"
>
{{ orderType.name }}
<div class="bar"></div>
</button>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../store/store';
export default defineComponent({
data() {
return {
orderTypeList: [
{
id: 'orderN',
name: 'N'
},
{
id: 'orderS',
name: 'S'
},
{
id: 'orderO',
name: 'O'
}
]
};
},
setup() {
return {
store: useStore()
};
},
methods: {
selectOrderType(type: any) {
if (type != this.store.chosenOrderType) this.store.chosenLocalOrderId = '';
this.store.chosenOrderType = type;
}
}
});
</script>
<style lang="scss" scoped>
@use '../styles/colors';
.sidebar_content {
display: grid;
grid-template-rows: repeat(3, 1fr);
gap: 0.25em;
font-size: 1.5em;
font-weight: bold;
height: 100%;
& > button {
position: relative;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
color: white;
background-color: colors.$bgColDarker;
width: 50px;
height: 85px;
.bar {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 4px;
transform: translateX(100%);
transition: all 200ms ease-in-out;
}
&[data-selected='true'] .bar {
transform: translateX(0);
background-color: colors.$accentCol;
}
&:hover {
cursor: pointer;
}
&:focus-visible {
background-color: #6d6d6d;
}
}
}
button.option-save {
background-color: #000000aa;
img {
width: 80%;
height: 80%;
}
&[data-selected='true'] {
background-color: colors.$accentCol;
}
}
@media screen and (max-width: 650px) {
.sidebar_content {
display: flex;
justify-content: space-between;
& > button {
height: 40px;
width: 100px;
.bar {
width: 100%;
height: 4px;
}
}
}
}
</style>
-3
View File
@@ -1,3 +0,0 @@
{
"orderS": ["D"]
}
+19
View File
@@ -0,0 +1,19 @@
import enLang from './locales/en.json';
import plLang from './locales/pl.json';
import { createI18n } from 'vue-i18n';
const i18n = createI18n({
locale: 'pl',
legacy: false,
warnHtmlMessage: false,
fallbackLocale: 'pl',
messages: {
en: enLang,
pl: plLang
},
enableLegacy: false
});
export default i18n;
+281
View File
@@ -0,0 +1,281 @@
{
"locale": {
"pl": "POL",
"en": "ENG"
},
"navbar": {
"OrderMessagePanel": "MESSAGE",
"OrderListPanel": "SAVED",
"OrderTrainPickerPanel": "TRAINS"
},
"update": {
"update-available-text": "New GeneraTOR version is available!",
"update-available-underline": "Click here to update!",
"title": "GeneraTOR update!",
"confirm": "ROGER THAT!",
"no-data": "No changelog available!",
"info-1": "This changelog will be available to see once again after clicking the version number in the footer",
"info-2": "The full app changelog available on <a href='https://github.com/Spythere/genera-tor' target='_blank'>the project's GitHub</a>"
},
"order-message": {
"title": "Message to display in the simulator's chatbox:",
"info": "Copy or save the content of the generated train order using buttons below:",
"button-save": "Save as new order",
"button-copy": "Copy order message",
"button-update": "Update order",
"button-reset": "Reset order",
"warning-outdated-clipboard": "Oops! Your browser may be a little bit depraceted since it's not supporting saving data to the clipboard! :/",
"warning-fill-inputs": "Fill all the empty fields before copying the order!",
"warning-add-instruction": "Add at least one order instruction before copying the order!",
"warning-fill-missing": "Fill rows in the order's header and footer before copying it!",
"warning-fill-top-save": "Fill at least fields A, B, C and D in the order's header before saving it!",
"warning-fill-top-update": "Fill at least fields A, B, C and D in the order's header before updating it!",
"warning-order-identical": "Last saved order is identical as the current one!",
"warning-no-order-selected": "Choose the already saved order first!",
"warning-conflicting-instructions": "Instruction {0} and {1} can't be on the same order!",
"error-update": "An error occurred while saving this order! :/",
"success-update-html": "Updated this <b class=\"text--accent\">order's</b> message!",
"success-save-html": "Saved <b class=\"text--accent\">order's</b> message in the browser memory!",
"success-copy-html": "<b class=\"text--accent\">Success!</b> You may paste the order message in the simulator's chatbox now!"
},
"order-footer": {
"field-stationName": "station",
"field-checkpointName": "checkpoint",
"field-hour": "hour",
"field-minutes": "minute",
"field-dispatcherName": "dispatcher",
"field-secondaryDispatcherName": "ordering dispatcher",
"field-dispatcherOrSecondaryName": "dispatcher (or ordering dispatcher)"
},
"order-options": {
"dark-mode": "Order dark theme",
"update-number-on-copy": "Update order number on copy",
"update-number-on-save": "Update order number on save",
"update-hours": "Update order hour on edit"
},
"order-list": {
"title": "Saved train orders",
"order-title": "Train order for no. {trainNo}",
"order-subtitle": "Selected instructions: {0}",
"no-saved-orders": "No saved orders!",
"order-added": "Added:",
"order-updated": "Updated:",
"button-order-select": "Select",
"button-order-remove": "Remove",
"warning-deprecated-version": "Deprecated version of the order - may generate incorrect information!",
"warning-removed-deprecated-orders": "Removed deprecated train orders ({count}) due to their replacement with a new format in Polish railway!"
},
"order-train-picker": {
"placeholder-scenery-name": "Scenery name",
"placeholder-region-name": "Region",
"placeholder-checkpoint-name": "Checkpoint name",
"autofill-checkpoint-id": "Autofill checkpoint's abbreviation",
"info": "Select scenery name to display active trains",
"title": "Active timetables and trains on the scenery",
"subtitle": "Click on the user below to fill the current order with their information",
"no-trains": "No trains to display"
},
"order": {
"title": "Polish Railway Train Order",
"header": {
"A": "A Train No {'|'} Shunting composition No",
"A-placeholder": "Train / shunting composition number",
"B": "B Date",
"C": "C Location of train {'|'} Location of shunting composition",
"C-placeholder": "Name of the post / line / scenery",
"D": "D Location of issuer",
"D-placeholder": "Name of the post / scenery"
},
"22": {
"text": "Applies to left-track traffic",
"message-html": "<b>Applies to left-track traffic</b>"
},
"99": {
"text": "Revoke the train order",
"x1": "x.1 Train order identifier",
"message-html": "<b>Revoke the train order</b> {0}"
},
"2110": {
"text": "{bold1} from track no. {track1} from {signalbox1} {br} to track no. {track2} in the direction of {signalbox2} {br} Pass Stop signals {signal1} and {signal2} and {signal3}",
"bold1": "Is allowed to exit",
"track1": "x.1 track",
"signalbox1": "x.2 signalbox",
"track2": "x.3 track",
"signalbox2": "x.4 signalbox",
"signal1": "x.5 signal",
"signal2": "x.6 signal",
"signal3": "x.7 signal",
"message-html": "<b>Is allowed to exit</b> from track no. {0} from {1} to track no. {2} in the direction of {3}. Pass Stop signals {4} and {5} and {6}"
},
"2115": {
"text": "{bold1} from track no. {track1} to {signalbox1} on track no. {track2} {br} Pass signals {signal1} and {signal2} and {signal3}",
"bold1": "Is allowed to enter",
"track1": "x.1 track",
"signalbox1": "x.2 signalbox",
"track2": "x.3 track",
"signal1": "x.4 signal",
"signal2": "x.5 signal",
"signal3": "x.6 signal",
"message-html": "<b>Is allowed to enter</b> from track no. {0} to {1} on track no. {2}. Pass signals {3} and {4} and {5}"
},
"2120": {
"text": "From {signalbox1} to {signalbox2} on track {track1} {br} {highlight1} {br} {underline1}{highlight2}.",
"highlight1": "SBL signalling indications are to be ignored.",
"underline1": "Stay alert after passing the signal with indicator",
"highlight2": " W18",
"signalbox1": "x.1 signalbox",
"signalbox2": "x.2 signalbox",
"track1": "x.3 track no",
"message-html": "From {0} to {1} on track {2} <b>SBL signals are to be ignored.</b> Stay alert after passing the signal with indicator <b>W18.</b>"
},
"2125": {
"text": "Is allowed to pass {select1} in the direction of {signalbox1} on track {track1} to km {km1} to hour {hour1}.",
"select1-a": "indicator W5",
"select1-b": "last switch",
"signalbox1": "x.1 signalbox",
"track1": "x.2 track",
"km1": "x.3 km",
"hour1": "x.4 hour",
"message-html": "Is allowed to pass {0} in the direction of {1} on track {2} to km {3} to hour {4}."
},
"2135": {
"text": "{bold1} on track no. {track1} in the direction of {signalbox1}.",
"bold1": "Is allowed to proceed",
"track1": "x.1 track",
"signalbox1": "x.2 signalbox",
"message-html": "<b>Is allowed to proceed</b> on track no. {0} in the direction of {1}"
},
"2140": {
"text": "{bold1} at post/line {signalbox1}{'|'}{signalbox2} at km {km1} for {other1}",
"bold1": "Train halt",
"signalbox1": "x.1 signalbox",
"signalbox2": "x.2 signalbox",
"km1": "x.3 km",
"other1": "x.96 cause",
"message-html": "<b>Train halt</b> at post {0} at km {2} for {3} | <b>Train halt</b> on line {0} {'|'} {1} at km {2} for {3} | <b>Train halt</b> on line {0} {'|'} {1} at km {2} for {3}"
},
"2145": {
"text": "At {signalbox1} the clear signal {signal1}{br}{bold1}",
"bold1": "is invalid. Stop the train before this signal.",
"signalbox1": "x.1 signalbox",
"signal1": "x.2 signal",
"message-html": "At {0} the clear signal {1} <b>is invalid, stop the train before this signal.</b>"
},
"2150": {
"text": "{bold1} at post/line {br} {signalbox1} / {signalbox2} referring to level crossing at km {km1}{br}{bold2}",
"bold1": "Level crossing distant signals (TOP)",
"bold2": "are to be ignored. Proceed with timetable speed.",
"signalbox1": "x.1 signalbox",
"signalbox2": "x.2 signalbox",
"km1": "x.3 km",
"message-html": "<b>Level crossing distant signals (TOP)</b> at post {0} referring to level crossing at km {1} <b>are to be ignored. Proceed with timetable speed.</b> | <b>Wskazania tarcz ostrzegawczych</b> na line {0} / {1} referring to level crossing at km {2} <b>are to be ignored. Proceed with timetable speed.</b> | <b>Wskazania tarcz ostrzegawczych</b> na line {0} / {1} referring to level crossing at km {2} <b>are to be ignored. Proceed with timetable speed.</b>"
},
"2155": {
"text": "{bold1} at post/line {signalbox1}{'|'}{signalbox2} referring to signal {signal1}.",
"bold1": "Damaged SHP device",
"signalbox1": "x.1 signalbox",
"signalbox2": "x.2 signalbox",
"signal1": "x.3 signal",
"message-html": "<b>Damaged SHP device</b> at post {0} referring to signal {2}|<b>Damaged SHP device</b> on line {0} {'|'} {1} referring to signal {2}|<b>Uszkodzone urządzenia SHP</b> on line {0} {'|'} {1} referring to signal {2}"
},
"2160": {
"text": "{bold1} from {signalbox1} to {signalbox2}, rerouted via {signalbox3} by line {line1} with the speed of {vmax1}.",
"bold1": "Route change",
"signalbox1": "x.1 signalbox",
"signalbox2": "x.2 signalbox",
"signalbox3": "x.3 signalbox",
"line1": "x.4 line",
"vmax1": "x.5 km/h",
"message-html": "<b>Route change</b> from {0} to {1}, rerouted via {2} by line {3} with the speed of {4}."
},
"2165": {
"text": "{bold1} from {km1} to {km2}.",
"bold1": "Run with lowered pantographs",
"km1": "x.1 km",
"km2": "x.2 km",
"message-html": "<b>Run with lowered pantographs</b> from {0} to {1}."
},
"2170": {
"text": "{bold1} {signalbox1} and {signalbox2} of the line no. {line1}{br}{bold2}",
"bold1": "At section between",
"bold2": "ride with the analog contact.",
"signalbox1": "x.1 signalbox",
"signalbox2": "x.2 signalbox",
"line1": "x.3 line",
"message-html": "<b>At section between</b> {0} and {1} of the line no. {2} <b>ride with the analog contact.</b>"
},
"2180": {
"text": "{bold1} no. {track1} in the directon of {signalbox1} to km {km1} turning to {signalbox2} until hour {hour1}",
"bold1": "Run on the closed track",
"track1": "x.1 tor",
"signalbox1": "x.2 signalbox",
"km1": "x.3 km",
"signalbox2": "x.4 signalbox",
"hour1": "x.5 hour",
"message-html": "<b>Run on the closed track</b> no. {0} in the directon of {1} to km {2} turning to {3} until hour {4}"
},
"2181": {
"text": "{bold1}",
"bold1": "Track is closed with no rolling stock",
"message-html": "<b>Track is closed with no rolling stock</b> "
},
"2182": {
"text": "Train {train1} {bold1} at km {km1}",
"bold1": "is working on track",
"train1": "x.1 train no.",
"km1": "x.2 km",
"message-html": "Train {0} <b>is working on track</b> at km {1}"
},
"2183": {
"text": "Train {train1} {bold1} to km {km1}",
"bold1": "will be dispatched",
"train1": "x.1 train no.",
"km1": "x.2 km",
"message-html": "Train {0} <b>will be dispatched</b> to km {1}"
},
"2185": {
"text": "{bold1} on track no. {track1} at km {km1} on line {signalbox1} {'|'} {signalbox2}",
"bold1": "Is allowed to insert a PSD vehicle",
"track1": "x.1 track",
"km1": "x.2 km",
"signalbox1": "x.3 signalbox",
"signalbox2": "x.4 signalbox",
"message-html": "<b>Is allowed to insert a PSD vehicle</b> on track no. {0} at km {1} on line {2} {'|'} {3}"
},
"2310": {
"text": "{bold1}{br}{text-list}",
"bold1": "Do not exceed the speed and stay alert:",
"text-list": "{bold} {signalbox1}/{signalbox2} track no. {track1} {v} {vmax1} from {km1} to {km2} {other1}",
"bold": "{0}. On post/line",
"signalbox1": "x.{0} signalbox",
"signalbox2": "x.{0} signalbox",
"track1": "x.{0} track",
"vmax1": "x.{0} km/h",
"km1": "x.{0} km",
"km2": "x.{0} km",
"other1": "x.{1} cause",
"message-html": "<b>Nie przekraczać prędkości i zachować ostrożność:</b>",
"message-html-list": "<b>{0}. On post</b> {1} track no. {3} v{4}km/h from {5}km to {6}km - cause: {7}|<b>{0}. On post</b> {1} tor nr {3} v{4} from {5}km to {6}km - cause: {7}|<b>{0}. On line</b> {1}/{2} tor nr {3} v{4} from {5}km to {6}km - cause: {7}"
},
"2311": {
"text": "{bold1}",
"bold1": "Use „Baczność” signal multiple times",
"message-html": "<b>Use „Baczność” signal multiple times</b>"
},
"2320": {
"text": "{other2320}",
"other2320": "x.96 other",
"message-html": "Other: {0}"
},
"footer": {
"V": "V Driver identifier",
"V-placeholder": "Driver nickname",
"W": "W Issuer identifier",
"W-placeholder": "Dispatcher nickname",
"Y": "Y Hour",
"Y-placeholder": "HH:MM, e.g. 09:25",
"Z": "Z Train order identifier",
"Z-placeholder": "RD-(order no.)-(sc. hash)-(2 last year digits)"
}
}
}
+281
View File
@@ -0,0 +1,281 @@
{
"locale": {
"pl": "POL",
"en": "ENG"
},
"update": {
"update-available-text": "Nowa wersja GeneraTORa dostępna!",
"update-available-underline": "Kliknij, aby odświeżyć aplikację!",
"title": "Aktualizacja GeneraTORa!",
"no-data": "Brak dostępnego changelogu!",
"confirm": "Przyjąłem!",
"info-1": "Ten changelog będzie zawsze dostępny po kliknięciu numeru wersji w stopce strony",
"info-2": "Pełny changelog dostępny na <a href='https://github.com/Spythere/genera-tor' target='_blank'>GitHubie projektu</a>"
},
"navbar": {
"OrderMessagePanel": "WIADOMOŚĆ",
"OrderListPanel": "ZAPISANE",
"OrderTrainPickerPanel": "POCIĄGI"
},
"order-message": {
"title": "Wiadomość do wyświetlenia na czacie symulatora:",
"info": "Po wygenerowaniu rozkazu skopiuj jego treść lub zapisz w pamięci przeglądarki za pomocą przycisków poniżej:",
"button-save": "Zapisz nowy rozkaz",
"button-copy": "Kopiuj treść rozkazu",
"button-update": "Zaktualizuj rozkaz",
"button-reset": "Wyczyść rozkaz",
"warning-outdated-clipboard": "Ups! Twoja przeglądarka musi być dosyć przestarzała, ponieważ nie obsługuje zapisu do schowka! :/",
"warning-fill-inputs": "Wypełnij puste rubryki rozkazu przed jego skopiowaniem!",
"warning-add-instruction": "Dodaj co najmniej jedną instrukcję rozkazu przed jego skopiowaniem!",
"warning-fill-missing": "Uzupełnij rubryki w nagłówku i stopce rozkazu przed jego skopiowaniem!",
"warning-fill-top-save": "Wypełnij co najmniej rubryki A, B, C i D w nagłówku rozkazu przed jego zapisaniem!",
"warning-fill-top-update": "Wypełnij co najmniej rubryki A, B, C i D w nagłówku rozkazu przed jego zaktualizowaniem!",
"warning-order-identical": "Ostatni zapisany rozkaz jest identyczny z obecnym!",
"warning-no-order-selected": "Wybierz rozkaz, który chcesz zaktualizować!",
"warning-conflicting-instructions": "Instrukcje {0} i {1} nie mogą znajdować się na tym samym rozkazie jednocześnie!",
"error-update": "Wystąpił błąd podczas aktualizowania tego rozkazu! :/",
"success-update-html": "Zaktualizowano treść <b class=\"text--accent\">rozkazu</b>!",
"success-save-html": "Zapisano treść <b class=\"text--accent\">rozkazu</b> w pamięci przeglądarki!",
"success-copy-html": "<b class=\"text--accent\">Skopiowano!</b> Możesz teraz wkleić treść rozkazu na czacie symulatora!"
},
"order-footer": {
"field-stationName": "stacja",
"field-checkpointName": "posterunek",
"field-hour": "godzina",
"field-minutes": "minuta",
"field-dispatcherName": "dyżurny ruchu",
"field-secondaryDispatcherName": "z polecenia dyżurnego ruchu",
"field-dispatcherOrSecondaryName": "dyżurny ruchu (lub z polecenia dyżurnego ruchu)"
},
"order-options": {
"dark-mode": "Ciemny motyw bloczka rozkazu",
"update-number-on-copy": "Aktualizuj numer rozkazu po skopiowaniu",
"update-number-on-save": "Aktualizuj numer rozkazu po zapisaniu",
"update-hours": "Aktualizuj godziny przy edycji"
},
"order-list": {
"title": "Zapisane rozkazy pisemne",
"no-saved-orders": "Brak zapisanych rozkazów!",
"order-title": "Rozkaz pisemny dla pociągu nr {trainNo}",
"order-subtitle": "Zaznaczone instrukcje: {0}",
"order-added": "Dodano:",
"order-updated": "Zaktualizowano:",
"button-order-select": "Wybierz",
"button-order-remove": "Usuń",
"warning-deprecated-version": "Przestarzała wersja rozkazu! Może generować złe informacje!",
"warning-removed-deprecated-orders": "Usunięto nieaktualne rozkazy pisemne ({count}) w związku z wejściem w życie ich nowego formatu!"
},
"order-train-picker": {
"placeholder-scenery-name": "Sceneria",
"placeholder-region-name": "Region",
"placeholder-checkpoint-name": "Posterunek",
"autofill-checkpoint-id": "Uzupełniaj skrót wybranego posterunku",
"info": "Wybierz dyżurnego oraz scenerię, aby zobaczyć pociągi",
"title": "Aktywne RJ i gracze na scenerii",
"subtitle": "Kliknij na gracza, aby wypełnić obecny rozkaz jego danymi",
"no-trains": "Brak pociągów do wyświetlenia"
},
"order": {
"title": "Rozkaz pisemny",
"header": {
"A": "A Nr pociągu {'|'} składu manewrowego",
"A-placeholder": "Numer pociągu lub manewru",
"B": "B Data",
"C": "C Lokalizacja pociągu {'|'} składu manewrowego",
"C-placeholder": "Nazwa posterunku / szlaku / scenerii",
"D": "D Lokalizacja nadawcy",
"D-placeholder": "Nazwa posterunku / scenerii"
},
"22": {
"text": "Dotyczy jazdy torem lewym",
"message-html": "<b>Dotyczy jazdy torem lewym</b>"
},
"99": {
"text": "Odwołanie rozkazu pisemnego",
"x1": "x.1 Identyfikator rozkazu pisemnego",
"message-html": "<b>Odwołanie rozkazu pisemnego</b> {0}"
},
"2110": {
"text": "{bold1} z toru nr {track1} z {signalbox1} {br} na tor nr {track2} w kierunku {signalbox2} {br} Pominięcie sygnałów stój {signal1} i {signal2} i {signal3}",
"bold1": "Zezwalam na wyjazd",
"track1": "x.1 tor",
"signalbox1": "x.2 posterunek",
"track2": "x.3 tor",
"signalbox2": "x.4 posterunek",
"signal1": "x.5 sygnalizator",
"signal2": "x.6 sygnalizator",
"signal3": "x.7 sygnalizator",
"message-html": "<b>Zezwalam na wyjazd</b> z toru nr {0} z {1} na tor nr {2} w kierunku {3}. Pominięcie sygnałów stój {4} i {5} i {6}"
},
"2115": {
"text": "{bold1} z toru nr {track1} do {signalbox1} na tor nr {track2} {br} Pominięcie sygnałów {signal1} i {signal2} i {signal3}",
"bold1": "Zezwalam na wjazd",
"track1": "x.1 tor",
"signalbox1": "x.2 posterunek",
"track2": "x.3 tor",
"signal1": "x.4 sygnalizator",
"signal2": "x.5 sygnalizator",
"signal3": "x.6 sygnalizator",
"message-html": "<b>Zezwalam na wjazd</b> z toru nr {0} do {1} na tor nr {2}. Pominięcie sygnałów {3} i {4} i {5}"
},
"2120": {
"text": "Od {signalbox1} do {signalbox2} po torze {track1} {br} {highlight1} {br} {underline1}{highlight2}.",
"highlight1": "wskazania semaforów SBL są nieważne.",
"underline1": "Zachować ostrożność od semafora ze wskaźnikiem",
"highlight2": " W18",
"signalbox1": "x.1 posterunek",
"signalbox2": "x.2 posterunek",
"track1": "x.3 nr toru",
"message-html": "Od {0} do {1} po torze {2} <b>wskazania semaforów SBL są nieważne.</b> Zachować ostrożność od semafora ze wskaźnikiem <b>W18.</b>"
},
"2125": {
"text": "Zezwalam przejechać za {select1} w kierunku {signalbox1} torem {track1} do km {km1} do godz. {hour1}.",
"select1-a": "wskaźnik W5",
"select1-b": "ostatni rozjazd",
"signalbox1": "x.1 posterunek",
"track1": "x.2 tor",
"km1": "x.3 km",
"hour1": "x.4 godzina",
"message-html": "Zezwalam przejechać za {0} w kierunku {1} torem {2} do km {3} do godz. {4}."
},
"2135": {
"text": "{bold1} po torze nr {track1} w kierunku {signalbox1}.",
"bold1": "Zezwalam na kontynuacje jazdy",
"track1": "x.1 tor",
"signalbox1": "x.2 posterunek",
"message-html": "<b>Zezwalam na kontynuacje jazdy</b> po torze nr {0} w kierunku {1}"
},
"2140": {
"text": "{bold1} na posterunku/szlaku {signalbox1}{'|'}{signalbox2} w km {km1} celem {other1}",
"bold1": "Zatrzymanie pociągu",
"signalbox1": "x.1 posterunek",
"signalbox2": "x.2 posterunek",
"km1": "x.3 km",
"other1": "x.96 inne",
"message-html": "<b>Zatrzymanie pociągu</b> na posterunku {0} w km {2} celem {3} | <b>Zatrzymanie pociągu</b> na szlaku {0} {'|'} {1} w km {2} celem {3} | <b>Zatrzymanie pociągu</b> na szlaku {0} {'|'} {1} w km {2} celem {3}"
},
"2145": {
"text": "Na {signalbox1} na sygnalizatorze {signal1}{br}{bold1}",
"bold1": "sygnał zezwalający jest nieważny, zatrzymać pociąg przed tym sygnalizatorem.",
"signalbox1": "x.1 posterunek",
"signal1": "x.2 sygnalizator",
"message-html": "Na {0} na sygnalizatorze {1} <b>sygnał zezwalający jest nieważny, zatrzymać pociąg przed tym sygnalizatorem.</b>"
},
"2150": {
"text": "{bold1} przejazdowych na posterunku/szlaku {br} {signalbox1} / {signalbox2} odnoszących się do przejazdu w km {km1}{br}{bold2}",
"bold1": "Wskazania tarcz ostrzegawczych",
"bold2": "są nieważne. Jazda z prędkością rozkładową.",
"signalbox1": "x.1 posterunek",
"signalbox2": "x.2 posterunek",
"km1": "x.3 km",
"message-html": "<b>Wskazania tarcz ostrzegawczych</b> przejazdowych na posterunku {0} odnoszących się do przejazdu w km {1} <b>są nieważne. Jazda z prędkością rozkładową.</b> | <b>Wskazania tarcz ostrzegawczych</b> przejazdowych na szlaku {0} / {1} odnoszących się do przejazdu w km {2} <b>są nieważne. Jazda z prędkością rozkładową.</b> | <b>Wskazania tarcz ostrzegawczych</b> przejazdowych na szlaku {0} / {1} odnoszących się do przejazdu w km {2} <b>są nieważne. Jazda z prędkością rozkładową.</b>"
},
"2155": {
"text": "{bold1} na posterunku/szlaku {signalbox1}{'|'}{signalbox2} odnoszące się do sygnalizatora {signal1}.",
"bold1": "Uszkodzone urządzenia SHP",
"signalbox1": "x.1 posterunek",
"signalbox2": "x.2 posterunek",
"signal1": "x.3 sygnalizator",
"message-html": "<b>Uszkodzone urządzenia SHP</b> na posterunku {0} odnoszące się do sygnalizatora {2}|<b>Uszkodzone urządzenia SHP</b> na szlaku {0} {'|'} {1} odnoszące się do sygnalizatora {2}|<b>Uszkodzone urządzenia SHP</b> na szlaku {0} {'|'} {1} odnoszące się do sygnalizatora {2}"
},
"2160": {
"text": "{bold1} na odcinku od {signalbox1} do {signalbox2}, jazda przez {signalbox3} linią {line1} z prędkością {vmax1}.",
"bold1": "Zmiana trasy",
"signalbox1": "x.1 posterunek",
"signalbox2": "x.2 posterunek",
"signalbox3": "x.3 posterunek",
"line1": "x.4 linia",
"vmax1": "x.5 km/h",
"message-html": "<b>Zmiana trasy</b> na odcinku od {0} do {1}, jazda przez {2} linią {3} z prędkością {4}."
},
"2165": {
"text": "{bold1} na odcinku od {km1} do {km2}.",
"bold1": "Jazda z opuszczonymi pantografami",
"km1": "x.1 km",
"km2": "x.2 km",
"message-html": "<b>Jazda z opuszczonymi pantografami</b> na odcinku od {0} do {1}."
},
"2170": {
"text": "{bold1} od {signalbox1} do {signalbox2} linii nr {line1}{br}{bold2}",
"bold1": "Na odcinku",
"bold2": "jazda pociągu z łącznością analogową.",
"signalbox1": "x.1 posterunek",
"signalbox2": "x.2 posterunek",
"line1": "x.3 linia",
"message-html": "<b>Na odcinku</b> na odcinku od {0} do {1} linii nr {2} <b>jazda pociągu z łącznością analogową.</b>"
},
"2180": {
"text": "{bold1} nr {track1} w kierunku {signalbox1} do km {km1} zjazd do {signalbox2} do godz. {hour1}",
"bold1": "Polecam jazdę po torze zamkniętym",
"track1": "x.1 tor",
"signalbox1": "x.2 posterunek",
"km1": "x.3 km",
"signalbox2": "x.4 posterunek",
"hour1": "x.5 godzina",
"message-html": "<b>Polecam jazdę po torze zamkniętym</b> nr {0} w kierunku {1} do km {2} zjazd do {3} do godz. {4}"
},
"2181": {
"text": "{bold1}",
"bold1": "Tor zamknięty wolny od taboru",
"message-html": "<b>Tor zamknięty wolny od taboru</b> "
},
"2182": {
"text": "{bold1} {train1} w km {km1}",
"bold1": "Na torze pracuje pociąg",
"train1": "x.1 numer pociągu",
"km1": "x.2 km",
"message-html": "<b>Na torze pracuje pociąg</b> {0} w km {1}"
},
"2183": {
"text": "{bold1} {train1} do km {km1}",
"bold1": "Na tor zostanie wyprawiony pociąg",
"train1": "x.1 numer pociągu",
"km1": "x.2 km",
"message-html": "<b>Na tor zostanie wyprawiony pociąg</b> {0} do km {1}"
},
"2185": {
"text": "{bold1} na tor nr {track1} w km {km1} na szlaku {signalbox1} {'|'} {signalbox2}",
"bold1": "Zezwalam na wstawienie PSD",
"track1": "x.1 tor",
"km1": "x.2 km",
"signalbox1": "x.3 posterunek",
"signalbox2": "x.4 posterunek",
"message-html": "<b>Zezwalam na wstawienie PSD</b> na tor nr {0} w km {1} na szlaku {2} {'|'} {3}"
},
"2310": {
"text": "{bold1}{br}{text-list}",
"bold1": "Nie przekraczać prędkości i zachować ostrożność:",
"text-list": "{bold} {signalbox1}/{signalbox2} tor nr {track1} {v} {vmax1} od {km1} do {km2} {other1}",
"bold": "{0}. Na posterunku/szlaku",
"signalbox1": "x.{0} posterunek",
"signalbox2": "x.{0} posterunek",
"track1": "x.{0} tor",
"vmax1": "x.{0} km/h",
"km1": "x.{0} km",
"km2": "x.{0} km",
"other1": "x.{1} przyczyna",
"message-html": "<b>Nie przekraczać prędkości i zachować ostrożność:</b>",
"message-html-list": "<b>{0}. Na posterunku</b> {1} tor nr {3} v{4}km/h od {5}km do {6}km - przyczyna: {7}|<b>{0}. Na posterunku</b> {1} tor nr {3} v{4} od {5}km do {6}km - przyczyna: {7}|<b>{0}. Na szlaku</b> {1}/{2} tor nr {3} v{4} od {5}km do {6}km - przyczyna: {7}"
},
"2311": {
"text": "{bold1}",
"bold1": "Podawać sygnał „Baczność”",
"message-html": "<b>Podawać sygnał „Baczność”</b>"
},
"2320": {
"text": "{other2320}",
"other2320": "x.96 inne",
"message-html": "Inne: {0}"
},
"footer": {
"V": "V Identyfikator maszynisty",
"V-placeholder": "Nick maszynisty",
"W": "W Identyfikator nadawcy",
"W-placeholder": "Nick dyżurnego",
"Y": "Y Godzina",
"Y-placeholder": "Godzina w formacie HH:MM np. 09:25",
"Z": "Z Identyfikator rozkazu pisemnego",
"Z-placeholder": "RD-(nr rozkazu)-(hash sc.)-(2 ostatnie cyfry roku)"
}
}
}
+3 -1
View File
@@ -3,4 +3,6 @@ import App from './App.vue';
import router from './router'; import router from './router';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
createApp(App).use(router).use(createPinia()).mount('#app'); import i18n from './i18n';
createApp(App).use(router).use(i18n).use(createPinia()).mount('#app');
+53
View File
@@ -0,0 +1,53 @@
export default class StorageManager {
static registerStorage(name: string) {
window.localStorage.setItem(name, '1');
}
static unregisterStorage(name: string) {
window.localStorage.removeItem(name);
}
static isRegistered(name: string) {
return window.localStorage.getItem(name) ? true : false;
}
static setBooleanValue(key: string, val: boolean) {
window.localStorage.setItem(key, val.toString());
}
static setNumericValue(key: string, val: number) {
window.localStorage.setItem(key, val.toString());
}
static setStringValue(key: string, val: string) {
window.localStorage.setItem(key, val);
}
static setValue(key: string, val: any) {
if (typeof val == 'boolean') this.setBooleanValue(key, val);
else if (typeof val == 'number') this.setNumericValue(key, val);
else if (typeof val == 'string') this.setStringValue(key, val);
else this.setStringValue(key, val);
}
static removeValue(key: string) {
window.localStorage.removeItem(key);
}
static getValue(key: string) {
return window.localStorage.getItem(key);
}
static getBooleanValue(key: string): boolean {
return window.localStorage.getItem(key) === 'true' ? true : false;
}
static getStringValue(key: string): string {
return window.localStorage.getItem(key) || '';
}
static getNumericValue(key: string): number {
const itemValue = window.localStorage.getItem(key);
return itemValue ? parseInt(itemValue) : 0;
}
}
-30
View File
@@ -1,30 +0,0 @@
import { defineComponent } from 'vue';
import { useStore } from '../store/store';
export default defineComponent({
setup() {
return {
store: useStore()
};
},
methods: {
generateFooter() {
const footer = this.store.orderFooter;
const messageArray = [];
if (footer.stationName) messageArray.push(`stacja: ${footer.stationName}`);
if (footer.checkpointName) messageArray.push(`posterunek: ${footer.checkpointName}`);
if (footer.hour) messageArray.push(`godz. ${footer.hour}`);
if (footer.minutes) messageArray.push(`min. ${footer.minutes}`);
if (footer.dispatcherName) messageArray.push(`dyżurny ruchu ${footer.dispatcherName}`);
if (footer.secondaryDispatcherName)
messageArray.push(`z polecenia dyżurnego ruchu ${footer.secondaryDispatcherName}`);
this.store.footerMessage = ` <b>|</b> ${messageArray.join(
', '
)} <b>|</b> Rozkaz otrzymałem, maszynista: (potwierdzić otrzymanie rozkazu)`;
}
}
});
-163
View File
@@ -1,163 +0,0 @@
import { defineComponent } from 'vue';
import { useStore } from '../store/store';
import { LocalStorageOrder } from '../types/orderTypes';
function alertWrongOrderFormat() {
alert('Wystąpił błąd podczas przetwarzania rozkazu! Informacje mogą być niepoprawne!');
console.warn('Zły format zapisanego rozkazu!');
}
export default defineComponent({
setup() {
return {
store: useStore()
};
},
methods: {
saveOrderSetting(key: string, value: string | number | boolean) {
window.localStorage.setItem(key, value.toString());
},
getOrderSetting(key: string) {
return window.localStorage.getItem(key);
},
removeOrderSetting(key: string) {
window.localStorage.removeItem(key);
},
saveLocalOrder() {
const orderObj: LocalStorageOrder = {
id: '',
orderType: this.store.chosenOrderType,
orderBody: this.store[this.store.chosenOrderType],
orderFooter: this.store.orderFooter,
createdAt: Date.now(),
orderVersion: import.meta.env['VITE_APP_ORDER_VERSION'] || '1'
};
const headerInfo = orderObj['orderBody']['header'];
if (!headerInfo['orderNo']) return -1;
if (!headerInfo['trainNo']) return -1;
if (!headerInfo['date']) return -1;
const localStorage = window.localStorage;
const localOrderCount = localStorage.getItem('orderCount') || '0';
if (localOrderCount == '0') localStorage.setItem('orderCount', '0');
const prevLocalOrder = localStorage.getItem(`order-${Number(localOrderCount)}`);
if (prevLocalOrder && prevLocalOrder == JSON.stringify(orderObj)) {
return 0;
}
const nextOrderCount = Number(localOrderCount) + 1;
const orderId = `order-${nextOrderCount}`;
orderObj['id'] = orderId;
localStorage.setItem('orderCount', `${nextOrderCount}`);
localStorage.setItem(orderId, JSON.stringify(orderObj));
this.store.chosenLocalOrderId = orderId;
return 1;
},
updateLocalOrder() {
if (!this.store.chosenLocalOrderId) return 0;
const localOrder = window.localStorage.getItem(this.store.chosenLocalOrderId);
if (!localOrder) return -1;
const orderObj: LocalStorageOrder = {
id: this.store.chosenLocalOrderId,
orderType: this.store.chosenOrderType,
orderBody: this.store[this.store.chosenOrderType],
orderFooter: this.store.orderFooter,
updatedAt: Date.now(),
orderVersion: import.meta.env['VITE_APP_ORDER_VERSION'] || '1'
};
window.localStorage.setItem(this.store.chosenLocalOrderId, JSON.stringify(orderObj));
return 1;
},
removeLocalOrder(order: LocalStorageOrder) {
localStorage.removeItem(order.id);
if (this.store.chosenLocalOrderId == order.id) this.store.chosenLocalOrderId = '';
// localStorage.setItem('orderCount', (Number(localStorage.getItem('orderCount')) - 1).toString());
},
selectLocalOrder(localOrder: LocalStorageOrder) {
// const localOrder = JSON.parse(JSON.stringify(order));
const { orderBody: localOrderBody, orderFooter: localOrderFooter } = localOrder;
this.store[localOrder.orderType].header.date = localOrderBody.header.date;
this.store[localOrder.orderType].header.orderNo = localOrderBody.header.orderNo;
this.store[localOrder.orderType].header.trainNo = localOrderBody.header.trainNo;
if (localOrder.orderType == 'orderN' || localOrder.orderType == 'orderS') {
const currentOrder = this.store[localOrder.orderType];
if (localOrderBody.rows.length != currentOrder.rows.length) {
alertWrongOrderFormat();
return;
}
for (let rowIndex = 0; rowIndex < currentOrder.rows.length; rowIndex++) {
const row = currentOrder.rows[rowIndex];
if (localOrderBody.rows[rowIndex] === undefined) {
alertWrongOrderFormat();
continue;
}
for (const rowProp in row) {
if (localOrderBody.rows[rowIndex][rowProp] === undefined) {
alertWrongOrderFormat();
continue;
}
(currentOrder.rows[rowIndex] as any)[rowProp] = localOrderBody.rows[rowIndex][rowProp];
}
}
}
if (localOrder.orderType == 'orderO') {
const currentOrder = this.store[localOrder.orderType];
for (let rowIndex = 0; rowIndex < currentOrder.orderList.length; rowIndex++) {
const row = currentOrder.orderList[rowIndex];
if (localOrderBody.orderList[rowIndex] === undefined) {
alertWrongOrderFormat();
continue;
}
for (const rowProp in row) {
if (localOrderBody.orderList[rowIndex][rowProp] === undefined) {
alertWrongOrderFormat();
continue;
}
(currentOrder.orderList[rowIndex] as any)[rowProp] =
localOrderBody.orderList[rowIndex][rowProp];
}
}
currentOrder.other = localOrderBody.other;
}
for (const key in this.store.orderFooter) {
(this.store.orderFooter as any)[key] = localOrderFooter[key];
}
this.store.chosenOrderType = localOrder.orderType;
this.store.chosenLocalOrderId = localOrder.id;
this.store.orderMode = 'OrderMessage';
}
}
});
-32
View File
@@ -1,32 +0,0 @@
import { defineComponent } from 'vue';
import { useStore } from '../store/store';
export default defineComponent({
setup() {
return {
store: useStore()
};
},
methods: {
verifyOrderFields() {
// const header = this.store[this.store.chosenOrderType].header;
const footer = this.store.orderFooter;
const fieldsToCorrect = [];
// if (!header.orderNo) fieldsToCorrect.push('numer rozkazu');
// if (!header.trainNo) fieldsToCorrect.push('numer pociągu / manewru');
// if (!header.date) fieldsToCorrect.push('data');
if (!footer.stationName) fieldsToCorrect.push('stacja');
if (!footer.checkpointName) fieldsToCorrect.push('posterunek');
if (!footer.hour) fieldsToCorrect.push('godzina');
if (!footer.minutes) fieldsToCorrect.push('minuta');
if (!footer.dispatcherName && !footer.secondaryDispatcherName)
fieldsToCorrect.push('dyżurny ruchu (lub z polecenia dyżurnego ruchu)');
return fieldsToCorrect;
}
}
});
+318 -176
View File
@@ -1,209 +1,351 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { IOrderN, IOrderO, IOrderS, TOrder } from '../types/orderTypes'; import { IOrderData } from '../types/orderTypes';
import {
currentFormattedDate, import StorageManager from '../managers/storageManager';
currentFormattedHours, import i18n from '../i18n';
currentFormattedMinutes import { TPanelMode } from '../types/dataTypes';
} from '../utils/dateUtils';
export const useStore = defineStore('store', { export const useStore = defineStore('store', {
state: () => { state: () => {
return { return {
helperModalOpen: false, currentAppLocale: 'pl',
appUpdateData: {
version: '',
changelog: '',
releaseURL: ''
},
updateCardOpen: false,
orderDarkMode: false, orderDarkMode: false,
chosenOrderType: 'orderN' as TOrder, panelMode: 'OrderMessagePanel' as TPanelMode,
chosenLocalOrderId: '', chosenLocalOrderId: '',
orderMode: 'OrderMessage', orderData: {
orderFooter: {
stationName: '',
checkpointName: '',
hour: currentFormattedHours(),
minutes: currentFormattedMinutes(),
dispatcherName: '',
secondaryDispatcherName: ''
},
orderMessage: '',
footerMessage: '',
orderO: {
header: { header: {
orderNo: '1', A: '',
trainNo: '', B: '',
date: currentFormattedDate() C: '',
D: ''
}, },
instructions: [
orderList: [
{ {
name: '', key: '22',
from: '', name: '22',
to: '', active: false,
vmax: '', inputFields: {},
jo: false, optionalFieldNames: [],
reason: '' textDirectives: []
}, },
{ {
name: '', key: '99',
from: '', name: '99',
to: '', active: false,
vmax: '', inputFields: {
jo: false, x1: ''
reason: '' },
optionalFieldNames: [],
textDirectives: []
}, },
{ {
name: '', key: '2110',
from: '', name: '21.10',
to: '', active: false,
vmax: '', inputFields: {
jo: false, track1: '',
reason: '' signalbox1: '',
track2: '',
signalbox2: '',
signal1: '',
signal2: '',
signal3: ''
},
optionalFieldNames: ['signal1', 'signal2', 'signal3'],
textDirectives: ['bold1', 'br']
}, },
{ {
name: '', key: '2115',
from: '', name: '21.15',
to: '', active: false,
vmax: '', inputFields: {
jo: false, track1: '',
reason: '' signalbox1: '',
track2: '',
signal1: '',
signal2: '',
signal3: ''
},
optionalFieldNames: ['signal1', 'signal2', 'signal3'],
textDirectives: ['bold1', 'br']
}, },
{ {
name: '', key: '2120',
from: '', name: '21.20',
to: '', active: false,
vmax: '', inputFields: {
jo: false, signalbox1: '',
reason: '' signalbox2: '',
track1: ''
},
optionalFieldNames: [],
textDirectives: ['highlight1', 'highlight2', 'underline1', 'br']
},
{
key: '2125',
name: '21.25',
active: false,
inputFields: {
select1: 'select1-a',
signalbox1: '',
track1: '',
km1: '',
hour1: ''
},
selectFields: {
select1: {
options: ['select1-a', 'select1-b']
}
},
optionalFieldNames: [],
textDirectives: []
},
{
key: '2135',
name: '21.35',
active: false,
inputFields: {
track1: '',
signalbox1: ''
},
optionalFieldNames: [],
textDirectives: ['bold1']
},
{
key: '2140',
name: '21.40',
active: false,
inputFields: {
signalbox1: '',
signalbox2: '',
km1: '',
other1: ''
},
optionalFieldNames: ['signalbox2'],
textDirectives: ['bold1']
},
{
key: '2145',
name: '21.45',
active: false,
inputFields: {
signalbox1: '',
signal1: ''
},
optionalFieldNames: [],
textDirectives: ['bold1', 'br']
},
{
key: '2150',
name: '21.50',
active: false,
inputFields: {
signalbox1: '',
signalbox2: '',
km1: ''
},
optionalFieldNames: ['signalbox2'],
textDirectives: ['bold1', 'br', 'bold2']
},
{
key: '2155',
name: '21.55',
active: false,
inputFields: {
signalbox1: '',
signalbox2: '',
signal1: ''
},
optionalFieldNames: ['signalbox2'],
textDirectives: ['bold1']
},
{
key: '2160',
name: '21.60',
active: false,
inputFields: {
signalbox1: '',
signalbox2: '',
signalbox3: '',
line1: '',
vmax1: ''
},
optionalFieldNames: [],
textDirectives: ['bold1']
},
{
key: '2165',
name: '21.65',
active: false,
inputFields: {
km1: '',
km2: ''
},
optionalFieldNames: [],
textDirectives: ['bold1']
},
{
key: '2170',
name: '21.70',
active: false,
inputFields: {
signalbox1: '',
signalbox2: '',
line1: ''
},
optionalFieldNames: [],
textDirectives: ['bold1', 'br', 'bold2']
},
{
key: '2180',
name: '21.80',
active: false,
inputFields: {
track1: '',
signalbox1: '',
km1: '',
signalbox2: '',
hour1: ''
},
optionalFieldNames: [],
textDirectives: ['bold1']
},
{
key: '2181',
name: '21.81',
active: false,
inputFields: {},
optionalFieldNames: [],
textDirectives: ['bold1']
},
{
key: '2182',
name: '21.82',
active: false,
inputFields: {
train1: '',
km1: ''
},
optionalFieldNames: [],
textDirectives: ['bold1']
},
{
key: '2183',
name: '21.83',
active: false,
inputFields: {
train1: '',
km1: ''
},
optionalFieldNames: [],
textDirectives: ['bold1']
},
{
key: '2185',
name: '21.85',
active: false,
inputFields: {
track1: '',
km1: '',
signalbox1: '',
signalbox2: ''
},
optionalFieldNames: [],
textDirectives: ['bold1']
},
{
key: '2310',
name: '23.10',
active: false,
inputFields: {},
listFields: [
{
active: false,
values: {
signalbox1: '',
signalbox2: '',
track1: '',
vmax1: '',
km1: '',
km2: '',
other1: ''
}
},
{
active: false,
values: {
signalbox1: '',
signalbox2: '',
track1: '',
vmax1: '',
km1: '',
km2: '',
other1: ''
}
},
{
active: false,
values: {
signalbox1: '',
signalbox2: '',
track1: '',
vmax1: '',
km1: '',
km2: '',
other1: ''
}
} }
], ],
other: '' optionalFieldNames: [],
} as IOrderO, textDirectives: ['bold1', 'br']
orderN: {
header: {
orderNo: '1',
trainNo: '',
date: currentFormattedDate()
},
rows: [
{
enabled: false,
from: '',
to: '',
trackNo: '',
trackNo2: ''
}, },
{ {
enabled: false, key: '2311',
option1: 'sygnału "Nakaz Jazdy"', name: '23.11',
option2: 'lewy', active: false,
option3: 'lewy', inputFields: {},
signal1: '', optionalFieldNames: [],
signal2: '', textDirectives: ['bold1']
signal3: '',
signalType: 'wyjazdowego',
checkbox: 'checkbox-2a',
direction1: '',
direction2: '',
trackNoFrom: '',
trackNoTo1: '',
trackNoTo2: ''
}, },
{ {
enabled: false, key: '2320',
option1: 'Jazda', name: '23.20',
option2: 'pociąg', active: false,
inputFields: {
direction: '', other2320: ''
toKilometer: '',
trackNo: '',
untilHour: '',
untilMin: ''
}, },
{ optionalFieldNames: [],
enabled: false, textDirectives: []
trackNo: '',
optionStation: 'stację',
stationName: '',
checkbox: 'checkbox-4a',
side: 'lewej'
},
{
enabled: false,
trackNo: '',
direction: '',
stationType: 'stację',
stationName: '',
on: ''
},
{
enabled: false,
content: '',
twoWay: {
enabled: false,
from: '',
to: '',
trackNo: ''
} }
],
footer: {
V: '',
W: '',
Y: '',
Z: ''
} }
] } as IOrderData,
} as IOrderN,
orderS: { orderMessage: ''
header: {
orderNo: '1',
trainNo: '',
for: 'pociągu',
date: currentFormattedDate()
},
rows: [
{
enabled: false,
option1: 'sygnału "nakaz jazdy"',
optionSignal: 'wyjazdowego',
radio1: 'radio-1a-1',
signal1: '',
trackNo: ''
},
{
enabled: false,
signalType: 'wyjazdowego',
signal1: '',
signal2: '',
signal3: '',
trackNo: ''
},
{
enabled: false,
from: '',
to: '',
trackNo: '',
trainNo: '',
arrivedTo: '',
hour: ''
},
{
enabled: false,
content: '',
w5: {
enabled: false,
maxHour: '',
borderType: 'wskaźnik przetaczania W5',
tmName: '',
maxKm: '',
returnWay: 'sygnał ręczny "Do mnie"',
trackNo: ''
}
}
]
} as IOrderS
}; };
},
actions: {
changeLang(lang: string) {
i18n.global.locale.value = lang as typeof i18n.global.locale.value;
this.currentAppLocale = lang;
StorageManager.setStringValue('lang', lang);
}
} }
}); });
+1 -1
View File
@@ -1,4 +1,4 @@
$bgCol: #141414; $bgCol: #0e0e0e;
$bgColLighter: #292929; $bgColLighter: #292929;
$bgColDarker: #080808; $bgColDarker: #080808;
$accentCol: #ff6060; $accentCol: #ff6060;
+25 -16
View File
@@ -1,24 +1,33 @@
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
/* libre-franklin-regular - latin_latin-ext */
@font-face { @font-face {
font-family: 'Libre Franklin';
src: url('/fonts/LibreFranklin-Bold.woff2') format('woff2');
font-weight: 800;
font-style: normal;
font-display: swap; font-display: swap;
}
@font-face {
font-family: 'Libre Franklin'; font-family: 'Libre Franklin';
src: url('/fonts/LibreFranklin-Regular.woff2') format('woff2'); font-style: normal;
font-weight: 400;
src: url('/fonts/libre-franklin-regular.woff2') format('woff2');
}
/* libre-franklin-500 - latin_latin-ext */
@font-face {
font-display: swap;
font-family: 'Libre Franklin';
font-style: normal;
font-weight: 500; font-weight: 500;
font-style: normal; src: url('/fonts/libre-franklin-500.woff2') format('woff2');
font-display: swap;
} }
/* libre-franklin-700 - latin_latin-ext */
@font-face { @font-face {
font-family: 'Libre Franklin';
src: url('/fonts/LibreFranklin-SemiBold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap; font-display: swap;
font-family: 'Libre Franklin';
font-style: normal;
font-weight: 700;
src: url('/fonts/libre-franklin-700.woff2') format('woff2');
}
/* libre-franklin-800 - latin_latin-ext */
@font-face {
font-display: swap;
font-family: 'Libre Franklin';
font-style: normal;
font-weight: 800;
src: url('/fonts/libre-franklin-800.woff2') format('woff2');
} }
+16 -18
View File
@@ -1,22 +1,6 @@
@use 'fonts'; @use 'fonts';
@use 'colors'; @use 'colors';
@use 'sass:color';
// Global scrollbar style
::-webkit-scrollbar {
width: 15px;
}
::-webkit-scrollbar-track {
background: #333;
}
::-webkit-scrollbar-thumb {
background: #888;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
body, body,
html { html {
@@ -76,7 +60,7 @@ button.g-button {
} }
&:hover:not([data-disabled='true']) { &:hover:not([data-disabled='true']) {
background-color: colors.$bgColLighter; background-color: color.adjust(colors.$bgColDarker, $lightness: 15%);
} }
} }
@@ -107,6 +91,16 @@ button.g-button {
color: colors.$accentCol; color: colors.$accentCol;
} }
} }
&.icon {
display: flex;
justify-content: center;
align-items: center;
&:focus-visible {
outline: 1px solid white;
}
}
} }
// Text styles // Text styles
@@ -118,6 +112,10 @@ button.g-button {
&--warn { &--warn {
color: colors.$warnCol; color: colors.$warnCol;
} }
&--grayed {
color: #ccc;
}
} }
// Select style // Select style
+157
View File
@@ -0,0 +1,157 @@
@use 'colors';
$darkModeTextCol: #eee;
.order {
width: 100%;
background-color: white;
color: black;
height: 100%;
overflow: auto;
&.dark {
background-color: colors.$bgColDarker;
color: $darkModeTextCol;
}
@media screen and (max-width: 550px) {
font-size: 0.85em;
}
}
table.order-table {
width: 100%;
overflow: auto;
table-layout: fixed;
min-width: 750px;
border: 2px solid black;
border-collapse: collapse;
td {
border: 2px solid black;
border-collapse: collapse;
text-align: justify;
vertical-align: top;
}
}
.order-input-box {
display: inline-block;
position: relative;
padding-bottom: 1.25em;
& > span {
position: absolute;
left: 50%;
top: 0;
transform: translate(-50%, 2em);
font-size: 0.8em;
width: 100%;
text-align: center;
}
}
input.order-input {
max-width: 100%;
background-color: transparent;
outline: none;
border: none;
font-size: 1em;
text-align: center;
border-bottom: 2px dotted black;
&:focus-visible {
border-bottom: 2px solid colors.$accentCol;
}
&.row-checkbox + input::placeholder {
color: red;
}
&[type='checkbox']:focus-visible,
&[type='radio']:focus-visible {
outline: 2px solid colors.$accentCol;
}
&[type='checkbox'],
&[type='radio'] {
margin-top: 0.5em;
margin-right: 0.5em;
font-size: 0.8em;
color: black;
}
&::placeholder {
font-size: 0.85em;
}
}
label.order-input-label {
display: block;
margin-top: 0.5rem;
font-size: 0.8rem;
}
select.order-select {
margin-top: 0.5em;
margin-right: 0.5em;
font-size: 0.8em;
color: black;
}
textarea.order-textarea {
width: 100%;
min-height: 200px;
resize: vertical;
&:focus-visible {
outline: 2px solid colors.$accentCol;
}
}
// Dark mode
.order.dark {
input.order-input {
border-color: $darkModeTextCol !important;
color: $darkModeTextCol !important;
&:focus-visible {
border-bottom: 2px solid colors.$accentCol !important;
}
&::placeholder {
color: #ccc !important;
}
}
select.order-select {
color: $darkModeTextCol !important;
border-color: $darkModeTextCol;
&:focus-visible {
border-color: colors.$accentCol;
}
}
select.order-select option,
textarea.order-textarea {
color: $darkModeTextCol !important;
border-color: $darkModeTextCol !important;
background-color: colors.$bgColDarker !important;
}
.horizontal-bar {
background-color: white;
}
.order_header,
.order_other,
table.order-table,
tr,
td {
border-color: $darkModeTextCol !important;
}
}
+2 -17
View File
@@ -33,20 +33,5 @@ export interface ISceneryData {
signalType: string; signalType: string;
url: string; url: string;
} }
// export interface ITrainData {
// trainNo: number; export type TPanelMode = 'OrderListPanel' | 'OrderMessagePanel' | 'OrderTrainPickerPanel';
// driverId: number;
// driverName: string;
// driverIsSupporter: boolean;
// dataSignal: string;
// dataSceneryConnection: string;
// dataDistance: number;
// dataCon: string;
// dataSpeed: number;
// dataMass: number;
// dataLength: number;
// region: string;
// isOnline: number;
// lastSeen: number;
// station?: ISceneryData;
// }
+37 -167
View File
@@ -1,6 +1,6 @@
export type TOrder = 'orderO' | 'orderS' | 'orderN'; export type TOrder = 'orderO' | 'orderS' | 'orderN';
export interface LocalStorageOrder { export interface LocalStorageOrderLegacy {
id: string; id: string;
orderType: TOrder; orderType: TOrder;
orderBody: any; orderBody: any;
@@ -10,177 +10,47 @@ export interface LocalStorageOrder {
orderVersion?: string; orderVersion?: string;
} }
export interface IOrderN { export interface IStorageOrderData {
header: { id: string;
orderNo: string; orderVersion: string;
trainNo: string; createdAt?: number;
date: string; updatedAt?: number;
}; orderData: IOrderData;
rows: [
{
enabled: boolean;
from: string;
to: string;
trackNo: string;
trackNo2: string;
},
{
enabled: boolean;
option1: string;
option2: string;
option3: string;
signal1: string;
signal2: string;
signal3: string;
signalType: string;
checkbox: string;
direction1: string;
direction2: string;
trackNoFrom: string;
trackNoTo1: string;
trackNoTo2: string;
},
{
enabled: boolean;
option1: string;
option2: string;
direction: string;
toKilometer: string;
trackNo: string;
untilHour: string;
untilMin: string;
},
{
enabled: boolean;
trackNo: string;
optionStation: string;
stationName: string;
checkbox: string;
side: string;
},
{
enabled: boolean;
trackNo: string;
direction: string;
stationType: string;
stationName: string;
on: string;
},
{
enabled: boolean;
content: string;
twoWay: {
enabled: boolean;
from: string;
to: string;
trackNo: string;
};
}
];
} }
export interface IOrderS { export interface IOrderData {
header: { header: IOrderHeader;
orderNo: string; instructions: IOrderInstruction[];
trainNo: string; footer: IOrderFooter;
for: string;
date: string;
};
rows: [
{
enabled: boolean;
option1: string;
optionSignal: string;
radio1: string;
signal1: string;
trackNo: string;
},
{
enabled: boolean;
signalType: string;
signal1: string;
signal2: string;
signal3: string;
trackNo: string;
},
{
enabled: boolean;
from: string;
to: string;
trackNo: string;
trainNo: string;
arrivedTo: string;
hour: string;
},
{
enabled: boolean;
content: string;
w5: {
enabled: boolean;
borderType: string;
trackNo: string;
maxKm: string;
returnWay: string;
maxHour: string;
tmName: string;
};
}
];
} }
export interface IOrderO { export interface IOrderHeader {
header: { A: string;
orderNo: string; B: string;
trainNo: string; C: string;
date: string; D: string;
}; }
orderList: [ export interface IOrderFooter {
{ V: string;
W: string;
Y: string;
Z: string;
}
export interface IOrderFieldItem {
active: false;
values: Record<string, string>;
}
export interface IOrderInstruction {
key: string;
name: string; name: string;
from: string; active: boolean;
to: string; inputFields: Record<string, string>;
vmax: string; optionalFieldNames: string[];
jo: boolean; textDirectives: string[];
reason: string;
},
{
name: string;
from: string;
to: string;
vmax: string;
jo: boolean;
reason: string;
},
{
name: string;
from: string;
to: string;
vmax: string;
jo: boolean;
reason: string;
},
{
name: string;
from: string;
to: string;
vmax: string;
jo: boolean;
reason: string;
},
{
name: string;
from: string;
to: string;
vmax: string;
jo: boolean;
reason: string;
}
];
other: string; selectFields?: Record<string, Record<string, string[]>>;
listFields?: IOrderFieldItem[];
} }
-18
View File
@@ -1,18 +0,0 @@
export function currentFormattedDate() {
return (
new Date().toLocaleDateString('pl-PL', {
day: 'numeric',
month: 'numeric',
year: 'numeric'
}) + 'r.'
);
}
export function currentFormattedMinutes() {
const date = new Date();
return (date.getMinutes() < 10 ? '0' : '') + date.getMinutes();
}
export function currentFormattedHours() {
return new Date().toLocaleTimeString('pl-PL', { hour: '2-digit' });
}
@@ -1,4 +1,7 @@
// For use inside OrderS and OrderN export const getOrderFullId = (orderNumber: number, sceneryHash: string) => {
return `RD-${orderNumber}-${sceneryHash}-${new Date().getUTCFullYear().toString().slice(2)}`;
};
export const handleOrderPlaceholders = (isRowEnabled: boolean, rowRef: HTMLTableElement) => { export const handleOrderPlaceholders = (isRowEnabled: boolean, rowRef: HTMLTableElement) => {
rowRef.querySelectorAll('input[type="text"]').forEach((node) => { rowRef.querySelectorAll('input[type="text"]').forEach((node) => {
if (!isRowEnabled) { if (!isRowEnabled) {
+114 -89
View File
@@ -1,31 +1,46 @@
<template> <template>
<!-- <OrderHelper v-if="store.helperModalOpen" /> -->
<div class="home"> <div class="home">
<div class="home_container"> <div class="home-container">
<div class="order_container"> <div class="order-container">
<SideBar /> <Order />
<OrderVue />
</div> </div>
<div class="message_container"> <div class="panel-container">
<div class="message_nav"> <div class="panel-nav">
<span v-for="(action, i) in navActions" :key="action.mode"> <button
<b v-if="i > 0">&bull;</b> key="OrderMessagePanel"
class="g-button"
:data-active="store.panelMode == 'OrderMessagePanel'"
@click="selectOrderMode('OrderMessagePanel')"
>
<MessageSquareTextIcon :size="20" />
{{ t(`navbar.OrderMessagePanel`) }}
</button>
<button <button
class="g-button option" key="OrderListPanel"
:data-active="store.orderMode == action.mode" class="g-button"
@click="selectOrderMode(action.mode)" :data-active="store.panelMode == 'OrderListPanel'"
@click="selectOrderMode('OrderListPanel')"
> >
{{ action.value }} <BookMarkedIcon :size="20" />
{{ t(`navbar.OrderListPanel`) }}
</button>
<button
key="OrderTrainPickerPanel"
class="g-button"
:data-active="store.panelMode == 'OrderTrainPickerPanel'"
@click="selectOrderMode('OrderTrainPickerPanel')"
>
<TrainFrontIcon :size="20" />
{{ t(`navbar.OrderTrainPickerPanel`) }}
</button> </button>
</span>
</div> </div>
<transition name="order-anim" mode="out-in"> <transition name="order-anim" mode="out-in">
<keep-alive> <keep-alive>
<Component :is="orderModeComponent" /> <Component :is="panelComponent" />
</keep-alive> </keep-alive>
</transition> </transition>
</div> </div>
@@ -33,70 +48,42 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { computed } from 'vue';
import OrderVue from '../components/Order.vue';
import SideBar from '../components/SideBar.vue';
import OrderMessage from '../components/OrderMessage.vue';
import OrderList from '../components/OrderList.vue';
import { useStore } from '../store/store'; import { useStore } from '../store/store';
import OrderHelper from '../components/OrderHelper.vue'; import { useI18n } from 'vue-i18n';
import OrderTrainPicker from '../components/OrderTrainPicker.vue';
export default defineComponent({ import Order from '../components/Order/Order.vue';
components: { OrderVue, SideBar, OrderHelper }, import OrderMessagePanel from '../components/Panels/OrderMessagePanel.vue';
import OrderListPanel from '../components/Panels/OrderListPanel.vue';
import OrderTrainPickerPanel from '../components/Panels/OrderTrainPickerPanel.vue';
import { TPanelMode } from '../types/dataTypes';
import { BookMarkedIcon, MessageSquareTextIcon, TrainFrontIcon } from 'lucide-vue-next';
data() { const { t } = useI18n();
return { const store = useStore();
navActions: [
{ function selectOrderMode(mode: TPanelMode) {
mode: 'OrderMessage', store.panelMode = mode;
value: 'TREŚĆ ROZKAZU'
},
{
mode: 'OrderList',
value: 'ZAPISANE ROZKAZY'
},
{
mode: 'OrderTrainPicker',
value: 'POCIĄGI'
} }
]
};
},
methods: { const panelComponent = computed(() => {
selectOrderMode(mode: string) { switch (store.panelMode) {
this.store.orderMode = mode; case 'OrderListPanel':
} return OrderListPanel;
}, case 'OrderTrainPickerPanel':
return OrderTrainPickerPanel;
setup() { case 'OrderMessagePanel':
return {
store: useStore()
};
},
computed: {
orderModeComponent() {
switch (this.store.orderMode) {
case 'OrderMessage':
return OrderMessage;
case 'OrderList':
return OrderList;
case 'OrderTrainPicker':
return OrderTrainPicker;
default: default:
return OrderMessage; return OrderMessagePanel;
}
}
} }
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@use '../styles/colors';
.home { .home {
min-height: 100vh;
overflow-x: auto; overflow-x: auto;
display: flex; display: flex;
@@ -104,52 +91,90 @@ export default defineComponent({
align-items: center; align-items: center;
width: 100%; width: 100%;
}
.home_container { .home-container {
display: flex; display: grid;
flex-wrap: wrap; grid-template-columns: 800px 500px;
justify-content: center; justify-content: center;
gap: 2em 1em; gap: 2em 1em;
padding: 0.5em; padding: 1em;
width: 100%; width: 100%;
@media screen and (max-width: 650px) { & > div {
height: calc(100vh - 5em);
overflow: auto;
}
@media screen and (max-width: 1350px) {
grid-template-columns: auto;
padding: 1em 0.5em; padding: 1em 0.5em;
} }
} }
.order_container { .order-container {
width: 100%;
max-width: 600px;
display: flex; display: flex;
align-items: start; align-items: start;
max-width: 800px;
@media screen and (max-width: 650px) { @media screen and (max-width: 650px) {
flex-direction: column; flex-direction: column;
} }
} }
.message_container { .panel-container {
width: 100%; padding: 2px;
max-width: 500px; max-width: 800px;
display: grid; display: grid;
grid-template-rows: auto 1fr; grid-template-rows: auto auto 1fr;
height: 95vh;
overflow: auto;
} }
.message_nav { .panel-nav {
display: flex; display: flex;
align-items: center;
justify-content: center; justify-content: center;
align-items: center;
gap: 0.25em;
flex-wrap: wrap; flex-wrap: wrap;
margin-bottom: 2em; margin-bottom: 1.5em;
}
.panel-nav > button {
position: relative;
display: flex;
justify-content: center;
align-items: center;
gap: 0.5em;
min-width: 8em;
padding: 0.25em 0.5em;
&:focus-visible {
outline: 1px solid white;
}
&::before {
position: absolute;
content: '';
bottom: -3px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 3px;
transition: all 0.25s;
background-color: colors.$accentCol;
}
&[data-active='true'] {
color: colors.$accentCol;
}
&[data-active='true']::before {
width: 100%;
} }
} }
</style> </style>
+1 -1
View File
@@ -23,7 +23,7 @@ export default defineConfig({
VitePWA({ VitePWA({
registerType: 'prompt', registerType: 'prompt',
workbox: { workbox: {
globPatterns: ['**/*.{js,css,html,png,svg,img}'], globPatterns: ['**/*.{js,css,html,png,svg,img,woff2}'],
runtimeCaching: [ runtimeCaching: [
{ {
urlPattern: /^https:\/\/stacjownik.spythere.eu\/\/api\/getSceneries/i, urlPattern: /^https:\/\/stacjownik.spythere.eu\/\/api\/getSceneries/i,
-4465
View File
File diff suppressed because it is too large Load Diff