diff --git a/package-lock.json b/package-lock.json index f835e031..1879df7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9017,6 +9017,14 @@ "lodash.difference": "^4.5.0" } }, + "moment-timezone": { + "version": "0.5.33", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.33.tgz", + "integrity": "sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w==", + "requires": { + "moment": ">= 2.9.0" + } + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", diff --git a/package.json b/package.json index 292c2fcf..1178ff70 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "js-cookie": "^2.2.1", "line-awesome": "^1.3.0", "moment": "^2.29.1", + "moment-timezone": "^0.5.33", "register-service-worker": "^1.7.2", "ua-parser-js": "^0.7.24", "vue": "^2.6.12", diff --git a/pkg/exchangerates/euro_central_bank_datasource.go b/pkg/exchangerates/euro_central_bank_datasource.go index 9f4b325d..9483d1a4 100644 --- a/pkg/exchangerates/euro_central_bank_datasource.go +++ b/pkg/exchangerates/euro_central_bank_datasource.go @@ -2,6 +2,7 @@ package exchangerates import ( "encoding/xml" + "time" "github.com/mayswind/lab/pkg/core" "github.com/mayswind/lab/pkg/errs" @@ -14,6 +15,9 @@ const euroCentralBankExchangeRateReferenceUrl = "https://www.ecb.europa.eu/stats const euroCentralBankDataSource = "European Central Bank" const euroCentralBankBaseCurrency = "EUR" +const euroCentralBankDataUpdateDateFormat = "2006-01-02 15" +const euroCentralBankDataUpdateDateTimezone = "Etc/GMT-1" // UTC+01:00 + // EuroCentralBankDataSource defines the structure of exchange rates data source of euro central bank type EuroCentralBankDataSource struct { ExchangeRatesDataSource @@ -55,10 +59,23 @@ func (e *EuroCentralBankExchangeRateData) ToLatestExchangeRateResponse() *models exchangeRates[i] = latestEuroCentralBankExchangeRate.ExchangeRates[i].ToLatestExchangeRate() } + timezone, err := time.LoadLocation(euroCentralBankDataUpdateDateTimezone) + + if err != nil { + return nil + } + + updateDateTime := latestEuroCentralBankExchangeRate.Date + " 16" // The reference rates are usually updated around 16:00 CET on every working day + updateTime, err := time.ParseInLocation(euroCentralBankDataUpdateDateFormat, updateDateTime, timezone) + + if err != nil { + return nil + } + latestExchangeRateResp := &models.LatestExchangeRateResponse{ DataSource: euroCentralBankDataSource, ReferenceUrl: euroCentralBankExchangeRateReferenceUrl, - Date: latestEuroCentralBankExchangeRate.Date, + UpdateTime: updateTime.Unix(), BaseCurrency: euroCentralBankBaseCurrency, ExchangeRates: exchangeRates, } diff --git a/pkg/models/exchange_rate.go b/pkg/models/exchange_rate.go index 328b3359..28319fbd 100644 --- a/pkg/models/exchange_rate.go +++ b/pkg/models/exchange_rate.go @@ -4,7 +4,7 @@ package models type LatestExchangeRateResponse struct { DataSource string `json:"dataSource"` ReferenceUrl string `json:"referenceUrl"` - Date string `json:"date"` + UpdateTime int64 `json:"updateTime"` BaseCurrency string `json:"baseCurrency"` ExchangeRates []*LatestExchangeRate `json:"exchangeRates"` } diff --git a/src/components/mobile/DateRangeSelectionSheet.vue b/src/components/mobile/DateRangeSelectionSheet.vue index c7f4578a..0dd75448 100644 --- a/src/components/mobile/DateRangeSelectionSheet.vue +++ b/src/components/mobile/DateRangeSelectionSheet.vue @@ -52,7 +52,7 @@ export default { data() { const self = this; let minDate = self.$utilities.getTodayFirstUnixTime(); - let maxDate = self.$utilities.getUnixTime(new Date()); + let maxDate = self.$utilities.getCurrentUnixTime(); if (self.minTime) { minDate = self.minTime; @@ -70,12 +70,12 @@ export default { watch: { 'currentMinDate': function (newValue) { if (!newValue) { - this.currentMinDate = this.$utilities.formatDate(new Date(), 'YYYY-MM-DDTHH:mm'); + this.currentMinDate = this.$utilities.formatUnixTime(this.$utilities.getCurrentUnixTime(), 'YYYY-MM-DDTHH:mm'); } }, 'currentMaxDate': function (newValue) { if (!newValue) { - this.currentMaxDate = this.$utilities.formatDate(new Date(), 'YYYY-MM-DDTHH:mm'); + this.currentMaxDate = this.$utilities.formatUnixTime(this.$utilities.getCurrentUnixTime(), 'YYYY-MM-DDTHH:mm'); } } }, diff --git a/src/lib/settings.js b/src/lib/settings.js index b189bd08..0d086339 100644 --- a/src/lib/settings.js +++ b/src/lib/settings.js @@ -8,6 +8,7 @@ const serverSettingsCookieKey = 'lab_server_settings'; const defaultSettings = { lang: 'en', + timeZone: '', debug: false, applicationLock: false, applicationLockWebAuthn: false, @@ -128,6 +129,8 @@ export default { isProduction: () => process.env.NODE_ENV === 'production', getLanguage: () => getOriginalOption('lang'), setLanguage: value => setOption('lang', value), + getTimezone: () => getOption('timeZone'), + setTimezone: value => setOption('timeZone', value), isEnableDebug: () => getOption('debug'), setEnableDebug: value => setOption('debug', value), isEnableApplicationLock: () => getOption('applicationLock'), diff --git a/src/lib/utils.js b/src/lib/utils.js index abfdda06..b7c7e992 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -34,12 +34,20 @@ function isBoolean(val) { return typeof(val) === 'boolean'; } -function parseDateFromUnixTime(unixTime) { - return moment.unix(unixTime); +function getTimezoneOffset(timezone) { + if (timezone) { + return moment().tz(timezone).format('Z'); + } else { + return moment().format('Z'); + } } -function formatDate(date, format) { - return moment(date).format(format); +function getCurrentUnixTime() { + return moment().unix(); +} + +function parseDateFromUnixTime(unixTime) { + return moment.unix(unixTime); } function formatUnixTime(unixTime, format) { @@ -107,7 +115,7 @@ function getMinuteLastUnixTime(date) { } function getTodayFirstUnixTime() { - return moment({ hour: 0, minute: 0, second: 0, millisecond: 0 }).unix(); + return moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).unix(); } function getTodayLastUnixTime() { @@ -562,8 +570,9 @@ export default { isString, isNumber, isBoolean, + getTimezoneOffset, + getCurrentUnixTime, parseDateFromUnixTime, - formatDate, formatUnixTime, getUnixTime, getYear, diff --git a/src/locales/en.js b/src/locales/en.js index 247a9c5f..41150fa1 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -680,6 +680,8 @@ export default { 'Filter Transaction Categories': 'Filter Transaction Categories', 'User Profile': 'User Profile', 'Language': 'Language', + 'Timezone': 'Timezone', + 'System Default': 'System Default', 'Auto Update Exchange Rates Data': 'Auto Update Exchange Rates Data', 'Enable Thousands Separator': 'Enable Thousands Separator', 'Currency Display Mode': 'Currency Display Mode', diff --git a/src/locales/zh_Hans.js b/src/locales/zh_Hans.js index eaaafb35..1a068867 100644 --- a/src/locales/zh_Hans.js +++ b/src/locales/zh_Hans.js @@ -680,6 +680,8 @@ export default { 'Filter Transaction Categories': '过滤交易类型', 'User Profile': '用户信息', 'Language': '语言', + 'Timezone': '时区', + 'System Default': '系统默认', 'Auto Update Exchange Rates Data': '自动更新汇率数据', 'Enable Thousands Separator': '启用千位分隔符', 'Currency Display Mode': '货币显示模式', diff --git a/src/mobile-main.js b/src/mobile-main.js index 0db26ec9..ecd0937e 100644 --- a/src/mobile-main.js +++ b/src/mobile-main.js @@ -6,7 +6,7 @@ import PincodeInput from 'vue-pincode-input'; import VueMoment from 'vue-moment'; import VueClipboard from 'vue-clipboard2'; -import moment from 'moment'; +import moment from 'moment-timezone'; import Framework7 from 'framework7/framework7-lite.esm.js'; import Framework7Dialog from 'framework7/components/dialog/dialog'; @@ -186,6 +186,7 @@ Vue.prototype.$logger = logger; Vue.prototype.$webauthn = webauthn; Vue.prototype.$settings = settings; Vue.prototype.$locale = { + defaultTimezoneOffset: utils.getTimezoneOffset(), getDefaultLanguage: getDefaultLanguage, getAllLanguages: getAllLanguages, getLanguage: getLanguage, @@ -200,6 +201,31 @@ Vue.prototype.$locale = { document.querySelector('html').setAttribute('lang', locale); return locale; }, + getTimezone: function () { + return settings.getTimezone(); + }, + setTimezone: function (timezone) { + if (timezone) { + settings.setTimezone(timezone); + moment.tz.setDefault(timezone); + } else { + settings.setTimezone(''); + moment.tz.setDefault(); + } + }, + getAllTimezones: function () { + const allTimezoneNames = moment.tz.names(); + const allTimezoneInfos = []; + + for (let i = 0; i < allTimezoneNames.length; i++) { + allTimezoneInfos.push({ + name: allTimezoneNames[i], + displayName: `(UTC${utils.getTimezoneOffset(allTimezoneNames[i])}) ${allTimezoneNames[i]}` + }); + } + + return allTimezoneInfos; + }, getAllCurrencies: function () { const allCurrencyCodes = currency.all; const allCurrencies = []; @@ -229,6 +255,13 @@ Vue.prototype.$locale = { logger.info(`No language is set, use browser default ${getDefaultLanguage()}`); this.setLanguage(getDefaultLanguage()); } + + if (settings.getTimezone()) { + logger.info(`Current timezone is ${settings.getTimezone()}`); + this.setTimezone(settings.getTimezone()); + } else { + logger.info(`No timezone is set, use browser default ${utils.getTimezoneOffset()} (maybe ${moment.tz.guess(true)})`); + } } }; diff --git a/src/store/exchangeRates.js b/src/store/exchangeRates.js index e72ca5b1..37583a8c 100644 --- a/src/store/exchangeRates.js +++ b/src/store/exchangeRates.js @@ -10,16 +10,16 @@ const exchangeRatesLocalStorageKey = 'lab_app_exchange_rates'; export function getLatestExchangeRates(context, { silent, force }) { const currentExchangeRateData = context.state.latestExchangeRates; - const now = new Date(); + const now = utils.getCurrentUnixTime(); if (!force) { if (currentExchangeRateData && currentExchangeRateData.time && currentExchangeRateData.data && - currentExchangeRateData.data.date === utils.formatDate(now, 'YYYY-MM-DD')) { + utils.formatUnixTime(currentExchangeRateData.data.updateTime, 'YYYY-MM-DD') === utils.formatUnixTime(now, 'YYYY-MM-DD')) { return currentExchangeRateData.data; } if (currentExchangeRateData && currentExchangeRateData.time && currentExchangeRateData.data && - utils.formatUnixTime(currentExchangeRateData.time, 'YYYY-MM-DD HH') === utils.formatDate(now, 'YYYY-MM-DD HH')) { + utils.formatUnixTime(currentExchangeRateData.time, 'YYYY-MM-DD HH') === utils.formatUnixTime(now, 'YYYY-MM-DD HH')) { return currentExchangeRateData.data; } } @@ -36,7 +36,7 @@ export function getLatestExchangeRates(context, { silent, force }) { } context.commit(STORE_LATEST_EXCHANGE_RATES, { - time: utils.getUnixTime(now), + time: now, data: data.result }); @@ -55,9 +55,9 @@ export function getLatestExchangeRates(context, { silent, force }) { }); } -export function exchangeRatesLastUpdateDate(state) { +export function exchangeRatesLastUpdateTime(state) { const exchangeRates = state.latestExchangeRates || {}; - return exchangeRates && exchangeRates.data ? exchangeRates.data.date : null; + return exchangeRates && exchangeRates.data ? exchangeRates.data.updateTime : null; } export function getExchangedAmount(state) { diff --git a/src/store/index.js b/src/store/index.js index 70716a12..7cbb0e61 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -85,7 +85,7 @@ import { import { getLatestExchangeRates, - exchangeRatesLastUpdateDate, + exchangeRatesLastUpdateTime, getExchangedAmount, getExchangeRatesFromLocalStorage, setExchangeRatesToLocalStorage, @@ -198,7 +198,7 @@ const stores = { currentUserFirstDayOfWeek, // exchange rates - exchangeRatesLastUpdateDate, + exchangeRatesLastUpdateTime, getExchangedAmount, // account diff --git a/src/views/mobile/Settings.vue b/src/views/mobile/Settings.vue index dfc77cad..522d6319 100644 --- a/src/views/mobile/Settings.vue +++ b/src/views/mobile/Settings.vue @@ -24,7 +24,7 @@ + smart-select :smart-select-params="{ openIn: 'sheet', closeOnSelect: true, sheetCloseLinkText: $t('Done'), scrollToSelectedItem: true }"> + + + + + @@ -104,6 +115,12 @@ export default { allLanguages() { return this.$locale.getAllLanguages(); }, + allTimezones() { + return this.$locale.getAllTimezones(); + }, + defaultTimezoneOffset() { + return this.$locale.defaultTimezoneOffset; + }, currentLocale: { get: function () { return this.$i18n.locale; @@ -112,6 +129,14 @@ export default { this.$locale.setLanguage(value); } }, + currentTimezone: { + get: function () { + return this.$locale.getTimezone(); + }, + set: function (value) { + this.$locale.setTimezone(value); + } + }, currentNickName() { return this.$store.getters.currentUserNickname || this.$t('User'); }, @@ -119,8 +144,8 @@ export default { return this.$settings.isDataExportingEnabled(); }, exchangeRatesLastUpdateDate() { - const exchangeRatesLastUpdateDate = this.$store.getters.exchangeRatesLastUpdateDate; - return exchangeRatesLastUpdateDate ? this.$utilities.formatDate(exchangeRatesLastUpdateDate, this.$t('format.date.long')) : ''; + const exchangeRatesLastUpdateTime = this.$store.getters.exchangeRatesLastUpdateTime; + return exchangeRatesLastUpdateTime ? this.$utilities.formatUnixTime(exchangeRatesLastUpdateTime, this.$t('format.date.long')) : ''; }, isAutoUpdateExchangeRatesData: { get: function () { diff --git a/src/views/mobile/transactions/Edit.vue b/src/views/mobile/transactions/Edit.vue index ffb8eec2..692ded41 100644 --- a/src/views/mobile/transactions/Edit.vue +++ b/src/views/mobile/transactions/Edit.vue @@ -250,7 +250,7 @@ export default { data() { const self = this; const query = self.$f7route.query; - const now = new Date(); + const now = self.$utilities.getCurrentUnixTime(); let defaultType = self.$constants.transaction.allTransactionTypes.Expense; @@ -265,8 +265,8 @@ export default { editTransactionId: null, transaction: { type: defaultType, - unixTime: self.$utilities.getUnixTime(now), - time: self.$utilities.formatDate(now, 'YYYY-MM-DDTHH:mm'), + unixTime: now, + time: self.$utilities.formatUnixTime(now, 'YYYY-MM-DDTHH:mm'), expenseCategory: '', incomeCategory: '', transferCategory: '', @@ -488,7 +488,7 @@ export default { }, 'transaction.time': function (newValue) { if (!newValue) { - newValue = this.$utilities.formatDate(new Date(), 'YYYY-MM-DDTHH:mm'); + newValue = this.$utilities.formatUnixTime(this.$utilities.getCurrentUnixTime(), 'YYYY-MM-DDTHH:mm'); this.transaction.time = newValue; }