refactor(journal): fetching heavy timetable details separately on demand

This commit is contained in:
2026-02-16 02:16:22 +01:00
parent dcef8cdac8
commit 329c85b858
7 changed files with 296 additions and 313 deletions
@@ -19,84 +19,86 @@
<div class="details-body" v-if="showExtraInfo"> <div class="details-body" v-if="showExtraInfo">
<div class="g-separator"></div> <div class="g-separator"></div>
<EntryStops :timetable="timetable" /> <div v-if="timetableDetails">
<EntryStops :timetable="timetableDetails" />
<div class="g-separator"></div> <div class="g-separator"></div>
<div class="timetable-specs"> <div class="timetable-specs">
<span class="badge specs-badge" v-if="timetable.authorName"> <span class="badge specs-badge" v-if="timetableDetails.authorName">
<span>{{ $t('journal.dispatcher-name') }}</span> <span>{{ $t('journal.dispatcher-name') }}</span>
<span>{{ timetable.authorName }}</span> <span>{{ timetableDetails.authorName }}</span>
</span> </span>
<span class="badge specs-badge" v-if="timetable.trainMaxSpeed"> <span class="badge specs-badge" v-if="timetableDetails.trainMaxSpeed">
<span>{{ $t('journal.stock-timetable-speed') }}</span> <span>{{ $t('journal.stock-timetable-speed') }}</span>
<span> {{ timetable.trainMaxSpeed }}km/h </span> <span> {{ timetableDetails.trainMaxSpeed }}km/h </span>
</span> </span>
<span class="badge specs-badge" v-if="timetable.maxSpeed"> <span class="badge specs-badge" v-if="timetableDetails.maxSpeed">
<span>{{ $t('journal.stock-max-speed') }}</span> <span>{{ $t('journal.stock-max-speed') }}</span>
<span>{{ timetable.maxSpeed }}km/h</span> <span>{{ timetableDetails.maxSpeed }}km/h</span>
</span> </span>
</div> </div>
<div class="stock-dangers" v-if="timetable.warningNotes"> <div class="stock-dangers" v-if="timetableDetails.warningNotes">
<div class="g-separator"></div> <div class="g-separator"></div>
<b>{{ $t('journal.stock-dangers') }}:</b> <b>{{ $t('journal.stock-dangers') }}:</b>
<ul> <ul>
<li v-if="timetable.twr"> <li v-if="timetableDetails.twr">
<b class="text--primary">{{ $t('warnings.TWR') }} (TWR)</b> <b class="text--primary">{{ $t('warnings.TWR') }} (TWR)</b>
</li> </li>
<li v-if="timetable.skr"> <li v-if="timetableDetails.skr">
<b class="text--primary">{{ $t('warnings.SKR') }}</b> <b class="text--primary">{{ $t('warnings.SKR') }}</b>
</li> </li>
<li v-if="timetable.hasDangerousCargo"> <li v-if="timetableDetails.hasDangerousCargo">
<b class="text--primary">{{ $t('warnings.TN') }}</b> <b class="text--primary">{{ $t('warnings.TN') }}</b>
</li> </li>
<li v-if="timetable.hasExtraDeliveries"> <li v-if="timetableDetails.hasExtraDeliveries">
<b class="text--primary">{{ $t('warnings.PN') }}</b> <b class="text--primary">{{ $t('warnings.PN') }}</b>
</li> </li>
</ul> </ul>
<div class="dangers-notes" v-if="timetable.warningNotes"> <div class="dangers-notes" v-if="timetableDetails.warningNotes">
<h4>{{ $t('warnings.header-title') }}</h4> <h4>{{ $t('warnings.header-title') }}</h4>
<p> <p>
<i>{{ timetable.warningNotes }}</i> <i>{{ timetableDetails.warningNotes }}</i>
</p> </p>
</div> </div>
</div> </div>
<!-- Historia zmian w składzie --> <!-- Historia zmian w składzie -->
<div v-if="timetable.stockString || stockHistory.length != 0"> <div v-if="timetableDetails.stockString || stockHistory.length != 0">
<div class="g-separator"></div> <div class="g-separator"></div>
<b>{{ $t('journal.stock-preview') }}:</b> <b>{{ $t('journal.stock-preview') }}:</b>
<div class="stock-specs" style="margin-top: 0.5em"> <div class="stock-specs" style="margin-top: 0.5em">
<span class="badge specs-badge" v-if="timetable.stockLength"> <span class="badge specs-badge" v-if="timetableDetails.stockLength">
<span>{{ $t('journal.stock-length') }}</span> <span>{{ $t('journal.stock-length') }}</span>
<span> <span>
{{ {{
currentHistoryIndex == 0 currentHistoryIndex == 0
? timetable.stockLength ? timetableDetails.stockLength
: stockHistory[currentHistoryIndex].stockLength || timetable.stockLength : stockHistory[currentHistoryIndex].stockLength || timetableDetails.stockLength
}}m }}m
</span> </span>
</span> </span>
<span class="badge specs-badge" v-if="timetable.stockMass"> <span class="badge specs-badge" v-if="timetableDetails.stockMass">
<span>{{ $t('journal.stock-mass') }}</span> <span>{{ $t('journal.stock-mass') }}</span>
<span> <span>
{{ {{
Math.floor( Math.floor(
(currentHistoryIndex == 0 (currentHistoryIndex == 0
? timetable.stockMass ? timetableDetails.stockMass
: stockHistory[currentHistoryIndex].stockMass || timetable.stockMass) / 1000 : stockHistory[currentHistoryIndex].stockMass || timetableDetails.stockMass) /
1000
) )
}}t }}t
</span> </span>
@@ -119,11 +121,11 @@
</button> </button>
</div> </div>
<div v-if="timetable.stockString" style="margin-top: 1em"> <div v-if="timetableDetails.stockString" style="margin-top: 1em">
<StockList <StockList
:trainStockList=" :trainStockList="
(currentHistoryIndex == 0 (currentHistoryIndex == 0
? timetable.stockString ? timetableDetails.stockString
: stockHistory[currentHistoryIndex].stockString : stockHistory[currentHistoryIndex].stockString
).split(';') ).split(';')
" "
@@ -132,46 +134,57 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { PropType, defineComponent } from 'vue'; import { computed, defineComponent, onActivated, onMounted, ref, watch } from 'vue';
import StockList from '../../Global/StockList.vue';
import { API } from '../../../typings/api';
import { RouteLocationRaw } from 'vue-router'; import { RouteLocationRaw } from 'vue-router';
import EntryStops from './EntryStops.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
export default defineComponent({ import StockList from '../../Global/StockList.vue';
components: { StockList, EntryStops }, import EntryStops from './EntryStops.vue';
import { API } from '../../../typings/api';
import { useApiStore } from '../../../store/apiStore';
emits: ['toggleExtraInfo'], const i18n = useI18n();
const apiStore = useApiStore();
props: { const props = defineProps({
showExtraInfo: { showExtraInfo: {
type: Boolean, type: Boolean,
required: true required: true
}, },
timetable: {
type: Object as PropType<API.TimetableHistory.Data>, timetableId: {
type: Number,
required: true required: true
} }
}, });
data() {
return { const emits = defineEmits(['toggleExtraInfo']);
currentHistoryIndex: 0, const currentHistoryIndex = ref(0);
i18n: useI18n()
}; const timetableDetails = ref<API.TimetableHistory.Data | null>(null);
},
computed: { watch(
stockHistory() { computed(() => props.showExtraInfo),
return this.timetable.stockHistory (state) => {
if (state == true) {
fetchTimetableDetails();
}
}
);
const stockHistory = computed(() => {
return (
timetableDetails.value?.stockHistory
.slice() .slice()
.reverse() .reverse()
.map((h) => { .map((h) => {
const historyData = h.split('@'); const historyData = h.split('@');
return { return {
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, { updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(i18n.locale.value, {
hour: '2-digit', hour: '2-digit',
minute: '2-digit' minute: '2-digit'
}), }),
@@ -179,49 +192,70 @@ export default defineComponent({
stockMass: Number(historyData[2]) || undefined, stockMass: Number(historyData[2]) || undefined,
stockLength: Number(historyData[3]) || undefined stockLength: Number(historyData[3]) || undefined
}; };
}) ?? []
);
}); });
},
driverRouteLocation(): RouteLocationRaw | null { const driverRouteLocation = computed<RouteLocationRaw | null>(() => {
if (this.timetable.terminated) return null; if (!timetableDetails.value || timetableDetails.value.terminated) return null;
return { return {
name: 'DriverView', name: 'DriverView',
query: { query: {
trainId: `${this.timetable.driverId}|${this.timetable.trainNo}|eu` trainId: `${timetableDetails.value.driverId}|${timetableDetails.value.trainNo}|eu`
} }
}; };
});
async function fetchTimetableDetails() {
try {
const responseData = await apiStore.client!.get<API.TimetableHistory.Response>(
'api/getTimetables',
{
params: {
timetableId: props.timetableId,
returnType: 'detailed'
} }
}, }
methods: { );
onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = '/images/icon-unknown.png';
},
toggleExtraInfo() { if (!responseData || responseData.data.length != 1) {
this.$emit('toggleExtraInfo', this.timetable.id); timetableDetails.value = null;
}, return;
}
timetableDetails.value = responseData.data[0];
} catch (error) {
// this.dataStatus = Status.Data.Error;
console.error(error);
}
}
function toggleExtraInfo() {
emits('toggleExtraInfo', props.timetableId);
}
function copyStockToClipboard() {
if (!timetableDetails.value) return;
copyStockToClipboard() {
const currentStockString = const currentStockString =
this.stockHistory[this.currentHistoryIndex]?.stockString ?? this.timetable.stockString; stockHistory.value[currentHistoryIndex.value]?.stockString ??
timetableDetails.value.stockString;
if (!currentStockString) { if (!currentStockString) {
alert(this.i18n.t('journal.stock-clipboard-failure')); alert(i18n.t('journal.stock-clipboard-failure'));
return; return;
} }
navigator.clipboard navigator.clipboard
.writeText(currentStockString) .writeText(currentStockString)
.then(() => { .then(() => {
prompt(this.i18n.t('journal.stock-clipboard-success'), currentStockString); prompt(i18n.t('journal.stock-clipboard-success'), currentStockString);
}) })
.catch(() => { .catch(() => {
alert(this.i18n.t('journal.stock-clipboard-failure')); alert(i18n.t('journal.stock-clipboard-failure'));
}); });
} }
}
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -128,7 +128,7 @@ export default defineComponent({
props: { props: {
timetable: { timetable: {
type: Object as PropType<API.TimetableHistory.Data>, type: Object as PropType<API.TimetableHistory.DataShort>,
required: true required: true
} }
} }
@@ -51,7 +51,7 @@ export default defineComponent({
components: { ProgressBar }, components: { ProgressBar },
props: { props: {
timetable: { timetable: {
type: Object as PropType<API.TimetableHistory.Data>, type: Object as PropType<API.TimetableHistory.DataShort>,
required: true required: true
} }
} }
@@ -17,7 +17,7 @@
<!-- Extra --> <!-- Extra -->
<EntryDetails <EntryDetails
:timetable="timetableEntry" :timetable-id="timetableEntry.id"
:show-extra-info="showExtraInfo" :show-extra-info="showExtraInfo"
@toggle-extra-info="toggleExtraInfo" @toggle-extra-info="toggleExtraInfo"
/> />
@@ -28,7 +28,6 @@
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import { API } from '../../../typings/api'; import { API } from '../../../typings/api';
import { useApiStore } from '../../../store/apiStore'; import { useApiStore } from '../../../store/apiStore';
import { Journal } from '../typings';
import trainCategoryMixin from '../../../mixins/trainCategoryMixin'; import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
import dateMixin from '../../../mixins/dateMixin'; import dateMixin from '../../../mixins/dateMixin';
@@ -41,7 +40,7 @@ import EntryDetails from './EntryDetails.vue';
export default defineComponent({ export default defineComponent({
props: { props: {
timetableEntry: { timetableEntry: {
type: Object as PropType<API.TimetableHistory.Data>, type: Object as PropType<API.TimetableHistory.DataShort>,
required: true required: true
}, },
showExtraInfo: { showExtraInfo: {
@@ -60,71 +59,6 @@ export default defineComponent({
}; };
}, },
computed: {
timetablePathDetails() {
if (!this.timetableEntry.path || this.timetableEntry.path == '') return null;
return this.timetableEntry.path.split(';').map((pathEl, i) => {
const [arrival, name, departure] = pathEl.split(',');
const sceneryName = name.split(' ').slice(0, -1).join(' ');
const sceneryHash = name.split(' ').pop()?.replace('.sc', '') ?? '';
return {
arrival,
sceneryName,
sceneryHash,
departure,
isVisited: this.timetableEntry.visitedSceneries?.includes(sceneryHash) ?? false
};
});
},
timetableStops(): Journal.TimetableStopDetails[] {
const timetableEntry = this.timetableEntry;
const stopNames = timetableEntry.sceneriesString.split('%');
return stopNames.reduce<Journal.TimetableStopDetails[]>((acc, stopName, i, arr) => {
const arrivalDate =
i == arr.length - 1
? (timetableEntry.checkpointArrivals.at(i) ?? timetableEntry.endDate)
: timetableEntry.checkpointArrivals.at(i);
const scheduledArrivalDate =
i == arr.length - 1
? (timetableEntry.checkpointArrivalsScheduled.at(i) ?? timetableEntry.scheduledEndDate)
: timetableEntry.checkpointArrivalsScheduled.at(i);
const departureDate =
i == 0
? (timetableEntry.checkpointDepartures.at(i) ?? timetableEntry.beginDate)
: timetableEntry.checkpointDepartures.at(i);
const scheduledDepartureDate =
i == 0
? (timetableEntry.checkpointDeparturesScheduled.at(i) ??
timetableEntry.scheduledBeginDate)
: timetableEntry.checkpointDeparturesScheduled.at(i);
const stopTime = Number(timetableEntry.checkpointStopTypes.at(i)?.split(',')[0]) || 0;
const stopType = timetableEntry.checkpointStopTypes.at(i)?.split(',')[1] || '';
acc.push({
stopName,
arrivalTimestamp: this.dateStringToTimestamp(arrivalDate),
scheduledArrivalTimestamp: this.dateStringToTimestamp(scheduledArrivalDate),
departureTimestamp: this.dateStringToTimestamp(departureDate),
scheduledDepartureTimestamp: this.dateStringToTimestamp(scheduledDepartureDate),
stopTime,
stopType,
isConfirmed: i < timetableEntry.confirmedStopsCount
});
return acc;
}, []);
}
},
methods: { methods: {
toggleExtraInfo() { toggleExtraInfo() {
this.$emit('toggleShowExtraInfo'); this.$emit('toggleShowExtraInfo');
@@ -61,7 +61,7 @@ export default defineComponent({
props: { props: {
timetableHistory: { timetableHistory: {
type: Array as PropType<API.TimetableHistory.Response>, type: Array as PropType<API.TimetableHistory.ResponseShort>,
required: true required: true
}, },
scrollNoMoreData: { scrollNoMoreData: {
+72 -28
View File
@@ -1,3 +1,4 @@
import { Journal } from '../components/JournalView/typings';
import { Status, Vehicle, VehicleGroup } from './common'; import { Status, Vehicle, VehicleGroup } from './common';
export enum APIDataStatus { export enum APIDataStatus {
@@ -225,35 +226,40 @@ export namespace API {
} }
export namespace TimetableHistory { export namespace TimetableHistory {
export interface Data extends DataShort { export interface QueryParams {
driverName?: string;
trainNo?: string;
timetableId?: string;
categoryCode?: string;
authorName?: string;
dateFrom?: string;
dateTo?: string;
issuedFrom?: string;
terminatingAt?: string;
via?: string;
includesScenery?: string;
countFrom?: number;
countLimit?: number;
fulfilled?: number;
terminated?: number;
twr?: number;
skr?: number;
pn?: number;
tn?: number;
returnType?: 'all' | 'short' | 'detailed';
sortBy?: Journal.TimetableSorter['id'];
}
export interface Data extends DataShort, DataDetailsOnly {
updatedAt: string; updatedAt: string;
timetableId: number;
sceneriesString: string;
endDate: string;
scheduledBeginDate: string;
scheduledEndDate: string;
stockString?: string;
stockHistory: string[];
stockMass?: number;
stockLength?: number;
maxSpeed?: number;
routeSceneries: string;
checkpointArrivals: string[];
checkpointDepartures: string[];
checkpointArrivalsScheduled: string[];
checkpointDeparturesScheduled: string[];
checkpointStopTypes: string[];
checkpointComments: string[];
visitedSceneries: string[];
sceneryNames: string[];
path: string;
warningNotes: string | null;
trainMaxSpeed?: number;
} }
export interface DataShort { export interface DataShort {
@@ -261,6 +267,7 @@ export namespace API {
createdAt: string; createdAt: string;
trainNo: number; trainNo: number;
trainCategoryCode: string; trainCategoryCode: string;
timetableId: number;
driverId: number; driverId: number;
driverName: string; driverName: string;
@@ -280,6 +287,9 @@ export namespace API {
allStopsCount: number; allStopsCount: number;
beginDate: string; beginDate: string;
endDate: string;
scheduledBeginDate: string;
scheduledEndDate: string;
terminated: boolean; terminated: boolean;
fulfilled: boolean; fulfilled: boolean;
@@ -293,8 +303,42 @@ export namespace API {
hasExtraDeliveries: boolean; hasExtraDeliveries: boolean;
} }
export interface DataDetailsOnly {
id: number;
timetableId: number;
sceneriesString: string;
stockString?: string;
stockHistory: string[];
stockMass?: number;
stockLength?: number;
maxSpeed?: number;
trainMaxSpeed?: number;
routeSceneries: string;
checkpointArrivals: string[];
checkpointDepartures: string[];
checkpointArrivalsScheduled: string[];
checkpointDeparturesScheduled: string[];
checkpointStopTypes: string[];
checkpointComments: string[];
visitedSceneries: string[];
sceneryNames: string[];
path: string;
warningNotes: string | null;
authorId?: number;
authorName?: string;
driverId: number;
driverName: string;
driverLanguageId: number | null;
}
export type Response = Data[]; export type Response = Data[];
export type ResponseShort = DataShort[]; export type ResponseShort = DataShort[];
export type ResponseDetailsOnly = DataDetailsOnly[];
} }
export namespace DailyStats { export namespace DailyStats {
+6 -35
View File
@@ -118,36 +118,6 @@ export const journalTimetableFilters: Journal.TimetableFilter[] = [
} }
]; ];
interface TimetablesQueryParams {
driverName?: string;
trainNo?: string;
timetableId?: string;
categoryCode?: string;
authorName?: string;
dateFrom?: string;
dateTo?: string;
issuedFrom?: string;
terminatingAt?: string;
via?: string;
includesScenery?: string;
countFrom?: number;
countLimit?: number;
fulfilled?: number;
terminated?: number;
twr?: number;
skr?: number;
pn?: number;
tn?: number;
sortBy?: Journal.TimetableSorter['id'];
}
export default defineComponent({ export default defineComponent({
components: { components: {
JournalOptions, JournalOptions,
@@ -170,7 +140,7 @@ export default defineComponent({
mainStore: useMainStore(), mainStore: useMainStore(),
apiStore: useApiStore(), apiStore: useApiStore(),
currentQueryParams: {} as TimetablesQueryParams, currentQueryParams: {} as API.TimetableHistory.QueryParams,
dataRefreshedAt: null as Date | null, dataRefreshedAt: null as Date | null,
scrollDataLoaded: true, scrollDataLoaded: true,
@@ -180,7 +150,7 @@ export default defineComponent({
currentOptionsActive: false, currentOptionsActive: false,
timetableHistory: [] as API.TimetableHistory.Response, timetableHistory: [] as API.TimetableHistory.ResponseShort,
dataStatus: Status.Data.Loading dataStatus: Status.Data.Loading
}), }),
@@ -230,7 +200,7 @@ export default defineComponent({
}, },
watch: { watch: {
currentQueryParams(q: TimetablesQueryParams) { currentQueryParams(q: API.TimetableHistory.QueryParams) {
this.currentOptionsActive = Object.values(q).some((v) => v !== undefined); this.currentOptionsActive = Object.values(q).some((v) => v !== undefined);
} }
}, },
@@ -328,7 +298,7 @@ export default defineComponent({
dateToISO = dateTo.toISOString(); dateToISO = dateTo.toISOString();
} }
const queryParams: TimetablesQueryParams = {}; const queryParams: API.TimetableHistory.QueryParams = {};
this.filterList this.filterList
.filter((f) => f.isActive) .filter((f) => f.isActive)
@@ -395,6 +365,7 @@ export default defineComponent({
queryParams['terminatingAt'] = terminatingAt; queryParams['terminatingAt'] = terminatingAt;
queryParams['via'] = via; queryParams['via'] = via;
queryParams['categoryCode'] = categoryCode; queryParams['categoryCode'] = categoryCode;
queryParams['returnType'] = 'short';
queryParams['issuedFrom'] = issuedFrom; queryParams['issuedFrom'] = issuedFrom;
queryParams['sortBy'] = queryParams['sortBy'] =
@@ -406,7 +377,7 @@ export default defineComponent({
this.currentQueryParams = queryParams; this.currentQueryParams = queryParams;
try { try {
const responseData: API.TimetableHistory.Response = await ( const responseData: API.TimetableHistory.ResponseShort = await (
await this.apiStore.client!.get('api/getTimetables', { await this.apiStore.client!.get('api/getTimetables', {
params: this.currentQueryParams params: this.currentQueryParams
}) })