diff --git a/package.json b/package.json index fe5d444..d3c327f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stacjownik", - "version": "1.16.1", + "version": "1.16.2", "private": true, "scripts": { "dev": "vite", diff --git a/src/components/JournalView/JournalStats.vue b/src/components/JournalView/JournalStats.vue index 47aab79..daceb0a 100644 --- a/src/components/JournalView/JournalStats.vue +++ b/src/components/JournalView/JournalStats.vue @@ -49,7 +49,7 @@ let data = reactive({ { name: 'driver', titlePath: 'journal.driver-stats-title', - inactive: true, + // inactive: true, }, ] as { name: TStatTab; titlePath: string; inactive?: boolean }[], }); diff --git a/src/components/JournalView/JournalTimetablesList.vue b/src/components/JournalView/JournalTimetablesList.vue index 2789d9c..51d2c0f 100644 --- a/src/components/JournalView/JournalTimetablesList.vue +++ b/src/components/JournalView/JournalTimetablesList.vue @@ -1,10 +1,10 @@ @@ -46,24 +71,35 @@ import Station from '../../scripts/interfaces/Station'; import { URLs } from '../../scripts/utils/apiURLs'; import Loading from '../Global/Loading.vue'; import styleMixin from '../../mixins/styleMixin'; +import listObserverMixin from '../../mixins/listObserverMixin'; export default defineComponent({ name: 'SceneryDispatchersHistory', - mixins: [dateMixin, styleMixin], + mixins: [dateMixin, styleMixin, listObserverMixin], props: { station: { type: Object as PropType, required: true, }, }, + data() { return { - dispatcherHistoryList: [] as DispatcherHistory[], + historyList: [] as DispatcherHistory[], dataStatus: DataStatus.Loading, }; }, + + mounted() { + this.mountObserver(this.fireObserverAction, this.$refs['bottomDiv'] as Element); + }, + + unmounted() { + this.unmountObserver(); + }, + activated() { - this.fetchAPIData(); + if (this.historyList.length == 0) this.fetchAPIData(); }, methods: { async fetchAPIData(countFrom = 0, countLimit = 30) { @@ -71,12 +107,17 @@ export default defineComponent({ const requestString = `${URLs.stacjownikAPI}/api/getDispatchers?stationName=${this.station.name}&countFrom=${countFrom}&countLimit=${countLimit}`; const historyAPIData: DispatcherHistory[] = await (await axios.get(requestString)).data; - this.dispatcherHistoryList = historyAPIData; + this.historyList.push(...historyAPIData); this.dataStatus = DataStatus.Loaded; } catch (error) { console.error(error); } }, + + fireObserverAction() { + if (this.historyList.length > 0 && this.dataStatus == DataStatus.Loaded) + this.fetchAPIData(this.historyList.length); + }, }, components: { Loading }, }); @@ -84,30 +125,10 @@ export default defineComponent({ diff --git a/src/locales/en.json b/src/locales/en.json index 18b305b..67d0ba9 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -274,6 +274,7 @@ "title": "DISPATCHER HISTORY", "loading": "Loading dispatcher history data...", "no-history": "No dispatcher history found!", + "data-refreshed-at": "Data refreshed at", "section-timetables": "TIMETABLES", "section-dispatchers": "DISPATCHERS", @@ -291,8 +292,10 @@ "online-since": "ONLINE SINCE", "duty-lasted": "The duty lasted", - "minutes": "{minutes} mins", - "hours": "{hours}h {minutes} mins", + + "hours": "{value} hour | {value} hours", + "minutes": "{value} min | {value} mins", + "seconds": "{value} s", "stock-info": "EXTRA INFO", "stock-length": "Length", @@ -329,7 +332,10 @@ "driver-stats-info": "Enter a proper nickname into filters [F] to see user's driving statistics!", "stats-loading": "Fetching statistics...", - "stats-error": "Oops! An unexpected error occurred while trying to fetch statistics! :/" + "stats-error": "Oops! An unexpected error occurred while trying to fetch statistics! :/", + + "timetable-location-signal": "signal:", + "timetable-location-route": "route:" }, "scenery": { "users": "PLAYERS ONLINE", diff --git a/src/locales/pl.json b/src/locales/pl.json index ee22ead..dba4f1e 100644 --- a/src/locales/pl.json +++ b/src/locales/pl.json @@ -279,6 +279,7 @@ "title": "HISTORIA DYŻURÓW", "loading": "Ładowanie historii dyżurów...", "no-history": "Brak historii dyżurów dla tej scenerii!", + "data-refreshed-at": "Dane odświeżone o", "section-timetables": "ROZKŁADY JAZDY", "section-dispatchers": "DYŻURNI", @@ -288,8 +289,9 @@ "online-since": "ONLINE OD", "duty-lasted": "Dyżur trwał", - "minutes": "{minutes} min.", - "hours": "{hours} godz. {minutes} min.", + "hours": "{value} godz.", + "minutes": "{value} min.", + "seconds": "{value} sek.", "route-length": "Kilometraż:", "station-count": "Stacje:", @@ -333,7 +335,10 @@ "driver-stats-info": "Wpisz nazwę użytkownika w filtrach [F], aby zobaczyć jego statystyki maszynisty!", "stats-loading": "Pobieranie statystyk...", - "stats-error": "Ups! Wystąpił błąd podczas próby pobrania statystyk! :/" + "stats-error": "Ups! Wystąpił błąd podczas próby pobrania statystyk! :/", + + "timetable-location-signal": "semafor:", + "timetable-location-route": "szlak:" }, "scenery": { "users": "GRACZE ONLINE", diff --git a/src/mixins/dateMixin.ts b/src/mixins/dateMixin.ts index 9d96b69..59bf1f6 100644 --- a/src/mixins/dateMixin.ts +++ b/src/mixins/dateMixin.ts @@ -1,50 +1,70 @@ -import { defineComponent } from 'vue'; - -export default defineComponent({ - methods: { - localeDate(dateString: string, locale: string) { - return new Date(dateString).toLocaleDateString(locale == 'pl' ? 'pl-PL' : 'en-GB', { - weekday: 'long', - day: 'numeric', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - }, - - localeDay(dateString: string, locale: string) { - return new Date(dateString).toLocaleDateString(locale == 'pl' ? 'pl-PL' : 'en-GB', { - day: 'numeric', - month: '2-digit', - year: 'numeric', - }); - }, - - localeTime(dateString: string, locale: string) { - return new Date(dateString).toLocaleTimeString(locale == 'pl' ? 'pl-PL' : 'en-GB', { - hour: '2-digit', - minute: '2-digit', - }); - }, - - timestampToString(timestamp: number | null) { - return timestamp - ? new Date(timestamp).toLocaleTimeString('pl-PL', { - hour: '2-digit', - minute: '2-digit', - }) - : ''; - }, - - calculateDuration(timestampMs: number) { - const minsTotal = Math.round(timestampMs / 60000); - const hoursTotal = Math.floor(minsTotal / 60); - const minsInHour = minsTotal % 60; - - return minsTotal > 60 - ? this.$t('journal.hours', { hours: hoursTotal, minutes: minsInHour }) - : this.$t('journal.minutes', { minutes: minsTotal }); - }, - }, -}); +import { defineComponent } from 'vue'; + +export default defineComponent({ + methods: { + localeDate(dateString: string, locale: string) { + return new Date(dateString).toLocaleDateString(locale == 'pl' ? 'pl-PL' : 'en-GB', { + weekday: 'long', + day: 'numeric', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }, + + localeDay(dateString: string, locale: string) { + return new Date(dateString).toLocaleDateString(locale == 'pl' ? 'pl-PL' : 'en-GB', { + day: 'numeric', + month: '2-digit', + year: 'numeric', + }); + }, + + localeTime(dateString: string, locale: string) { + return new Date(dateString).toLocaleTimeString(locale == 'pl' ? 'pl-PL' : 'en-GB', { + hour: '2-digit', + minute: '2-digit', + }); + }, + + stringToDate(dateString?: string) { + return dateString ? new Date(dateString) : null; + }, + + parseDateToTimeString(date: Date | null) { + return ( + date?.toLocaleTimeString('pl-PL', { + hour: '2-digit', + minute: '2-digit', + }) || '' + ); + }, + + timestampToString(timestamp: number | null) { + return timestamp + ? new Date(timestamp).toLocaleTimeString('pl-PL', { + hour: '2-digit', + minute: '2-digit', + }) + : ''; + }, + + calculateDuration(timestampMs: number, showSeconds = false) { + const secondsTotal = Math.floor(timestampMs / 1000); + const minsTotal = Math.round(timestampMs / 60000); + const hoursTotal = Math.floor(minsTotal / 60); + const minsInHour = minsTotal % 60; + + return minsTotal >= 60 + ? `${this.$t('journal.hours', { value: hoursTotal }, hoursTotal)} ${this.$t( + 'journal.minutes', + { value: minsInHour }, + minsInHour + )}` + : showSeconds && secondsTotal <= 60 + ? this.$t('journal.seconds', { value: secondsTotal }, secondsTotal) + : this.$t('journal.minutes', { value: minsTotal }, minsTotal); + }, + }, +}); diff --git a/src/mixins/listObserverMixin.ts b/src/mixins/listObserverMixin.ts new file mode 100644 index 0000000..9a0097a --- /dev/null +++ b/src/mixins/listObserverMixin.ts @@ -0,0 +1,24 @@ +import { defineComponent } from 'vue'; + +export default defineComponent({ + data: () => ({ + observer: null as IntersectionObserver | null, + observerTarget: null as Element | null, + }), + + methods: { + mountObserver(actionFunction: () => void, target: Element) { + this.observer = new IntersectionObserver((entries) => { + if (entries[0].intersectionRatio > 0) actionFunction(); + }); + + this.observer.observe(target); + }, + + unmountObserver() { + if (!this.observerTarget) return; + + this.observer?.unobserve(this.observerTarget); + }, + }, +}); diff --git a/src/scripts/interfaces/api/TimetablesAPIData.ts b/src/scripts/interfaces/api/TimetablesAPIData.ts index a45502c..e64a231 100644 --- a/src/scripts/interfaces/api/TimetablesAPIData.ts +++ b/src/scripts/interfaces/api/TimetablesAPIData.ts @@ -16,6 +16,7 @@ export interface TimetableHistory { twr: number; skr: number; sceneriesString: string; + currentLocation: string[]; routeDistance: number; currentDistance: number; @@ -52,6 +53,11 @@ export interface TimetableHistory { checkpointArrivals?: string[]; checkpointDepartures?: string[]; + + checkpointArrivalsScheduled?: string[]; + checkpointDeparturesScheduled?: string[]; + + checkpointStopTypes?: string[]; } export interface SceneryTimetableHistory { diff --git a/src/styles/JournalSection.scss b/src/styles/JournalSection.scss index 6db9a7c..f7be3d8 100644 --- a/src/styles/JournalSection.scss +++ b/src/styles/JournalSection.scss @@ -23,6 +23,15 @@ padding: 1em 0; } +.journal_refreshed-date { + background-color: #333; + color: #ddd; + text-align: end; + + padding: 0.25em; + margin: 0.5em 0; +} + .journal_warning { text-align: center; font-size: 1.3em; @@ -71,6 +80,10 @@ justify-content: center; flex-wrap: wrap; } + + .journal_refreshed-date { + text-align: center; + } } @media (orientation: landscape) { diff --git a/src/styles/SceneryView/styles.scss b/src/styles/SceneryView/styles.scss deleted file mode 100644 index 379a32e..0000000 --- a/src/styles/SceneryView/styles.scss +++ /dev/null @@ -1,11 +0,0 @@ -.scenery-section { - position: relative; - height: 100%; - overflow-y: scroll; -} - -.list-warning { - padding: 1em 0.5em; - background-color: #444; - font-size: 1.2em; -} diff --git a/src/styles/badge.scss b/src/styles/badge.scss index 3148bbf..65b6598 100644 --- a/src/styles/badge.scss +++ b/src/styles/badge.scss @@ -30,7 +30,6 @@ display: flex; justify-content: center; align-items: center; - font-size: 0.9em; &.driver { border-radius: 50%; diff --git a/src/styles/sceneryViewTables.scss b/src/styles/sceneryViewTables.scss new file mode 100644 index 0000000..83d9d30 --- /dev/null +++ b/src/styles/sceneryViewTables.scss @@ -0,0 +1,31 @@ +table.scenery-history-table { + width: 100%; + border-collapse: collapse; + + thead { + position: sticky; + top: 0; + background-color: #222222; + } + + th { + padding: 0.5em; + } + + tr { + background-color: #353535; + border: none; + } + + td { + padding: 0.75em; + border-bottom: solid 5px #111; + } +} + +.no-history { + padding: 1em 0.5em; + background-color: #444; + font-size: 1.2em; + color: #ccc; +} diff --git a/src/views/JournalDispatchers.vue b/src/views/JournalDispatchers.vue index f68aa79..ce1b622 100644 --- a/src/views/JournalDispatchers.vue +++ b/src/views/JournalDispatchers.vue @@ -13,6 +13,10 @@ optionsType="dispatchers" /> +
+ {{ $t('journal.data-refreshed-at') }}: {{ dataRefreshedAt.toLocaleString($i18n.locale) }} +
+
@@ -104,6 +108,7 @@ export default defineComponent({ data: () => ({ currentQuery: '', currentQueryArray: [] as string[], + dataRefreshedAt: null as Date | null, scrollDataLoaded: true, scrollNoMoreData: false, @@ -273,6 +278,7 @@ export default defineComponent({ ? this.historyList[0].dispatcherName : ''; + this.dataRefreshedAt = new Date(); this.dataStatus = DataStatus.Loaded; } catch (error) { this.dataStatus = DataStatus.Error; diff --git a/src/views/JournalTimetables.vue b/src/views/JournalTimetables.vue index 23e0da1..11b110b 100644 --- a/src/views/JournalTimetables.vue +++ b/src/views/JournalTimetables.vue @@ -16,6 +16,10 @@ +
+ {{ $t('journal.data-refreshed-at') }}: {{ dataRefreshedAt.toLocaleString($i18n.locale) }} +
+
@@ -101,6 +105,7 @@ export default defineComponent({ data: () => ({ currentQueryParams: {} as TimetablesQueryParams, + dataRefreshedAt: null as Date | null, scrollDataLoaded: true, scrollNoMoreData: false, @@ -326,6 +331,7 @@ export default defineComponent({ : ''; this.dataStatus = DataStatus.Loaded; + this.dataRefreshedAt = new Date(); } catch (error) { this.dataStatus = DataStatus.Error; this.dataErrorMessage = 'Ups! Coś poszło nie tak!'; diff --git a/src/views/SceneryView.vue b/src/views/SceneryView.vue index ea4833b..1656dfe 100644 --- a/src/views/SceneryView.vue +++ b/src/views/SceneryView.vue @@ -1,4 +1,4 @@ -