feat/restruct: saving timetables to local storage

This commit is contained in:
2025-01-30 21:52:17 +01:00
parent c334b5bfd3
commit 5f264e8b63
8 changed files with 385 additions and 71 deletions
+3 -3
View File
@@ -121,13 +121,13 @@
<table class="h-full w-full border-collapse">
<tbody>
<tr class="border-b-[1px] border-b-black dark:border-b-white">
<td>{{ row.headLocos[0] }}</td>
<td>{{ row.headUnits[0] }}</td>
</tr>
<tr class="border-b-[1px] border-b-black dark:border-b-white">
<td>{{ row.headLocos[1] ?? '&nbsp;' }}</td>
<td>{{ row.headUnits[1] ?? '&nbsp;' }}</td>
</tr>
<tr>
<td>{{ row.headLocos[2] ?? '&nbsp;' }}</td>
<td>{{ row.headUnits[2] ?? '&nbsp;' }}</td>
</tr>
</tbody>
</table>
+67 -16
View File
@@ -1,11 +1,23 @@
<template>
<div class="flex gap-2 mb-2">
<button
class="p-1 rounded-md"
:class="{
'bg-zinc-800 hover:bg-zinc-700': globalStore.viewMode == 'active',
'bg-green-500 hover:bg-green-400': globalStore.viewMode == 'storage',
}"
@click="toggleViewMode"
>
<ArchiveBoxArrowDownIcon class="size-6" />
</button>
<select
name="trains"
id="trains-select"
class="bg-zinc-800 p-1 rounded-md print:hidden w-full"
:disabled="apiStore.activeDataStatus != DataStatus.SUCCESS"
v-model="selectedTrainId"
v-if="globalStore.viewMode == 'active'"
@change="selectTrain"
>
<option :value="null" disabled>{{ apiStore.activeDataStatus == DataStatus.LOADING ? 'Ładowanie danych...' : 'Wybierz pociąg z listy' }}</option>
@@ -14,29 +26,42 @@
</option>
</select>
<input
type="text"
v-if="globalStore.viewMode == 'storage'"
class="bg-zinc-800 p-1 rounded-md print:hidden w-full"
placeholder="Wpisz szczegóły RJ, który chcesz znaleźć (np. numer, relacja)"
/>
<button class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700" @click="toggleDarkMode">
<SunIcon v-if="globalStore.darkMode" class="text-white size-6" />
<MoonIcon v-else class="text-white size-6" />
<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" @click="openPrintingWindow">
<PrinterIcon class="text-white size-6" />
</button>
<button class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700 relative" @click="refreshData">
<ArrowPathIcon class="text-white size-6" />
<div v-if="apiStore.isActiveDataOutdated" class="size-3 bg-green-300 rounded-full absolute -top-1 -right-1"></div>
<button
class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700"
:class="{
'bg-green-600': isTimetableSaved,
}"
@click="saveToStorage"
>
<ArrowDownTrayIcon class="text-white size-6" />
</button>
</div>
</template>
<script setup lang="ts">
import { ref, type Ref } from 'vue';
import { computed, ref, type Ref } from 'vue';
import { useApiStore } from '../../stores/api.store';
import { DataStatus } from '../../types/api.types';
import { useGlobalStore } from '../../stores/global.store';
import { PrinterIcon, ArrowPathIcon, MoonIcon, SunIcon } from '@heroicons/vue/16/solid';
import { PrinterIcon, MoonIcon, SunIcon, ArchiveBoxArrowDownIcon, ArrowDownTrayIcon } from '@heroicons/vue/16/solid';
import { getRegionNameById } from '../../utils/trainUtils';
import type { TimetableData } from '../../types/common.types';
// Stores
const apiStore = useApiStore();
@@ -45,38 +70,64 @@ const globalStore = useGlobalStore();
// Variables & refs
let selectedTrainId = ref(null) as Ref<string | null>;
// Computed
const isTimetableSaved = computed(() => {
if (!globalStore.currentTimetableData) return false;
return Object.keys(globalStore.storageTimetables).includes(`${globalStore.currentTimetableData.timetableId}`);
});
// Methods
function selectTrain() {
if (!apiStore.activeData) return;
globalStore.selectedTrain = globalStore.activeTimetableTrains.find((train) => train.id == selectedTrainId.value) ?? null;
globalStore.selectedActiveTrain = globalStore.activeTimetableTrains.find((train) => train.id == selectedTrainId.value) ?? null;
if (globalStore.selectedTrain != null) {
if (globalStore.selectedActiveTrain != null) {
globalStore.generatedDate = new Date();
}
}
function toggleViewMode() {
globalStore.viewMode = globalStore.viewMode == 'active' ? 'storage' : 'active';
}
function toggleDarkMode() {
globalStore.darkMode = !globalStore.darkMode;
window.localStorage.setItem('currentTheme', globalStore.darkMode ? 'dark' : 'light');
}
function saveToStorage() {
if (globalStore.currentTimetableData == null) return;
try {
const savedTimetablesStorage = localStorage.getItem('savedTimetables');
let savedTimetablesJSON: Record<number, TimetableData> = savedTimetablesStorage ? JSON.parse(savedTimetablesStorage) : {};
savedTimetablesJSON[globalStore.currentTimetableData.timetableId] = { ...globalStore.currentTimetableData, savedTimestamp: Date.now() };
localStorage.setItem('savedTimetables', JSON.stringify(savedTimetablesJSON));
globalStore.storageTimetables = savedTimetablesJSON;
} catch (error) {}
}
function openPrintingWindow() {
if (globalStore.selectedTrain != null) {
if (globalStore.selectedActiveTrain != null) {
const date = `${globalStore.generatedDate!.toLocaleDateString('pl-PL').replace(/\./g, '-')}--${globalStore
.generatedDate!.toLocaleTimeString('pl-PL')
.replace(/:/g, '-')}`;
document.title = `${globalStore.selectedTrain.driverName} ; ${globalStore.selectedTrain.timetable!.category} ${globalStore.selectedTrain.trainNo}
${globalStore.selectedTrain.timetable?.route.replace('|', ' - ')} ; ${date}`;
document.title = `${globalStore.selectedActiveTrain.driverName} ; ${globalStore.selectedActiveTrain.timetable!.category} ${
globalStore.selectedActiveTrain.trainNo
}
${globalStore.selectedActiveTrain.timetable?.route.replace('|', ' - ')} ; ${date}`;
}
window.print();
}
function refreshData() {
apiStore.fetchActiveData();
selectTrain();
}
// function refreshData() {
// apiStore.fetchActiveData();
// selectTrain();
// }
</script>
@@ -0,0 +1,31 @@
<template>
<div>
<div v-if="globalStore.selectedStorageTimetable == null && Object.keys(globalStore.storageTimetables).length == 0">
<div class="font-bold text-xl">TRYB WYSZUKIWANA ZAPISANYCH ROZKŁADÓW JAZDY</div>
<div>Użyj funkcji zapisu rozkładu jazdy, aby go tutaj wyświetlić.</div>
</div>
<div v-else>
<ul class="flex flex-col gap-3">
<li v-for="timetable in globalStore.storageTimetables" class="text-left">
<button class="bg-zinc-900 p-2 w-full text-zinc-300 cursor-pointer hover:bg-zinc-800" @click="selectTimetable(timetable)">
<b>{{ timetable.category }} {{ timetable.trainNo }}</b> {{ timetable.route.replace('|', ' - ') }}
</button>
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { useGlobalStore } from '../../stores/global.store';
import type { TimetableData } from '../../types/common.types';
const globalStore = useGlobalStore();
function selectTimetable(timetable: TimetableData) {
globalStore.selectedStorageTimetable = timetable;
}
</script>
<style scoped></style>
+84 -42
View File
@@ -1,70 +1,68 @@
<template>
<div
:class="{ dark: globalStore.darkMode }"
v-if="globalStore.selectedTrain"
v-if="globalStore.currentTimetableData != null"
class="overflow-auto p-1 bg-white print:bg-white dark:bg-zinc-950 print:text-black text-black dark:text-white min-h-full"
>
<div>
<div class="p-1 font-bold w-max">
{{ globalStore.selectedTrain.timetable!.category }} {{ globalStore.selectedTrain.trainNo }} Relacja
{{ globalStore.selectedTrain.timetable?.route.replace('|', ' - ') }}
<div class="p-1 bg-zinc-900 my-2 print:hidden flex justify-between" v-if="globalStore.currentTimetableData.savedTimestamp">
<div>
Przeglądasz teraz zapisany rozkład jazdy o ID: <b>#{{ globalStore.currentTimetableData.timetableId }}</b>
</div>
<table class="table-fixed mt-2 w-full border-collapse" v-if="computedTimetable.length > 0">
<button class="font-bold" @click="globalStore.selectedStorageTimetable = null">Powróć do listy</button>
</div>
<div>
<div class="p-1 font-bold w-max">
{{ globalStore.currentTimetableData.category }} {{ globalStore.currentTimetableData.trainNo }} Relacja
{{ globalStore.currentTimetableData.route.replace('|', ' - ') }}
</div>
<table class="table-fixed mt-2 w-full border-collapse" v-if="computedTimetableRows.length > 0">
<TimetableHeader />
<TimetableBody :computed-timetable="computedTimetable" />
<TimetableBody :computed-timetable="computedTimetableRows" />
</table>
</div>
</div>
<div class="overflow-auto text-center font-bold text-zinc-400 p-1 min-h-full" v-else>Wybierz aktywny pociąg, aby wygenerować SRJP</div>
<div class="overflow-auto text-center font-bold text-zinc-400 p-1 min-h-full" v-else>
<div v-if="globalStore.viewMode == 'active'">
<div>Wybierz aktywny pociąg, aby wygenerować SRJP</div>
</div>
<div v-else>
<TimetableStorage />
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { computed } from 'vue';``
import { useApiStore } from '../../stores/api.store';
import { useGlobalStore } from '../../stores/global.store';
import TimetableBody from './TimetableBody.vue';
import TimetableHeader from './TimetableHeader.vue';
import type { SceneryRoute, StopRow } from '../../types/common.types';
import type { SceneryRoute, StopRow, TimetablePathData } from '../../types/common.types';
import TimetableStorage from './TimetableStorage.vue';
const globalStore = useGlobalStore();
const apiStore = useApiStore();
const computedTimetable = computed(() => {
if (!globalStore.selectedTrain || !globalStore.selectedTrain.timetable) return [];
// Tymczasowa tabelka z posterunkami APO
// const apoNames = ['Stary Kisielin, pe', 'Czerwony Dwór, pe', 'Szczejkowice, pe'];
const { timetable, stockString, mass, length } = globalStore.selectedTrain;
const computedTimetableRows = computed(() => {
const timetableData = globalStore.currentTimetableData;
if (!timetableData) return [];
let timeFrom = Date.now();
const headLocos = stockString
.split(';')
.slice(0, 3)
.filter((s, i) => i == 0 || /-\d+$/.test(s))
.map((s) => s.slice(0, s.indexOf('-')));
const stockVmax = timetable.trainMaxSpeed,
stockMass = Math.floor(mass / 1000),
const stockVmax = timetableData.trainMaxSpeed,
stockMass = Math.floor(timetableData.mass / 1000),
stockLength = length;
const timetablePath = timetable.path.split(';').map((pathEl) => {
const [arrivalLine, scenery, departureLine] = pathEl.split(',');
const sceneryName = scenery.split(' ').slice(0, -1).join(' ');
const sceneryData = apiStore.sceneryData?.find((sc) => sc.name == sceneryName) ?? null;
const arrivalLineData = arrivalLine ? sceneryData?.routesInfo.find((rt) => rt.routeName == arrivalLine) ?? null : null;
const departureLineData = departureLine ? sceneryData?.routesInfo.find((rt) => rt.routeName == departureLine) ?? null : null;
return {
sceneryName,
sceneryData: sceneryData ?? null,
arrivalLine: arrivalLine ?? '',
arrivalLineData,
departureLine: departureLine ?? '',
departureLineData,
};
});
const timetablePath = parseTimetablePath(timetableData.path);
const stopRows: StopRow[] = [];
@@ -94,7 +92,9 @@ const computedTimetable = computed(() => {
// console.debug('=========== ' + this.selectedTrain.trainNo + ' ===========');
for (const stop of timetable.stopList) {
const stopList = parseStopListString(timetableData.stopListString);
for (const stop of stopList) {
if (stop.arrivalLine && stop.arrivalLine == currentPath.arrivalLine) {
arrivalKm = stop.stopDistance;
@@ -109,7 +109,7 @@ const computedTimetable = computed(() => {
departureTracks = arrivalTracks;
}
if (/^<strong>|, (podg|po)$|^(!_, pe)$/.test(stop.stopName)) {
if (stop.mainStop || (/^podg|po|pe$/.test(stop.stopNameRAW) && !/^sbl/i.test(stop.stopNameRAW))) {
let correctedDepartureSpeed = 0,
correctedDepartureTracks = 0;
@@ -128,7 +128,7 @@ const computedTimetable = computed(() => {
}
let rowData: StopRow = {
isMain: /^<strong>/.test(stop.stopName),
isMain: stop.mainStop,
pointKm: stop.stopDistance.toFixed(3),
pointName: stop.stopNameRAW,
scheduledArrivalDate: stop.arrivalTimestamp ? new Date(stop.arrivalTimestamp) : null,
@@ -150,12 +150,14 @@ const computedTimetable = computed(() => {
departureSpeed: departureSpeed,
departureTracks: departureTracks,
headLocos,
headUnits: timetableData.headUnits,
stockVmax,
stockLength,
stockMass,
};
// if (apoNames.includes(stop.stopNameRAW)) abbrevs.unshift(`APO ${currentPath.sceneryData?.abbr}`);
// console.debug(stop.stopNameRAW, stop.departureLine);
arrivalKm = stop.stopDistance;
@@ -200,6 +202,46 @@ const computedTimetable = computed(() => {
return stopRows;
});
function parseTimetablePath(path: string): TimetablePathData[] {
return path.split(';').map((pathEl) => {
const [arrivalLine, scenery, departureLine] = pathEl.split(',');
const sceneryName = scenery.split(' ').slice(0, -1).join(' ');
const sceneryData = apiStore.sceneryData?.find((sc) => sc.name == sceneryName) ?? null;
const arrivalLineData = arrivalLine ? sceneryData?.routesInfo.find((rt) => rt.routeName == arrivalLine) ?? null : null;
const departureLineData = departureLine ? sceneryData?.routesInfo.find((rt) => rt.routeName == departureLine) ?? null : null;
return {
sceneryName,
sceneryData: sceneryData ?? null,
arrivalLine: arrivalLine ?? '',
departureLine: departureLine ?? '',
arrivalLineData,
departureLineData,
};
});
}
function parseStopListString(stopsString: string) {
//${stop.arrivalLine ?? ''};${stop.arrivalTimestamp};${stop.stopNameRAW};${stop.stopTime ? stop.stopTime + '_' + stop.stopType : ''};${stop.mainStop};${stop.stopDistance};${stop.departureTimestamp};${stop.departureLine ?? ''}
return stopsString.split('~~').map((stop) => {
const [arrivalLine, arrivalTimestamp, stopNameRAW, stopDetails, isMainStop, stopDistance, departureTimestamp, departureLine] = stop.split(';');
const [stopTime, stopType] = stopDetails.split('_');
return {
arrivalLine,
arrivalTimestamp: parseInt(arrivalTimestamp),
stopNameRAW,
stopTime: stopTime ?? 0,
stopType: stopType ?? null,
mainStop: isMainStop == 'true',
stopDistance: parseFloat(stopDistance),
departureTimestamp: parseInt(departureTimestamp),
departureLine,
};
});
}
function getAbbrevs(routeData: SceneryRoute) {
const abbrevs = [];