feat: journal timetable view mode

This commit is contained in:
2025-04-25 02:14:45 +02:00
parent 3b68056acc
commit 17d5574d0c
15 changed files with 628 additions and 224 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}
+113
View File
@@ -0,0 +1,113 @@
#!/bin/bash
# Logger Function
log() {
local message="$1"
local type="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local color
local endcolor="\033[0m"
case "$type" in
"info") color="\033[38;5;79m" ;;
"success") color="\033[1;32m" ;;
"error") color="\033[1;31m" ;;
*) color="\033[1;34m" ;;
esac
echo -e "${color}${timestamp} - ${message}${endcolor}"
}
# Error handler function
handle_error() {
local exit_code=$1
local error_message="$2"
log "Error: $error_message (Exit Code: $exit_code)" "error"
exit $exit_code
}
# Function to check for command availability
command_exists() {
command -v "$1" &> /dev/null
}
check_os() {
if ! [ -f "/etc/debian_version" ]; then
echo "Error: This script is only supported on Debian-based systems."
exit 1
fi
}
# Function to Install the script pre-requisites
install_pre_reqs() {
log "Installing pre-requisites" "info"
# Run 'apt-get update'
if ! apt-get update -y; then
handle_error "$?" "Failed to run 'apt-get update'"
fi
# Run 'apt-get install'
if ! apt-get install -y apt-transport-https ca-certificates curl gnupg; then
handle_error "$?" "Failed to install packages"
fi
if ! mkdir -p /usr/share/keyrings; then
handle_error "$?" "Makes sure the path /usr/share/keyrings exist or run ' mkdir -p /usr/share/keyrings' with sudo"
fi
rm -f /usr/share/keyrings/nodesource.gpg || true
rm -f /etc/apt/sources.list.d/nodesource.list || true
# Run 'curl' and 'gpg' to download and import the NodeSource signing key
if ! curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /usr/share/keyrings/nodesource.gpg; then
handle_error "$?" "Failed to download and import the NodeSource signing key"
fi
# Explicitly set the permissions to ensure the file is readable by all
if ! chmod 644 /usr/share/keyrings/nodesource.gpg; then
handle_error "$?" "Failed to set correct permissions on /usr/share/keyrings/nodesource.gpg"
fi
}
# Function to configure the Repo
configure_repo() {
local node_version=$1
arch=$(dpkg --print-architecture)
if [ "$arch" != "amd64" ] && [ "$arch" != "arm64" ] && [ "$arch" != "armhf" ]; then
handle_error "1" "Unsupported architecture: $arch. Only amd64, arm64, and armhf are supported."
fi
echo "deb [arch=$arch signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$node_version nodistro main" | tee /etc/apt/sources.list.d/nodesource.list > /dev/null
# N|solid Config
echo "Package: nsolid" | tee /etc/apt/preferences.d/nsolid > /dev/null
echo "Pin: origin deb.nodesource.com" | tee -a /etc/apt/preferences.d/nsolid > /dev/null
echo "Pin-Priority: 600" | tee -a /etc/apt/preferences.d/nsolid > /dev/null
# Nodejs Config
echo "Package: nodejs" | tee /etc/apt/preferences.d/nodejs > /dev/null
echo "Pin: origin deb.nodesource.com" | tee -a /etc/apt/preferences.d/nodejs > /dev/null
echo "Pin-Priority: 600" | tee -a /etc/apt/preferences.d/nodejs > /dev/null
# Run 'apt-get update'
if ! apt-get update -y; then
handle_error "$?" "Failed to run 'apt-get update'"
else
log "Repository configured successfully."
log "To install Node.js, run: apt-get install nodejs -y" "info"
log "You can use N|solid Runtime as a node.js alternative" "info"
log "To install N|solid Runtime, run: apt-get install nsolid -y \n" "success"
fi
}
# Define Node.js version
NODE_VERSION="23.x"
# Check OS
check_os
# Main execution
install_pre_reqs || handle_error $? "Failed installing pre-requisites"
configure_repo "$NODE_VERSION" || handle_error $? "Failed configuring repository"
+118 -55
View File
@@ -1,16 +1,68 @@
<template> <template>
<div class="flex gap-2 mb-2 print:hidden"> <div class="flex gap-2 justify-between flex-wrap mb-2 print:hidden">
<button <div class="flex gap-2">
class="p-1 rounded-md" <button
:class="{ :class="`p-1 rounded-md ${
'bg-zinc-800 hover:bg-zinc-700': globalStore.viewMode == 'active', globalStore.viewMode == 'active'
'bg-green-600 hover:bg-green-500': globalStore.viewMode == 'storage', ? 'bg-green-600 hover:bg-green-500'
}" : 'bg-zinc-800 hover:bg-zinc-700'
@click="toggleViewMode" }`"
> @click="toggleViewMode('active')"
<ArchiveBoxArrowDownIcon class="size-6" /> >
</button> <WifiIcon class="size-6" />
</button>
<button
:class="`p-1 rounded-md ${
globalStore.viewMode == 'storage'
? 'bg-green-600 hover:bg-green-500'
: 'bg-zinc-800 hover:bg-zinc-700'
}`"
@click="toggleViewMode('storage')"
>
<ArchiveBoxArrowDownIcon class="size-6" />
</button>
<button
:class="`p-1 rounded-md ${
globalStore.viewMode == 'journal'
? 'bg-green-600 hover:bg-green-500'
: 'bg-zinc-800 hover:bg-zinc-700'
}`"
@click="toggleViewMode('journal')"
>
<CloudIcon class="size-6" />
</button>
</div>
<div class="flex gap-2">
<button class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700 self-end" @click="toggleDarkMode">
<MoonIcon v-if="globalStore.darkMode" class="text-white size-6" />
<SunIcon v-else class="text-white size-6" />
</button>
<button
class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700 disabled:opacity-60 disabled:hover:bg-zinc-800"
:disabled="globalStore.currentTimetableData == null"
@click="openPrintingWindow"
>
<PrinterIcon class="text-white size-6" />
</button>
<button
class="p-1 rounded-md disabled:opacity-60 disabled:hover:bg-zinc-800"
:disabled="globalStore.currentTimetableData == null"
:class="{
'bg-green-600 hover:bg-green-700': isTimetableSaved,
'bg-zinc-800 hover:bg-zinc-700': !isTimetableSaved
}"
@click="saveToStorage"
>
<ArrowDownTrayIcon class="text-white size-6" />
</button>
</div>
<!-- Active Data Search -->
<select <select
name="trains" name="trains"
id="trains-select" id="trains-select"
@@ -21,45 +73,37 @@
@change="selectTrain" @change="selectTrain"
> >
<option :value="null" disabled> <option :value="null" disabled>
{{ apiStore.activeDataStatus == DataStatus.LOADING ? $t('data-loading-text') : $t('train-select-placeholder') }} {{
apiStore.activeDataStatus == DataStatus.LOADING
? $t('data-loading-text')
: $t('train-select-placeholder')
}}
</option> </option>
<option :value="train.id" v-for="train in globalStore.activeTimetableTrains"> <option :value="train.id" v-for="train in globalStore.activeTimetableTrains">
{{ train.driverName }} | {{ train.timetable?.category }} {{ train.trainNo }} [{{ getRegionNameById(train.region) }}] {{ train.driverName }} | {{ train.timetable?.category }} {{ train.trainNo }} [{{
getRegionNameById(train.region)
}}]
</option> </option>
</select> </select>
<!-- Local Storage Search -->
<input <input
type="text" type="text"
v-if="globalStore.viewMode == 'storage'" v-if="globalStore.viewMode == 'storage'"
v-model="globalStore.timetableSearch" v-model="globalStore.localTimetableSearch"
class="bg-zinc-800 p-1 rounded-md print:hidden w-full" class="bg-zinc-800 p-1 rounded-md print:hidden w-full"
:placeholder="$t('train-search-placeholder')" :placeholder="$t('train-search-placeholder')"
/> />
<button class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700" @click="toggleDarkMode"> <!-- Journal Serach -->
<MoonIcon v-if="globalStore.darkMode" class="text-white size-6" /> <input
<SunIcon v-else class="text-white size-6" /> type="text"
</button> v-else-if="globalStore.viewMode == 'journal'"
v-model="globalStore.journalTimetableSearch"
<button @change="fetchJournalTimetables"
class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700 disabled:opacity-60 disabled:hover:bg-zinc-800" class="bg-zinc-800 p-1 rounded-md print:hidden w-full"
:disabled="globalStore.currentTimetableData == null" :placeholder="$t('journal-search-placeholder')"
@click="openPrintingWindow" />
>
<PrinterIcon class="text-white size-6" />
</button>
<button
class="p-1 rounded-md disabled:opacity-60 disabled:hover:bg-zinc-800"
:disabled="globalStore.currentTimetableData == null"
:class="{
'bg-green-600 hover:bg-green-700': isTimetableSaved,
'bg-zinc-800 hover:bg-zinc-700': !isTimetableSaved,
}"
@click="saveToStorage"
>
<ArrowDownTrayIcon class="text-white size-6" />
</button>
</div> </div>
</template> </template>
@@ -68,9 +112,17 @@ import { computed } from 'vue';
import { useApiStore } from '../../stores/api.store'; import { useApiStore } from '../../stores/api.store';
import { DataStatus } from '../../types/api.types'; import { DataStatus } from '../../types/api.types';
import { useGlobalStore } from '../../stores/global.store'; import { useGlobalStore } from '../../stores/global.store';
import { PrinterIcon, MoonIcon, SunIcon, ArchiveBoxArrowDownIcon, ArrowDownTrayIcon } from '@heroicons/vue/16/solid'; import {
PrinterIcon,
MoonIcon,
SunIcon,
ArchiveBoxArrowDownIcon,
ArrowDownTrayIcon,
CloudIcon,
WifiIcon
} from '@heroicons/vue/16/solid';
import { getRegionNameById } from '../../utils/trainUtils'; import { getRegionNameById } from '../../utils/trainUtils';
import type { TimetableData } from '../../types/common.types'; import type { TimetableData, ViewMode } from '../../types/common.types';
// Stores // Stores
const apiStore = useApiStore(); const apiStore = useApiStore();
@@ -80,22 +132,26 @@ const globalStore = useGlobalStore();
const isTimetableSaved = computed(() => { const isTimetableSaved = computed(() => {
if (!globalStore.currentTimetableData) return false; if (!globalStore.currentTimetableData) return false;
return Object.keys(globalStore.storageTimetables).includes(`${globalStore.currentTimetableData.timetableId}`); return Object.keys(globalStore.storageTimetables).includes(
`${globalStore.currentTimetableData.timetableId}`
);
}); });
// Methods // Methods
function selectTrain() { function selectTrain() {
if (!apiStore.activeData) return; if (!apiStore.activeData) return;
globalStore.selectedActiveTrain = globalStore.activeTimetableTrains.find((train) => train.id == globalStore.selectedTrainId) ?? null; globalStore.selectedActiveTrain =
globalStore.activeTimetableTrains.find((train) => train.id == globalStore.selectedTrainId) ??
null;
if (globalStore.selectedActiveTrain != null) { if (globalStore.selectedActiveTrain != null) {
globalStore.generatedDate = new Date(); globalStore.generatedDate = new Date();
} }
} }
function toggleViewMode() { function toggleViewMode(viewMode: ViewMode) {
globalStore.viewMode = globalStore.viewMode == 'active' ? 'storage' : 'active'; globalStore.viewMode = viewMode;
} }
function toggleDarkMode() { function toggleDarkMode() {
@@ -109,16 +165,22 @@ function saveToStorage() {
try { try {
const savedTimetablesStorage = localStorage.getItem('savedTimetables'); const savedTimetablesStorage = localStorage.getItem('savedTimetables');
let savedTimetablesJSON: Record<number, TimetableData> = savedTimetablesStorage ? JSON.parse(savedTimetablesStorage) : {}; let savedTimetablesJSON: Record<number, TimetableData> = savedTimetablesStorage
? JSON.parse(savedTimetablesStorage)
: {};
if (savedTimetablesJSON[globalStore.currentTimetableData.timetableId] !== undefined) { if (savedTimetablesJSON[globalStore.currentTimetableData.timetableId] !== undefined) {
globalStore.selectedStorageTimetable = savedTimetablesJSON[globalStore.currentTimetableData.timetableId]; globalStore.selectedStorageTimetable =
savedTimetablesJSON[globalStore.currentTimetableData.timetableId];
globalStore.viewMode = 'storage'; globalStore.viewMode = 'storage';
return; return;
} }
savedTimetablesJSON[globalStore.currentTimetableData.timetableId] = { ...globalStore.currentTimetableData, savedTimestamp: Date.now() }; savedTimetablesJSON[globalStore.currentTimetableData.timetableId] = {
...globalStore.currentTimetableData,
savedTimestamp: Date.now()
};
localStorage.setItem('savedTimetables', JSON.stringify(savedTimetablesJSON)); localStorage.setItem('savedTimetables', JSON.stringify(savedTimetablesJSON));
globalStore.storageTimetables = savedTimetablesJSON; globalStore.storageTimetables = savedTimetablesJSON;
@@ -127,21 +189,22 @@ function saveToStorage() {
function openPrintingWindow() { function openPrintingWindow() {
if (globalStore.selectedActiveTrain != null) { if (globalStore.selectedActiveTrain != null) {
const date = `${globalStore.generatedDate!.toLocaleDateString('pl-PL').replace(/\./g, '-')}--${globalStore const date = `${globalStore
.generatedDate!.toLocaleDateString('pl-PL')
.replace(/\./g, '-')}--${globalStore
.generatedDate!.toLocaleTimeString('pl-PL') .generatedDate!.toLocaleTimeString('pl-PL')
.replace(/:/g, '-')}`; .replace(/:/g, '-')}`;
document.title = `${globalStore.selectedActiveTrain.driverName} ; ${globalStore.selectedActiveTrain.timetable!.category} ${ document.title = `${globalStore.selectedActiveTrain.driverName} ; ${
globalStore.selectedActiveTrain.trainNo globalStore.selectedActiveTrain.timetable!.category
} } ${globalStore.selectedActiveTrain.trainNo}
${globalStore.selectedActiveTrain.timetable?.route.replace('|', ' - ')} ; ${date}`; ${globalStore.selectedActiveTrain.timetable?.route.replace('|', ' - ')} ; ${date}`;
} }
window.print(); window.print();
} }
// function refreshData() { async function fetchJournalTimetables() {
// apiStore.fetchActiveData(); apiStore.fetchJournalTimetables(globalStore.journalTimetableSearch);
// selectTrain(); }
// }
</script> </script>
+45 -4
View File
@@ -1,4 +1,5 @@
<template> <template>
<!-- Local -->
<div class="my-2 print:hidden" v-if="globalStore.currentTimetableData?.savedTimestamp"> <div class="my-2 print:hidden" v-if="globalStore.currentTimetableData?.savedTimestamp">
<div class="flex gap-2"> <div class="flex gap-2">
<div class="flex items-center gap-2 bg-zinc-900 p-1 w-full"> <div class="flex items-center gap-2 bg-zinc-900 p-1 w-full">
@@ -18,11 +19,49 @@
</i18n-t> </i18n-t>
</div> </div>
<button class="font-bold bg-zinc-900 p-1 hover:bg-zinc-800" @click="removeTimetable(globalStore.currentTimetableData.timetableId)"> <button
class="font-bold bg-zinc-900 p-1 hover:bg-zinc-800"
@click="removeTimetable(globalStore.currentTimetableData.timetableId)"
>
<TrashIcon class="text-white size-6" /> <TrashIcon class="text-white size-6" />
</button> </button>
<button class="font-bold bg-zinc-900 p-1 hover:bg-zinc-800" @click="globalStore.selectedStorageTimetable = null"> <button
class="font-bold bg-zinc-900 p-1 hover:bg-zinc-800"
@click="globalStore.selectedStorageTimetable = null"
>
<ArrowUturnLeftIcon class="text-white size-6" />
</button>
</div>
</div>
<!-- Journal -->
<div class="my-2 print:hidden" v-if="globalStore.currentTimetableData?.journalCreatedAt">
<div class="flex gap-2">
<div class="flex items-center gap-2 bg-zinc-900 p-1 w-full">
<div>
<InformationCircleIcon class="size-5" />
</div>
<i18n-t keypath="journal-preview-info" tag="span">
<template v-slot:id>
<b>#{{ globalStore.currentTimetableData.timetableId }}</b>
</template>
<template v-slot:driverName>
<b>{{ globalStore.currentTimetableData.driverName }}</b>
</template>
<template v-slot:date>
<b>{{
new Date(globalStore.currentTimetableData.journalCreatedAt).toLocaleString()
}}</b>
</template>
</i18n-t>
</div>
<button
class="font-bold bg-zinc-900 p-1 hover:bg-zinc-800"
@click="globalStore.selectedJournalTimetable = null"
>
<ArrowUturnLeftIcon class="text-white size-6" /> <ArrowUturnLeftIcon class="text-white size-6" />
</button> </button>
</div> </div>
@@ -45,12 +84,14 @@ function removeTimetable(timetableId: number) {
try { try {
const savedTimetablesStorage = localStorage.getItem('savedTimetables'); const savedTimetablesStorage = localStorage.getItem('savedTimetables');
let savedTimetablesJSON: Record<number, TimetableData> = savedTimetablesStorage ? JSON.parse(savedTimetablesStorage) : {}; let savedTimetablesJSON: Record<number, TimetableData> = savedTimetablesStorage
? JSON.parse(savedTimetablesStorage)
: {};
delete savedTimetablesJSON[timetableId]; delete savedTimetablesJSON[timetableId];
localStorage.setItem('savedTimetables', JSON.stringify(savedTimetablesJSON)); localStorage.setItem('savedTimetables', JSON.stringify(savedTimetablesJSON));
globalStore.storageTimetables = savedTimetablesJSON; globalStore.storageTimetables = savedTimetablesJSON;
globalStore.selectedStorageTimetable = null; globalStore.selectedStorageTimetable = null;
} catch (error) {} } catch (error) {}
} }
+10 -11
View File
@@ -1,12 +1,11 @@
<template> <template>
<div <!-- Timetable render based on current view mode -->
:class="{ dark: globalStore.darkMode }" <div :class="{ dark: globalStore.darkMode }" v-if="globalStore.currentTimetableData != null"
v-if="globalStore.currentTimetableData != null" class="overflow-auto p-1 bg-white print:bg-white dark:bg-zinc-950 print:text-black text-black dark:text-white min-h-full">
class="overflow-auto p-1 bg-white print:bg-white dark:bg-zinc-950 print:text-black text-black dark:text-white min-h-full"
>
<div> <div>
<div class="p-1 font-bold w-max"> <div class="p-1 font-bold w-max">
{{ globalStore.currentTimetableData.category }} {{ globalStore.currentTimetableData.trainNo }} {{ $t('headers.relation') }} {{ globalStore.currentTimetableData.category }} {{ globalStore.currentTimetableData.trainNo }} {{
$t('headers.relation') }}
{{ globalStore.currentTimetableData.route.replace('|', ' - ') }} {{ globalStore.currentTimetableData.route.replace('|', ' - ') }}
</div> </div>
@@ -22,9 +21,8 @@
<div>{{ $t('train-select-info') }}</div> <div>{{ $t('train-select-info') }}</div>
</div> </div>
<div v-else> <LocalStorageView v-else-if="globalStore.viewMode == 'storage'" />
<StorageView /> <JournalStorageView v-else />
</div>
</div> </div>
</template> </template>
@@ -35,8 +33,9 @@ import { useApiStore } from '../../stores/api.store';
import { useGlobalStore } from '../../stores/global.store'; import { useGlobalStore } from '../../stores/global.store';
import TimetableBody from './TimetableBody.vue'; import TimetableBody from './TimetableBody.vue';
import TimetableHeader from './TimetableHeader.vue'; import TimetableHeader from './TimetableHeader.vue';
import type { SceneryRoute, StopRow, TimetablePathData } from '../../types/common.types'; import { type SceneryRoute, type StopRow, type TimetablePathData } from '../../types/common.types';
import StorageView from '../TimetableStorage/StorageView.vue'; import LocalStorageView from '../TimetableViews/LocalStorageView.vue';
import JournalStorageView from '../TimetableViews/JournalStorageView.vue';
const globalStore = useGlobalStore(); const globalStore = useGlobalStore();
const apiStore = useApiStore(); const apiStore = useApiStore();
@@ -1,5 +0,0 @@
<template>
<div>API STORAGE</div>
</template>
<script setup lang="ts"></script>
@@ -1,56 +0,0 @@
<template>
<div class="text-white">
<div v-if="globalStore.selectedStorageTimetable == null && Object.keys(globalStore.storageTimetables).length == 0">
<div class="font-bold text-xl">{{ $t('storage-empty-header') }}</div>
<div>{{ $t('storage-empty-info') }}</div>
</div>
<div v-else>
<div class="font-bold p-2 bg-zinc-700 mb-3">
<div class="text-2xl">{{ $t('storage-preview-title') }}</div>
<div class="flex gap-2 justify-center">
<template v-for="(mode, i) in storageModeList">
<span v-if="i != 0">&bull;</span>
<button class="hover:text-green-300" :class="{ 'underline text-green-300': currentStorageModeIndex == i }" @click="selectStorageMode(i)">
{{ $t(`storage-mode.${mode.key}`) }}
</button>
</template>
</div>
</div>
<!-- Current storage mode component -->
<component :is="storageModeList[currentStorageModeIndex].component" />
</div>
</div>
</template>
<script setup lang="ts">
import { useGlobalStore } from '../../stores/global.store';
import { StorageMode } from '../../types/common.types';
import { ref, type Component, type Ref } from 'vue';
import LocalStorage from '../TimetableStorage/LocalStorage.vue';
import ApiStorage from '../TimetableStorage/ApiStorage.vue';
interface CurrentStorageMode {
key: StorageMode;
component: Component;
}
const globalStore = useGlobalStore();
const currentStorageModeIndex: Ref<number> = ref(0);
const storageModeList: CurrentStorageMode[] = [
{
key: StorageMode.LOCAL,
component: LocalStorage,
},
{
key: StorageMode.API,
component: ApiStorage,
},
];
function selectStorageMode(index: number) {
currentStorageModeIndex.value = index;
}
</script>
@@ -0,0 +1,72 @@
<template>
<div class="text-white">
<h2 class="font-bold p-2 bg-zinc-700 mb-3 text-2xl">
{{ $t('journal-preview-title') }}
</h2>
<div v-if="apiStore.journalDataStatus == DataStatus.LOADING" class="bg-zinc-900 p-2">
{{ $t('data-loading-text') }}
</div>
<div v-else-if="apiStore.journalDataStatus == DataStatus.ERROR" class="bg-red-500 p-2">
{{ $t('data-loading-error-text') }}
</div>
<div
v-else-if="!apiStore.journalTimetablesData"
class="text-zinc-400 mt-2"
v-html="$t('journal-empty-info')"
></div>
<div v-else-if="apiStore.journalTimetablesData.length == 0">
<p class="text-zinc-300 mb-2">
{{ $t('journal-no-data') }}
</p>
<b class="text-red-300">
{{ $t('journal-reminder-text') }}
</b>
</div>
<div v-else>
<ul>
<li
v-for="timetable in apiStore.journalTimetablesData"
class="flex gap-1 w-full my-2"
@click="fetchTimetableDetails(timetable.id)"
>
<button class="bg-zinc-900 p-2 w-full cursor-pointer hover:bg-zinc-800 text-left">
<div class="text-zinc-300">
#{{ timetable.id }} &bull;
{{ new Date(timetable.createdAt!).toLocaleString() }}
</div>
<b>
{{ timetable.driverName }} | {{ timetable.trainCategoryCode }}
{{ timetable.trainNo }}
</b>
{{ timetable.route.replace('|', ' > ') }}
</button>
</li>
</ul>
<div v-if="apiStore.journalTimetablesData.length > 0">
<hr class="border-t-2 border-t-gray-500" />
<p class="text-zinc-400 text-sm">
{{ $t('journal-footer-text') }}
</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useApiStore } from '../../stores/api.store';
import { DataStatus } from '../../types/api.types';
const apiStore = useApiStore();
function fetchTimetableDetails(id: number) {
apiStore.fetchJournalTimetableDetails(id);
}
</script>
@@ -1,16 +1,42 @@
<template> <template>
<div> <div class="text-white">
<div class="font-bold p-2 bg-zinc-800 mb-3" v-if="filteredTimetables.length == 0">{{ $t('storage-preview-empty') }}</div> <div class="font-bold p-2 bg-zinc-700 mb-3">
<div class="text-2xl">{{ $t('storage-preview-title') }}</div>
</div>
<ul> <div
v-if="
globalStore.selectedStorageTimetable == null &&
Object.keys(globalStore.storageTimetables).length == 0
"
class="text-zinc-400"
>
{{ $t('storage-empty-info') }}
</div>
<div class="font-bold p-2 bg-zinc-800 mb-3" v-else-if="filteredTimetables.length == 0">
{{ $t('storage-preview-empty') }}
</div>
<ul v-else>
<li v-for="timetable in filteredTimetables" class="flex gap-1 w-full my-2"> <li v-for="timetable in filteredTimetables" class="flex gap-1 w-full my-2">
<button class="bg-zinc-900 p-2 w-full cursor-pointer hover:bg-zinc-800 text-left" @click="selectTimetable(timetable)"> <button
<div class="text-zinc-300">#{{ timetable.timetableId }} &bull; {{ new Date(timetable.savedTimestamp!).toLocaleString() }}</div> class="bg-zinc-900 p-2 w-full cursor-pointer hover:bg-zinc-800 text-left"
<b>{{ timetable.driverName }} | {{ timetable.category }} {{ timetable.trainNo }}</b> {{ timetable.route.replace('|', ' > ') }} @click="selectTimetable(timetable)"
>
<div class="text-zinc-300">
#{{ timetable.timetableId }} &bull;
{{ new Date(timetable.savedTimestamp!).toLocaleString() }}
</div>
<b>{{ timetable.driverName }} | {{ timetable.category }} {{ timetable.trainNo }}</b>
{{ timetable.route.replace('|', ' > ') }}
</button> </button>
<button class="bg-zinc-900 p-2 hover:bg-zinc-800" @click="removeTimetable(timetable.timetableId)"> <button
<TrashIcon class="size-5 text-white" /> class="bg-zinc-900 p-2 hover:bg-zinc-800"
@click="removeTimetable(timetable.timetableId)"
>
<TrashIcon class="size-6 text-white" />
</button> </button>
</li> </li>
</ul> </ul>
@@ -18,11 +44,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useGlobalStore } from '../../stores/global.store'; import { useGlobalStore } from '../../stores/global.store';
import { computed } from 'vue';
import { TrashIcon } from '@heroicons/vue/16/solid';
import type { TimetableData } from '../../types/common.types'; import type { TimetableData } from '../../types/common.types';
import { TrashIcon } from '@heroicons/vue/16/solid';
const globalStore = useGlobalStore(); const globalStore = useGlobalStore();
const i18n = useI18n(); const i18n = useI18n();
@@ -30,11 +56,11 @@ const i18n = useI18n();
const filteredTimetables = computed(() => { const filteredTimetables = computed(() => {
let timetables = Object.values(globalStore.storageTimetables); let timetables = Object.values(globalStore.storageTimetables);
if (globalStore.timetableSearch.length != 0) if (globalStore.localTimetableSearch.length != 0)
timetables = timetables.filter((st) => timetables = timetables.filter((st) =>
`${st.timetableId} ${st.driverName} ${st.route} ${st.category} ${st.trainNo}` `${st.timetableId} ${st.driverName} ${st.route} ${st.category} ${st.trainNo}`
.toLocaleLowerCase() .toLocaleLowerCase()
.includes(globalStore.timetableSearch.toLocaleLowerCase()) .includes(globalStore.localTimetableSearch.toLocaleLowerCase())
); );
timetables.sort((a, b) => { timetables.sort((a, b) => {
@@ -55,11 +81,16 @@ function removeTimetable(timetableId: number) {
try { try {
const savedTimetablesStorage = localStorage.getItem('savedTimetables'); const savedTimetablesStorage = localStorage.getItem('savedTimetables');
let savedTimetablesJSON: Record<number, TimetableData> = savedTimetablesStorage ? JSON.parse(savedTimetablesStorage) : {}; let savedTimetablesJSON: Record<number, TimetableData> = savedTimetablesStorage
? JSON.parse(savedTimetablesStorage)
: {};
delete savedTimetablesJSON[timetableId]; delete savedTimetablesJSON[timetableId];
localStorage.setItem('savedTimetables', JSON.stringify(savedTimetablesJSON)); localStorage.setItem('savedTimetables', JSON.stringify(savedTimetablesJSON));
globalStore.storageTimetables = savedTimetablesJSON; globalStore.storageTimetables = savedTimetablesJSON;
} catch (error) {} } catch (error) {
console.error(error);
}
} }
</script> </script>
+35 -29
View File
@@ -1,30 +1,36 @@
{ {
"data-loading-text": "Loading data...", "data-loading-text": "Loading data...",
"train-select-placeholder": "Choose active train from the list", "data-loading-error-text": "Oops! An error occurent while loading data from the server!",
"train-select-info": "Choose active train to generate SRJP timetable", "train-select-placeholder": "Choose active train from the list",
"train-search-placeholder": "Enter TT details (number, route, user)", "train-select-info": "Choose active train to generate SRJP timetable",
"headers": { "train-search-placeholder": "Enter TT details (number, route, user)",
"line_no": "Line\nno.", "headers": {
"line_km": "Km", "line_no": "Line\nno.",
"station": "Station", "line_km": "Km",
"time": "Time", "station": "Station",
"loco_1": "Loco I", "time": "Time",
"loco_2": "Loco II", "loco_1": "Loco I",
"loco_3": "Loco III", "loco_2": "Loco II",
"mass": "Loco load", "loco_3": "Loco III",
"length": "Train len.", "mass": "Loco load",
"vmax": "Vmax", "length": "Train len.",
"relation": "Route" "vmax": "Vmax",
}, "relation": "Route"
"storage-empty-header": "ARCHIVED TIMETABLES SEARCH MODE", },
"storage-empty-info": "Timetables will be shown here after their archiving.", "storage-empty-header": "ARCHIVED TIMETABLES SEARCH MODE",
"storage-preview-title": "ARCHIVED TIMETABLES", "storage-empty-info": "Timetables will be shown here after their archiving.",
"storage-preview-empty": "No entries found for given parameters", "storage-preview-title": "ARCHIVED TIMETABLES",
"storage-preview-info": "Archived timetable {id} for user {driverName} from: {date}", "storage-preview-empty": "No entries found for given parameters",
"storage-preview-button-text": "Return", "storage-preview-info": "Archived timetable {id} for user {driverName} from: {date}",
"delete-timetable-confirm": "Are you sure that you want to delete this timetable?", "storage-preview-button-text": "Return",
"storage-mode": { "delete-timetable-confirm": "Are you sure that you want to delete this timetable?",
"local": "LOCALLY",
"api": "STACJOWNIK" "journal-preview-title": "TIMETABLES JOURNAL",
} "journal-empty-info": "Enter timetable details in the text field above - it may be: <br> id (#number); nickname (nick:Spythere); date (date:11.01.2025); starting point (from:Krnów)<br>Up to 15 newest timetables will be shown.",
} "journal-search-placeholder": "nick:Spythere date:02.04.2025 from:Krnów to:Biała_Sudecka",
"journal-preview-info": "Historical timetable {id} for user {driverName} from: {date}",
"journal-no-data": "No data for the current search! Check if the data you entered is correct.",
"journal-reminder-text": "Warning: detailed timetables data for SRJP purpose are collected since 1st February 2025 and only for users who support Stacjownik project!",
"journal-footer-text": "Detailed timetables data for SRJP purpose are collected since 1st February 2025 and only for users who support Stacjownik project!"
}
+35 -29
View File
@@ -1,30 +1,36 @@
{ {
"data-loading-text": "Ładowanie danych...", "data-loading-text": "Ładowanie danych...",
"train-select-placeholder": "Wybierz pociąg z listy", "data-loading-error-text": "Ups! Wystąpił błąd podczas pobierania danych z serwera!",
"train-select-info": "Wybierz aktywny pociąg, aby wygenerować SRJP", "train-select-placeholder": "Wybierz pociąg z listy",
"train-search-placeholder": "Wpisz szczegóły RJ (nr, relacja, gracz)", "train-select-info": "Wybierz aktywny pociąg, aby wygenerować SRJP",
"headers": { "train-search-placeholder": "Wpisz szczegóły RJ (nr, relacja, gracz)",
"line_no": "Nr\nlinii", "headers": {
"line_km": "Km", "line_no": "Nr\nlinii",
"station": "Stacja", "line_km": "Km",
"time": "Godzina", "station": "Stacja",
"loco_1": "Lok I", "time": "Godzina",
"loco_2": "Lok II", "loco_1": "Lok I",
"loco_3": "Lok III", "loco_2": "Lok II",
"mass": "Obc. lok.", "loco_3": "Lok III",
"length": "Dł. poc.", "mass": "Obc. lok.",
"vmax": "Vmax", "length": "Dł. poc.",
"relation": "Relacja" "vmax": "Vmax",
}, "relation": "Relacja"
"storage-empty-header": "TRYB WYSZUKIWANA ZAPISANYCH ROZKŁADÓW JAZDY", },
"storage-empty-info": "Użyj funkcji zapisu rozkładu jazdy, aby go tutaj wyświetlić.", "storage-empty-header": "TRYB WYSZUKIWANA ZAPISANYCH ROZKŁADÓW JAZDY",
"storage-preview-title": "ZAPISANE ROZKŁADY JAZDY", "storage-empty-info": "Użyj funkcji zapisu rozkładu jazdy, aby go tutaj wyświetlić.",
"storage-preview-empty": "Nie znaleziono żadnych wpisów dla podanych parametrów", "storage-preview-title": "ZAPISANE ROZKŁADY JAZDY",
"storage-preview-info": "Rozkład archiwalny {id} maszynisty {driverName} z dnia {date}", "storage-preview-empty": "Nie znaleziono żadnych wpisów dla podanych parametrów",
"storage-preview-button-text": "Powróć", "storage-preview-info": "Rozkład archiwalny {id} maszynisty {driverName} z dnia {date}",
"delete-timetable-confirm": "Czy na pewno chcesz usunąć ten rozkład jazdy z archiwum?", "storage-preview-button-text": "Powróć",
"storage-mode": { "delete-timetable-confirm": "Czy na pewno chcesz usunąć ten rozkład jazdy z archiwum?",
"local": "LOKALNIE",
"api": "STACJOWNIK" "journal-preview-title": "DZIENNIK ROZKŁADÓW JAZDY",
} "journal-empty-info": "Wpisz dane rozkładu korzystając z pola tekstowego powyżej - mogą nimi być: <br> id (#numer); nickname (nick:Spythere); data (date:11.01.2025); punkt startowy (from:Krnów)<br>W przypadku wielu rozkładów jazdy wyświetli się maks. 15 najnowszych.",
} "journal-search-placeholder": "nick:Spythere date:02.04.2025 from:Krnów to:Biała_Sudecka",
"journal-preview-info": "Rozkład historyczny {id} maszynisty {driverName} z dnia {date}",
"journal-no-data": "Brak wyników dla obecnego wyszukiwania! Sprawdź czy wpisałeś poprawnie dane.",
"journal-reminder-text": "Uwaga: szczegółowe rozkłady jazdy są zapisywane od 1 lutego 2025r. wyłącznie dla osób wspierających projekt Stacjownika!",
"journal-footer-text": "Szczegółowe dane o rozkładach jazdy do wygenerowania SRJP są zbierane od 1 lutego 2025r. wyłącznie dla maszynistów wspierających projekt Stacjownika!"
}
+85 -5
View File
@@ -1,8 +1,19 @@
import type { AxiosInstance } from 'axios'; import type { AxiosInstance } from 'axios';
import axios from 'axios'; import axios from 'axios';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { DataStatus, type ActiveDataResponse, type SceneriesDataResponse } from '../types/api.types'; import {
import type { ActiveData, SceneryData } from '../types/common.types'; DataStatus,
type ActiveDataResponse,
type SceneriesDataResponse,
type JournalTimetablesShortResponse
} from '../types/api.types';
import type {
ActiveData,
JournalTimetableDetailed,
JournalTimetableShort,
SceneryData
} from '../types/common.types';
import { useGlobalStore } from './global.store';
export const useApiStore = defineStore('api', { export const useApiStore = defineStore('api', {
state() { state() {
@@ -11,11 +22,13 @@ export const useApiStore = defineStore('api', {
activeData: null as ActiveData | null, activeData: null as ActiveData | null,
sceneryData: null as SceneryData[] | null, sceneryData: null as SceneryData[] | null,
journalTimetablesData: null as JournalTimetableShort[] | null,
outdatedTimerId: -1, outdatedTimerId: -1,
isActiveDataOutdated: false, isActiveDataOutdated: false,
activeDataStatus: DataStatus.LOADING, activeDataStatus: DataStatus.LOADING,
journalDataStatus: DataStatus.SUCCESS
}; };
}, },
@@ -37,13 +50,12 @@ export const useApiStore = defineStore('api', {
} }
this.client = axios.create({ this.client = axios.create({
baseURL, baseURL
}); });
this.fetchSceneriesData(); this.fetchSceneriesData();
await this.fetchActiveData(); await this.fetchActiveData();
setInterval(() => { setInterval(() => {
this.fetchActiveData(); this.fetchActiveData();
}, 25000); }, 25000);
@@ -76,5 +88,73 @@ export const useApiStore = defineStore('api', {
console.error(error); console.error(error);
} }
}, },
},
async fetchJournalTimetables(searchValue: string) {
// if (searchValue.trim().length == 0) {
// this.journalDataStatus = DataStatus.SUCCESS;
// this.journalTimetablesData = null;
// return;
// }
let searchObj: Record<string, any> = {};
const searchParams = searchValue.split(' ');
searchParams.forEach((param) => {
const [key, value] = param.split(':');
if (key == 'nick') searchObj['driverName'] = value;
else if (key == 'date') {
let dateFromStr = new Date(value).toISOString();
let dateTo = new Date(dateFromStr);
dateTo.setDate(dateTo.getDate() + 1);
searchObj['dateFrom'] = dateFromStr;
searchObj['dateTo'] = dateTo.toISOString();
} else if (key == 'from') searchObj['issuedFrom'] = value.replace(/_/g, ' ');
else if (key == 'to') searchObj['terminatingAt'] = value.replace(/_/g, ' ');
});
searchObj['hasStopsDetails'] = 1;
searchObj['returnType'] = 'short';
try {
this.journalDataStatus = DataStatus.LOADING;
const response = (
await this.client!.get<JournalTimetablesShortResponse>('/api/getTimetables', {
params: searchObj
})
).data;
this.journalDataStatus = DataStatus.SUCCESS;
this.journalTimetablesData = response;
} catch (error) {
this.journalDataStatus = DataStatus.ERROR;
this.journalTimetablesData = null;
console.error(error);
}
},
async fetchJournalTimetableDetails(id: number) {
const globalStore = useGlobalStore();
try {
const response = (
await this.client!.get<JournalTimetableDetailed[]>('/api/getTimetables', {
params: {
timetableId: id,
hasStopsDetails: 1
}
})
).data;
if (response.length > 0) globalStore.selectedJournalTimetable = response[0];
} catch (error) {
globalStore.selectedJournalTimetable = null;
console.error(error);
}
}
}
}); });
+56 -13
View File
@@ -1,6 +1,12 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { useApiStore } from './api.store'; import { useApiStore } from './api.store';
import type { ActiveTrain, TimetableData, ViewMode } from '../types/common.types'; import type {
ActiveTrain,
JournalTimetableDetailed,
JournalTimetableShort,
TimetableData,
ViewMode
} from '../types/common.types';
import { unitNameCorrections } from '../utils/trainUtils'; import { unitNameCorrections } from '../utils/trainUtils';
export const useGlobalStore = defineStore('global', { export const useGlobalStore = defineStore('global', {
@@ -11,6 +17,8 @@ export const useGlobalStore = defineStore('global', {
selectedTrainId: null as string | null, selectedTrainId: null as string | null,
selectedActiveTrain: null as ActiveTrain | null, selectedActiveTrain: null as ActiveTrain | null,
selectedStorageTimetable: null as TimetableData | null, selectedStorageTimetable: null as TimetableData | null,
selectedJournalTimetable: null as JournalTimetableDetailed | null,
storageTimetables: {} as Record<number, TimetableData>, storageTimetables: {} as Record<number, TimetableData>,
timetableWarnings: [] as string[], timetableWarnings: [] as string[],
@@ -18,9 +26,10 @@ export const useGlobalStore = defineStore('global', {
generatedDate: null as Date | null, generatedDate: null as Date | null,
generatedMs: 0, generatedMs: 0,
timetableSearch: '', localTimetableSearch: '',
journalTimetableSearch: '',
showSettings: false, showSettings: false
}), }),
getters: { getters: {
activeTimetableTrains() { activeTimetableTrains() {
@@ -28,7 +37,9 @@ export const useGlobalStore = defineStore('global', {
if (!apiStore.activeData) return []; if (!apiStore.activeData) return [];
return apiStore.activeData.trains.filter((train) => train.timetable).sort((t1, t2) => t1.driverName.localeCompare(t2.driverName, 'pl-PL')); return apiStore.activeData.trains
.filter((train) => train.timetable)
.sort((t1, t2) => t1.driverName.localeCompare(t2.driverName, 'pl-PL'));
}, },
currentTimetableData(): TimetableData | null { currentTimetableData(): TimetableData | null {
@@ -52,12 +63,14 @@ export const useGlobalStore = defineStore('global', {
trainMaxSpeed: selectedTrain.timetable.trainMaxSpeed, trainMaxSpeed: selectedTrain.timetable.trainMaxSpeed,
timetableId: selectedTrain.timetable.timetableId, timetableId: selectedTrain.timetable.timetableId,
stopListString: selectedTrain.timetable.stopList stopListString: selectedTrain.timetable.stopList
.filter((stop) => stop.mainStop || (/^podg|po|pe$/.test(stop.stopNameRAW))) .filter((stop) => stop.mainStop || /^podg|po|pe$/.test(stop.stopNameRAW))
.map( .map(
(stop) => (stop) =>
`${stop.arrivalLine ?? ''};${stop.arrivalTimestamp};${stop.stopNameRAW};${stop.stopTime ? stop.stopTime + '_' + stop.stopType : ''};${ `${stop.arrivalLine ?? ''};${stop.arrivalTimestamp};${stop.stopNameRAW};${
stop.mainStop stop.stopTime ? stop.stopTime + '_' + stop.stopType : ''
};${stop.stopDistance};${stop.departureTimestamp};${stop.departureLine ?? ''}` };${stop.mainStop};${stop.stopDistance};${stop.departureTimestamp};${
stop.departureLine ?? ''
}`
) )
.join('~~'), .join('~~'),
headUnits: selectedTrain.stockString headUnits: selectedTrain.stockString
@@ -68,13 +81,43 @@ export const useGlobalStore = defineStore('global', {
const unitName = s.slice(0, s.indexOf('-')); const unitName = s.slice(0, s.indexOf('-'));
return unitNameCorrections[unitName] ?? unitName; return unitNameCorrections[unitName] ?? unitName;
}), })
};
} else if (this.viewMode == 'journal') {
const selectedTimetable = this.selectedJournalTimetable;
if (!selectedTimetable || !selectedTimetable.stopListString) return null;
return {
journalCreatedAt: new Date(selectedTimetable.createdAt).getTime(),
trainNo: selectedTimetable.trainNo,
mass: selectedTimetable.stockMass,
length: selectedTimetable.stockLength,
driverId: selectedTimetable.driverId,
driverName: selectedTimetable.driverName,
category: selectedTimetable.trainCategoryCode,
hasDangerousCargo: selectedTimetable.hasDangerousCargo,
hasExtraDeliveries: selectedTimetable.hasExtraDeliveries,
warningNotes: selectedTimetable.warningNotes,
path: selectedTimetable.path,
route: selectedTimetable.route,
trainMaxSpeed: selectedTimetable.trainMaxSpeed,
timetableId: selectedTimetable.id,
stopListString: selectedTimetable.stopListString,
headUnits: selectedTimetable.stockString
.split(';')
.slice(0, 3)
.filter((s, i) => i == 0 || /-\d+$/.test(s))
.map((s) => {
const unitName = s.slice(0, s.indexOf('-'));
return unitNameCorrections[unitName] ?? unitName;
})
}; };
} else { } else {
const selectedStorageTimetable = this.selectedStorageTimetable; return this.selectedStorageTimetable;
return selectedStorageTimetable;
} }
}, }
}, },
actions: {}, actions: {}
}); });
+4 -1
View File
@@ -1,10 +1,13 @@
import type { ActiveData, SceneryData } from './common.types'; import type { ActiveData, JournalTimetableShort, SceneryData } from './common.types';
export type ActiveDataResponse = ActiveData; export type ActiveDataResponse = ActiveData;
export type SceneriesDataResponse = SceneryData[]; export type SceneriesDataResponse = SceneryData[];
export type JournalTimetablesShortResponse = JournalTimetableShort[];
export enum DataStatus { export enum DataStatus {
'INIT' = -1,
'LOADING' = 0, 'LOADING' = 0,
'SUCCESS' = 1, 'SUCCESS' = 1,
'ERROR' = 2, 'ERROR' = 2,
+3 -2
View File
@@ -1,4 +1,4 @@
export type ViewMode = 'active' | 'storage'; export type ViewMode = 'active' | 'storage' | 'journal';
export enum StorageMode { export enum StorageMode {
LOCAL = 'local', LOCAL = 'local',
@@ -245,7 +245,7 @@ export interface JournalTimetableDetailed extends JournalTimetableShort {
warningNotes: string; warningNotes: string;
hasDangerousCargo: boolean; hasDangerousCargo: boolean;
hasExtraDeliveries: boolean; hasExtraDeliveries: boolean;
stopListString: any; stopListString?: string;
} }
export interface TimetableData { export interface TimetableData {
@@ -265,4 +265,5 @@ export interface TimetableData {
stopListString: string; stopListString: string;
headUnits: string[]; headUnits: string[];
savedTimestamp?: number; savedTimestamp?: number;
journalCreatedAt?: number;
} }