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
+118 -55
View File
@@ -1,16 +1,68 @@
<template>
<div class="flex gap-2 mb-2 print:hidden">
<button
class="p-1 rounded-md"
:class="{
'bg-zinc-800 hover:bg-zinc-700': globalStore.viewMode == 'active',
'bg-green-600 hover:bg-green-500': globalStore.viewMode == 'storage',
}"
@click="toggleViewMode"
>
<ArchiveBoxArrowDownIcon class="size-6" />
</button>
<div class="flex gap-2 justify-between flex-wrap mb-2 print:hidden">
<div class="flex gap-2">
<button
:class="`p-1 rounded-md ${
globalStore.viewMode == 'active'
? 'bg-green-600 hover:bg-green-500'
: 'bg-zinc-800 hover:bg-zinc-700'
}`"
@click="toggleViewMode('active')"
>
<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
name="trains"
id="trains-select"
@@ -21,45 +73,37 @@
@change="selectTrain"
>
<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 :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>
</select>
<!-- Local Storage Search -->
<input
type="text"
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"
:placeholder="$t('train-search-placeholder')"
/>
<button class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700" @click="toggleDarkMode">
<MoonIcon v-if="globalStore.darkMode" class="text-white size-6" />
<SunIcon v-else class="text-white size-6" />
</button>
<button
class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700 disabled:opacity-60 disabled:hover:bg-zinc-800"
:disabled="globalStore.currentTimetableData == null"
@click="openPrintingWindow"
>
<PrinterIcon class="text-white size-6" />
</button>
<button
class="p-1 rounded-md disabled:opacity-60 disabled:hover:bg-zinc-800"
:disabled="globalStore.currentTimetableData == null"
:class="{
'bg-green-600 hover:bg-green-700': isTimetableSaved,
'bg-zinc-800 hover:bg-zinc-700': !isTimetableSaved,
}"
@click="saveToStorage"
>
<ArrowDownTrayIcon class="text-white size-6" />
</button>
<!-- Journal Serach -->
<input
type="text"
v-else-if="globalStore.viewMode == 'journal'"
v-model="globalStore.journalTimetableSearch"
@change="fetchJournalTimetables"
class="bg-zinc-800 p-1 rounded-md print:hidden w-full"
:placeholder="$t('journal-search-placeholder')"
/>
</div>
</template>
@@ -68,9 +112,17 @@ import { computed } from 'vue';
import { useApiStore } from '../../stores/api.store';
import { DataStatus } from '../../types/api.types';
import { useGlobalStore } from '../../stores/global.store';
import { PrinterIcon, MoonIcon, SunIcon, ArchiveBoxArrowDownIcon, ArrowDownTrayIcon } from '@heroicons/vue/16/solid';
import {
PrinterIcon,
MoonIcon,
SunIcon,
ArchiveBoxArrowDownIcon,
ArrowDownTrayIcon,
CloudIcon,
WifiIcon
} from '@heroicons/vue/16/solid';
import { getRegionNameById } from '../../utils/trainUtils';
import type { TimetableData } from '../../types/common.types';
import type { TimetableData, ViewMode } from '../../types/common.types';
// Stores
const apiStore = useApiStore();
@@ -80,22 +132,26 @@ const globalStore = useGlobalStore();
const isTimetableSaved = computed(() => {
if (!globalStore.currentTimetableData) return false;
return Object.keys(globalStore.storageTimetables).includes(`${globalStore.currentTimetableData.timetableId}`);
return Object.keys(globalStore.storageTimetables).includes(
`${globalStore.currentTimetableData.timetableId}`
);
});
// Methods
function selectTrain() {
if (!apiStore.activeData) return;
globalStore.selectedActiveTrain = globalStore.activeTimetableTrains.find((train) => train.id == globalStore.selectedTrainId) ?? null;
globalStore.selectedActiveTrain =
globalStore.activeTimetableTrains.find((train) => train.id == globalStore.selectedTrainId) ??
null;
if (globalStore.selectedActiveTrain != null) {
globalStore.generatedDate = new Date();
}
}
function toggleViewMode() {
globalStore.viewMode = globalStore.viewMode == 'active' ? 'storage' : 'active';
function toggleViewMode(viewMode: ViewMode) {
globalStore.viewMode = viewMode;
}
function toggleDarkMode() {
@@ -109,16 +165,22 @@ function saveToStorage() {
try {
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) {
globalStore.selectedStorageTimetable = savedTimetablesJSON[globalStore.currentTimetableData.timetableId];
globalStore.selectedStorageTimetable =
savedTimetablesJSON[globalStore.currentTimetableData.timetableId];
globalStore.viewMode = 'storage';
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));
globalStore.storageTimetables = savedTimetablesJSON;
@@ -127,21 +189,22 @@ function saveToStorage() {
function openPrintingWindow() {
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')
.replace(/:/g, '-')}`;
document.title = `${globalStore.selectedActiveTrain.driverName} ; ${globalStore.selectedActiveTrain.timetable!.category} ${
globalStore.selectedActiveTrain.trainNo
}
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();
// }
async function fetchJournalTimetables() {
apiStore.fetchJournalTimetables(globalStore.journalTimetableSearch);
}
</script>
+45 -4
View File
@@ -1,4 +1,5 @@
<template>
<!-- Local -->
<div class="my-2 print:hidden" v-if="globalStore.currentTimetableData?.savedTimestamp">
<div class="flex gap-2">
<div class="flex items-center gap-2 bg-zinc-900 p-1 w-full">
@@ -18,11 +19,49 @@
</i18n-t>
</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" />
</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" />
</button>
</div>
@@ -45,12 +84,14 @@ function removeTimetable(timetableId: number) {
try {
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];
localStorage.setItem('savedTimetables', JSON.stringify(savedTimetablesJSON));
globalStore.storageTimetables = savedTimetablesJSON;
globalStore.selectedStorageTimetable = null;
} catch (error) {}
}
+10 -11
View File
@@ -1,12 +1,11 @@
<template>
<div
:class="{ dark: globalStore.darkMode }"
v-if="globalStore.currentTimetableData != null"
class="overflow-auto p-1 bg-white print:bg-white dark:bg-zinc-950 print:text-black text-black dark:text-white min-h-full"
>
<!-- Timetable render based on current view mode -->
<div :class="{ dark: globalStore.darkMode }" v-if="globalStore.currentTimetableData != null"
class="overflow-auto p-1 bg-white print:bg-white dark:bg-zinc-950 print:text-black text-black dark:text-white min-h-full">
<div>
<div class="p-1 font-bold w-max">
{{ globalStore.currentTimetableData.category }} {{ globalStore.currentTimetableData.trainNo }} {{ $t('headers.relation') }}
{{ globalStore.currentTimetableData.category }} {{ globalStore.currentTimetableData.trainNo }} {{
$t('headers.relation') }}
{{ globalStore.currentTimetableData.route.replace('|', ' - ') }}
</div>
@@ -22,9 +21,8 @@
<div>{{ $t('train-select-info') }}</div>
</div>
<div v-else>
<StorageView />
</div>
<LocalStorageView v-else-if="globalStore.viewMode == 'storage'" />
<JournalStorageView v-else />
</div>
</template>
@@ -35,8 +33,9 @@ import { useApiStore } from '../../stores/api.store';
import { useGlobalStore } from '../../stores/global.store';
import TimetableBody from './TimetableBody.vue';
import TimetableHeader from './TimetableHeader.vue';
import type { SceneryRoute, StopRow, TimetablePathData } from '../../types/common.types';
import StorageView from '../TimetableStorage/StorageView.vue';
import { type SceneryRoute, type StopRow, type TimetablePathData } from '../../types/common.types';
import LocalStorageView from '../TimetableViews/LocalStorageView.vue';
import JournalStorageView from '../TimetableViews/JournalStorageView.vue';
const globalStore = useGlobalStore();
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>
<div>
<div class="font-bold p-2 bg-zinc-800 mb-3" v-if="filteredTimetables.length == 0">{{ $t('storage-preview-empty') }}</div>
<div class="text-white">
<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">
<button class="bg-zinc-900 p-2 w-full cursor-pointer hover:bg-zinc-800 text-left" @click="selectTimetable(timetable)">
<div class="text-zinc-300">#{{ timetable.timetableId }} &bull; {{ new Date(timetable.savedTimestamp!).toLocaleString() }}</div>
<b>{{ timetable.driverName }} | {{ timetable.category }} {{ timetable.trainNo }}</b> {{ timetable.route.replace('|', ' > ') }}
<button
class="bg-zinc-900 p-2 w-full cursor-pointer hover:bg-zinc-800 text-left"
@click="selectTimetable(timetable)"
>
<div class="text-zinc-300">
#{{ timetable.timetableId }} &bull;
{{ new Date(timetable.savedTimestamp!).toLocaleString() }}
</div>
<b>{{ timetable.driverName }} | {{ timetable.category }} {{ timetable.trainNo }}</b>
{{ timetable.route.replace('|', ' > ') }}
</button>
<button class="bg-zinc-900 p-2 hover:bg-zinc-800" @click="removeTimetable(timetable.timetableId)">
<TrashIcon class="size-5 text-white" />
<button
class="bg-zinc-900 p-2 hover:bg-zinc-800"
@click="removeTimetable(timetable.timetableId)"
>
<TrashIcon class="size-6 text-white" />
</button>
</li>
</ul>
@@ -18,11 +44,11 @@
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
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 { TrashIcon } from '@heroicons/vue/16/solid';
const globalStore = useGlobalStore();
const i18n = useI18n();
@@ -30,11 +56,11 @@ const i18n = useI18n();
const filteredTimetables = computed(() => {
let timetables = Object.values(globalStore.storageTimetables);
if (globalStore.timetableSearch.length != 0)
if (globalStore.localTimetableSearch.length != 0)
timetables = timetables.filter((st) =>
`${st.timetableId} ${st.driverName} ${st.route} ${st.category} ${st.trainNo}`
.toLocaleLowerCase()
.includes(globalStore.timetableSearch.toLocaleLowerCase())
.includes(globalStore.localTimetableSearch.toLocaleLowerCase())
);
timetables.sort((a, b) => {
@@ -55,11 +81,16 @@ function removeTimetable(timetableId: number) {
try {
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];
localStorage.setItem('savedTimetables', JSON.stringify(savedTimetablesJSON));
globalStore.storageTimetables = savedTimetablesJSON;
} catch (error) {}
} catch (error) {
console.error(error);
}
}
</script>