Aktualizacja 1.10.4

Aktualizacja Stacjownika do wersji 1.10.4
This commit is contained in:
Spythere
2022-09-11 14:06:59 +02:00
committed by GitHub
26 changed files with 1047 additions and 996 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.10.3", "version": "1.10.4",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -2,48 +2,55 @@
<section class="journal-timetables"> <section class="journal-timetables">
<div class="journal_wrapper"> <div class="journal_wrapper">
<JournalOptions <JournalOptions
@on-filter-change="search" @on-search-confirm="searchHistory"
@on-input-change="search" @on-options-reset="resetOptions"
@on-sorter-change="search"
:sorter-option-ids="['timestampFrom', 'duration']" :sorter-option-ids="['timestampFrom', 'duration']"
:data-status="dataStatus"
/> />
<div class="timetables_wrapper" ref="scrollElement"> <div class="list_wrapper" @scroll="handleScroll">
<transition name="warning" mode="out-in"> <!-- <transition name="warning" mode="out-in"> -->
<div :key="dataStatus"> <!-- <div :key="dataStatus"> -->
<Loading v-if="dataStatus == (DataStatus.Loading || DataStatus.Initialized)" /> <Loading
v-if="dataStatus == DataStatus.Initialized || (dataStatus == DataStatus.Loading && historyList.length == 0)"
/>
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error"> <div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
{{ $t('app.error') }} {{ $t('app.error') }}
</div> </div>
<div class="journal_warning" v-else-if="historyList.length == 0"> <div class="journal_warning" v-else-if="historyList.length == 0 && dataStatus != DataStatus.Loading">
{{ $t('app.no-result') }} {{ $t('app.no-result') }}
</div> </div>
<div v-else> <div v-else>
<JournalDispatchersList :dispatcherHistory="computedHistoryList" /> <JournalDispatchersList :dispatcherHistory="computedHistoryList" />
<button <button
class="btn btn--option btn--load-data" class="btn btn--option btn--load-data"
v-if="!scrollNoMoreData && scrollDataLoaded" v-if="!scrollNoMoreData && scrollDataLoaded && computedHistoryList.length > 15"
@click="addHistoryData" @click="addHistoryData"
> >
{{ $t('journal.load-data') }} {{ $t('journal.load-data') }}
</button> </button>
</div> </div>
</div> <!-- </div>
</transition> </transition> -->
<div class="journal_warning" v-if="scrollNoMoreData">
{{ $t('journal.no-further-data') }}
</div>
<div class="journal_warning" v-else-if="!scrollDataLoaded">
{{ $t('journal.loading-further-data') }}
</div>
</div> </div>
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
<div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, JournalFilter, provide, reactive, Ref, ref } from 'vue'; import { defineComponent, provide, reactive, Ref, ref } from 'vue';
import axios from 'axios'; import axios from 'axios';
import ActionButton from '../../components/Global/ActionButton.vue'; import ActionButton from '../../components/Global/ActionButton.vue';
@@ -56,8 +63,9 @@ import { URLs } from '../../scripts/utils/apiURLs';
import { DataStatus } from '../../scripts/enums/DataStatus'; import { DataStatus } from '../../scripts/enums/DataStatus';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import JournalDispatchersList from './JournalDispatchersList.vue'; import JournalDispatchersList from './JournalDispatchersList.vue';
import { JournalDispatcherSearcher } from '../../types/JournalDispatcherTypes'; import { JournalDispatcherSearcher, JournalDispatcherSorter } from '../../types/Journal/JournalDispatcherTypes';
import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData'; import { DispatcherHistory } from '../../scripts/interfaces/api/DispatchersAPIData';
import { JournalTimetableFilter } from '../../types/Journal/JournalTimetablesTypes';
const DISPATCHERS_API_URL = `${URLs.stacjownikAPI}/api/getDispatchers`; const DISPATCHERS_API_URL = `${URLs.stacjownikAPI}/api/getDispatchers`;
@@ -92,12 +100,13 @@ export default defineComponent({
}), }),
setup() { setup() {
const sorterActive = ref({ id: 'timestampFrom', dir: -1 }); const sorterActive: JournalDispatcherSorter = reactive({ id: 'timestampFrom', dir: -1 });
const journalFilterActive = ref({}); const journalFilterActive = ref({});
const searchersValues = reactive({ const searchersValues = reactive({
'search-dispatcher': '', 'search-dispatcher': '',
'search-station': '', 'search-station': '',
'search-date': '',
} as JournalDispatcherSearcher); } as JournalDispatcherSearcher);
const countFromIndex = ref(0); const countFromIndex = ref(0);
@@ -135,42 +144,36 @@ export default defineComponent({
if (this.sceneryName || this.dispatcherName) { if (this.sceneryName || this.dispatcherName) {
this.searchersValues['search-station'] = this.sceneryName?.toString() || ''; this.searchersValues['search-station'] = this.sceneryName?.toString() || '';
this.searchersValues['search-dispatcher'] = this.dispatcherName?.toString() || ''; this.searchersValues['search-dispatcher'] = this.dispatcherName?.toString() || '';
this.search(); this.searchHistory();
} }
window.addEventListener('scroll', this.handleScroll);
}, },
mounted() { mounted() {
if (!this.sceneryName && !this.dispatcherName) { if (!this.sceneryName && !this.dispatcherName) {
this.search(); this.searchHistory();
} }
}, },
deactivated() {
window.removeEventListener('scroll', this.handleScroll);
},
methods: { methods: {
closeDispatcherStatsCard() { handleScroll(e: Event) {
this.statsCardOpen = false; const listElement = e.target as HTMLElement;
const scrollTop = listElement.scrollTop;
const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
if (scrollTop > elementHeight * 0.85) this.addHistoryData();
}, },
handleScroll() { resetOptions() {
this.showReturnButton = window.scrollY > window.innerHeight; this.searchersValues['search-station'] = '';
this.searchersValues['search-dispatcher'] = '';
this.sorterActive.id = 'timestampFrom';
const element = this.$refs.scrollElement as HTMLElement; this.searchHistory();
if (
element.getBoundingClientRect().bottom * 0.85 < window.innerHeight &&
this.scrollDataLoaded &&
!this.scrollNoMoreData &&
this.dataStatus == DataStatus.Loaded
)
this.addHistoryData();
}, },
search() { searchHistory() {
this.fetchHistoryData({ this.fetchHistoryData({
searchers: this.searchersValues, searchers: this.searchersValues,
}); });
@@ -202,21 +205,22 @@ export default defineComponent({
async fetchHistoryData( async fetchHistoryData(
props: { props: {
searchers?: JournalDispatcherSearcher; searchers?: JournalDispatcherSearcher;
filter?: JournalFilter; filter?: JournalTimetableFilter;
} = {} } = {}
) { ) {
this.dataStatus = DataStatus.Loading; this.dataStatus = DataStatus.Loading;
const queries: string[] = []; const queries: string[] = [];
// const dispatcher = props.searchers?.find((s) => s.id == 'search-dispatcher')?.value.trim();
// const station = props.searchers?.find((s) => s.id == 'search-station')?.value.trim();
const dispatcher = props.searchers?.['search-dispatcher'].trim(); const dispatcher = props.searchers?.['search-dispatcher'].trim();
const station = props.searchers?.['search-station'].trim(); const station = props.searchers?.['search-station'].trim();
const dateString = props.searchers?.['search-date'].trim();
const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
if (dispatcher) queries.push(`dispatcherName=${dispatcher}`); if (dispatcher) queries.push(`dispatcherName=${dispatcher}`);
if (station) queries.push(`stationName=${station}`); if (station) queries.push(`stationName=${station}`);
if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance']; // Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
if (this.sorterActive.id == 'timestampFrom') queries.push('sortBy=timestampFrom'); if (this.sorterActive.id == 'timestampFrom') queries.push('sortBy=timestampFrom');
@@ -1,41 +1,42 @@
<template> <template>
<ul class="journal-list"> <ul class="journal-list">
<transition-group name="journal-list-anim"> <!-- <transition-group name="journal-list-anim"> -->
<li v-for="(doc, i) in dispatcherHistory" :key="doc.id"> <li v-for="item in computedDispatcherHistory" :class="{ sticky: typeof item == 'string' }">
<div class="journal_day" v-if="isAnotherDay(i - 1, i)"> <div v-if="typeof item == 'string'" class="journal_day">
<span>{{ new Date(doc.timestampFrom).toLocaleDateString('pl-PL') }}</span> {{ item }}
</div> </div>
<div <div
class="journal_item" v-else
:class="{ online: doc.isOnline }" class="journal_item"
@click="navigateToScenery(doc.stationName, doc.isOnline)" :class="{ online: item.isOnline }"
@keydown.enter="navigateToScenery(doc.stationName, doc.isOnline)" @click="navigateToScenery(item.stationName, item.isOnline)"
tabindex="0" @keydown.enter="navigateToScenery(item.stationName, item.isOnline)"
> tabindex="0"
>
<span>
<b class="text--primary">{{ item.dispatcherName }}</b> &bull; <b>{{ item.stationName }}</b>
<span class="text--grayed">&nbsp;#{{ item.stationHash }}&nbsp;</span>
<span class="region-badge" :class="item.region">PL1</span>
</span>
<span>
<span :data-status="item.isOnline"> {{ item.isOnline ? $t('journal.online-since') : 'OFFLINE' }}&nbsp; </span>
<span> <span>
<b class="text--primary">{{ doc.dispatcherName }}</b> &bull; <b>{{ doc.stationName }}</b> {{ new Date(item.timestampFrom).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
<span class="text--grayed">&nbsp;#{{ doc.stationHash }}&nbsp;</span>
<span class="region-badge" :class="doc.region">PL1</span>
</span> </span>
<span> <span v-if="item.currentDuration && item.isOnline"> ({{ calculateDuration(item.currentDuration) }}) </span>
<span :data-status="doc.isOnline"> {{ doc.isOnline ? $t('journal.online-since') : 'OFFLINE' }}&nbsp; </span>
<span>
{{ new Date(doc.timestampFrom).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
</span>
<span v-if="doc.currentDuration && doc.isOnline"> ({{ calculateDuration(doc.currentDuration) }}) </span> <span v-if="item.timestampTo">
&gt;
<span v-if="doc.timestampTo"> {{ new Date(item.timestampTo).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
&gt; ({{ $t('journal.duty-lasted') }} {{ calculateDuration(item.currentDuration!) }})
{{ new Date(doc.timestampTo).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
({{ $t('journal.duty-lasted') }} {{ calculateDuration(doc.currentDuration!) }})
</span>
</span> </span>
</div> </span>
</li> </div>
</transition-group> </li>
<!-- </transition-group> -->
</ul> </ul>
</template> </template>
@@ -54,6 +55,17 @@ export default defineComponent({
mixins: [dateMixin], mixins: [dateMixin],
computed: {
computedDispatcherHistory() {
return this.dispatcherHistory.reduce((acc, historyItem, i) => {
if (this.isAnotherDay(i - 1, i)) acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL'));
acc.push(historyItem);
return acc;
}, [] as (DispatcherHistory | string)[]);
},
},
methods: { methods: {
navigateToScenery(name: string, isOnline: boolean) { navigateToScenery(name: string, isOnline: boolean) {
if (!isOnline) return; if (!isOnline) return;
@@ -87,6 +99,11 @@ export default defineComponent({
} }
} }
li.sticky {
position: sticky;
top: 0;
}
.journal_item { .journal_item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -108,36 +125,19 @@ export default defineComponent({
} }
.journal_day { .journal_day {
position: relative; margin: 1em 0;
text-align: center; padding: 0.5em;
background-color: #4d4d4d; font-weight: bold;
margin-top: 1em; background-color: #333;
span { span {
position: relative; position: relative;
background-color: #4d4d4d; background-color: inherit;
z-index: 10; z-index: 10;
padding-right: 1em;
padding: 0 0.5em; font-weight: bold;
}
&::after {
position: absolute;
content: '';
z-index: 0;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
height: 3px;
width: 60%;
min-width: 200px;
background-color: white;
} }
} }
+240 -132
View File
@@ -1,81 +1,100 @@
<template> <template>
<div class="journal-options"> <div class="journal-options">
<div class="options_wrapper"> <div class="bg" v-if="showOptions" @click="showOptions = false"></div>
<div class="options_content">
<div class="content_select">
<select-box
:itemList="translatedSorterOptions"
:defaultItemIndex="0"
@selected="onSorterChange"
:prefix="$t('journal.sort-prefix')"
/>
</div>
<div class="content_search"> <button class="btn--image" @click="showOptions = !showOptions">
<div class="search-box" v-for="(value, propName) in searchersValues" :key="propName"> <img :src="getIcon('filter2')" alt="Open filters" />
<input {{ $t('options.filters') }}
class="search-input" </button>
:placeholder="$t(`journal.${propName}`)"
v-model="searchersValues[propName]"
@keydown.enter="onInputSearch"
/>
<button class="search-exit"> <transition name="options-anim">
<img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear(propName)" /> <div class="options_wrapper" v-if="showOptions">
<div class="options_content">
<h1>{{ $t('options.sort-title') }}</h1>
<div class="options_sorters">
<div v-for="opt in translatedSorterOptions">
<button class="sort-option" :data-selected="opt.id == sorterActive.id" @click="onSorterChange(opt)">
{{ opt.value.toUpperCase() }}
</button>
</div>
</div>
<h1 v-if="filters.length != 0">{{ $t('options.filter-title') }}</h1>
<div class="options_filters">
<button
v-for="filter in filters"
class="filter-option btn--option"
:class="{ checked: journalFilterActive.id === filter.id }"
:id="filter.id"
@click="onFilterChange(filter)"
>
{{ $t(`options.filter-${filter.id}`) }}
</button> </button>
</div> </div>
<!-- <div class="search-box">
<input
class="search-input"
v-model="searchedTrain"
:placeholder="$t('journal.search-train')"
@keydown.enter="search"
/>
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="clearTrain" /> <h1>{{ $t('options.search-title') }}</h1>
<div class="search_content">
<div class="search" v-for="(_, propName) in searchersValues" :key="propName">
<label v-if="propName == 'search-date'" for="date">{{ $t('options.search-date') }}</label>
<div class="search-box">
<input
v-if="propName == 'search-date'"
class="search-input"
id="date"
type="date"
min="2022-02-01"
@keydown.enter="onSearchConfirm"
v-model="searchersValues[propName]"
/>
<input
v-else
class="search-input"
@keydown.enter="onSearchConfirm"
:placeholder="$t(`options.${propName}`)"
v-model="searchersValues[propName]"
/>
<button class="search-exit">
<img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear(propName)" />
</button>
</div>
</div>
<div class="search_actions">
<action-button class="search-button" @click="onResetButtonClick">
{{ $t('options.reset-button') }}
</action-button>
<action-button class="search-button" @click="onSearchConfirm">
{{ $t('options.search-button') }}
</action-button>
</div>
</div> </div>
<div class="search-box">
<input
class="search-input"
v-model="searchedDriver"
:placeholder="$t('journal.search-driver')"
@keydown.enter="search"
/>
<img class="search-exit" :src="exitIcon" alt="exit-icon" @click="clearDriver" />
</div> -->
<action-button class="search-button" @click="onInputSearch">
{{ $t('journal.search') }}
</action-button>
</div> </div>
</div>
<div class="options_filters"> <!-- <div class="data-status">
<button <span v-if="dataStatus == DataStatus.Loading"> Pobieranie danych...</span>
v-for="filter in filters" <span v-if="dataStatus == DataStatus.Loaded"> Pobrano dane </span>
class="journal-filter-option btn--option" </div> -->
:class="{ checked: journalFilterActive.id === filter.id }"
:id="filter.id"
@click="onFilterChange(filter)"
>
{{ $t(`journal.filter-${filter.id}`) }}
</button>
</div> </div>
</div> </transition>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, inject, JournalFilter, PropType } from 'vue'; import { defineComponent, inject, Prop, PropType } from 'vue';
import imageMixin from '../../mixins/imageMixin'; import imageMixin from '../../mixins/imageMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { JournalTimetableFilter } from '../../types/Journal/JournalTimetablesTypes';
import ActionButton from '../Global/ActionButton.vue'; import ActionButton from '../Global/ActionButton.vue';
import SelectBox from '../Global/SelectBox.vue'; import SelectBox from '../Global/SelectBox.vue';
export default defineComponent({ export default defineComponent({
components: { SelectBox, ActionButton }, components: { SelectBox, ActionButton },
emits: ['onSorterChange', 'onInputChange', 'onFilterChange'], emits: ['onSearchConfirm', 'onOptionsReset'],
mixins: [imageMixin], mixins: [imageMixin],
props: { props: {
@@ -85,16 +104,28 @@ export default defineComponent({
}, },
filters: { filters: {
type: Array as PropType<JournalFilter[]>, type: Array as PropType<JournalTimetableFilter[]>,
default: [], default: [],
}, },
dataStatus: {
type: Number as PropType<DataStatus>,
default: DataStatus.Initialized,
},
},
data() {
return {
showOptions: false,
DataStatus,
};
}, },
setup() { setup() {
return { return {
searchersValues: inject('searchersValues') as { [key: string]: string }, searchersValues: inject('searchersValues') as { [key: string]: string },
sorterActive: inject('sorterActive') as { id: string | number; dir: number }, sorterActive: inject('sorterActive') as { id: string | number; dir: number },
journalFilterActive: inject('journalFilterActive') as JournalFilter, journalFilterActive: inject('journalFilterActive') as JournalTimetableFilter,
}; };
}, },
@@ -102,7 +133,7 @@ export default defineComponent({
translatedSorterOptions() { translatedSorterOptions() {
return this.$props.sorterOptionIds.map((id) => ({ return this.$props.sorterOptionIds.map((id) => ({
id, id,
value: this.$t(`journal.option-${id}`), value: this.$t(`options.sort-${id}`),
})); }));
}, },
}, },
@@ -111,108 +142,185 @@ export default defineComponent({
onSorterChange(item: { id: string | number; value: string }) { onSorterChange(item: { id: string | number; value: string }) {
this.sorterActive.id = item.id; this.sorterActive.id = item.id;
this.sorterActive.dir = -1; this.sorterActive.dir = -1;
this.$emit('onSearchConfirm');
this.$emit('onSorterChange');
}, },
onFilterChange(filter: JournalFilter) { onFilterChange(filter: JournalTimetableFilter) {
this.journalFilterActive = filter; this.journalFilterActive = filter;
this.$emit('onFilterChange'); this.$emit('onSearchConfirm');
},
onInputSearch() {
this.$emit('onInputChange');
}, },
onInputClear(id: any) { onInputClear(id: any) {
this.searchersValues[id] = ''; this.searchersValues[id] = '';
this.onInputSearch(); this.$emit('onSearchConfirm');
},
onSearchConfirm() {
this.$emit('onSearchConfirm');
},
onResetButtonClick() {
this.$emit('onOptionsReset');
}, },
}, },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive'; @import '../../styles/responsive.scss';
@import '../../styles/option.scss';
@import '../../styles/search_box.scss'; @import '../../styles/search_box.scss';
@import '../../styles/variables.scss';
.options { .options-anim {
&_wrapper { &-enter-from,
display: flex; &-leave-to {
flex-direction: column; opacity: 0;
transform: translateY(10px);
} }
&_content { &-enter-active,
display: flex; &-leave-active {
flex-wrap: wrap; transition: all 150ms ease;
}
}
.content_search, .bg {
.content_select { position: fixed;
display: flex; top: 0;
align-items: center; left: 0;
flex-wrap: wrap; width: 100vw;
height: 100vh;
padding: 0.25em 0.25em 0 0; z-index: 10;
} }
.journal-options {
position: relative;
}
.options_wrapper {
position: absolute;
background-color: #111111ee;
box-shadow: 0 0 10px 2px #111;
width: 100%;
max-width: 500px;
padding: 1em;
z-index: 100;
}
h1 {
position: relative;
font-size: 1.1em;
margin: 0.7em 0 0.25em 0;
&::before {
content: '';
position: absolute;
top: -4px;
width: 50%;
height: 2px;
background-color: white;
border-radius: 2px;
}
}
.options_sorters {
display: flex;
align-items: center;
flex-wrap: wrap;
padding: 0.25em 0.25em 0 0;
}
.options_filters {
display: flex;
flex-wrap: wrap;
margin: 0.5em 0 0 0;
}
.sort-option,
.filter-option {
margin: 0 0.25em 0 0;
}
.sort-option[data-selected='true'] {
color: $accentCol;
font-weight: bold;
}
.filter-option {
&#abandoned {
color: salmon;
} }
&_filters { &#fulfilled {
display: flex; color: lightgreen;
flex-wrap: wrap;
margin: 0.5em 0 0 0;
.journal-filter-option {
margin: 0 0.25em 0 0;
&#abandoned {
color: salmon;
}
&#fulfilled {
color: lightgreen;
}
&#active {
color: lightblue;
}
}
} }
&#active {
color: lightblue;
}
}
.search_content > .search {
margin: 0.5em auto;
}
.search_content > button {
display: flex;
justify-content: center;
margin: 0 auto;
}
.search_content > .search_actions {
display: flex;
margin: 1em 0 0.5em 0;
button {
margin: 0.25em 0.5em;
}
}
.data-status {
display: flex;
justify-content: center;
font-size: 1.1em;
height: 1.5em;
} }
@include smallScreen() { @include smallScreen() {
.journal-options { h1 {
width: 100%; text-align: center;
&::before {
width: 75%;
left: 50%;
transform: translateX(-50%);
}
} }
.options { .options_wrapper {
&_wrapper { max-width: 100%;
justify-content: center; }
align-items: center;
}
&_content { .btn--image {
padding: 0 1em; margin: 0 auto;
}
flex-direction: column; .filter-option,
.sort-option {
margin: 0.25em 0.25em;
}
.content_select { .options_filters,
margin: 0 auto; .options_sorters {
padding: 0; justify-content: center;
}
.content_search {
justify-content: center;
}
}
&_filters {
justify-content: center;
.journal-filter-option {
margin: 0.25em 0.25em;
}
}
} }
} }
</style> </style>
@@ -6,54 +6,58 @@
<div class="journal_wrapper"> <div class="journal_wrapper">
<JournalOptions <JournalOptions
@on-input-change="searchHistory" @on-search-confirm="searchHistory"
@on-filter-change="searchHistory" @on-options-reset="resetOptions"
@on-sorter-change="searchHistory"
:sorter-option-ids="['timetableId', 'beginDate', 'distance', 'total-stops']" :sorter-option-ids="['timetableId', 'beginDate', 'distance', 'total-stops']"
:filters="journalTimetableFilters" :filters="journalTimetableFilters"
:data-status="dataStatus"
/> />
<div class="timetables_wrapper" ref="scrollElement"> <div class="list_wrapper" @scroll="handleScroll">
<transition name="warning" mode="out-in"> <!-- <transition name="warning" mode="out-in"> -->
<div :key="dataStatus"> <!-- <div :key="dataStatus"> -->
<Loading v-if="dataStatus == (DataStatus.Loading || DataStatus.Initialized)" /> <Loading
v-if="
dataStatus == DataStatus.Initialized || (dataStatus == DataStatus.Loading && timetableHistory.length == 0)
"
/>
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error"> <div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
{{ $t('app.error') }} {{ $t('app.error') }}
</div> </div>
<div v-else-if="timetableHistory.length == 0" class="journal_warning"> <div v-else-if="timetableHistory.length == 0 && dataStatus != DataStatus.Loading" class="journal_warning">
{{ $t('app.no-result') }} {{ $t('app.no-result') }}
</div> </div>
<div v-else> <div v-else>
<JournalTimetablesList :timetableHistory="timetableHistory" /> <JournalTimetablesList :timetableHistory="timetableHistory" />
<button <button
class="btn btn--option btn--load-data" class="btn btn--option btn--load-data"
v-if="!scrollNoMoreData && scrollDataLoaded" v-if="!scrollNoMoreData && scrollDataLoaded && timetableHistory.length >= 15"
@click="addHistoryData" @click="addHistoryData"
> >
{{ $t('journal.load-data') }} {{ $t('journal.load-data') }}
</button> </button>
</div> </div>
</div> <!-- </div> -->
</transition> <!-- </transition> -->
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
<div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div>
</div> </div>
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
<div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, JournalFilter, provide, reactive, Ref, ref } from 'vue'; import { defineComponent, provide, reactive, Ref, ref } from 'vue';
import axios from 'axios'; import axios from 'axios';
import DriverStats from './DriverStats.vue'; import DriverStats from './DriverStats.vue';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import { journalTimetableFilters } from '../../data/journalFilters'; import { JournalTimetableFilter, JournalTimetableSorter } from '../../types/Journal/JournalTimetablesTypes';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import routerMixin from '../../mixins/routerMixin'; import routerMixin from '../../mixins/routerMixin';
import { DataStatus } from '../../scripts/enums/DataStatus'; import { DataStatus } from '../../scripts/enums/DataStatus';
@@ -62,10 +66,11 @@ import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData
import { URLs } from '../../scripts/utils/apiURLs'; import { URLs } from '../../scripts/utils/apiURLs';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import JournalOptions from './JournalOptions.vue'; import JournalOptions from './JournalOptions.vue';
import { JournalTimetableSearcher } from '../../types/JournalTimetablesTypes'; import { JorunalTimetableSearchType } from '../../types/Journal/JournalTimetablesTypes';
import modalTrainMixin from '../../mixins/modalTrainMixin'; import modalTrainMixin from '../../mixins/modalTrainMixin';
import imageMixin from '../../mixins/imageMixin'; import imageMixin from '../../mixins/imageMixin';
import JournalTimetablesList from './JournalTimetablesList.vue'; import JournalTimetablesList from './JournalTimetablesList.vue';
import { journalTimetableFilters } from '../../constants/Journal/JournalTimetablesConsts';
const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`; const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`;
@@ -99,13 +104,14 @@ export default defineComponent({
}), }),
setup() { setup() {
const sorterActive = ref({ id: 'timetableId', dir: -1 }); const sorterActive: JournalTimetableSorter = reactive({ id: 'timetableId', dir: 1 });
const journalFilterActive = ref(journalTimetableFilters[0]); const journalFilterActive = ref(journalTimetableFilters[0]);
const searchersValues = reactive({ const searchersValues = reactive({
'search-train': '', 'search-train': '',
'search-driver': '', 'search-driver': '',
} as JournalTimetableSearcher); 'search-date': '',
} as JorunalTimetableSearchType);
const countFromIndex = ref(0); const countFromIndex = ref(0);
const countLimit = 15; const countLimit = 15;
@@ -130,35 +136,36 @@ export default defineComponent({
}, },
activated() { activated() {
window.addEventListener('scroll', this.handleScroll);
if (this.timetableId) { if (this.timetableId) {
this.searchersValues['search-train'] = `#${this.timetableId}`; this.searchersValues['search-train'] = `#${this.timetableId}`;
this.searchHistory(); this.searchHistory();
} }
}, },
deactivated() {
window.removeEventListener('scroll', this.handleScroll);
},
mounted() { mounted() {
if (!this.timetableId) this.searchHistory(); if (!this.timetableId) this.searchHistory();
}, },
methods: { methods: {
handleScroll() { handleScroll(e: Event) {
this.showReturnButton = window.scrollY > window.innerHeight; const listElement = e.target as HTMLElement;
const scrollTop = listElement.scrollTop;
const elementHeight = listElement.scrollHeight - listElement.offsetHeight;
const element = this.$refs.scrollElement as HTMLElement; if (!this.scrollDataLoaded || this.scrollNoMoreData || this.dataStatus != DataStatus.Loaded) return;
if ( if (scrollTop > elementHeight * 0.85) this.addHistoryData();
element.getBoundingClientRect().bottom * 0.85 < window.innerHeight && },
this.scrollDataLoaded &&
!this.scrollNoMoreData && resetOptions() {
this.dataStatus == DataStatus.Loaded this.searchersValues['search-date'] = '';
) this.searchersValues['search-driver'] = '';
this.addHistoryData(); this.searchersValues['search-train'] = '';
this.journalFilterActive = this.journalTimetableFilters[0];
this.sorterActive.id = 'timetableId';
this.searchHistory();
}, },
searchHistory() { searchHistory() {
@@ -193,8 +200,8 @@ export default defineComponent({
async fetchHistoryData( async fetchHistoryData(
props: { props: {
searchers?: JournalTimetableSearcher; searchers?: JorunalTimetableSearchType;
filter?: JournalFilter; filter?: JournalTimetableFilter;
} = {} } = {}
) { ) {
this.dataStatus = DataStatus.Loading; this.dataStatus = DataStatus.Loading;
@@ -204,8 +211,13 @@ export default defineComponent({
const driver = props.searchers?.['search-driver'].trim(); const driver = props.searchers?.['search-driver'].trim();
const train = props.searchers?.['search-train'].trim(); const train = props.searchers?.['search-train'].trim();
const dateString = props.searchers?.['search-date'].trim();
const timestampFrom = dateString ? Date.parse(new Date(dateString).toISOString()) - 120 * 60 * 1000 : undefined;
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
if (driver) queries.push(`driverName=${driver}`); if (driver) queries.push(`driverName=${driver}`);
if (train) queries.push(train.startsWith('#') ? `timetableId=${train.replace('#', '')}` : `trainNo=${train}`); if (train) queries.push(train.startsWith('#') ? `timetableId=${train.replace('#', '')}` : `trainNo=${train}`);
if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance']; // Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
if (this.sorterActive.id == 'distance') queries.push('sortBy=routeDistance'); if (this.sorterActive.id == 'distance') queries.push('sortBy=routeDistance');
@@ -257,8 +269,6 @@ export default defineComponent({
: ''; : '';
this.dataStatus = DataStatus.Loaded; this.dataStatus = DataStatus.Loaded;
console.log(this.dataStatus);
} catch (error) { } catch (error) {
this.dataStatus = DataStatus.Error; this.dataStatus = DataStatus.Error;
this.dataErrorMessage = 'Ups! Coś poszło nie tak!'; this.dataErrorMessage = 'Ups! Coś poszło nie tak!';
@@ -91,7 +91,7 @@
<img :src="getIcon(`arrow-${item.showStock.value ? 'asc' : 'desc'}`)" alt="Arrow" /> <img :src="getIcon(`arrow-${item.showStock.value ? 'asc' : 'desc'}`)" alt="Arrow" />
</button> </button>
<div class="info-extended" v-if="timetable.stockString" v-show="item.showStock.value"> <div class="info-extended" v-if="timetable.stockString && item.showStock.value">
<hr /> <hr />
<div> <div>
@@ -1,8 +1,8 @@
<template> <template>
<section class="filter-card" v-click-outside="closeCard"> <section class="filter-card" v-click-outside="closeCard">
<div class="card_btn"> <div class="card_btn">
<button class="btn btn--option" @click="toggleCard"> <button class="btn--image" @click="toggleCard">
<img class="button_icon" :src="getIcon('filter2')" alt="icon-filter" /> <img class="button_icon" :src="getIcon('filter2')" alt="filter icon" />
{{ $t('options.filters') }} {{ $t('options.filters') }}
</button> </button>
</div> </div>
@@ -91,7 +91,6 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, inject } from 'vue'; import { defineComponent, inject } from 'vue';
import inputData from '../../data/options.json'; import inputData from '../../data/options.json';
import imageMixin from '../../mixins/imageMixin'; import imageMixin from '../../mixins/imageMixin';
@@ -107,7 +106,6 @@ export default defineComponent({
mixins: [imageMixin], mixins: [imageMixin],
data: () => ({ data: () => ({
inputs: { ...inputData }, inputs: { ...inputData },
saveOptions: false, saveOptions: false,
STORAGE_KEY: 'options_saved', STORAGE_KEY: 'options_saved',
@@ -263,6 +261,7 @@ export default defineComponent({
} }
} }
.card { .card {
&_btn { &_btn {
button { button {
@@ -389,6 +388,7 @@ export default defineComponent({
input { input {
width: 100%; width: 100%;
padding: 0.5em; padding: 0.5em;
border: 1px solid white;
} }
} }
@@ -437,6 +437,7 @@ export default defineComponent({
&::-webkit-slider-thumb { &::-webkit-slider-thumb {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none;
height: 20px; height: 20px;
width: 20px; width: 20px;
+218 -163
View File
@@ -1,230 +1,285 @@
<template> <template>
<div class="train-options"> <div class="train-options">
<div class="options_wrapper"> <div class="bg" v-if="showOptions" @click="showOptions = false"></div>
<div class="options_content">
<div class="content_select">
<select-box
:itemList="translatedSorterOptions"
:defaultItemIndex="0"
@selected="changeSorter"
:prefix="$t('trains.sorter-prefix')"
/>
</div>
<div class="content_search"> <button class="btn--open" @click="showOptions = !showOptions">
<div class="search-box"> <img :src="getIcon('filter2')" alt="Open filters" />
<input class="search-input" v-model="searchedTrain" :placeholder="$t('trains.search-train')" /> {{ $t('options.filters') }}
</button>
<button class="search-exit"> <transition name="options-anim">
<img :src="getIcon('exit')" alt="exit-icon" @click="() => (searchedTrain = '')" /> <div class="options_wrapper" v-if="showOptions">
</button> <div class="options_content">
<h1>{{ $t('options.sort-title') }}</h1>
<div class="options_sorters">
<div v-for="opt in translatedSorterOptions">
<button class="sort-option" :data-selected="opt.id == sorterActive.id" @click="onSorterChange(opt)">
{{ opt.value.toUpperCase() }}
</button>
</div>
</div> </div>
<div class="search-box"> <h1 v-if="trainFilterList.length != 0">{{ $t('options.filter-title') }}</h1>
<input class="search-input" v-model="searchedDriver" :placeholder="$t('trains.search-driver')" /> <div class="options_filters">
<div class="filter-option" v-for="filter in trainFilterList">
<button class="search-exit"> <button class="btn--option" :data-disabled="!filter.isActive" @click="onFilterChange(filter)">
<img :src="getIcon('exit')" alt="exit-icon" @click="() => (searchedDriver = '')" /> {{ $t(`options.filter-${filter.id}`) }}
</button> </button>
</div>
</div>
<div class="options_filters">
<div class="filter-option">
<button @click="clearAllFilters">{{ $t('options.filter-clear') }}</button>
</div>
<div class="filter-option">
<button @click="resetAllFilters">{{ $t('options.filter-reset') }}</button>
</div>
</div>
<h1>{{ $t('options.search-title') }}</h1>
<div class="content_search">
<div class="search-box">
<input class="search-input" :placeholder="$t(`options.search-train`)" v-model="searchedTrain" />
<button class="search-exit">
<img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear('train')" />
</button>
</div>
<div class="search-box">
<input class="search-input" :placeholder="$t(`options.search-driver`)" v-model="searchedDriver" />
<button class="search-exit">
<img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear('driver')" />
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </transition>
<div class="filters">
<span
:class="{ active: filter.isActive }"
class="filter"
v-for="filter in filterList"
:key="filter.id"
tabindex="0"
@contextmenu="
(e) => {
e.preventDefault();
return false;
}
"
@click.left="toggleFilter(filter)"
@keydown.enter="toggleFilter(filter)"
@click.right="setFilterOnly(filter)"
@keydown.space="setFilterOnly(filter)"
>
{{ $t(`trains.filter-${filter.id}`) }}
</span>
<span class="filter reset-btn" @click="resetFilters" tabindex="0">
{{ $t('trains.filter-reset') }}
</span>
</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, inject, TrainFilter } from 'vue'; import { defineComponent, inject, PropType } from 'vue';
import { useI18n } from 'vue-i18n';
import imageMixin from '../../mixins/imageMixin'; import imageMixin from '../../mixins/imageMixin';
import { TrainFilter } from '../../types/Trains/TrainOptionsTypes';
import ActionButton from '../Global/ActionButton.vue';
import SelectBox from '../Global/SelectBox.vue'; import SelectBox from '../Global/SelectBox.vue';
export default defineComponent({ export default defineComponent({
components: { SelectBox }, components: { SelectBox, ActionButton },
emits: ['changeSearchedTrain', 'changeSearchedDriver', 'changeSorter'],
mixins: [imageMixin], mixins: [imageMixin],
setup() { props: {
const { t } = useI18n(); sorterOptionIds: {
type: Array as PropType<Array<string>>,
const sorterOptions = [ required: true,
{ },
id: 'distance', },
value: 'kilometraż',
},
{
id: 'progress',
value: 'przebyta trasa',
},
{
id: 'delay',
value: 'opóźnienie',
},
{
id: 'mass',
value: 'masa',
},
{
id: 'speed',
value: 'prędkość',
},
{
id: 'length',
value: 'długość',
},
];
let filterList = inject('filterList') as TrainFilter[];
const translatedSorterOptions = computed(() =>
sorterOptions.map(({ id }) => ({
id,
value: t(`trains.option-${id}`),
}))
);
data() {
return { return {
translatedSorterOptions, showOptions: false,
searchedTrain: inject('searchedTrain') as string,
searchedDriver: inject('searchedDriver') as string,
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
filterList,
}; };
}, },
setup() {
return {
searchedTrain: inject('searchedTrain') as string,
searchedDriver: inject('searchedDriver') as string,
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
trainFilterList: inject('filterList') as TrainFilter[],
};
},
computed: {
translatedSorterOptions() {
return this.$props.sorterOptionIds.map((id) => ({
id,
value: this.$t(`options.sort-${id}`),
}));
},
},
methods: { methods: {
changeSorter(item: { id: string | number; value: string }) { onSorterChange(item: { id: string | number; value: string }) {
this.sorterActive.id = item.id; this.sorterActive.id = item.id;
this.sorterActive.dir = -1; this.sorterActive.dir = -1;
}, },
toggleFilter(filter: TrainFilter) { onFilterChange(filter: TrainFilter) {
filter.isActive = !filter.isActive; filter.isActive = !filter.isActive;
}, },
setFilterOnly(filter: TrainFilter) { clearAllFilters() {
this.filterList.forEach((f) => (f.isActive = f.id == filter.id)); this.trainFilterList.forEach((filter) => {
filter.isActive = false;
});
}, },
resetFilters() { resetAllFilters() {
this.filterList.forEach((f) => (f.isActive = true)); this.trainFilterList.forEach((filter) => {
this.searchedDriver = ''; filter.isActive = true;
this.searchedTrain = ''; });
},
onInputClear(id: 'driver' | 'train') {
if (id == 'driver') this.searchedDriver = '';
if (id == 'train') this.searchedTrain = '';
},
test(e: Event) {
console.log(e.target);
}, },
}, },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive'; @import '../../styles/responsive.scss';
@import '../../styles/search_box.scss'; @import '../../styles/search_box.scss';
@import '../../styles/variables.scss';
.train-options { .options-anim {
@include smallScreen() { &-enter-from,
width: 100%; &-leave-to {
font-size: 1.25em; opacity: 0;
transform: translateY(10px);
}
&-enter-active,
&-leave-active {
transition: all 150ms ease;
} }
} }
.options { .bg {
&_wrapper { position: fixed;
display: flex; top: 0;
} left: 0;
width: 100vw;
height: 100vh;
&_content { z-index: 10;
display: flex;
flex-wrap: wrap;
.content_search,
.content_select {
display: flex;
align-items: center;
flex-wrap: wrap;
padding: 0.25em 0.25em 0 0;
}
}
} }
.filters { .journal-options {
position: relative;
margin-bottom: 0.5em;
}
.options_wrapper {
position: absolute;
background-color: #111111ee;
box-shadow: 0 0 10px 2px #111;
width: 100%;
max-width: 500px;
padding: 1em;
z-index: 100;
}
.btn--open {
display: flex; display: flex;
padding: 0.4em 1em;
font-weight: bold;
font-size: 1em;
border-radius: 0.75em 0.75em 0 0;
img {
height: 1.3em;
margin-right: 0.5em;
}
}
h1 {
position: relative;
font-size: 1.1em;
margin: 0.7em 0 0.25em 0;
&::before {
content: '';
position: absolute;
top: -4px;
width: 50%;
height: 2px;
background-color: white;
border-radius: 2px;
}
}
.options_sorters {
display: flex;
align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
margin-top: 0.5em; padding: 0.25em 0.25em 0 0;
@include smallScreen() {
justify-content: center;
}
} }
.filter { .content_search > div {
background: #333; margin: 0.5em auto;
padding: 0.2em 0.25em; }
margin: 0.25em 0.25em 0 0;
.content_search > button {
display: flex;
justify-content: center;
margin: 0 auto;
}
.options_filters {
display: flex;
flex-wrap: wrap;
margin: 0.5em 0 0 0;
}
.sort-option,
.filter-option {
margin: 0.25em 0.25em 0.25em 0;
}
.sort-option[data-selected='true'] {
color: $accentCol;
font-weight: bold; font-weight: bold;
}
cursor: pointer; .filter-option {
color: gray; button {
color: white;
font-weight: bold;
&.active { &[data-disabled='true'] {
color: var(--clr-primary); color: #888;
} }
&.reset-btn {
color: salmon;
} }
} }
@include smallScreen() { @include smallScreen() {
.journal-options { h1 {
width: 100%; text-align: center;
&::before {
width: 75%;
left: 50%;
transform: translateX(-50%);
}
} }
.options { .options_wrapper {
&_wrapper { max-width: 100%;
justify-content: center; }
}
&_content { .btn--open {
padding: 0 1em; margin: 0 auto;
}
flex-direction: column; .options_filters,
.options_sorters {
.content_select { justify-content: center;
margin: 0 auto;
padding: 0;
}
.content_search {
justify-content: center;
}
}
} }
} }
</style> </style>
@@ -1,144 +0,0 @@
<template>
<section class="filter-card" v-click-outside="closeCard">
<div class="card_btn">
<action-button @click="toggleCard">
<img class="button_icon" :src="getIcon('filter2')" alt="icon-filter" />
<p>{{ $t('options.filters') }}</p>
</action-button>
</div>
<transition name="card-anim">
<div class="card_content card" v-if="isVisible">
<div class="card_exit" @click="closeCard"></div>
<div class="options_wrapper">
<div class="options_content">
<div class="content_select">
<select-box
:itemList="translatedSorterOptions"
:defaultItemIndex="0"
@selected="changeSorter"
:prefix="$t('trains.sorter-prefix')"
/>
</div>
<div class="content_search">
<div class="search-box">
<input class="search-input" v-model="searchedTrain" :placeholder="$t('trains.search-train')" />
<img class="search-exit" :src="getIcon('exit')" alt="exit-icon" @click="() => (searchedTrain = '')" />
</div>
<div class="search-box">
<input class="search-input" v-model="searchedDriver" :placeholder="$t('trains.search-driver')" />
<img class="search-exit" :src="getIcon('exit')" alt="exit-icon" @click="() => (searchedDriver = '')" />
</div>
</div>
</div>
</div>
<section class="card_actions flex">
<action-button class="outlined">
{{ $t('filters.reset') }}
</action-button>
<action-button class="outlined" @click="closeCard">{{ $t('filters.close') }}</action-button>
</section>
</div>
</transition>
</section>
</template>
<script lang="ts">
import inputData from "../../data/options.json";
import { TrainFilter, computed, defineComponent, inject } from 'vue';
import { useI18n } from 'vue-i18n';
import SelectBox from '../Global/SelectBox.vue';
import ActionButton from '../Global/ActionButton.vue';
import { sorterOptions } from '../../data/trainOptions';
import imageMixin from "../../mixins/imageMixin";
export default defineComponent({
components: { ActionButton, SelectBox },
emits: ['changeFilterValue', 'invertFilters', 'resetFilters'],
mixins: [imageMixin],
data: () => ({
inputs: { ...inputData },
}),
setup() {
const isVisible = inject('isTrainOptionsCardVisible');
const { t } = useI18n();
let filterList = inject('filterList') as TrainFilter[];
const translatedSorterOptions = computed(() =>
sorterOptions.map(({ id }) => ({
id,
value: t(`trains.option-${id}`),
}))
);
return {
translatedSorterOptions,
searchedTrain: inject('searchedTrain') as string,
searchedDriver: inject('searchedDriver') as string,
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
filterList,
isVisible,
};
},
methods: {
closeCard() {
this.isVisible = false;
},
toggleCard() {
this.isVisible = !this.isVisible;
},
changeSorter(item: { id: string | number; value: string }) {
this.sorterActive.id = item.id;
this.sorterActive.dir = -1;
},
toggleFilter(filter: TrainFilter) {
filter.isActive = !filter.isActive;
},
setFilterOnly(filter: TrainFilter) {
this.filterList.forEach((f) => (f.isActive = f.id == filter.id));
},
resetFilters() {
this.filterList.forEach((f) => (f.isActive = true));
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive';
@import '../../styles/card';
.card {
section {
margin: 0.5em 0;
}
&_title {
font-size: 2em;
font-weight: 700;
color: $accentCol;
margin: 0.5em 0;
text-align: center;
}
}
</style>
+1 -4
View File
@@ -118,11 +118,10 @@ export default defineComponent({
text-align: center; text-align: center;
padding: 1em 0; padding: 1em 0;
margin: 1em 0;
font-size: 1.5em; font-size: 1.5em;
background: #333; background: #1a1a1a;
} }
img.train-image { img.train-image {
@@ -139,8 +138,6 @@ img.train-image {
&-list { &-list {
overflow: auto; overflow: auto;
margin-top: 1em;
@include smallScreen() { @include smallScreen() {
width: 100%; width: 100%;
} }
@@ -0,0 +1,28 @@
import { JournalFilterType } from "../../scripts/enums/JournalFilterType";
import { JournalTimetableFilter } from "../../types/Journal/JournalTimetablesTypes";
export const journalTimetableFilters: JournalTimetableFilter[] = [
{
id: JournalFilterType.all,
filterSection: 'timetable-status',
isActive: true,
},
{
id: JournalFilterType.active,
filterSection: 'timetable-status',
isActive: false,
},
{
id: JournalFilterType.fulfilled,
filterSection: 'timetable-status',
isActive: false,
},
{
id: JournalFilterType.abandoned,
filterSection: 'timetable-status',
isActive: false,
},
];
@@ -1,5 +1,5 @@
import { TrainFilter } from "vue"; import { TrainFilterType } from '../../scripts/enums/TrainFilterType';
import { TrainFilterType } from "../scripts/enums/TrainFilterType"; import { TrainFilter } from '../../types/Trains/TrainOptionsTypes';
export const trainFilters: TrainFilter[] = [ export const trainFilters: TrainFilter[] = [
{ {
@@ -56,5 +56,5 @@ export const sorterOptions = [
{ {
id: 'length', id: 'length',
value: 'długość', value: 'długość',
} },
]; ];
-30
View File
@@ -1,30 +0,0 @@
import { JournalFilter } from "vue";
import { JournalFilterType } from "../scripts/enums/JournalFilterType";
export const journalTimetableFilters: JournalFilter[] = [
{
id: JournalFilterType.all,
filterSection: "timetable-status",
isActive: true
},
{
id: JournalFilterType.active,
filterSection: "timetable-status",
isActive: false
},
{
id: JournalFilterType.fulfilled,
filterSection: "timetable-status",
isActive: false
},
{
id: JournalFilterType.abandoned,
filterSection: "timetable-status",
isActive: false
},
]
export const journalDispatcherFilters: JournalFilter[] = []
+50 -48
View File
@@ -11,10 +11,10 @@
"migration-confirm": "Roger that!" "migration-confirm": "Roger that!"
}, },
"update": { "update": {
"title": "New Stacjownik version is available!", "title": "New Stacjownik version is available!",
"paragraph1": "Enjoy the application and may the green signal be with you!", "paragraph1": "Enjoy the application and may the green signal be with you!",
"release-link": "Click here to browse version changelog (GitHub)", "release-link": "Click here to browse version changelog (GitHub)",
"confirm-button": "Understood!" "confirm-button": "Understood!"
}, },
"data-status": { "data-status": {
"S1a-connection": "<b>S1a signal</b> <br> Cannot connect with Stacjownik API service!", "S1a-connection": "<b>S1a signal</b> <br> Cannot connect with Stacjownik API service!",
@@ -72,7 +72,51 @@
}, },
"options": { "options": {
"filters": "FILTERS", "filters": "FILTERS",
"donate": "DONATE" "donate": "DONATE",
"search-button": "Search",
"reset-button": "Reset",
"sort-title": "SORT BY:",
"filter-title": "FILTER BY:",
"search-title": "SEARCH:",
"search-train-no": "Train no. / #",
"search-train": "Train no.",
"search-driver": "Driver name",
"search-dispatcher": "Dispatcher name",
"search-station": "Scenery name",
"search-date": "Timetable date (CEST / GMT+2)",
"sort-mass": "mass",
"sort-speed": "speed",
"sort-length": "length",
"sort-distance": "distance",
"sort-timetable": "train no.",
"sort-progress": "route progress",
"sort-delay": "current delay",
"sort-total-stops": "total stops",
"sort-beginDate": "date",
"sort-timetableId": "timetable ID",
"sort-timestampFrom": "date",
"sort-duration": "duration",
"filter-comments": "COMMENTS",
"filter-twr": "TWR",
"filter-skr": "SKR",
"filter-passenger": "PASSENGER",
"filter-freight": "FREIGHT",
"filter-other": "OTHER",
"filter-noTimetable": "NO TIMETABLE",
"filter-reset": "RESET FILTERS",
"filter-clear": "CLEAR FILTERS",
"filter-all": "ALL ENTRIES",
"filter-abandoned": "ABANDONED",
"filter-fulfilled": "FULFILLED",
"filter-active": "ACTIVE"
}, },
"filters": { "filters": {
"endingStatus": "ENDS SOON", "endingStatus": "ENDS SOON",
@@ -116,7 +160,7 @@
"hour": "h", "hour": "h",
"no-limit": "NO LIMIT", "no-limit": "NO LIMIT",
"include-selected": "INCLUDE SELECTED", "include-selected": "INCLUDE SELECTED",
"save": "SAVE FILTERS", "save": "SAVE FILTERS",
"reset": "RESET FILTERS", "reset": "RESET FILTERS",
"close": "CLOSE FILTERS" "close": "CLOSE FILTERS"
}, },
@@ -150,28 +194,6 @@
"current-signal": "at signal", "current-signal": "at signal",
"current-track": "on track", "current-track": "on track",
"option-mass": "mass",
"option-speed": "speed",
"option-length": "length",
"option-distance": "distance",
"option-timetable": "train no.",
"option-progress": "route progress",
"option-delay": "current delay",
"option-comments": "comments",
"filter-comments": "comments",
"filter-twr": "TWR",
"filter-skr": "SKR",
"filter-passenger": "passenger",
"filter-freight": "freight",
"filter-other": "other",
"filter-noTimetable": "no timetable",
"filter-reset": "X RESET",
"sorter-prefix": "Sort: ",
"search-train": "Train no.",
"search-driver": "Driver name",
"delayed": "Delayed: ", "delayed": "Delayed: ",
"preponed": "Ahead of schedule: ", "preponed": "Ahead of schedule: ",
"on-time": "On time", "on-time": "On time",
@@ -205,26 +227,6 @@
"section-timetables": "TIMETABLES", "section-timetables": "TIMETABLES",
"section-dispatchers": "DISPATCHERS", "section-dispatchers": "DISPATCHERS",
"search": "Search",
"search-train": "Train no. / #",
"search-driver": "Driver name",
"search-dispatcher": "Dispatcher name",
"search-station": "Scenery name",
"sort-prefix": "Sort: ",
"option-distance": "distance",
"option-total-stops": "total stops",
"option-beginDate": "date",
"option-timetableId": "timetable ID",
"option-timestampFrom": "date",
"option-duration": "duration",
"filter-all": "ALL ENTRIES",
"filter-abandoned": "ABANDONED",
"filter-fulfilled": "FULFILLED",
"filter-active": "ACTIVE",
"no-further-data": "No further data for current parameters", "no-further-data": "No further data for current parameters",
"loading-further-data": "Loading...", "loading-further-data": "Loading...",
+46 -43
View File
@@ -74,7 +74,52 @@
}, },
"options": { "options": {
"filters": "FILTRY", "filters": "FILTRY",
"donate": "WESPRZYJ" "donate": "WESPRZYJ",
"search-button": "Szukaj",
"reset-button": "Zresetuj",
"sort-title": "SORTUJ WG:",
"filter-title": "FILTRUJ WG:",
"search-title": "SZUKAJ:",
"search-train-no": "Nr pociągu",
"search-train": "Nr pociągu / #",
"search-driver": "Nick maszynisty",
"search-dispatcher": "Nick dyżurnego",
"search-station": "Nazwa scenerii",
"search-date": "Data rozkładu jazdy (czas polski)",
"sort-distance": "kilometraż",
"sort-total-stops": "stacje",
"sort-beginDate": "data",
"sort-timetableId": "ID rozkładu",
"sort-timestampFrom": "data",
"sort-duration": "czas dyżuru",
"sort-mass": "masa",
"sort-speed": "prędkość",
"sort-length": "długość",
"sort-timetable": "nr pociągu",
"sort-progress": "przebyta trasa",
"sort-delay": "opóźnienie",
"sort-comments": "uwagi ekspl.",
"filter-comments": "UWAGI EKSPLOATACYJNE",
"filter-twr": "TWR",
"filter-skr": "PRZEKR. SKRAJNIA",
"filter-passenger": "PASAŻERSKIE",
"filter-freight": "TOWAROWE",
"filter-other": "INNE",
"filter-noTimetable": "BEZ RJ",
"filter-reset": "ZRESETUJ FILTRY",
"filter-clear": "WYŁĄCZ FILTRY",
"filter-all": "WSZYSTKIE",
"filter-abandoned": "PORZUCONE",
"filter-fulfilled": "WYPEŁNIONE",
"filter-active": "AKTYWNE"
}, },
"filters": { "filters": {
"endingStatus": "KOŃCZY", "endingStatus": "KOŃCZY",
@@ -152,28 +197,6 @@
"current-signal": "przy semaforze", "current-signal": "przy semaforze",
"current-track": "na szlaku", "current-track": "na szlaku",
"option-mass": "masa",
"option-speed": "prędkość",
"option-length": "długość",
"option-distance": "kilometraż",
"option-timetable": "nr pociągu",
"option-progress": "przebyta trasa",
"option-delay": "opóźnienie",
"option-comments": "uwagi ekspl.",
"filter-comments": "uwagi ekspl.",
"filter-twr": "TWR",
"filter-skr": "SKR",
"filter-passenger": "pasażerskie",
"filter-freight": "towarowe",
"filter-other": "inne",
"filter-noTimetable": "bez RJ",
"filter-reset": "X RESETUJ",
"sorter-prefix": "Sortuj: ",
"search-train": "Numer pociągu",
"search-driver": "Nick maszynisty",
"delayed": "Opóźniony: ", "delayed": "Opóźniony: ",
"preponed": "Przed czasem: ", "preponed": "Przed czasem: ",
"on-time": "Planowo", "on-time": "Planowo",
@@ -207,26 +230,6 @@
"section-timetables": "ROZKŁADY JAZDY", "section-timetables": "ROZKŁADY JAZDY",
"section-dispatchers": "DYŻURNI", "section-dispatchers": "DYŻURNI",
"search": "Szukaj",
"search-train": "Nr pociągu / #",
"search-driver": "Nick maszynisty",
"search-dispatcher": "Nick dyżurnego",
"search-station": "Nazwa scenerii",
"sort-prefix": "Sortuj: ",
"option-distance": "kilometraż",
"option-total-stops": "stacje",
"option-beginDate": "data",
"option-timetableId": "ID rozkładu",
"option-timestampFrom": "data",
"option-duration": "czas dyżuru",
"filter-all": "WSZYSTKIE",
"filter-abandoned": "PORZUCONE",
"filter-fulfilled": "WYPEŁNIONE",
"filter-active": "AKTYWNE",
"no-further-data": "Brak dalszych wyników dla podanych parametrów", "no-further-data": "Brak dalszych wyników dla podanych parametrów",
"loading-further-data": "Ładowanie...", "loading-further-data": "Ładowanie...",
+1 -1
View File
@@ -1,4 +1,4 @@
import { TrainFilter } from "vue"; import { TrainFilter } from "../../types/Trains/TrainOptionsTypes";
import { TrainFilterType } from "../enums/TrainFilterType"; import { TrainFilterType } from "../enums/TrainFilterType";
import Train from "../interfaces/Train"; import Train from "../interfaces/Train";
import TrainStop from "../interfaces/TrainStop"; import TrainStop from "../interfaces/TrainStop";
+18 -4
View File
@@ -8,16 +8,24 @@
} }
&-enter-active { &-enter-active {
transition: all 150ms ease-out; transition: all 150ms 100ms ease-out;
} }
&-leave-active { &-leave-active {
transition: all 150ms ease-out; transition: all 150ms 100ms ease-out;
} }
} }
//Styles //Styles
.list_wrapper {
overflow-y: scroll;
height: 90vh;
min-height: 550px;
padding-right: 0.2em;
}
.journal_wrapper { .journal_wrapper {
max-width: 1350px; max-width: 1350px;
width: 100%; width: 100%;
@@ -40,9 +48,9 @@
.journal_item, .journal_item,
.journal_warning { .journal_warning {
background: #202020; background-color: #1a1a1a;
padding: 1em; padding: 1em;
margin: 1em 0; margin-bottom: 1em;
} }
.journal_top-bar { .journal_top-bar {
@@ -69,3 +77,9 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
} }
@media (orientation: landscape) {
.journal_wrapper {
font-size: 1em;
}
}
+40 -29
View File
@@ -12,6 +12,24 @@
--clr-error: #df3e3e; --clr-error: #df3e3e;
--clr-warning: #c59429; --clr-warning: #c59429;
font-size: 16px;
}
::-webkit-scrollbar {
width: 1rem;
height: 1rem;
background-color: transparent;
&-track {
border-radius: 0.5em;
background-color: #333;
}
&-thumb {
border-radius: 0.5em;
background-color: #666;
}
} }
html { html {
@@ -36,28 +54,6 @@ body {
} }
} }
*:focus-visible {
outline: 1px solid white;
outline-offset: 1px;
}
:root {
font-size: 16px;
}
::-webkit-scrollbar {
width: 1rem;
height: 1rem;
&-track {
background-color: #222;
}
&-thumb {
background-color: #777;
}
}
.g-tooltip { .g-tooltip {
position: relative; position: relative;
@@ -113,7 +109,6 @@ select {
} }
input { input {
border: 1px solid white;
background: none; background: none;
color: white; color: white;
font-size: 1em; font-size: 1em;
@@ -190,6 +185,16 @@ ul {
} }
} }
button {
cursor: pointer;
color: white;
background-color: #333;
border-radius: 0.25em;
padding: 0.25em 0.5em;
}
.btn { .btn {
background: none; background: none;
cursor: pointer; cursor: pointer;
@@ -211,8 +216,18 @@ ul {
} }
&--image { &--image {
color: white; display: flex;
transition: color 0.3s;
padding: 0.4em 1em;
font-weight: bold;
font-size: 1em;
border-radius: 0.75em 0.75em 0 0;
img {
height: 1.3em;
margin-right: 0.5em;
}
} }
&--option { &--option {
@@ -224,10 +239,6 @@ ul {
border-radius: 0.25em; border-radius: 0.25em;
padding: 0.25em 0.5em; padding: 0.25em 0.5em;
&:hover:not(:disabled) {
background-color: #3c3c3c;
}
&.checked { &.checked {
color: var(--clr-primary); color: var(--clr-primary);
font-weight: bold; font-weight: bold;
+11 -7
View File
@@ -1,6 +1,12 @@
@import 'responsive.scss'; @import 'responsive.scss';
.search { .search {
label {
display: block;
color: #ccc;
margin-bottom: 0.25em;
}
&-box { &-box {
position: relative; position: relative;
@@ -9,7 +15,6 @@
border-radius: 0.5em; border-radius: 0.5em;
min-width: 200px; min-width: 200px;
margin-right: 0.25em; margin-right: 0.25em;
background-color: #333;
} }
&-input { &-input {
@@ -18,7 +23,6 @@
background-color: #333; background-color: #333;
padding: 0.35em 0.5em; padding: 0.35em 0.5em;
margin-right: 0.2em;
width: 100%; width: 100%;
} }
@@ -33,6 +37,11 @@
} }
} }
&-button {
width: 80%;
max-width: 300px;
}
@include smallScreen { @include smallScreen {
&-box, &-box,
&-button { &-button {
@@ -42,10 +51,5 @@
&-box { &-box {
width: 100%; width: 100%;
} }
&-button {
width: 80%;
max-width: 300px;
}
} }
} }
@@ -0,0 +1,8 @@
export type JournalDispatcherSearcher = {
[key in 'search-dispatcher' | 'search-station' | 'search-date']: string;
};
export interface JournalDispatcherSorter {
id: 'timestampFrom' | 'duration';
dir: -1 | 1;
}
@@ -0,0 +1,16 @@
import { JournalFilterType } from '../../scripts/enums/JournalFilterType';
export type JorunalTimetableSearchType = {
[key in 'search-driver' | 'search-train' | 'search-date']: string;
};
export interface JournalTimetableFilter {
id: JournalFilterType;
filterSection: string;
isActive: boolean;
}
export interface JournalTimetableSorter {
id: 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
dir: -1 | 1;
}
-3
View File
@@ -1,3 +0,0 @@
export type JournalDispatcherSearcher = {
[key in 'search-dispatcher' | 'search-station']: string;
};
-3
View File
@@ -1,3 +0,0 @@
export type JournalTimetableSearcher = {
[key in 'search-driver' | 'search-train']: string;
};
+6
View File
@@ -0,0 +1,6 @@
import { TrainFilterType } from "../../scripts/enums/TrainFilterType";
export interface TrainFilter {
id: TrainFilterType;
isActive: boolean;
}
+4 -10
View File
@@ -2,7 +2,7 @@
<section class="trains-view"> <section class="trains-view">
<div class="wrapper"> <div class="wrapper">
<div class="options-bar"> <div class="options-bar">
<train-options /> <TrainOptions :sorter-option-ids="['distance', 'progress', 'delay', 'mass', 'speed', 'length']" />
</div> </div>
<TrainTable :trains="computedTrains" /> <TrainTable :trains="computedTrains" />
@@ -11,14 +11,15 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, ComputedRef, defineComponent, provide, reactive, ref, TrainFilter } from 'vue'; import { computed, ComputedRef, defineComponent, provide, reactive, ref } from 'vue';
import TrainOptions from '../components/TrainsView/TrainOptions.vue'; import TrainOptions from '../components/TrainsView/TrainOptions.vue';
import TrainStats from '../components/TrainsView/TrainStats.vue'; import TrainStats from '../components/TrainsView/TrainStats.vue';
import TrainTable from '../components/TrainsView/TrainTable.vue'; import TrainTable from '../components/TrainsView/TrainTable.vue';
import { trainFilters } from '../data/trainOptions'; import { trainFilters } from '../constants/Trains/TrainOptionsConsts';
import Train from '../scripts/interfaces/Train'; import Train from '../scripts/interfaces/Train';
import { filteredTrainList } from '../scripts/managers/trainFilterManager'; import { filteredTrainList } from '../scripts/managers/trainFilterManager';
import { useStore } from '../store/store'; import { useStore } from '../store/store';
import { TrainFilter } from '../types/Trains/TrainOptionsTypes';
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -48,7 +49,6 @@ export default defineComponent({
const sorterActive = ref({ id: 'distance', dir: -1 }); const sorterActive = ref({ id: 'distance', dir: -1 });
const filterList = reactive([...trainFilters]) as TrainFilter[]; const filterList = reactive([...trainFilters]) as TrainFilter[];
const isTrainOptionsCardVisible = ref(false);
const searchedDriver = ref(''); const searchedDriver = ref('');
const searchedTrain = ref(''); const searchedTrain = ref('');
@@ -57,7 +57,6 @@ export default defineComponent({
provide('searchedDriver', searchedDriver); provide('searchedDriver', searchedDriver);
provide('sorterActive', sorterActive); provide('sorterActive', sorterActive);
provide('filterList', filterList); provide('filterList', filterList);
provide('isTrainOptionsCardVisible', isTrainOptionsCardVisible);
const computedTrains: ComputedRef<Train[]> = computed(() => { const computedTrains: ComputedRef<Train[]> = computed(() => {
return filteredTrainList( return filteredTrainList(
@@ -82,10 +81,6 @@ export default defineComponent({
this.searchedTrain = this.train; this.searchedTrain = this.train;
this.searchedDriver = this.driver || ''; this.searchedDriver = this.driver || '';
} }
// if (this.train) {
// this.searchedTrain = this.train;
// if(this.x) this.searchedDriver = this.x;
// }
}, },
}); });
</script> </script>
@@ -102,5 +97,4 @@ export default defineComponent({
margin: 1rem auto; margin: 1rem auto;
max-width: 1350px; max-width: 1350px;
} }
</style> </style>
-30
View File
@@ -1,30 +0,0 @@
import { ComponentCustomProperties } from 'vue'
import { Store } from 'vuex'
import { JournalFilterType } from './scripts/enums/JournalFilterType';
import { TrainFilterType } from './scripts/enums/TrainFilterType';
declare module '@vue/runtime-core' {
// declare your own store states
interface State {
count: number
}
// provide typings for `this.$store`
interface ComponentCustomProperties {
$store: Store<State>
}
// Train filter for TrainView
interface TrainFilter {
id: TrainFilterType;
isActive: boolean;
}
interface JournalFilter {
id: JournalFilterType;
filterSection: string;
isActive: boolean;
}
}