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",
"version": "1.10.3",
"version": "1.10.4",
"private": true,
"scripts": {
"dev": "vite",
@@ -2,48 +2,55 @@
<section class="journal-timetables">
<div class="journal_wrapper">
<JournalOptions
@on-filter-change="search"
@on-input-change="search"
@on-sorter-change="search"
@on-search-confirm="searchHistory"
@on-options-reset="resetOptions"
:sorter-option-ids="['timestampFrom', 'duration']"
:data-status="dataStatus"
/>
<div class="timetables_wrapper" ref="scrollElement">
<transition name="warning" mode="out-in">
<div :key="dataStatus">
<Loading v-if="dataStatus == (DataStatus.Loading || DataStatus.Initialized)" />
<div class="list_wrapper" @scroll="handleScroll">
<!-- <transition name="warning" mode="out-in"> -->
<!-- <div :key="dataStatus"> -->
<Loading
v-if="dataStatus == DataStatus.Initialized || (dataStatus == DataStatus.Loading && historyList.length == 0)"
/>
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
{{ $t('app.error') }}
</div>
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
{{ $t('app.error') }}
</div>
<div class="journal_warning" v-else-if="historyList.length == 0">
{{ $t('app.no-result') }}
</div>
<div class="journal_warning" v-else-if="historyList.length == 0 && dataStatus != DataStatus.Loading">
{{ $t('app.no-result') }}
</div>
<div v-else>
<JournalDispatchersList :dispatcherHistory="computedHistoryList" />
<div v-else>
<JournalDispatchersList :dispatcherHistory="computedHistoryList" />
<button
class="btn btn--option btn--load-data"
v-if="!scrollNoMoreData && scrollDataLoaded"
@click="addHistoryData"
>
{{ $t('journal.load-data') }}
</button>
</div>
</div>
</transition>
<button
class="btn btn--option btn--load-data"
v-if="!scrollNoMoreData && scrollDataLoaded && computedHistoryList.length > 15"
@click="addHistoryData"
>
{{ $t('journal.load-data') }}
</button>
</div>
<!-- </div>
</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 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>
</section>
</template>
<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 ActionButton from '../../components/Global/ActionButton.vue';
@@ -56,8 +63,9 @@ import { URLs } from '../../scripts/utils/apiURLs';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { useStore } from '../../store/store';
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 { JournalTimetableFilter } from '../../types/Journal/JournalTimetablesTypes';
const DISPATCHERS_API_URL = `${URLs.stacjownikAPI}/api/getDispatchers`;
@@ -92,12 +100,13 @@ export default defineComponent({
}),
setup() {
const sorterActive = ref({ id: 'timestampFrom', dir: -1 });
const sorterActive: JournalDispatcherSorter = reactive({ id: 'timestampFrom', dir: -1 });
const journalFilterActive = ref({});
const searchersValues = reactive({
'search-dispatcher': '',
'search-station': '',
'search-date': '',
} as JournalDispatcherSearcher);
const countFromIndex = ref(0);
@@ -135,42 +144,36 @@ export default defineComponent({
if (this.sceneryName || this.dispatcherName) {
this.searchersValues['search-station'] = this.sceneryName?.toString() || '';
this.searchersValues['search-dispatcher'] = this.dispatcherName?.toString() || '';
this.search();
this.searchHistory();
}
window.addEventListener('scroll', this.handleScroll);
},
mounted() {
if (!this.sceneryName && !this.dispatcherName) {
this.search();
this.searchHistory();
}
},
deactivated() {
window.removeEventListener('scroll', this.handleScroll);
},
methods: {
closeDispatcherStatsCard() {
this.statsCardOpen = false;
handleScroll(e: Event) {
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() {
this.showReturnButton = window.scrollY > window.innerHeight;
resetOptions() {
this.searchersValues['search-station'] = '';
this.searchersValues['search-dispatcher'] = '';
this.sorterActive.id = 'timestampFrom';
const element = this.$refs.scrollElement as HTMLElement;
if (
element.getBoundingClientRect().bottom * 0.85 < window.innerHeight &&
this.scrollDataLoaded &&
!this.scrollNoMoreData &&
this.dataStatus == DataStatus.Loaded
)
this.addHistoryData();
this.searchHistory();
},
search() {
searchHistory() {
this.fetchHistoryData({
searchers: this.searchersValues,
});
@@ -202,21 +205,22 @@ export default defineComponent({
async fetchHistoryData(
props: {
searchers?: JournalDispatcherSearcher;
filter?: JournalFilter;
filter?: JournalTimetableFilter;
} = {}
) {
this.dataStatus = DataStatus.Loading;
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 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 (station) queries.push(`stationName=${station}`);
if (timestampFrom && timestampTo) queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
// Z API: const SORT_TYPES = ['allStopsCount', 'endDate', 'beginDate', 'routeDistance'];
if (this.sorterActive.id == 'timestampFrom') queries.push('sortBy=timestampFrom');
@@ -1,41 +1,42 @@
<template>
<ul class="journal-list">
<transition-group name="journal-list-anim">
<li v-for="(doc, i) in dispatcherHistory" :key="doc.id">
<div class="journal_day" v-if="isAnotherDay(i - 1, i)">
<span>{{ new Date(doc.timestampFrom).toLocaleDateString('pl-PL') }}</span>
</div>
<!-- <transition-group name="journal-list-anim"> -->
<li v-for="item in computedDispatcherHistory" :class="{ sticky: typeof item == 'string' }">
<div v-if="typeof item == 'string'" class="journal_day">
{{ item }}
</div>
<div
class="journal_item"
:class="{ online: doc.isOnline }"
@click="navigateToScenery(doc.stationName, doc.isOnline)"
@keydown.enter="navigateToScenery(doc.stationName, doc.isOnline)"
tabindex="0"
>
<div
v-else
class="journal_item"
:class="{ online: item.isOnline }"
@click="navigateToScenery(item.stationName, item.isOnline)"
@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>
<b class="text--primary">{{ doc.dispatcherName }}</b> &bull; <b>{{ doc.stationName }}</b>
<span class="text--grayed">&nbsp;#{{ doc.stationHash }}&nbsp;</span>
<span class="region-badge" :class="doc.region">PL1</span>
{{ new Date(item.timestampFrom).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
</span>
<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="item.currentDuration && item.isOnline"> ({{ calculateDuration(item.currentDuration) }}) </span>
<span v-if="doc.currentDuration && doc.isOnline"> ({{ calculateDuration(doc.currentDuration) }}) </span>
<span v-if="doc.timestampTo">
&gt;
{{ new Date(doc.timestampTo).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
({{ $t('journal.duty-lasted') }} {{ calculateDuration(doc.currentDuration!) }})
</span>
<span v-if="item.timestampTo">
&gt;
{{ new Date(item.timestampTo).toLocaleTimeString('pl-PL', { timeStyle: 'short' }) }}
({{ $t('journal.duty-lasted') }} {{ calculateDuration(item.currentDuration!) }})
</span>
</div>
</li>
</transition-group>
</span>
</div>
</li>
<!-- </transition-group> -->
</ul>
</template>
@@ -54,6 +55,17 @@ export default defineComponent({
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: {
navigateToScenery(name: string, isOnline: boolean) {
if (!isOnline) return;
@@ -87,6 +99,11 @@ export default defineComponent({
}
}
li.sticky {
position: sticky;
top: 0;
}
.journal_item {
display: flex;
justify-content: space-between;
@@ -108,36 +125,19 @@ export default defineComponent({
}
.journal_day {
position: relative;
text-align: center;
background-color: #4d4d4d;
margin: 1em 0;
padding: 0.5em;
font-weight: bold;
margin-top: 1em;
background-color: #333;
span {
position: relative;
background-color: #4d4d4d;
background-color: inherit;
z-index: 10;
padding-right: 1em;
padding: 0 0.5em;
}
&::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;
font-weight: bold;
}
}
+241 -133
View File
@@ -1,81 +1,100 @@
<template>
<div class="journal-options">
<div class="options_wrapper">
<div class="options_content">
<div class="content_select">
<select-box
:itemList="translatedSorterOptions"
:defaultItemIndex="0"
@selected="onSorterChange"
:prefix="$t('journal.sort-prefix')"
/>
</div>
<div class="bg" v-if="showOptions" @click="showOptions = false"></div>
<div class="content_search">
<div class="search-box" v-for="(value, propName) in searchersValues" :key="propName">
<input
class="search-input"
:placeholder="$t(`journal.${propName}`)"
v-model="searchersValues[propName]"
@keydown.enter="onInputSearch"
/>
<button class="btn--image" @click="showOptions = !showOptions">
<img :src="getIcon('filter2')" alt="Open filters" />
{{ $t('options.filters') }}
</button>
<button class="search-exit">
<img :src="getIcon('exit')" alt="exit-icon" @click="onInputClear(propName)" />
<transition name="options-anim">
<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>
</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 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 class="options_filters">
<button
v-for="filter in filters"
class="journal-filter-option btn--option"
:class="{ checked: journalFilterActive.id === filter.id }"
:id="filter.id"
@click="onFilterChange(filter)"
>
{{ $t(`journal.filter-${filter.id}`) }}
</button>
<!-- <div class="data-status">
<span v-if="dataStatus == DataStatus.Loading"> Pobieranie danych...</span>
<span v-if="dataStatus == DataStatus.Loaded"> Pobrano dane </span>
</div> -->
</div>
</div>
</transition>
</div>
</template>
<script lang="ts">
import { defineComponent, inject, JournalFilter, PropType } from 'vue';
import { defineComponent, inject, Prop, PropType } from 'vue';
import imageMixin from '../../mixins/imageMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { JournalTimetableFilter } from '../../types/Journal/JournalTimetablesTypes';
import ActionButton from '../Global/ActionButton.vue';
import SelectBox from '../Global/SelectBox.vue';
export default defineComponent({
components: { SelectBox, ActionButton },
emits: ['onSorterChange', 'onInputChange', 'onFilterChange'],
emits: ['onSearchConfirm', 'onOptionsReset'],
mixins: [imageMixin],
props: {
@@ -85,16 +104,28 @@ export default defineComponent({
},
filters: {
type: Array as PropType<JournalFilter[]>,
type: Array as PropType<JournalTimetableFilter[]>,
default: [],
},
dataStatus: {
type: Number as PropType<DataStatus>,
default: DataStatus.Initialized,
},
},
data() {
return {
showOptions: false,
DataStatus,
};
},
setup() {
return {
searchersValues: inject('searchersValues') as { [key: string]: string },
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() {
return this.$props.sorterOptionIds.map((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 }) {
this.sorterActive.id = item.id;
this.sorterActive.dir = -1;
this.$emit('onSorterChange');
this.$emit('onSearchConfirm');
},
onFilterChange(filter: JournalFilter) {
onFilterChange(filter: JournalTimetableFilter) {
this.journalFilterActive = filter;
this.$emit('onFilterChange');
},
onInputSearch() {
this.$emit('onInputChange');
this.$emit('onSearchConfirm');
},
onInputClear(id: any) {
this.searchersValues[id] = '';
this.onInputSearch();
this.$emit('onSearchConfirm');
},
onSearchConfirm() {
this.$emit('onSearchConfirm');
},
onResetButtonClick() {
this.$emit('onOptionsReset');
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive';
@import '../../styles/option.scss';
@import '../../styles/responsive.scss';
@import '../../styles/search_box.scss';
@import '../../styles/variables.scss';
.options {
&_wrapper {
display: flex;
flex-direction: column;
.options-anim {
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateY(10px);
}
&_content {
display: flex;
flex-wrap: wrap;
&-enter-active,
&-leave-active {
transition: all 150ms ease;
}
}
.content_search,
.content_select {
display: flex;
align-items: center;
flex-wrap: wrap;
.bg {
position: fixed;
top: 0;
left: 0;
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 {
display: flex;
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;
}
}
&#fulfilled {
color: lightgreen;
}
&#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() {
.journal-options {
width: 100%;
h1 {
text-align: center;
&::before {
width: 75%;
left: 50%;
transform: translateX(-50%);
}
}
.options {
&_wrapper {
justify-content: center;
align-items: center;
}
.options_wrapper {
max-width: 100%;
}
&_content {
padding: 0 1em;
.btn--image {
margin: 0 auto;
}
flex-direction: column;
.filter-option,
.sort-option {
margin: 0.25em 0.25em;
}
.content_select {
margin: 0 auto;
padding: 0;
}
.content_search {
justify-content: center;
}
}
&_filters {
justify-content: center;
.journal-filter-option {
margin: 0.25em 0.25em;
}
}
.options_filters,
.options_sorters {
justify-content: center;
}
}
</style>
</style>
@@ -6,54 +6,58 @@
<div class="journal_wrapper">
<JournalOptions
@on-input-change="searchHistory"
@on-filter-change="searchHistory"
@on-sorter-change="searchHistory"
@on-search-confirm="searchHistory"
@on-options-reset="resetOptions"
:sorter-option-ids="['timetableId', 'beginDate', 'distance', 'total-stops']"
:filters="journalTimetableFilters"
:data-status="dataStatus"
/>
<div class="timetables_wrapper" ref="scrollElement">
<transition name="warning" mode="out-in">
<div :key="dataStatus">
<Loading v-if="dataStatus == (DataStatus.Loading || DataStatus.Initialized)" />
<div class="list_wrapper" @scroll="handleScroll">
<!-- <transition name="warning" mode="out-in"> -->
<!-- <div :key="dataStatus"> -->
<Loading
v-if="
dataStatus == DataStatus.Initialized || (dataStatus == DataStatus.Loading && timetableHistory.length == 0)
"
/>
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
{{ $t('app.error') }}
</div>
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
{{ $t('app.error') }}
</div>
<div v-else-if="timetableHistory.length == 0" class="journal_warning">
{{ $t('app.no-result') }}
</div>
<div v-else-if="timetableHistory.length == 0 && dataStatus != DataStatus.Loading" class="journal_warning">
{{ $t('app.no-result') }}
</div>
<div v-else>
<JournalTimetablesList :timetableHistory="timetableHistory" />
<div v-else>
<JournalTimetablesList :timetableHistory="timetableHistory" />
<button
class="btn btn--option btn--load-data"
v-if="!scrollNoMoreData && scrollDataLoaded"
@click="addHistoryData"
>
{{ $t('journal.load-data') }}
</button>
</div>
</div>
</transition>
<button
class="btn btn--option btn--load-data"
v-if="!scrollNoMoreData && scrollDataLoaded && timetableHistory.length >= 15"
@click="addHistoryData"
>
{{ $t('journal.load-data') }}
</button>
</div>
<!-- </div> -->
<!-- </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 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>
</section>
</template>
<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 DriverStats from './DriverStats.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 routerMixin from '../../mixins/routerMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
@@ -62,10 +66,11 @@ import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData
import { URLs } from '../../scripts/utils/apiURLs';
import { useStore } from '../../store/store';
import JournalOptions from './JournalOptions.vue';
import { JournalTimetableSearcher } from '../../types/JournalTimetablesTypes';
import { JorunalTimetableSearchType } from '../../types/Journal/JournalTimetablesTypes';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import imageMixin from '../../mixins/imageMixin';
import JournalTimetablesList from './JournalTimetablesList.vue';
import { journalTimetableFilters } from '../../constants/Journal/JournalTimetablesConsts';
const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`;
@@ -99,13 +104,14 @@ export default defineComponent({
}),
setup() {
const sorterActive = ref({ id: 'timetableId', dir: -1 });
const sorterActive: JournalTimetableSorter = reactive({ id: 'timetableId', dir: 1 });
const journalFilterActive = ref(journalTimetableFilters[0]);
const searchersValues = reactive({
'search-train': '',
'search-driver': '',
} as JournalTimetableSearcher);
'search-date': '',
} as JorunalTimetableSearchType);
const countFromIndex = ref(0);
const countLimit = 15;
@@ -130,35 +136,36 @@ export default defineComponent({
},
activated() {
window.addEventListener('scroll', this.handleScroll);
if (this.timetableId) {
this.searchersValues['search-train'] = `#${this.timetableId}`;
this.searchHistory();
}
},
deactivated() {
window.removeEventListener('scroll', this.handleScroll);
},
mounted() {
if (!this.timetableId) this.searchHistory();
},
methods: {
handleScroll() {
this.showReturnButton = window.scrollY > window.innerHeight;
handleScroll(e: Event) {
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 (
element.getBoundingClientRect().bottom * 0.85 < window.innerHeight &&
this.scrollDataLoaded &&
!this.scrollNoMoreData &&
this.dataStatus == DataStatus.Loaded
)
this.addHistoryData();
if (scrollTop > elementHeight * 0.85) this.addHistoryData();
},
resetOptions() {
this.searchersValues['search-date'] = '';
this.searchersValues['search-driver'] = '';
this.searchersValues['search-train'] = '';
this.journalFilterActive = this.journalTimetableFilters[0];
this.sorterActive.id = 'timetableId';
this.searchHistory();
},
searchHistory() {
@@ -193,8 +200,8 @@ export default defineComponent({
async fetchHistoryData(
props: {
searchers?: JournalTimetableSearcher;
filter?: JournalFilter;
searchers?: JorunalTimetableSearchType;
filter?: JournalTimetableFilter;
} = {}
) {
this.dataStatus = DataStatus.Loading;
@@ -204,8 +211,13 @@ export default defineComponent({
const driver = props.searchers?.['search-driver'].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 (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'];
if (this.sorterActive.id == 'distance') queries.push('sortBy=routeDistance');
@@ -257,8 +269,6 @@ export default defineComponent({
: '';
this.dataStatus = DataStatus.Loaded;
console.log(this.dataStatus);
} catch (error) {
this.dataStatus = DataStatus.Error;
this.dataErrorMessage = 'Ups! Coś poszło nie tak!';
@@ -91,7 +91,7 @@
<img :src="getIcon(`arrow-${item.showStock.value ? 'asc' : 'desc'}`)" alt="Arrow" />
</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 />
<div>
@@ -1,8 +1,8 @@
<template>
<section class="filter-card" v-click-outside="closeCard">
<div class="card_btn">
<button class="btn btn--option" @click="toggleCard">
<img class="button_icon" :src="getIcon('filter2')" alt="icon-filter" />
<button class="btn--image" @click="toggleCard">
<img class="button_icon" :src="getIcon('filter2')" alt="filter icon" />
{{ $t('options.filters') }}
</button>
</div>
@@ -91,7 +91,6 @@
</template>
<script lang="ts">
import { defineComponent, inject } from 'vue';
import inputData from '../../data/options.json';
import imageMixin from '../../mixins/imageMixin';
@@ -107,7 +106,6 @@ export default defineComponent({
mixins: [imageMixin],
data: () => ({
inputs: { ...inputData },
saveOptions: false,
STORAGE_KEY: 'options_saved',
@@ -263,6 +261,7 @@ export default defineComponent({
}
}
.card {
&_btn {
button {
@@ -389,6 +388,7 @@ export default defineComponent({
input {
width: 100%;
padding: 0.5em;
border: 1px solid white;
}
}
@@ -437,6 +437,7 @@ export default defineComponent({
&::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
height: 20px;
width: 20px;
+218 -163
View File
@@ -1,230 +1,285 @@
<template>
<div class="train-options">
<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="bg" v-if="showOptions" @click="showOptions = false"></div>
<div class="content_search">
<div class="search-box">
<input class="search-input" v-model="searchedTrain" :placeholder="$t('trains.search-train')" />
<button class="btn--open" @click="showOptions = !showOptions">
<img :src="getIcon('filter2')" alt="Open filters" />
{{ $t('options.filters') }}
</button>
<button class="search-exit">
<img :src="getIcon('exit')" alt="exit-icon" @click="() => (searchedTrain = '')" />
</button>
<transition name="options-anim">
<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>
<div class="search-box">
<input class="search-input" v-model="searchedDriver" :placeholder="$t('trains.search-driver')" />
<button class="search-exit">
<img :src="getIcon('exit')" alt="exit-icon" @click="() => (searchedDriver = '')" />
</button>
<h1 v-if="trainFilterList.length != 0">{{ $t('options.filter-title') }}</h1>
<div class="options_filters">
<div class="filter-option" v-for="filter in trainFilterList">
<button class="btn--option" :data-disabled="!filter.isActive" @click="onFilterChange(filter)">
{{ $t(`options.filter-${filter.id}`) }}
</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 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>
</transition>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, inject, TrainFilter } from 'vue';
import { useI18n } from 'vue-i18n';
import { defineComponent, inject, PropType } from 'vue';
import imageMixin from '../../mixins/imageMixin';
import { TrainFilter } from '../../types/Trains/TrainOptionsTypes';
import ActionButton from '../Global/ActionButton.vue';
import SelectBox from '../Global/SelectBox.vue';
export default defineComponent({
components: { SelectBox },
emits: ['changeSearchedTrain', 'changeSearchedDriver', 'changeSorter'],
components: { SelectBox, ActionButton },
mixins: [imageMixin],
setup() {
const { t } = useI18n();
const sorterOptions = [
{
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}`),
}))
);
props: {
sorterOptionIds: {
type: Array as PropType<Array<string>>,
required: true,
},
},
data() {
return {
translatedSorterOptions,
searchedTrain: inject('searchedTrain') as string,
searchedDriver: inject('searchedDriver') as string,
sorterActive: inject('sorterActive') as { id: string | number; dir: number },
filterList,
showOptions: false,
};
},
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: {
changeSorter(item: { id: string | number; value: string }) {
onSorterChange(item: { id: string | number; value: string }) {
this.sorterActive.id = item.id;
this.sorterActive.dir = -1;
},
toggleFilter(filter: TrainFilter) {
onFilterChange(filter: TrainFilter) {
filter.isActive = !filter.isActive;
},
setFilterOnly(filter: TrainFilter) {
this.filterList.forEach((f) => (f.isActive = f.id == filter.id));
clearAllFilters() {
this.trainFilterList.forEach((filter) => {
filter.isActive = false;
});
},
resetFilters() {
this.filterList.forEach((f) => (f.isActive = true));
this.searchedDriver = '';
this.searchedTrain = '';
resetAllFilters() {
this.trainFilterList.forEach((filter) => {
filter.isActive = true;
});
},
onInputClear(id: 'driver' | 'train') {
if (id == 'driver') this.searchedDriver = '';
if (id == 'train') this.searchedTrain = '';
},
test(e: Event) {
console.log(e.target);
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive';
@import '../../styles/responsive.scss';
@import '../../styles/search_box.scss';
@import '../../styles/variables.scss';
.train-options {
@include smallScreen() {
width: 100%;
font-size: 1.25em;
.options-anim {
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateY(10px);
}
&-enter-active,
&-leave-active {
transition: all 150ms ease;
}
}
.options {
&_wrapper {
display: flex;
}
.bg {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
&_content {
display: flex;
flex-wrap: wrap;
.content_search,
.content_select {
display: flex;
align-items: center;
flex-wrap: wrap;
padding: 0.25em 0.25em 0 0;
}
}
z-index: 10;
}
.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;
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;
margin-top: 0.5em;
@include smallScreen() {
justify-content: center;
}
padding: 0.25em 0.25em 0 0;
}
.filter {
background: #333;
padding: 0.2em 0.25em;
margin: 0.25em 0.25em 0 0;
.content_search > div {
margin: 0.5em auto;
}
.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;
}
cursor: pointer;
color: gray;
.filter-option {
button {
color: white;
font-weight: bold;
&.active {
color: var(--clr-primary);
}
&.reset-btn {
color: salmon;
&[data-disabled='true'] {
color: #888;
}
}
}
@include smallScreen() {
.journal-options {
width: 100%;
h1 {
text-align: center;
&::before {
width: 75%;
left: 50%;
transform: translateX(-50%);
}
}
.options {
&_wrapper {
justify-content: center;
}
.options_wrapper {
max-width: 100%;
}
&_content {
padding: 0 1em;
.btn--open {
margin: 0 auto;
}
flex-direction: column;
.content_select {
margin: 0 auto;
padding: 0;
}
.content_search {
justify-content: center;
}
}
.options_filters,
.options_sorters {
justify-content: center;
}
}
</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>
+2 -5
View File
@@ -118,11 +118,10 @@ export default defineComponent({
text-align: center;
padding: 1em 0;
margin: 1em 0;
font-size: 1.5em;
background: #333;
background: #1a1a1a;
}
img.train-image {
@@ -139,8 +138,6 @@ img.train-image {
&-list {
overflow: auto;
margin-top: 1em;
@include smallScreen() {
width: 100%;
}
@@ -197,4 +194,4 @@ img.train-image {
text-align: center;
}
}
</style>
</style>
@@ -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,60 +1,60 @@
import { TrainFilter } from "vue";
import { TrainFilterType } from "../scripts/enums/TrainFilterType";
export const trainFilters: TrainFilter[] = [
{
id: TrainFilterType.twr,
isActive: true,
},
{
id: TrainFilterType.skr,
isActive: true,
},
{
id: TrainFilterType.passenger,
isActive: true,
},
{
id: TrainFilterType.freight,
isActive: true,
},
{
id: TrainFilterType.other,
isActive: true,
},
{
id: TrainFilterType.comments,
isActive: true,
},
{
id: TrainFilterType.noTimetable,
isActive: true,
},
];
export const sorterOptions = [
{
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ść',
}
];
import { TrainFilterType } from '../../scripts/enums/TrainFilterType';
import { TrainFilter } from '../../types/Trains/TrainOptionsTypes';
export const trainFilters: TrainFilter[] = [
{
id: TrainFilterType.twr,
isActive: true,
},
{
id: TrainFilterType.skr,
isActive: true,
},
{
id: TrainFilterType.passenger,
isActive: true,
},
{
id: TrainFilterType.freight,
isActive: true,
},
{
id: TrainFilterType.other,
isActive: true,
},
{
id: TrainFilterType.comments,
isActive: true,
},
{
id: TrainFilterType.noTimetable,
isActive: true,
},
];
export const sorterOptions = [
{
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ść',
},
];
-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!"
},
"update": {
"title": "New Stacjownik version is available!",
"paragraph1": "Enjoy the application and may the green signal be with you!",
"release-link": "Click here to browse version changelog (GitHub)",
"confirm-button": "Understood!"
"title": "New Stacjownik version is available!",
"paragraph1": "Enjoy the application and may the green signal be with you!",
"release-link": "Click here to browse version changelog (GitHub)",
"confirm-button": "Understood!"
},
"data-status": {
"S1a-connection": "<b>S1a signal</b> <br> Cannot connect with Stacjownik API service!",
@@ -72,7 +72,51 @@
},
"options": {
"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": {
"endingStatus": "ENDS SOON",
@@ -116,7 +160,7 @@
"hour": "h",
"no-limit": "NO LIMIT",
"include-selected": "INCLUDE SELECTED",
"save": "SAVE FILTERS",
"save": "SAVE FILTERS",
"reset": "RESET FILTERS",
"close": "CLOSE FILTERS"
},
@@ -150,28 +194,6 @@
"current-signal": "at signal",
"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: ",
"preponed": "Ahead of schedule: ",
"on-time": "On time",
@@ -205,26 +227,6 @@
"section-timetables": "TIMETABLES",
"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",
"loading-further-data": "Loading...",
+46 -43
View File
@@ -74,7 +74,52 @@
},
"options": {
"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": {
"endingStatus": "KOŃCZY",
@@ -152,28 +197,6 @@
"current-signal": "przy semaforze",
"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: ",
"preponed": "Przed czasem: ",
"on-time": "Planowo",
@@ -207,26 +230,6 @@
"section-timetables": "ROZKŁADY JAZDY",
"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",
"loading-further-data": "Ładowanie...",
+114 -114
View File
@@ -1,115 +1,115 @@
import { TrainFilter } from "vue";
import { TrainFilterType } from "../enums/TrainFilterType";
import Train from "../interfaces/Train";
import TrainStop from "../interfaces/TrainStop";
function confirmedPercentage(stops: TrainStop[] | undefined) {
if (!stops) return -1;
return Number(((stops.filter((stop) => stop.confirmed).length / stops.length) * 100).toFixed(0));
};
function currentDelay(stops: TrainStop[] | undefined) {
if (!stops) return -Infinity;
const delay =
stops.find((stop, i) => (i == 0 && !stop.confirmed) || (i > 0 && stops[i - 1].confirmed && !stop.confirmed))
?.departureDelay || 0;
return delay;
};
function filterTrainList(trainList: Train[], searchedTrain: string, searchedDriver: string, filters: TrainFilter[]) {
return trainList.filter(
(train) => {
const isFiltered = filters.every(f => {
if (f.isActive) return true;
if (!train.timetableData) return filters.find(filter => filter.id == TrainFilterType.noTimetable)!.isActive;
switch (f.id) {
case TrainFilterType.comments:
return !train.timetableData.followingStops.some(stop => stop.comments);
case TrainFilterType.twr:
return !train.timetableData.TWR;
case TrainFilterType.skr:
return !train.timetableData.SKR;
case TrainFilterType.passenger:
return !/^[AMRE]\D{2}$/.test(train.timetableData.category);
case TrainFilterType.freight:
return !train.timetableData.category.startsWith('T');
case TrainFilterType.other:
return !/^[PXZL]\D{2}$/.test(train.timetableData.category);
default:
return true;
}
})
return (searchedTrain.length > 0 ? train.trainNo.toString().startsWith(searchedTrain) : true) &&
(searchedDriver.length > 0 ? train.driverName.toLowerCase().startsWith(searchedDriver.toLowerCase()) : true) && isFiltered
}
);
}
function sortTrainList(trainList: Train[], sorterActive: { id: string; dir: number }) {
return trainList.sort((a: Train, b: Train) => {
switch (sorterActive.id) {
case 'mass':
if (a.mass > b.mass) return sorterActive.dir;
return -sorterActive.dir;
case 'distance':
if ((a.timetableData?.routeDistance || -1) > (b.timetableData?.routeDistance || -1)) return sorterActive.dir;
return -sorterActive.dir;
case 'progress':
if (confirmedPercentage(a.timetableData?.followingStops) > confirmedPercentage(b.timetableData?.followingStops))
return sorterActive.dir;
return -sorterActive.dir;
case 'delay':
if (currentDelay(a.timetableData?.followingStops) > currentDelay(b.timetableData?.followingStops))
return sorterActive.dir;
return -sorterActive.dir;
case 'speed':
if (a.speed > b.speed) return sorterActive.dir;
return -sorterActive.dir;
case 'timetable':
if (a.trainNo > b.trainNo) return sorterActive.dir;
return -sorterActive.dir;
case 'length':
if (a.length > b.length) return sorterActive.dir;
return -sorterActive.dir;
default:
break;
}
return 0;
});
}
export function filteredTrainList(
trainList: Train[],
searchedTrain: string,
searchedDriver: string,
sorterActive: { id: string; dir: number },
filters: TrainFilter[]
) {
const filtered = filterTrainList(trainList, searchedTrain, searchedDriver, filters);
return [...sortTrainList(filtered, sorterActive)];
import { TrainFilter } from "../../types/Trains/TrainOptionsTypes";
import { TrainFilterType } from "../enums/TrainFilterType";
import Train from "../interfaces/Train";
import TrainStop from "../interfaces/TrainStop";
function confirmedPercentage(stops: TrainStop[] | undefined) {
if (!stops) return -1;
return Number(((stops.filter((stop) => stop.confirmed).length / stops.length) * 100).toFixed(0));
};
function currentDelay(stops: TrainStop[] | undefined) {
if (!stops) return -Infinity;
const delay =
stops.find((stop, i) => (i == 0 && !stop.confirmed) || (i > 0 && stops[i - 1].confirmed && !stop.confirmed))
?.departureDelay || 0;
return delay;
};
function filterTrainList(trainList: Train[], searchedTrain: string, searchedDriver: string, filters: TrainFilter[]) {
return trainList.filter(
(train) => {
const isFiltered = filters.every(f => {
if (f.isActive) return true;
if (!train.timetableData) return filters.find(filter => filter.id == TrainFilterType.noTimetable)!.isActive;
switch (f.id) {
case TrainFilterType.comments:
return !train.timetableData.followingStops.some(stop => stop.comments);
case TrainFilterType.twr:
return !train.timetableData.TWR;
case TrainFilterType.skr:
return !train.timetableData.SKR;
case TrainFilterType.passenger:
return !/^[AMRE]\D{2}$/.test(train.timetableData.category);
case TrainFilterType.freight:
return !train.timetableData.category.startsWith('T');
case TrainFilterType.other:
return !/^[PXZL]\D{2}$/.test(train.timetableData.category);
default:
return true;
}
})
return (searchedTrain.length > 0 ? train.trainNo.toString().startsWith(searchedTrain) : true) &&
(searchedDriver.length > 0 ? train.driverName.toLowerCase().startsWith(searchedDriver.toLowerCase()) : true) && isFiltered
}
);
}
function sortTrainList(trainList: Train[], sorterActive: { id: string; dir: number }) {
return trainList.sort((a: Train, b: Train) => {
switch (sorterActive.id) {
case 'mass':
if (a.mass > b.mass) return sorterActive.dir;
return -sorterActive.dir;
case 'distance':
if ((a.timetableData?.routeDistance || -1) > (b.timetableData?.routeDistance || -1)) return sorterActive.dir;
return -sorterActive.dir;
case 'progress':
if (confirmedPercentage(a.timetableData?.followingStops) > confirmedPercentage(b.timetableData?.followingStops))
return sorterActive.dir;
return -sorterActive.dir;
case 'delay':
if (currentDelay(a.timetableData?.followingStops) > currentDelay(b.timetableData?.followingStops))
return sorterActive.dir;
return -sorterActive.dir;
case 'speed':
if (a.speed > b.speed) return sorterActive.dir;
return -sorterActive.dir;
case 'timetable':
if (a.trainNo > b.trainNo) return sorterActive.dir;
return -sorterActive.dir;
case 'length':
if (a.length > b.length) return sorterActive.dir;
return -sorterActive.dir;
default:
break;
}
return 0;
});
}
export function filteredTrainList(
trainList: Train[],
searchedTrain: string,
searchedDriver: string,
sorterActive: { id: string; dir: number },
filters: TrainFilter[]
) {
const filtered = filterTrainList(trainList, searchedTrain, searchedDriver, filters);
return [...sortTrainList(filtered, sorterActive)];
};
+18 -4
View File
@@ -8,16 +8,24 @@
}
&-enter-active {
transition: all 150ms ease-out;
transition: all 150ms 100ms ease-out;
}
&-leave-active {
transition: all 150ms ease-out;
transition: all 150ms 100ms ease-out;
}
}
//Styles
.list_wrapper {
overflow-y: scroll;
height: 90vh;
min-height: 550px;
padding-right: 0.2em;
}
.journal_wrapper {
max-width: 1350px;
width: 100%;
@@ -40,9 +48,9 @@
.journal_item,
.journal_warning {
background: #202020;
background-color: #1a1a1a;
padding: 1em;
margin: 1em 0;
margin-bottom: 1em;
}
.journal_top-bar {
@@ -69,3 +77,9 @@
flex-wrap: wrap;
}
}
@media (orientation: landscape) {
.journal_wrapper {
font-size: 1em;
}
}
+40 -29
View File
@@ -12,6 +12,24 @@
--clr-error: #df3e3e;
--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 {
@@ -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 {
position: relative;
@@ -113,7 +109,6 @@ select {
}
input {
border: 1px solid white;
background: none;
color: white;
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 {
background: none;
cursor: pointer;
@@ -211,8 +216,18 @@ ul {
}
&--image {
color: white;
transition: color 0.3s;
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;
}
}
&--option {
@@ -224,10 +239,6 @@ ul {
border-radius: 0.25em;
padding: 0.25em 0.5em;
&:hover:not(:disabled) {
background-color: #3c3c3c;
}
&.checked {
color: var(--clr-primary);
font-weight: bold;
+11 -7
View File
@@ -1,6 +1,12 @@
@import 'responsive.scss';
.search {
label {
display: block;
color: #ccc;
margin-bottom: 0.25em;
}
&-box {
position: relative;
@@ -9,7 +15,6 @@
border-radius: 0.5em;
min-width: 200px;
margin-right: 0.25em;
background-color: #333;
}
&-input {
@@ -18,7 +23,6 @@
background-color: #333;
padding: 0.35em 0.5em;
margin-right: 0.2em;
width: 100%;
}
@@ -33,6 +37,11 @@
}
}
&-button {
width: 80%;
max-width: 300px;
}
@include smallScreen {
&-box,
&-button {
@@ -42,10 +51,5 @@
&-box {
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">
<div class="wrapper">
<div class="options-bar">
<train-options />
<TrainOptions :sorter-option-ids="['distance', 'progress', 'delay', 'mass', 'speed', 'length']" />
</div>
<TrainTable :trains="computedTrains" />
@@ -11,14 +11,15 @@
</template>
<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 TrainStats from '../components/TrainsView/TrainStats.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 { filteredTrainList } from '../scripts/managers/trainFilterManager';
import { useStore } from '../store/store';
import { TrainFilter } from '../types/Trains/TrainOptionsTypes';
export default defineComponent({
components: {
@@ -48,7 +49,6 @@ export default defineComponent({
const sorterActive = ref({ id: 'distance', dir: -1 });
const filterList = reactive([...trainFilters]) as TrainFilter[];
const isTrainOptionsCardVisible = ref(false);
const searchedDriver = ref('');
const searchedTrain = ref('');
@@ -57,7 +57,6 @@ export default defineComponent({
provide('searchedDriver', searchedDriver);
provide('sorterActive', sorterActive);
provide('filterList', filterList);
provide('isTrainOptionsCardVisible', isTrainOptionsCardVisible);
const computedTrains: ComputedRef<Train[]> = computed(() => {
return filteredTrainList(
@@ -82,10 +81,6 @@ export default defineComponent({
this.searchedTrain = this.train;
this.searchedDriver = this.driver || '';
}
// if (this.train) {
// this.searchedTrain = this.train;
// if(this.x) this.searchedDriver = this.x;
// }
},
});
</script>
@@ -102,5 +97,4 @@ export default defineComponent({
margin: 1rem auto;
max-width: 1350px;
}
</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;
}
}