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,209 +19,243 @@
<div class="details-body" v-if="showExtraInfo">
<div class="g-separator"></div>
<EntryStops :timetable="timetable" />
<div v-if="timetableDetails">
<EntryStops :timetable="timetableDetails" />
<div class="g-separator"></div>
<div class="timetable-specs">
<span class="badge specs-badge" v-if="timetable.authorName">
<span>{{ $t('journal.dispatcher-name') }}</span>
<span>{{ timetable.authorName }}</span>
</span>
<span class="badge specs-badge" v-if="timetable.trainMaxSpeed">
<span>{{ $t('journal.stock-timetable-speed') }}</span>
<span> {{ timetable.trainMaxSpeed }}km/h </span>
</span>
<span class="badge specs-badge" v-if="timetable.maxSpeed">
<span>{{ $t('journal.stock-max-speed') }}</span>
<span>{{ timetable.maxSpeed }}km/h</span>
</span>
</div>
<div class="stock-dangers" v-if="timetable.warningNotes">
<div class="g-separator"></div>
<b>{{ $t('journal.stock-dangers') }}:</b>
<ul>
<li v-if="timetable.twr">
<b class="text--primary">{{ $t('warnings.TWR') }} (TWR)</b>
</li>
<li v-if="timetable.skr">
<b class="text--primary">{{ $t('warnings.SKR') }}</b>
</li>
<li v-if="timetable.hasDangerousCargo">
<b class="text--primary">{{ $t('warnings.TN') }}</b>
</li>
<li v-if="timetable.hasExtraDeliveries">
<b class="text--primary">{{ $t('warnings.PN') }}</b>
</li>
</ul>
<div class="dangers-notes" v-if="timetable.warningNotes">
<h4>{{ $t('warnings.header-title') }}</h4>
<p>
<i>{{ timetable.warningNotes }}</i>
</p>
</div>
</div>
<!-- Historia zmian w składzie -->
<div v-if="timetable.stockString || stockHistory.length != 0">
<div class="g-separator"></div>
<b>{{ $t('journal.stock-preview') }}:</b>
<div class="stock-specs" style="margin-top: 0.5em">
<span class="badge specs-badge" v-if="timetable.stockLength">
<span>{{ $t('journal.stock-length') }}</span>
<span>
{{
currentHistoryIndex == 0
? timetable.stockLength
: stockHistory[currentHistoryIndex].stockLength || timetable.stockLength
}}m
</span>
<div class="timetable-specs">
<span class="badge specs-badge" v-if="timetableDetails.authorName">
<span>{{ $t('journal.dispatcher-name') }}</span>
<span>{{ timetableDetails.authorName }}</span>
</span>
<span class="badge specs-badge" v-if="timetable.stockMass">
<span>{{ $t('journal.stock-mass') }}</span>
<span>
{{
Math.floor(
(currentHistoryIndex == 0
? timetable.stockMass
: stockHistory[currentHistoryIndex].stockMass || timetable.stockMass) / 1000
)
}}t
</span>
<span class="badge specs-badge" v-if="timetableDetails.trainMaxSpeed">
<span>{{ $t('journal.stock-timetable-speed') }}</span>
<span> {{ timetableDetails.trainMaxSpeed }}km/h </span>
</span>
<span class="badge specs-badge" v-if="timetableDetails.maxSpeed">
<span>{{ $t('journal.stock-max-speed') }}</span>
<span>{{ timetableDetails.maxSpeed }}km/h</span>
</span>
</div>
<div class="stock-history">
<button class="btn btn--action" @click="copyStockToClipboard()">
<i class="fa-regular fa-copy"></i> {{ $t('journal.stock-copy') }}
</button>
<div class="stock-dangers" v-if="timetableDetails.warningNotes">
<div class="g-separator"></div>
<button
v-for="(sh, i) in stockHistory"
:key="i"
class="btn--action"
:data-checked="i == currentHistoryIndex"
@click.stop="currentHistoryIndex = i"
>
{{ sh.updatedAt }}
</button>
<b>{{ $t('journal.stock-dangers') }}:</b>
<ul>
<li v-if="timetableDetails.twr">
<b class="text--primary">{{ $t('warnings.TWR') }} (TWR)</b>
</li>
<li v-if="timetableDetails.skr">
<b class="text--primary">{{ $t('warnings.SKR') }}</b>
</li>
<li v-if="timetableDetails.hasDangerousCargo">
<b class="text--primary">{{ $t('warnings.TN') }}</b>
</li>
<li v-if="timetableDetails.hasExtraDeliveries">
<b class="text--primary">{{ $t('warnings.PN') }}</b>
</li>
</ul>
<div class="dangers-notes" v-if="timetableDetails.warningNotes">
<h4>{{ $t('warnings.header-title') }}</h4>
<p>
<i>{{ timetableDetails.warningNotes }}</i>
</p>
</div>
</div>
<div v-if="timetable.stockString" style="margin-top: 1em">
<StockList
:trainStockList="
(currentHistoryIndex == 0
? timetable.stockString
: stockHistory[currentHistoryIndex].stockString
).split(';')
"
/>
<!-- Historia zmian w składzie -->
<div v-if="timetableDetails.stockString || stockHistory.length != 0">
<div class="g-separator"></div>
<b>{{ $t('journal.stock-preview') }}:</b>
<div class="stock-specs" style="margin-top: 0.5em">
<span class="badge specs-badge" v-if="timetableDetails.stockLength">
<span>{{ $t('journal.stock-length') }}</span>
<span>
{{
currentHistoryIndex == 0
? timetableDetails.stockLength
: stockHistory[currentHistoryIndex].stockLength || timetableDetails.stockLength
}}m
</span>
</span>
<span class="badge specs-badge" v-if="timetableDetails.stockMass">
<span>{{ $t('journal.stock-mass') }}</span>
<span>
{{
Math.floor(
(currentHistoryIndex == 0
? timetableDetails.stockMass
: stockHistory[currentHistoryIndex].stockMass || timetableDetails.stockMass) /
1000
)
}}t
</span>
</span>
</div>
<div class="stock-history">
<button class="btn btn--action" @click="copyStockToClipboard()">
<i class="fa-regular fa-copy"></i> {{ $t('journal.stock-copy') }}
</button>
<button
v-for="(sh, i) in stockHistory"
:key="i"
class="btn--action"
:data-checked="i == currentHistoryIndex"
@click.stop="currentHistoryIndex = i"
>
{{ sh.updatedAt }}
</button>
</div>
<div v-if="timetableDetails.stockString" style="margin-top: 1em">
<StockList
:trainStockList="
(currentHistoryIndex == 0
? timetableDetails.stockString
: stockHistory[currentHistoryIndex].stockString
).split(';')
"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import StockList from '../../Global/StockList.vue';
import { API } from '../../../typings/api';
<script lang="ts" setup>
import { computed, defineComponent, onActivated, onMounted, ref, watch } from 'vue';
import { RouteLocationRaw } from 'vue-router';
import EntryStops from './EntryStops.vue';
import { useI18n } from 'vue-i18n';
export default defineComponent({
components: { StockList, EntryStops },
import StockList from '../../Global/StockList.vue';
import EntryStops from './EntryStops.vue';
import { API } from '../../../typings/api';
import { useApiStore } from '../../../store/apiStore';
emits: ['toggleExtraInfo'],
const i18n = useI18n();
const apiStore = useApiStore();
props: {
showExtraInfo: {
type: Boolean,
required: true
},
timetable: {
type: Object as PropType<API.TimetableHistory.Data>,
required: true
}
const props = defineProps({
showExtraInfo: {
type: Boolean,
required: true
},
data() {
return {
currentHistoryIndex: 0,
i18n: useI18n()
};
},
computed: {
stockHistory() {
return this.timetable.stockHistory
.slice()
.reverse()
.map((h) => {
const historyData = h.split('@');
return {
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, {
hour: '2-digit',
minute: '2-digit'
}),
stockString: historyData[1],
stockMass: Number(historyData[2]) || undefined,
stockLength: Number(historyData[3]) || undefined
};
});
},
driverRouteLocation(): RouteLocationRaw | null {
if (this.timetable.terminated) return null;
return {
name: 'DriverView',
query: {
trainId: `${this.timetable.driverId}|${this.timetable.trainNo}|eu`
}
};
}
},
methods: {
onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = '/images/icon-unknown.png';
},
toggleExtraInfo() {
this.$emit('toggleExtraInfo', this.timetable.id);
},
copyStockToClipboard() {
const currentStockString =
this.stockHistory[this.currentHistoryIndex]?.stockString ?? this.timetable.stockString;
if (!currentStockString) {
alert(this.i18n.t('journal.stock-clipboard-failure'));
return;
}
navigator.clipboard
.writeText(currentStockString)
.then(() => {
prompt(this.i18n.t('journal.stock-clipboard-success'), currentStockString);
})
.catch(() => {
alert(this.i18n.t('journal.stock-clipboard-failure'));
});
}
timetableId: {
type: Number,
required: true
}
});
const emits = defineEmits(['toggleExtraInfo']);
const currentHistoryIndex = ref(0);
const timetableDetails = ref<API.TimetableHistory.Data | null>(null);
watch(
computed(() => props.showExtraInfo),
(state) => {
if (state == true) {
fetchTimetableDetails();
}
}
);
const stockHistory = computed(() => {
return (
timetableDetails.value?.stockHistory
.slice()
.reverse()
.map((h) => {
const historyData = h.split('@');
return {
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(i18n.locale.value, {
hour: '2-digit',
minute: '2-digit'
}),
stockString: historyData[1],
stockMass: Number(historyData[2]) || undefined,
stockLength: Number(historyData[3]) || undefined
};
}) ?? []
);
});
const driverRouteLocation = computed<RouteLocationRaw | null>(() => {
if (!timetableDetails.value || timetableDetails.value.terminated) return null;
return {
name: 'DriverView',
query: {
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'
}
}
);
if (!responseData || responseData.data.length != 1) {
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;
const currentStockString =
stockHistory.value[currentHistoryIndex.value]?.stockString ??
timetableDetails.value.stockString;
if (!currentStockString) {
alert(i18n.t('journal.stock-clipboard-failure'));
return;
}
navigator.clipboard
.writeText(currentStockString)
.then(() => {
prompt(i18n.t('journal.stock-clipboard-success'), currentStockString);
})
.catch(() => {
alert(i18n.t('journal.stock-clipboard-failure'));
});
}
</script>
<style lang="scss" scoped>
@@ -299,7 +333,7 @@ hr {
}
}
@include responsive.smallScreen{
@include responsive.smallScreen {
.timetable-specs {
justify-content: center;
}
@@ -128,7 +128,7 @@ export default defineComponent({
props: {
timetable: {
type: Object as PropType<API.TimetableHistory.Data>,
type: Object as PropType<API.TimetableHistory.DataShort>,
required: true
}
}
@@ -51,7 +51,7 @@ export default defineComponent({
components: { ProgressBar },
props: {
timetable: {
type: Object as PropType<API.TimetableHistory.Data>,
type: Object as PropType<API.TimetableHistory.DataShort>,
required: true
}
}
@@ -17,7 +17,7 @@
<!-- Extra -->
<EntryDetails
:timetable="timetableEntry"
:timetable-id="timetableEntry.id"
:show-extra-info="showExtraInfo"
@toggle-extra-info="toggleExtraInfo"
/>
@@ -28,7 +28,6 @@
import { defineComponent, PropType } from 'vue';
import { API } from '../../../typings/api';
import { useApiStore } from '../../../store/apiStore';
import { Journal } from '../typings';
import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
import dateMixin from '../../../mixins/dateMixin';
@@ -41,7 +40,7 @@ import EntryDetails from './EntryDetails.vue';
export default defineComponent({
props: {
timetableEntry: {
type: Object as PropType<API.TimetableHistory.Data>,
type: Object as PropType<API.TimetableHistory.DataShort>,
required: true
},
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: {
toggleExtraInfo() {
this.$emit('toggleShowExtraInfo');
@@ -145,7 +79,7 @@ export default defineComponent({
display: flex;
}
@include responsive.smallScreen{
@include responsive.smallScreen {
.entry-route {
justify-content: center;
text-align: center;
@@ -61,7 +61,7 @@ export default defineComponent({
props: {
timetableHistory: {
type: Array as PropType<API.TimetableHistory.Response>,
type: Array as PropType<API.TimetableHistory.ResponseShort>,
required: true
},
scrollNoMoreData: {
+72 -28
View File
@@ -1,3 +1,4 @@
import { Journal } from '../components/JournalView/typings';
import { Status, Vehicle, VehicleGroup } from './common';
export enum APIDataStatus {
@@ -225,35 +226,40 @@ export namespace API {
}
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;
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 {
@@ -261,6 +267,7 @@ export namespace API {
createdAt: string;
trainNo: number;
trainCategoryCode: string;
timetableId: number;
driverId: number;
driverName: string;
@@ -280,6 +287,9 @@ export namespace API {
allStopsCount: number;
beginDate: string;
endDate: string;
scheduledBeginDate: string;
scheduledEndDate: string;
terminated: boolean;
fulfilled: boolean;
@@ -293,8 +303,42 @@ export namespace API {
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 ResponseShort = DataShort[];
export type ResponseDetailsOnly = DataDetailsOnly[];
}
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({
components: {
JournalOptions,
@@ -170,7 +140,7 @@ export default defineComponent({
mainStore: useMainStore(),
apiStore: useApiStore(),
currentQueryParams: {} as TimetablesQueryParams,
currentQueryParams: {} as API.TimetableHistory.QueryParams,
dataRefreshedAt: null as Date | null,
scrollDataLoaded: true,
@@ -180,7 +150,7 @@ export default defineComponent({
currentOptionsActive: false,
timetableHistory: [] as API.TimetableHistory.Response,
timetableHistory: [] as API.TimetableHistory.ResponseShort,
dataStatus: Status.Data.Loading
}),
@@ -230,7 +200,7 @@ export default defineComponent({
},
watch: {
currentQueryParams(q: TimetablesQueryParams) {
currentQueryParams(q: API.TimetableHistory.QueryParams) {
this.currentOptionsActive = Object.values(q).some((v) => v !== undefined);
}
},
@@ -328,7 +298,7 @@ export default defineComponent({
dateToISO = dateTo.toISOString();
}
const queryParams: TimetablesQueryParams = {};
const queryParams: API.TimetableHistory.QueryParams = {};
this.filterList
.filter((f) => f.isActive)
@@ -395,6 +365,7 @@ export default defineComponent({
queryParams['terminatingAt'] = terminatingAt;
queryParams['via'] = via;
queryParams['categoryCode'] = categoryCode;
queryParams['returnType'] = 'short';
queryParams['issuedFrom'] = issuedFrom;
queryParams['sortBy'] =
@@ -406,7 +377,7 @@ export default defineComponent({
this.currentQueryParams = queryParams;
try {
const responseData: API.TimetableHistory.Response = await (
const responseData: API.TimetableHistory.ResponseShort = await (
await this.apiStore.client!.get('api/getTimetables', {
params: this.currentQueryParams
})