support user custom exchange rates data

This commit is contained in:
MaysWind
2025-05-26 00:47:19 +08:00
parent c4d20c539f
commit 817291c9a7
41 changed files with 1257 additions and 73 deletions
+8
View File
@@ -133,5 +133,13 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error {
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction picture table maintained successfully")
err = datastore.Container.UserDataStore.SyncStructs(new(models.UserCustomExchangeRate))
if err != nil {
return err
}
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] user custom exchange rate table maintained successfully")
return nil
}
+2
View File
@@ -359,6 +359,8 @@ func startWebServer(c *core.CliContext) error {
// Exchange Rates
apiV1Route.GET("/exchange_rates/latest.json", bindApi(api.ExchangeRates.LatestExchangeRateHandler))
apiV1Route.POST("/exchange_rates/user_custom/update.json", bindApi(api.ExchangeRates.UserCustomExchangeRateUpdateHandler))
apiV1Route.POST("/exchange_rates/user_custom/delete.json", bindApi(api.ExchangeRates.UserCustomExchangeRateDeleteHandler))
}
}
+1
View File
@@ -363,6 +363,7 @@ custom_map_tile_server_default_zoom_level = 14
# "national_bank_of_ukraine": https://bank.gov.ua/ua/markets/exchangerates
# "central_bank_of_uzbekistan": https://cbu.uz/en/arkhiv-kursov-valyut/
# "international_monetary_fund": https://www.imf.org/external/np/fin/data/param_rms_mth.aspx
# "user_custom": users set their own exchange rates data in the UI
data_source = euro_central_bank
# Requesting exchange rates data timeout (0 - 4294967295 milliseconds)
+9
View File
@@ -28,6 +28,7 @@ type DataManagementsApi struct {
tags *services.TransactionTagService
pictures *services.TransactionPictureService
templates *services.TransactionTemplateService
userCustomExchangeRates *services.UserCustomExchangeRatesService
}
// Initialize a data management api singleton instance
@@ -44,6 +45,7 @@ var (
tags: services.TransactionTags,
pictures: services.TransactionPictures,
templates: services.TransactionTemplates,
userCustomExchangeRates: services.UserCustomExchangeRates,
}
)
@@ -179,6 +181,13 @@ func (a *DataManagementsApi) ClearDataHandler(c *core.WebContext) (any, *errs.Er
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.userCustomExchangeRates.DeleteAllCustomExchangeRates(c, uid)
if err != nil {
log.Errorf(c, "[data_managements.ClearDataHandler] failed to delete all user custom exchange rates, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[data_managements.ClearDataHandler] user \"uid:%d\" has cleared all data", uid)
return true, nil
}
+73
View File
@@ -4,12 +4,17 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/exchangerates"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// ExchangeRatesApi represents exchange rate api
type ExchangeRatesApi struct {
ApiUsingConfig
users *services.UserService
userCustomExchangeRates *services.UserCustomExchangeRatesService
}
// Initialize a exchange rate api singleton instance
@@ -18,6 +23,8 @@ var (
ApiUsingConfig: ApiUsingConfig{
container: settings.Container,
},
users: services.Users,
userCustomExchangeRates: services.UserCustomExchangeRates,
}
)
@@ -37,3 +44,69 @@ func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.WebContext) (any, *
return exchangeRateResponse, nil
}
// UserCustomExchangeRateUpdateHandler updates user custom exchange rates data by request parameters for current user
func (a *ExchangeRatesApi) UserCustomExchangeRateUpdateHandler(c *core.WebContext) (any, *errs.Error) {
var customExchangeRateUpdateReq models.UserCustomExchangeRateUpdateRequest
err := c.ShouldBindJSON(&customExchangeRateUpdateReq)
if err != nil {
log.Warnf(c, "[exchange_rates.UserCustomExchangeRateUpdateHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
log.Errorf(c, "[exchange_rates.UserCustomExchangeRateUpdateHandler] failed to get user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if customExchangeRateUpdateReq.Currency == user.DefaultCurrency {
return nil, errs.ErrCannotUpdateExchangeRateForDefaultCurrency
}
newCustomExchangeRate, defaultCurrencyExchangeRate, err := a.userCustomExchangeRates.UpdateCustomExchangeRate(c, uid, customExchangeRateUpdateReq.Currency, customExchangeRateUpdateReq.Rate, user.DefaultCurrency)
if err != nil {
log.Errorf(c, "[exchange_rates.UserCustomExchangeRateUpdateHandler] failed to update user custom exchange rate \"currency:%s\" for user \"uid:%d\", because %s", customExchangeRateUpdateReq.Currency, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[exchange_rates.UserCustomExchangeRateUpdateHandler] user \"uid:%d\" has updated user custom exchange rate \"currency:%s\" successfully", uid, customExchangeRateUpdateReq.Currency)
return newCustomExchangeRate.ToUserCustomExchangeRateUpdateResponse(defaultCurrencyExchangeRate.Rate), nil
}
// UserCustomExchangeRateDeleteHandler deletes an existed user custom exchange rates data by request parameters for current user
func (a *ExchangeRatesApi) UserCustomExchangeRateDeleteHandler(c *core.WebContext) (any, *errs.Error) {
var customExchangeRateDeleteReq models.UserCustomExchangeRateDeleteRequest
err := c.ShouldBindJSON(&customExchangeRateDeleteReq)
if err != nil {
log.Warnf(c, "[exchange_rates.UserCustomExchangeRateDeleteHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
log.Errorf(c, "[exchange_rates.UserCustomExchangeRateDeleteHandler] failed to get user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if customExchangeRateDeleteReq.Currency == user.DefaultCurrency {
return nil, errs.ErrCannotDeleteExchangeRateForDefaultCurrency
}
err = a.userCustomExchangeRates.DeleteCustomExchangeRate(c, uid, customExchangeRateDeleteReq.Currency)
if err != nil {
log.Errorf(c, "[exchange_rates.UserCustomExchangeRateDeleteHandler] failed to delete user custom exchange rate \"currency:%s\" for user \"uid:%d\", because %s", customExchangeRateDeleteReq.Currency, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[exchange_rates.UserCustomExchangeRateDeleteHandler] user \"uid:%d\" has deleted user custom exchange rate \"currency:%s\"", uid, customExchangeRateDeleteReq.Currency)
return true, nil
}
+1
View File
@@ -38,6 +38,7 @@ const (
NormalSubcategoryTemplate = 10
NormalSubcategoryPicture = 11
NormalSubcategoryConverter = 12
NormalSubcategoryUserCustomExchangeRate = 13
)
// Error represents the specific error returned to user
+10
View File
@@ -0,0 +1,10 @@
package errs
import "net/http"
// Error codes related to user custom exchange rates
var (
ErrUserCustomExchangeRateNotFound = NewNormalError(NormalSubcategoryUserCustomExchangeRate, 0, http.StatusBadRequest, "user custom exchange rate data not found")
ErrCannotUpdateExchangeRateForDefaultCurrency = NewNormalError(NormalSubcategoryUserCustomExchangeRate, 1, http.StatusBadRequest, "cannot update exchange rate data for base currency")
ErrCannotDeleteExchangeRateForDefaultCurrency = NewNormalError(NormalSubcategoryUserCustomExchangeRate, 2, http.StatusBadRequest, "cannot delete exchange rate data for base currency")
)
@@ -68,6 +68,9 @@ func InitializeExchangeRatesDataSource(config *settings.Config) error {
} else if config.ExchangeRatesDataSource == settings.InternationalMonetaryFundDataSource {
Container.Current = newCommonHttpExchangeRatesDataSource(&InternationalMonetaryFundDataSource{})
return nil
} else if config.ExchangeRatesDataSource == settings.UserCustomExchangeRatesDataSource {
Container.Current = newUserCustomExchangeRatesDataSource()
return nil
}
return errs.ErrInvalidExchangeRatesDataSource
+101
View File
@@ -0,0 +1,101 @@
package exchangerates
import (
"sort"
"time"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/validators"
)
const userDataSourceType = "user_custom"
// UserCustomExchangeRatesDataSource defines the structure of user custom exchange rates data source
type UserCustomExchangeRatesDataSource struct {
ExchangeRatesDataSource
users *services.UserService
userCustomExchangeRates *services.UserCustomExchangeRatesService
}
func (e *UserCustomExchangeRatesDataSource) GetLatestExchangeRates(c core.Context, uid int64, currentConfig *settings.Config) (*models.LatestExchangeRateResponse, error) {
user, err := e.users.GetUserById(c, uid)
if err != nil {
log.Errorf(c, "[user_custom_datasource.GetLatestExchangeRates] failed to get user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
customExchangeRates, err := e.userCustomExchangeRates.GetAllCustomExchangeRatesByUid(c, uid)
if err != nil {
log.Errorf(c, "[user_custom_datasource.GetLatestExchangeRates] failed to get user custom exchange rates for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
baseCurrencyRate := int64(0)
hasDefaultCurrencyRate := false
for i := 0; i < len(customExchangeRates); i++ {
customExchangeRate := customExchangeRates[i]
if customExchangeRate.Currency == user.DefaultCurrency {
baseCurrencyRate = customExchangeRate.Rate
hasDefaultCurrencyRate = true
break
}
}
allExchangeRates := make(models.LatestExchangeRateSlice, 0, len(customExchangeRates))
latestUpdateTime := int64(0)
for i := 0; i < len(customExchangeRates); i++ {
customExchangeRate := customExchangeRates[i]
if _, exists := validators.AllCurrencyNames[customExchangeRate.Currency]; !exists {
continue
}
if customExchangeRate.UpdatedUnixTime > latestUpdateTime {
latestUpdateTime = customExchangeRate.UpdatedUnixTime
}
if hasDefaultCurrencyRate && baseCurrencyRate > 0 {
allExchangeRates = append(allExchangeRates, customExchangeRate.ToLatestExchangeRate(baseCurrencyRate))
}
}
sort.Sort(allExchangeRates)
if latestUpdateTime < 1 {
latestUpdateTime = time.Now().Unix()
}
if !hasDefaultCurrencyRate {
allExchangeRates = append(allExchangeRates, &models.LatestExchangeRate{
Currency: user.DefaultCurrency,
Rate: "1",
})
}
finalExchangeRateResponse := &models.LatestExchangeRateResponse{
DataSource: userDataSourceType,
ReferenceUrl: "",
UpdateTime: latestUpdateTime,
BaseCurrency: user.DefaultCurrency,
ExchangeRates: allExchangeRates,
}
return finalExchangeRateResponse, nil
}
func newUserCustomExchangeRatesDataSource() *UserCustomExchangeRatesDataSource {
return &UserCustomExchangeRatesDataSource{
users: services.Users,
userCustomExchangeRates: services.UserCustomExchangeRates,
}
}
+81 -1
View File
@@ -1,6 +1,39 @@
package models
import "strings"
import (
"strings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
const UserCustomExchangeRateFactorInDatabase = int64(100000000)
// UserCustomExchangeRate represents user custom exchange rate data
type UserCustomExchangeRate struct {
Uid int64 `xorm:"PK NOT NULL"`
DeletedUnixTime int64 `xorm:"PK NOT NULL"`
Currency string `xorm:"PK VARCHAR(3) NOT NULL"`
Rate int64 `xorm:"NOT NULL"`
CreatedUnixTime int64
UpdatedUnixTime int64
}
// UserCustomExchangeRateUpdateRequest represents all parameters of user custom exchange rate data updating request
type UserCustomExchangeRateUpdateRequest struct {
Currency string `json:"currency" binding:"required,len=3,validCurrency"`
Rate string `json:"rate"`
}
// UserCustomExchangeRateDeleteRequest represents all parameters of user custom exchange rate data deleting request
type UserCustomExchangeRateDeleteRequest struct {
Currency string `json:"currency" binding:"required,len=3,validCurrency"`
}
// UserCustomExchangeRateUpdateResponse represents a view-object of the result of updating user custom exchange rate data
type UserCustomExchangeRateUpdateResponse struct {
LatestExchangeRate
UpdateTime int64 `json:"updateTime"`
}
// LatestExchangeRateResponse returns a view-object which contains latest exchange rate
type LatestExchangeRateResponse struct {
@@ -17,6 +50,53 @@ type LatestExchangeRate struct {
Rate string `json:"rate"`
}
// ToLatestExchangeRate returns a data pair of currency and exchange rate according to database model
func (r *UserCustomExchangeRate) ToLatestExchangeRate(baseCurrencyRate int64) *LatestExchangeRate {
rate := float64(0)
if baseCurrencyRate > 0 {
rate = float64(r.Rate) / float64(baseCurrencyRate)
}
return &LatestExchangeRate{
Currency: r.Currency,
Rate: utils.Float64ToString(rate),
}
}
// ToUserCustomExchangeRateUpdateResponse returns a view-object of the result of updating user custom exchange rate data according to database model
func (r *UserCustomExchangeRate) ToUserCustomExchangeRateUpdateResponse(baseCurrencyRate int64) *UserCustomExchangeRateUpdateResponse {
return &UserCustomExchangeRateUpdateResponse{
LatestExchangeRate: *r.ToLatestExchangeRate(baseCurrencyRate),
UpdateTime: r.UpdatedUnixTime,
}
}
// CreateUserCustomExchangeRate returns a user custom exchange rate database model according to currency and rate
func CreateUserCustomExchangeRate(uid int64, currency string, exchangeRate string, baseCurrencyRate int64) (*UserCustomExchangeRate, error) {
if baseCurrencyRate <= 0 {
return &UserCustomExchangeRate{
Uid: uid,
Currency: currency,
Rate: UserCustomExchangeRateFactorInDatabase,
}, nil
}
rate, err := utils.StringToFloat64(exchangeRate)
if err != nil {
return nil, err
}
rate = rate * float64(baseCurrencyRate)
return &UserCustomExchangeRate{
Uid: uid,
Currency: currency,
Rate: int64(rate),
}, nil
}
// LatestExchangeRateSlice represents the slice data structure of LatestExchangeRate
type LatestExchangeRateSlice []*LatestExchangeRate
+153
View File
@@ -0,0 +1,153 @@
package services
import (
"time"
"xorm.io/xorm"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/datastore"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// UserCustomExchangeRatesService represents user custom exchange rate data service
type UserCustomExchangeRatesService struct {
ServiceUsingDB
}
// Initialize a user custom exchange rate data service singleton instance
var (
UserCustomExchangeRates = &UserCustomExchangeRatesService{
ServiceUsingDB: ServiceUsingDB{
container: datastore.Container,
},
}
)
// GetAllCustomExchangeRatesByUid returns all user exchange rate data models of user
func (s *UserCustomExchangeRatesService) GetAllCustomExchangeRatesByUid(c core.Context, uid int64) ([]*models.UserCustomExchangeRate, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
var customExchangeRates []*models.UserCustomExchangeRate
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted_unix_time=?", uid, 0).Find(&customExchangeRates)
return customExchangeRates, err
}
// UpdateCustomExchangeRate updates user exchange rate data model to database
func (s *UserCustomExchangeRatesService) UpdateCustomExchangeRate(c core.Context, uid int64, currency string, rate string, defaultCurrency string) (*models.UserCustomExchangeRate, *models.UserCustomExchangeRate, error) {
if uid <= 0 {
return nil, nil, errs.ErrUserIdInvalid
}
now := time.Now().Unix()
newCustomExchangeRate := &models.UserCustomExchangeRate{}
defaultCurrencyExchangeRate := &models.UserCustomExchangeRate{}
err := s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
oldCustomExchangeRate := &models.UserCustomExchangeRate{}
has, err := sess.Where("uid=? AND deleted_unix_time=? AND currency=?", uid, 0, currency).Get(oldCustomExchangeRate)
if err != nil {
return err
}
if has {
updateOldExchangeRateModel := &models.UserCustomExchangeRate{
DeletedUnixTime: now,
}
_, err = sess.Cols("deleted_unix_time").Where("uid=? AND deleted_unix_time=? AND currency=?", uid, 0, currency).Update(updateOldExchangeRateModel)
if err != nil {
return err
}
}
if currency != defaultCurrency {
has, err := sess.Where("uid=? AND deleted_unix_time=? AND currency=?", uid, 0, defaultCurrency).Get(defaultCurrencyExchangeRate)
if err != nil {
return err
}
if !has {
defaultCurrencyExchangeRate, _ = models.CreateUserCustomExchangeRate(uid, defaultCurrency, "1", 0)
defaultCurrencyExchangeRate.CreatedUnixTime = now
defaultCurrencyExchangeRate.UpdatedUnixTime = now
defaultCurrencyExchangeRate.DeletedUnixTime = 0
_, err = sess.Insert(defaultCurrencyExchangeRate)
if err != nil {
return err
}
}
} else {
defaultCurrencyExchangeRate = oldCustomExchangeRate
}
newCustomExchangeRate, err = models.CreateUserCustomExchangeRate(uid, currency, rate, defaultCurrencyExchangeRate.Rate)
newCustomExchangeRate.CreatedUnixTime = now
newCustomExchangeRate.UpdatedUnixTime = now
newCustomExchangeRate.DeletedUnixTime = 0
_, err = sess.Insert(newCustomExchangeRate)
return err
})
if err != nil {
return nil, nil, err
}
return newCustomExchangeRate, defaultCurrencyExchangeRate, err
}
// DeleteCustomExchangeRate deletes an existed user exchange rate data from database
func (s *UserCustomExchangeRatesService) DeleteCustomExchangeRate(c core.Context, uid int64, currency string) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
now := time.Now().Unix()
updateModel := &models.UserCustomExchangeRate{
DeletedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
deletedRows, err := sess.Cols("deleted_unix_time").Where("uid=? AND deleted_unix_time=? AND currency=?", uid, 0, currency).Update(updateModel)
if err != nil {
return err
} else if deletedRows < 1 {
return errs.ErrUserCustomExchangeRateNotFound
}
return err
})
}
// DeleteAllCustomExchangeRates deletes all existed user exchange rate data from database
func (s *UserCustomExchangeRatesService) DeleteAllCustomExchangeRates(c core.Context, uid int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
now := time.Now().Unix()
updateModel := &models.UserCustomExchangeRate{
DeletedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
_, err := sess.Cols("deleted_unix_time").Where("uid=? AND deleted_unix_time=?", uid, 0).Update(updateModel)
if err != nil {
return err
}
return nil
})
}
+3 -1
View File
@@ -117,6 +117,7 @@ const (
NationalBankOfUkraineDataSource string = "national_bank_of_ukraine"
CentralBankOfUzbekistanDataSource string = "central_bank_of_uzbekistan"
InternationalMonetaryFundDataSource string = "international_monetary_fund"
UserCustomExchangeRatesDataSource string = "user_custom"
)
const (
@@ -914,7 +915,8 @@ func loadExchangeRatesConfiguration(config *Config, configFile *ini.File, sectio
dataSource == SwissNationalBankDataSource ||
dataSource == NationalBankOfUkraineDataSource ||
dataSource == CentralBankOfUzbekistanDataSource ||
dataSource == InternationalMonetaryFundDataSource {
dataSource == InternationalMonetaryFundDataSource ||
dataSource == UserCustomExchangeRatesDataSource {
config.ExchangeRatesDataSource = dataSource
} else {
return errs.ErrInvalidExchangeRatesDataSource
+9
View File
@@ -37,6 +37,9 @@ import type {
DataStatisticsResponse
} from '@/models/data_management.ts';
import type {
UserCustomExchangeRateUpdateRequest,
UserCustomExchangeRateDeleteRequest,
UserCustomExchangeRateUpdateResponse,
LatestExchangeRateResponse
} from '@/models/exchange_rate.ts';
import type {
@@ -574,6 +577,12 @@ export default {
timeout: getExchangeRatesRequestTimeout() || DEFAULT_API_TIMEOUT
} as ApiRequestConfig);
},
updateUserCustomExchangeRate: (req: UserCustomExchangeRateUpdateRequest): ApiResponsePromise<UserCustomExchangeRateUpdateResponse> => {
return axios.post<ApiResponse<UserCustomExchangeRateUpdateResponse>>('v1/exchange_rates/user_custom/update.json', req);
},
deleteUserCustomExchangeRate: (req: UserCustomExchangeRateDeleteRequest): ApiResponsePromise<boolean> => {
return axios.post<ApiResponse<boolean>>('v1/exchange_rates/user_custom/delete.json', req);
},
generateQrCodeUrl: (qrCodeName: string): string => {
return `${getBasePath()}${BASE_QRCODE_PATH}/${qrCodeName}.png`;
},
+9
View File
@@ -1171,6 +1171,9 @@
"invalid beancount file": "Invalid Beancount file",
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
"invalid amount expression": "Amount expression is invalid",
"user custom exchange rate data not found": "User custom exchange rate data is not found",
"cannot update exchange rate data for base currency": "Cannot update exchange rate data for base currency",
"cannot delete exchange rate data for base currency": "Cannot delete exchange rate data for base currency",
"query items cannot be blank": "Abfrageelemente dürfen nicht leer sein",
"query items too much": "Zu viele Abfrageelemente",
"query items have invalid item": "Ungültiges Element in Abfrageelementen",
@@ -2039,6 +2042,7 @@
"WebAuthn is not enabled": "WebAuthn ist nicht aktiviert",
"Are you sure you want to re-login?": "Sind Sie sicher, dass Sie sich erneut anmelden möchten?",
"Exchange Rates Data": "Wechselkursdaten",
"User Custom": "User Custom",
"Base Currency": "Basiswährung",
"Base Amount": "Basisbetrag",
"Set as Base": "Als Basis festlegen",
@@ -2049,6 +2053,11 @@
"Exchange rates data has been updated": "Wechselkursdaten wurden aktualisiert",
"Exchange rates data is up to date": "Wechselkursdaten sind aktuell",
"Unable to retrieve exchange rates data": "Wechselkursdaten können nicht abgerufen werden",
"Update User Custom Exchange Rate": "Update User Custom Exchange Rate",
"You have updated exchange rate": "You have updated exchange rate",
"Unable to update user custom exchange rate": "Unable to update user custom exchange rate",
"Are you sure you want to delete this user custom exchange rate?": "Are you sure you want to delete this user custom exchange rate?",
"Unable to delete this user custom exchange rate": "Unable to delete this user custom exchange rate",
"Use on Mobile Device": "Auf Mobilgerät verwenden",
"You can scan the QR code below on your mobile device.": "Sie können den untenstehenden QR-Code auf Ihrem Mobilgerät scannen.",
"Switch to Mobile Version": "Zur mobilen Version wechseln",
+9
View File
@@ -1171,6 +1171,9 @@
"invalid beancount file": "Invalid Beancount file",
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
"invalid amount expression": "Amount expression is invalid",
"user custom exchange rate data not found": "User custom exchange rate data is not found",
"cannot update exchange rate data for base currency": "Cannot update exchange rate data for base currency",
"cannot delete exchange rate data for base currency": "Cannot delete exchange rate data for base currency",
"query items cannot be blank": "There are no query items",
"query items too much": "There are too many query items",
"query items have invalid item": "There is invalid item in query items",
@@ -2039,6 +2042,7 @@
"WebAuthn is not enabled": "WebAuthn is not enabled",
"Are you sure you want to re-login?": "Are you sure you want to re-login?",
"Exchange Rates Data": "Exchange Rates Data",
"User Custom": "User Custom",
"Base Currency": "Base Currency",
"Base Amount": "Base Amount",
"Set as Base": "Set as Base",
@@ -2049,6 +2053,11 @@
"Exchange rates data has been updated": "Exchange rates data has been updated",
"Exchange rates data is up to date": "Exchange rates data is up to date",
"Unable to retrieve exchange rates data": "Unable to retrieve exchange rates data",
"Update User Custom Exchange Rate": "Update User Custom Exchange Rate",
"You have updated exchange rate": "You have updated exchange rate",
"Unable to update user custom exchange rate": "Unable to update user custom exchange rate",
"Are you sure you want to delete this user custom exchange rate?": "Are you sure you want to delete this user custom exchange rate?",
"Unable to delete this user custom exchange rate": "Unable to delete this user custom exchange rate",
"Use on Mobile Device": "Use on Mobile Device",
"You can scan the QR code below on your mobile device.": "You can scan the QR code below on your mobile device.",
"Switch to Mobile Version": "Switch to Mobile Version",
+9
View File
@@ -1170,6 +1170,9 @@
"invalid beancount file": "Invalid Beancount file",
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
"invalid amount expression": "Amount expression is invalid",
"user custom exchange rate data not found": "User custom exchange rate data is not found",
"cannot update exchange rate data for base currency": "Cannot update exchange rate data for base currency",
"cannot delete exchange rate data for base currency": "Cannot delete exchange rate data for base currency",
"query items cannot be blank": "--",
"query items too much": "--",
"query items have invalid item": "Hay un elemento no válido en los elementos de consulta",
@@ -2038,6 +2041,7 @@
"WebAuthn is not enabled": "WebAuthn no está habilitado",
"Are you sure you want to re-login?": "¿Está seguro de que desea volver a iniciar sesión?",
"Exchange Rates Data": "Datos de tipos de cambio",
"User Custom": "User Custom",
"Base Currency": "Moneda base",
"Base Amount": "Importe base",
"Set as Base": "Establecer como base",
@@ -2048,6 +2052,11 @@
"Exchange rates data has been updated": "Se han actualizado los datos de tipos de cambio.",
"Exchange rates data is up to date": "Los datos de tipos de cambio están actualizados.",
"Unable to retrieve exchange rates data": "No se pueden recuperar datos de tipos de cambio",
"Update User Custom Exchange Rate": "Update User Custom Exchange Rate",
"You have updated exchange rate": "You have updated exchange rate",
"Unable to update user custom exchange rate": "Unable to update user custom exchange rate",
"Are you sure you want to delete this user custom exchange rate?": "Are you sure you want to delete this user custom exchange rate?",
"Unable to delete this user custom exchange rate": "Unable to delete this user custom exchange rate",
"Use on Mobile Device": "Usar en dispositivo móvil",
"You can scan the QR code below on your mobile device.": "Puede escanear el código QR a continuación en su dispositivo móvil.",
"Switch to Mobile Version": "Cambiar a la versión móvil",
+9
View File
@@ -1171,6 +1171,9 @@
"invalid beancount file": "File Beancount non valido",
"not support include directive for beancount file": "Direttiva \"include\" non supportata per il file Beancount",
"invalid amount expression": "Espressione dell'importo non valida",
"user custom exchange rate data not found": "User custom exchange rate data is not found",
"cannot update exchange rate data for base currency": "Cannot update exchange rate data for base currency",
"cannot delete exchange rate data for base currency": "Cannot delete exchange rate data for base currency",
"query items cannot be blank": "Non ci sono elementi di query",
"query items too much": "Ci sono troppi elementi di query",
"query items have invalid item": "C'è un elemento non valido negli elementi di query",
@@ -2039,6 +2042,7 @@
"WebAuthn is not enabled": "WebAuthn non è abilitato",
"Are you sure you want to re-login?": "Sei sicuro di voler accedere di nuovo?",
"Exchange Rates Data": "Dati tassi di cambio",
"User Custom": "User Custom",
"Base Currency": "Valuta di base",
"Base Amount": "Importo di base",
"Set as Base": "Imposta come base",
@@ -2049,6 +2053,11 @@
"Exchange rates data has been updated": "I dati sui tassi di cambio sono stati aggiornati",
"Exchange rates data is up to date": "I dati sui tassi di cambio sono aggiornati",
"Unable to retrieve exchange rates data": "Impossibile recuperare i dati sui tassi di cambio",
"Update User Custom Exchange Rate": "Update User Custom Exchange Rate",
"You have updated exchange rate": "You have updated exchange rate",
"Unable to update user custom exchange rate": "Unable to update user custom exchange rate",
"Are you sure you want to delete this user custom exchange rate?": "Are you sure you want to delete this user custom exchange rate?",
"Unable to delete this user custom exchange rate": "Unable to delete this user custom exchange rate",
"Use on Mobile Device": "Usa su dispositivo mobile",
"You can scan the QR code below on your mobile device.": "Puoi scansionare il codice QR qui sotto sul tuo dispositivo mobile.",
"Switch to Mobile Version": "Passa alla versione mobile",
+9
View File
@@ -1171,6 +1171,9 @@
"invalid beancount file": "Invalid Beancount file",
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
"invalid amount expression": "Amount expression is invalid",
"user custom exchange rate data not found": "User custom exchange rate data is not found",
"cannot update exchange rate data for base currency": "Cannot update exchange rate data for base currency",
"cannot delete exchange rate data for base currency": "Cannot delete exchange rate data for base currency",
"query items cannot be blank": "クエリ項目がありません",
"query items too much": "クエリ項目が多すぎます",
"query items have invalid item": "クエリ項目に無効な項目があります",
@@ -2039,6 +2042,7 @@
"WebAuthn is not enabled": "WebAuthnが有効になっていません",
"Are you sure you want to re-login?": "再ログインしますか?",
"Exchange Rates Data": "為替レートデータ",
"User Custom": "User Custom",
"Base Currency": "ベース通貨",
"Base Amount": "ベース金額",
"Set as Base": "ベースとして設定",
@@ -2049,6 +2053,11 @@
"Exchange rates data has been updated": "為替レートデータが更新されました",
"Exchange rates data is up to date": "為替レートデータは最新です",
"Unable to retrieve exchange rates data": "為替レートデータを取得できません",
"Update User Custom Exchange Rate": "Update User Custom Exchange Rate",
"You have updated exchange rate": "You have updated exchange rate",
"Unable to update user custom exchange rate": "Unable to update user custom exchange rate",
"Are you sure you want to delete this user custom exchange rate?": "Are you sure you want to delete this user custom exchange rate?",
"Unable to delete this user custom exchange rate": "Unable to delete this user custom exchange rate",
"Use on Mobile Device": "モバイルデバイスで使う",
"You can scan the QR code below on your mobile device.": "以下のQRコードをモバイルデバイスでスキャンできます。",
"Switch to Mobile Version": "モバイルバージョンに切り替え",
+9
View File
@@ -1171,6 +1171,9 @@
"invalid beancount file": "Invalid Beancount file",
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
"invalid amount expression": "Amount expression is invalid",
"user custom exchange rate data not found": "User custom exchange rate data is not found",
"cannot update exchange rate data for base currency": "Cannot update exchange rate data for base currency",
"cannot delete exchange rate data for base currency": "Cannot delete exchange rate data for base currency",
"query items cannot be blank": "Нет элементов запроса",
"query items too much": "Слишком много элементов запроса",
"query items have invalid item": "В элементах запроса присутствует недопустимый элемент",
@@ -2039,6 +2042,7 @@
"WebAuthn is not enabled": "WebAuthn не включен",
"Are you sure you want to re-login?": "Вы уверены, что хотите войти снова?",
"Exchange Rates Data": "Данные о курсах валют",
"User Custom": "User Custom",
"Base Currency": "Базовая валюта",
"Base Amount": "Базовая сумма",
"Set as Base": "Установить как базовую",
@@ -2049,6 +2053,11 @@
"Exchange rates data has been updated": "Данные о курсах валют обновлены",
"Exchange rates data is up to date": "Данные о курсах валют актуальны",
"Unable to retrieve exchange rates data": "Не удалось получить данные о курсах валют",
"Update User Custom Exchange Rate": "Update User Custom Exchange Rate",
"You have updated exchange rate": "You have updated exchange rate",
"Unable to update user custom exchange rate": "Unable to update user custom exchange rate",
"Are you sure you want to delete this user custom exchange rate?": "Are you sure you want to delete this user custom exchange rate?",
"Unable to delete this user custom exchange rate": "Unable to delete this user custom exchange rate",
"Use on Mobile Device": "Использовать на мобильном устройстве",
"You can scan the QR code below on your mobile device.": "Вы можете отсканировать QR-код ниже на своем мобильном устройстве.",
"Switch to Mobile Version": "Переключиться на мобильную версию",
+9
View File
@@ -1171,6 +1171,9 @@
"invalid beancount file": "Недійсний файл Beancount",
"not support include directive for beancount file": "Не підтримується директива \"include\" у файлі Beancount",
"invalid amount expression": "Недійсний вираз суми",
"user custom exchange rate data not found": "User custom exchange rate data is not found",
"cannot update exchange rate data for base currency": "Cannot update exchange rate data for base currency",
"cannot delete exchange rate data for base currency": "Cannot delete exchange rate data for base currency",
"query items cannot be blank": "Елементи запиту не можуть бути порожніми",
"query items too much": "Занадто багато елементів запиту",
"query items have invalid item": "Запит містить недійсний елемент",
@@ -2039,6 +2042,7 @@
"WebAuthn is not enabled": "WebAuthn не увімкнено",
"Are you sure you want to re-login?": "Ви впевнені, що хочете увійти знову?",
"Exchange Rates Data": "Дані про курси валют",
"User Custom": "User Custom",
"Base Currency": "Базова валюта",
"Base Amount": "Базова сума",
"Set as Base": "Зробити базовою",
@@ -2049,6 +2053,11 @@
"Exchange rates data has been updated": "Курси валют оновлено",
"Exchange rates data is up to date": "Курси валют оновлено",
"Unable to retrieve exchange rates data": "Не вдалося отримати курси валют",
"Update User Custom Exchange Rate": "Update User Custom Exchange Rate",
"You have updated exchange rate": "You have updated exchange rate",
"Unable to update user custom exchange rate": "Unable to update user custom exchange rate",
"Are you sure you want to delete this user custom exchange rate?": "Are you sure you want to delete this user custom exchange rate?",
"Unable to delete this user custom exchange rate": "Unable to delete this user custom exchange rate",
"Use on Mobile Device": "Використовувати на мобільному пристрої",
"You can scan the QR code below on your mobile device.": "Ви можете відсканувати наведений нижче QR-код на своєму мобільному пристрої.",
"Switch to Mobile Version": "Переключитися на мобільну версію",
+9
View File
@@ -1171,6 +1171,9 @@
"invalid beancount file": "Invalid Beancount file",
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
"invalid amount expression": "Amount expression is invalid",
"user custom exchange rate data not found": "User custom exchange rate data is not found",
"cannot update exchange rate data for base currency": "Cannot update exchange rate data for base currency",
"cannot delete exchange rate data for base currency": "Cannot delete exchange rate data for base currency",
"query items cannot be blank": "Không có mục truy vấn",
"query items too much": "Có quá nhiều mục truy vấn",
"query items have invalid item": "Có mục không hợp lệ trong các mục truy vấn",
@@ -2039,6 +2042,7 @@
"WebAuthn is not enabled": "WebAuthn chưa được bật",
"Are you sure you want to re-login?": "Bạn có chắc chắn muốn đăng nhập lại không?",
"Exchange Rates Data": "Dữ liệu tỷ giá hối đoái",
"User Custom": "User Custom",
"Base Currency": "Tiền tệ cơ sở",
"Base Amount": "Số tiền cơ sở",
"Set as Base": "Đặt làm cơ sở",
@@ -2049,6 +2053,11 @@
"Exchange rates data has been updated": "Dữ liệu tỷ giá hối đoái đã được cập nhật",
"Exchange rates data is up to date": "Dữ liệu tỷ giá hối đoái đã được cập nhật",
"Unable to retrieve exchange rates data": "Không thể truy xuất dữ liệu tỷ giá hối đoái",
"Update User Custom Exchange Rate": "Update User Custom Exchange Rate",
"You have updated exchange rate": "You have updated exchange rate",
"Unable to update user custom exchange rate": "Unable to update user custom exchange rate",
"Are you sure you want to delete this user custom exchange rate?": "Are you sure you want to delete this user custom exchange rate?",
"Unable to delete this user custom exchange rate": "Unable to delete this user custom exchange rate",
"Use on Mobile Device": "Sử dụng trên thiết bị di động",
"You can scan the QR code below on your mobile device.": "Bạn có thể quét mã QR bên dưới trên thiết bị di động của mình.",
"Switch to Mobile Version": "Chuyển sang phiên bản di động",
+9
View File
@@ -1171,6 +1171,9 @@
"invalid beancount file": "无效的 Beancount 文件",
"not support include directive for beancount file": "不支持 Beancount 文件的 \"include\" 指令",
"invalid amount expression": "金额表达式无效",
"user custom exchange rate data not found": "用户自定义汇率数据不存在",
"cannot update exchange rate data for base currency": "不能更新默认货币的汇率数据",
"cannot delete exchange rate data for base currency": "不能删除默认货币的汇率数据",
"query items cannot be blank": "请求项目不能为空",
"query items too much": "请求项目过多",
"query items have invalid item": "请求项目中有非法项目",
@@ -2039,6 +2042,7 @@
"WebAuthn is not enabled": "WebAuthn 没有启用",
"Are you sure you want to re-login?": "您确定要重新登录?",
"Exchange Rates Data": "汇率数据",
"User Custom": "用户自定义",
"Base Currency": "基准货币",
"Base Amount": "基准金额",
"Set as Base": "设置为基准",
@@ -2049,6 +2053,11 @@
"Exchange rates data has been updated": "汇率数据已更新",
"Exchange rates data is up to date": "汇率数据已是最新",
"Unable to retrieve exchange rates data": "无法获取汇率数据",
"Update User Custom Exchange Rate": "更新用户自定义汇率",
"You have updated exchange rate": "您已经更新了汇率",
"Unable to update user custom exchange rate": "无法更新用户自定义汇率",
"Are you sure you want to delete this user custom exchange rate?": "您确定要删除该用户自定义汇率?",
"Unable to delete this user custom exchange rate": "无法删除该用户自定义汇率",
"Use on Mobile Device": "在移动设备使用",
"You can scan the QR code below on your mobile device.": "您可以在您的移动设备上扫描下方二维码。",
"Switch to Mobile Version": "切换到移动版",
+9
View File
@@ -1171,6 +1171,9 @@
"invalid beancount file": "無效的 Beancount 檔案",
"not support include directive for beancount file": "不支援 Beancount 檔案的 \"include\" 指令",
"invalid amount expression": "金額表達式無效",
"user custom exchange rate data not found": "使用者自訂匯率資料不存在",
"cannot update exchange rate data for base currency": "不能更新基準貨幣的匯率資料",
"cannot delete exchange rate data for base currency": "不能刪除基準貨幣的匯率資料",
"query items cannot be blank": "查詢項目不能為空",
"query items too much": "查詢項目過多",
"query items have invalid item": "查詢項目中有非法項目",
@@ -2039,6 +2042,7 @@
"WebAuthn is not enabled": "WebAuthn 沒有啟用",
"Are you sure you want to re-login?": "您確定要重新登入?",
"Exchange Rates Data": "匯率資料",
"User Custom": "使用者自訂",
"Base Currency": "基準貨幣",
"Base Amount": "基準金額",
"Set as Base": "設定為基準",
@@ -2049,6 +2053,11 @@
"Exchange rates data has been updated": "匯率資料已更新",
"Exchange rates data is up to date": "匯率資料已是最新",
"Unable to retrieve exchange rates data": "無法取得匯率資料",
"Update User Custom Exchange Rate": "更新使用者自訂匯率",
"You have updated exchange rate": "您已經更新了匯率",
"Unable to update user custom exchange rate": "無法更新使用者自訂匯率",
"Are you sure you want to delete this user custom exchange rate?": "您確定要刪除此使用者自訂匯率?",
"Unable to delete this user custom exchange rate": "無法刪除此使用者自訂匯率",
"Use on Mobile Device": "在行動裝置使用",
"You can scan the QR code below on your mobile device.": "您可以在您的行動裝置上掃描下方的 QR Code。",
"Switch to Mobile Version": "切換到行動版",
+14 -1
View File
@@ -1,3 +1,16 @@
export interface UserCustomExchangeRateUpdateRequest {
readonly currency: string;
readonly rate: string;
}
export interface UserCustomExchangeRateDeleteRequest {
readonly currency: string;
}
export interface UserCustomExchangeRateUpdateResponse extends LatestExchangeRate {
readonly updateTime: number;
}
export interface LatestExchangeRate {
readonly currency: string;
readonly rate: string;
@@ -6,7 +19,7 @@ export interface LatestExchangeRate {
export interface LatestExchangeRateResponse {
readonly dataSource: string;
readonly referenceUrl: string;
readonly updateTime: number;
updateTime: number;
readonly baseCurrency: string;
readonly exchangeRates: LatestExchangeRate[];
}
+2 -2
View File
@@ -28,7 +28,7 @@ import TransactionTemplateListPage from '@/views/desktop/templates/ListPage.vue'
import UserSettingsPage from '@/views/desktop/user/UserSettingsPage.vue';
import AppSettingsPage from '@/views/desktop/app/AppSettingsPage.vue';
import ExchangeRatesPage from '@/views/desktop/ExchangeRatesPage.vue';
import ExchangeRatesListPage from '@/views/desktop/exchangerates/ListPage.vue';
import AboutPage from '@/views/desktop/AboutPage.vue';
function checkLogin(): NavigationGuardReturn {
@@ -168,7 +168,7 @@ const router = createRouter({
},
{
path: '/exchange_rates',
component: ExchangeRatesPage,
component: ExchangeRatesListPage,
beforeEnter: checkLogin
},
{
+8 -2
View File
@@ -25,7 +25,8 @@ import TransactionTagFilterSettingsPage from '@/views/mobile/settings/Transactio
import SettingsPage from '@/views/mobile/SettingsPage.vue';
import ApplicationLockPage from '@/views/mobile/ApplicationLockPage.vue';
import ExchangeRatesPage from '@/views/mobile/ExchangeRatesPage.vue';
import ExchangeRatesListPage from '@/views/mobile/exchangerates/ListPage.vue';
import ExchangeRatesUpdatePage from '@/views/mobile/exchangerates/UpdatePage.vue';
import AboutPage from '@/views/mobile/AboutPage.vue';
import UserProfilePage from '@/views/mobile/users/UserProfilePage.vue';
@@ -236,7 +237,12 @@ const routes: Router.RouteParameters[] = [
},
{
path: '/exchange_rates',
async: asyncResolve(ExchangeRatesPage),
async: asyncResolve(ExchangeRatesListPage),
beforeEnter: [checkLogin]
},
{
path: '/exchange_rates/update',
async: asyncResolve(ExchangeRatesUpdatePage),
beforeEnter: [checkLogin]
},
{
+138 -1
View File
@@ -1,7 +1,13 @@
import { ref, computed } from 'vue';
import { defineStore } from 'pinia';
import type { LatestExchangeRate, LatestExchangeRateResponse } from '@/models/exchange_rate.ts';
import type { BeforeResolveFunction } from '@/core/base.ts';
import type {
UserCustomExchangeRateUpdateResponse,
LatestExchangeRate,
LatestExchangeRateResponse
} from '@/models/exchange_rate.ts';
import { isEquals } from '@/lib/common.ts';
import { getCurrentUnixTime, formatUnixTime } from '@/lib/datetime.ts';
@@ -11,6 +17,7 @@ import logger from '@/lib/logger.ts';
import services from '@/lib/services.ts';
const exchangeRatesLocalStorageKey = 'ebk_app_exchange_rates';
const userDataSourceType = 'user_custom';
interface LatestExchangeRates {
readonly time?: number;
@@ -34,6 +41,14 @@ function clearExchangeRatesFromLocalStorage(): void {
export const useExchangeRatesStore = defineStore('exchangeRates', () => {
const latestExchangeRates = ref<LatestExchangeRates>(getExchangeRatesFromLocalStorage());
const isUserCustomExchangeRates = computed((): boolean => {
if (!latestExchangeRates.value || !latestExchangeRates.value.data) {
return false;
}
return latestExchangeRates.value.data.dataSource === userDataSourceType;
});
const exchangeRatesLastUpdateTime = computed<number | null>(() => {
const exchangeRates = latestExchangeRates.value || {};
return exchangeRates && exchangeRates.data ? exchangeRates.data.updateTime : null;
@@ -54,6 +69,55 @@ export const useExchangeRatesStore = defineStore('exchangeRates', () => {
return exchangeRateMap;
});
function updateExchangeRateToLatestExchangeRateList(exchangeRate: LatestExchangeRate, updateTime: number): void {
if (!latestExchangeRates.value || !latestExchangeRates.value.data || !latestExchangeRates.value.data.exchangeRates) {
return;
}
const exchangeRates = latestExchangeRates.value.data.exchangeRates;
let changed = false;
for (let i = 0; i < exchangeRates.length; i++) {
if (exchangeRates[i].currency === exchangeRate.currency) {
exchangeRates.splice(i, 1, exchangeRate);
changed = true;
break;
}
}
if (!changed) {
exchangeRates.push(exchangeRate);
changed = true;
}
latestExchangeRates.value.data.updateTime = updateTime;
if (changed) {
setExchangeRatesToLocalStorage(latestExchangeRates.value);
}
}
function removeExchangeRateFromLatestExchangeRateList(currency: string): void {
if (!latestExchangeRates.value || !latestExchangeRates.value.data || !latestExchangeRates.value.data.exchangeRates) {
return;
}
const exchangeRates = latestExchangeRates.value.data.exchangeRates;
let changed = false;
for (let i = 0; i < exchangeRates.length; i++) {
if (exchangeRates[i].currency === currency) {
exchangeRates.splice(i, 1);
changed = true;
break;
}
}
if (changed) {
setExchangeRatesToLocalStorage(latestExchangeRates.value);
}
}
function resetLatestExchangeRates(): void {
latestExchangeRates.value = {};
clearExchangeRatesFromLocalStorage();
@@ -114,6 +178,76 @@ export const useExchangeRatesStore = defineStore('exchangeRates', () => {
});
}
function updateUserCustomExchangeRate({ currency, rate }: { currency: string, rate: number }): Promise<UserCustomExchangeRateUpdateResponse> {
return new Promise((resolve, reject) => {
services.updateUserCustomExchangeRate({
currency: currency,
rate: rate.toString()
}).then(response => {
const data = response.data;
if (!data || !data.success || !data.result) {
reject({ message: 'Unable to update user custom exchange rate' });
return;
}
const exchangeRate: LatestExchangeRate = {
currency: data.result.currency,
rate: data.result.rate
};
updateExchangeRateToLatestExchangeRateList(exchangeRate, data.result.updateTime);
resolve(data.result);
}).catch(error => {
logger.error('failed to update user custom exchange rate', error);
if (error.response && error.response.data && error.response.data.errorMessage) {
reject({ error: error.response.data });
} else if (!error.processed) {
reject({ message: 'Unable to update user custom exchange rate' });
} else {
reject(error);
}
});
});
}
function deleteUserCustomExchangeRate({ currency, beforeResolve }: { currency: string, beforeResolve?: BeforeResolveFunction }): Promise<boolean> {
return new Promise((resolve, reject) => {
services.deleteUserCustomExchangeRate({
currency: currency
}).then(response => {
const data = response.data;
if (!data || !data.success || !data.result) {
reject({ message: 'Unable to delete this user custom exchange rate' });
return;
}
if (beforeResolve) {
beforeResolve(() => {
removeExchangeRateFromLatestExchangeRateList(currency);
});
} else {
removeExchangeRateFromLatestExchangeRateList(currency);
}
resolve(data.result);
}).catch(error => {
logger.error('failed to delete user custom exchange rate', error);
if (error.response && error.response.data && error.response.data.errorMessage) {
reject({ error: error.response.data });
} else if (!error.processed) {
reject({ message: 'Unable to delete this user custom exchange rate' });
} else {
reject(error);
}
});
});
}
function getExchangedAmount(amount: number, fromCurrency: string, toCurrency: string): number | null {
if (amount === 0) {
return 0;
@@ -145,11 +279,14 @@ export const useExchangeRatesStore = defineStore('exchangeRates', () => {
// states
latestExchangeRates,
// computed states
isUserCustomExchangeRates,
exchangeRatesLastUpdateTime,
latestExchangeRateMap,
// functions
resetLatestExchangeRates,
getLatestExchangeRates,
updateUserCustomExchangeRate,
deleteUserCustomExchangeRate,
getExchangedAmount
};
});
+6
View File
@@ -378,6 +378,8 @@ export const useRootStore = defineStore('root', () => {
}
function updateUserProfile(req: UserProfileUpdateRequest): Promise<UserProfileUpdateResponse> {
const userDefaultCurrency = userStore.currentUserDefaultCurrency;
return new Promise((resolve, reject) => {
services.updateProfile(req).then(response => {
const data = response.data;
@@ -407,6 +409,10 @@ export const useRootStore = defineStore('root', () => {
statisticsStore.updateTransactionStatisticsInvalidState(true);
}
if (data.result.user && data.result.user.defaultCurrency !== userDefaultCurrency) {
exchangeRatesStore.resetLatestExchangeRates();
}
resolve(data.result);
}).catch(error => {
logger.error('failed to save user profile', error);
+1
View File
@@ -47,6 +47,7 @@
--ebk-icon-font-size: 28px;
--ebk-icon-text-margin: 2px;
--ebk-hide-icon-font-size: 18px;
--ebk-separate-icon-font-size: 24px;
--ebk-big-icon-button-size: 42px;
--ebk-icon-after-text-font-size: 16px;
--ebk-right-bottom-icon-font-size: 13px;
+1
View File
@@ -47,6 +47,7 @@
--ebk-icon-font-size: 29px;
--ebk-icon-text-margin: 3px;
--ebk-hide-icon-font-size: 19px;
--ebk-separate-icon-font-size: 28px;
--ebk-big-icon-button-size: 44px;
--ebk-icon-after-text-font-size: 17px;
--ebk-right-bottom-icon-font-size: 13px;
+1
View File
@@ -47,6 +47,7 @@
--ebk-icon-font-size: 28px;
--ebk-icon-text-margin: 2px;
--ebk-hide-icon-font-size: 18px;
--ebk-separate-icon-font-size: 20px;
--ebk-big-icon-button-size: 42px;
--ebk-icon-after-text-font-size: 15px;
--ebk-right-bottom-icon-font-size: 13px;
+1
View File
@@ -47,6 +47,7 @@
--ebk-icon-font-size: 30px;
--ebk-icon-text-margin: 3px;
--ebk-hide-icon-font-size: 20px;
--ebk-separate-icon-font-size: 32px;
--ebk-big-icon-button-size: 48px;
--ebk-icon-after-text-font-size: 17px;
--ebk-right-bottom-icon-font-size: 14px;
+1
View File
@@ -47,6 +47,7 @@
--ebk-icon-font-size: 32px;
--ebk-icon-text-margin: 4px;
--ebk-hide-icon-font-size: 24px;
--ebk-separate-icon-font-size: 36px;
--ebk-big-icon-button-size: 54px;
--ebk-icon-after-text-font-size: 18px;
--ebk-right-bottom-icon-font-size: 15px;
@@ -47,6 +47,7 @@
--ebk-icon-font-size: 34px;
--ebk-icon-text-margin: 4px;
--ebk-hide-icon-font-size: 26px;
--ebk-separate-icon-font-size: 40px;
--ebk-big-icon-button-size: 58px;
--ebk-icon-after-text-font-size: 20px;
--ebk-right-bottom-icon-font-size: 17px;
@@ -47,6 +47,7 @@
--ebk-icon-font-size: 36px;
--ebk-icon-text-margin: 4px;
--ebk-hide-icon-font-size: 28px;
--ebk-separate-icon-font-size: 44px;
--ebk-big-icon-button-size: 64px;
--ebk-icon-after-text-font-size: 22px;
--ebk-right-bottom-icon-font-size: 19px;
+4
View File
@@ -152,6 +152,10 @@ i.icon.la, i.icon.las, i.icon.lab {
font-size: var(--ebk-hide-icon-font-size);
}
.separate-icon {
font-size: var(--ebk-separate-icon-font-size);
}
.transaction-tag-icon {
font-size: var(--ebk-transaction-tag-icon-font-size);
align-self: center;
+4
View File
@@ -22,7 +22,9 @@ export function useExchangeRatesPageBase() {
const baseCurrency = ref<string>(userStore.currentUserDefaultCurrency);
const baseAmount = ref<number>(100);
const defaultCurrency = computed<string>(() => userStore.currentUserDefaultCurrency);
const exchangeRatesData = computed<LatestExchangeRateResponse | undefined>(() => exchangeRatesStore.latestExchangeRates.data);
const isUserCustomExchangeRates = computed<boolean>(() => exchangeRatesStore.isUserCustomExchangeRates);
const exchangeRatesDataUpdateTime = computed<string>(() => {
const exchangeRatesLastUpdateTime = exchangeRatesStore.exchangeRatesLastUpdateTime;
@@ -55,7 +57,9 @@ export function useExchangeRatesPageBase() {
baseCurrency,
baseAmount,
// computed states
defaultCurrency,
exchangeRatesData,
isUserCustomExchangeRates,
exchangeRatesDataUpdateTime,
availableExchangeRates,
// functions
@@ -7,8 +7,9 @@
<div class="mx-6 my-4">
<span class="text-subtitle-2">{{ tt('Data source') }}</span>
<p class="text-body-1 mt-1 mb-3">
<a tabindex="-1" target="_blank" :href="exchangeRatesData.referenceUrl" v-if="!loading && exchangeRatesData && exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</a>
<span v-else-if="!loading && exchangeRatesData && !exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</span>
<a tabindex="-1" target="_blank" :href="exchangeRatesData.referenceUrl" v-if="!loading && exchangeRatesData && !isUserCustomExchangeRates && exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</a>
<span v-else-if="!loading && exchangeRatesData && !isUserCustomExchangeRates && !exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</span>
<span v-else-if="!loading && exchangeRatesData && isUserCustomExchangeRates">{{ tt('User Custom') }}</span>
<span v-else-if="!loading && !exchangeRatesData">{{ tt('None') }}</span>
<span v-else-if="loading">
<v-skeleton-loader class="skeleton-no-margin mt-3 mb-4" type="text" :loading="true"></v-skeleton-loader>
@@ -65,6 +66,9 @@
<v-icon :icon="mdiMenu" size="24" />
</v-btn>
<span>{{ tt('Exchange Rates Data') }}</span>
<v-btn class="ml-3" color="default" variant="outlined"
:disabled="loading" @click="update"
v-if="isUserCustomExchangeRates">{{ tt('Update') }}</v-btn>
<v-btn density="compact" color="default" variant="text" size="24"
class="ml-2" :icon="true" :loading="loading" @click="reload(true)">
<template #loader>
@@ -107,15 +111,30 @@
<div class="d-flex align-center">
<span class="text-sm">{{ exchangeRate.currencyDisplayName }}</span>
<span class="text-caption ml-1">{{ exchangeRate.currencyCode }}</span>
<v-spacer/>
<v-btn class="px-2 ml-2 mr-3" color="default"
<v-btn class="px-2 ml-2" color="default"
density="comfortable" variant="text"
:class="{ 'd-none': loading, 'hover-display': !loading }"
v-if="exchangeRate.currencyCode !== baseCurrency"
@click="setAsBaseline(exchangeRate.currencyCode, getFinalConvertedAmount(exchangeRate))">
{{ tt('Set as Base') }}
</v-btn>
<span>{{ getFinalConvertedAmount(exchangeRate) }}</span>
<v-btn class="px-2" color="default"
density="comfortable" variant="text"
:class="{ 'd-none': loading, 'hover-display': !loading }"
:prepend-icon="mdiDeleteOutline"
:loading="customExchangeRateRemoving[exchangeRate.currencyCode]"
:disabled="loading || updating"
v-if="exchangeRate.currencyCode !== defaultCurrency && isUserCustomExchangeRates"
@click="remove(exchangeRate.currencyCode)">
<template #loader>
<v-progress-circular indeterminate size="20" width="2"/>
</template>
{{ tt('Delete') }}
</v-btn>
<span class="ml-3">{{ getFinalConvertedAmount(exchangeRate) }}</span>
</div>
</td>
</tr>
@@ -130,11 +149,16 @@
</v-col>
</v-row>
<update-dialog ref="updateDialog" />
<confirm-dialog ref="confirmDialog"/>
<snack-bar ref="snackbar" />
</template>
<script setup lang="ts">
import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue';
import SnackBar from '@/components/desktop/SnackBar.vue';
import UpdateDialog from './list/dialogs/UpdateDialog.vue';
import { ref, useTemplateRef, watch } from 'vue';
import { useDisplay } from 'vuetify';
@@ -150,22 +174,39 @@ import logger from '@/lib/logger.ts';
import {
mdiRefresh,
mdiMenu
mdiMenu,
mdiDeleteOutline
} from '@mdi/js';
type ConfirmDialogType = InstanceType<typeof ConfirmDialog>;
type SnackBarType = InstanceType<typeof SnackBar>;
type UpdateDialogType = InstanceType<typeof UpdateDialog>;
const { mdAndUp } = useDisplay();
const { tt, formatExchangeRateAmount } = useI18n();
const { baseCurrency, baseAmount, exchangeRatesData, exchangeRatesDataUpdateTime, availableExchangeRates, getConvertedAmount, setAsBaseline } = useExchangeRatesPageBase();
const {
baseCurrency,
baseAmount,
defaultCurrency,
exchangeRatesData,
isUserCustomExchangeRates,
exchangeRatesDataUpdateTime,
availableExchangeRates,
getConvertedAmount,
setAsBaseline
} = useExchangeRatesPageBase();
const exchangeRatesStore = useExchangeRatesStore();
const confirmDialog = useTemplateRef<ConfirmDialogType>('confirmDialog');
const snackbar = useTemplateRef<SnackBarType>('snackbar');
const updateDialog = useTemplateRef<UpdateDialogType>('updateDialog');
const activeTab = ref<string>('exchangeRatesPage');
const loading = ref<boolean>(true);
const updating = ref<boolean>(false);
const customExchangeRateRemoving = ref<Record<string, boolean>>({});
const alwaysShowNav = ref<boolean>(mdAndUp.value);
const showNav = ref<boolean>(mdAndUp.value);
@@ -205,6 +246,43 @@ function reload(force: boolean): void {
});
}
function update(): void {
updateDialog.value?.open().then(result => {
if (result && result.message) {
snackbar.value?.showMessage(result.message);
}
}).catch(error => {
if (error) {
snackbar.value?.showError(error);
}
});
}
function remove(currency: string): void {
confirmDialog.value?.open('Are you sure you want to delete this user custom exchange rate?').then(() => {
updating.value = true;
customExchangeRateRemoving.value[currency] = true;
exchangeRatesStore.deleteUserCustomExchangeRate({
currency: currency
}).then(() => {
if (currency === baseCurrency.value) {
baseCurrency.value = defaultCurrency.value;
}
updating.value = false;
customExchangeRateRemoving.value[currency] = false;
}).catch(error => {
updating.value = false;
customExchangeRateRemoving.value[currency] = false;
if (!error.processed) {
snackbar.value?.showError(error);
}
});
});
}
function getFinalConvertedAmount(toExchangeRate: LocalizedLatestExchangeRate): string {
if (!baseCurrency.value) {
return '0';
@@ -0,0 +1,141 @@
<template>
<v-dialog width="800" :persistent="submitting || (!!defaultCurrencyAmount && defaultCurrencyAmount !== 1) || currency !== defaultCurrency || (!!targetCurrencyAmount && targetCurrencyAmount !== 1)" v-model="showState">
<v-card class="pa-2 pa-sm-4 pa-md-4">
<template #title>
<div class="d-flex align-center justify-center">
<div class="d-flex w-100 align-center justify-center">
<h4 class="text-h4">{{ tt('Update User Custom Exchange Rate') }}</h4>
</div>
</div>
</template>
<v-card-text class="my-md-4 w-100 d-flex justify-center">
<v-row>
<v-col cols="12" md="6">
<v-text-field type="number"
:disabled="submitting"
:label="tt('Amount')"
:placeholder="tt('Amount')"
:persistent-placeholder="true"
v-model="defaultCurrencyAmount"/>
</v-col>
<v-col cols="12" md="6">
<currency-select :disabled="true"
:label="tt('Currency')"
:placeholder="tt('Currency')"
v-model="defaultCurrency" />
</v-col>
<v-col cols="12" class="text-center my-2">
<v-icon :icon="mdiSwapVertical" size="24" />
</v-col>
<v-col cols="12" md="6">
<v-text-field type="number"
:disabled="submitting"
:label="tt('Amount')"
:placeholder="tt('Amount')"
:persistent-placeholder="true"
v-model="targetCurrencyAmount"/>
</v-col>
<v-col cols="12" md="6">
<currency-select :disabled="submitting"
:label="tt('Currency')"
:placeholder="tt('Currency')"
v-model="currency" />
</v-col>
</v-row>
</v-card-text>
<v-card-text class="overflow-y-visible">
<div class="w-100 d-flex justify-center gap-4">
<v-btn :disabled="submitting || !defaultCurrencyAmount || !currency || !targetCurrencyAmount" @click="confirm">
{{ tt('OK') }}
<v-progress-circular indeterminate size="22" class="ml-2" v-if="submitting"></v-progress-circular>
</v-btn>
<v-btn color="secondary" variant="tonal" :disabled="submitting" @click="cancel">{{ tt('Cancel') }}</v-btn>
</div>
</v-card-text>
</v-card>
</v-dialog>
<snack-bar ref="snackbar" />
</template>
<script setup lang="ts">
import SnackBar from '@/components/desktop/SnackBar.vue';
import { ref, useTemplateRef } from 'vue';
import { useI18n } from '@/locales/helpers.ts';
import { useUserStore } from '@/stores/user.ts';
import { useExchangeRatesStore } from '@/stores/exchangeRates.ts';
import {
mdiSwapVertical
} from '@mdi/js';
interface UserCustomExchangeRateUpdateResponse {
message: string;
}
type SnackBarType = InstanceType<typeof SnackBar>;
defineProps<{
show?: boolean;
}>();
const { tt } = useI18n();
const userStore = useUserStore();
const exchangeRatesStore = useExchangeRatesStore();
const showState = ref<boolean>(false);
const submitting = ref<boolean>(false);
const defaultCurrency = ref<string>(userStore.currentUserDefaultCurrency);
const defaultCurrencyAmount = ref<number>(1);
const currency = ref<string>(userStore.currentUserDefaultCurrency);
const targetCurrencyAmount = ref<number>(1);
const snackbar = useTemplateRef<SnackBarType>('snackbar');
let resolveFunc: ((response: UserCustomExchangeRateUpdateResponse) => void) | null = null;
let rejectFunc: ((reason?: unknown) => void) | null = null;
function open(): Promise<UserCustomExchangeRateUpdateResponse> {
showState.value = true;
defaultCurrencyAmount.value = 1;
currency.value = userStore.currentUserDefaultCurrency;
targetCurrencyAmount.value = 1;
return new Promise((resolve, reject) => {
resolveFunc = resolve;
rejectFunc = reject;
});
}
function confirm(): void {
submitting.value = true;
exchangeRatesStore.updateUserCustomExchangeRate({
currency: currency.value,
rate: targetCurrencyAmount.value / defaultCurrencyAmount.value
}).then(() => {
submitting.value = false;
resolveFunc?.({ message: 'You have updated exchange rate' });
showState.value = false;
}).catch(error => {
submitting.value = false;
if (!error.processed) {
snackbar.value?.showError(error);
}
});
}
function cancel(): void {
rejectFunc?.();
showState.value = false;
}
defineExpose({
open
});
</script>
@@ -1,5 +1,5 @@
<template>
<f7-page ptr @ptr:refresh="update">
<f7-page ptr @ptr:refresh="reload">
<f7-navbar>
<f7-nav-left :back-link="tt('Back')"></f7-nav-left>
<f7-nav-title :title="tt('Exchange Rates Data')"></f7-nav-title>
@@ -56,16 +56,27 @@
<f7-list strong inset dividers class="margin-vertical" v-if="exchangeRatesData && exchangeRatesData.exchangeRates && exchangeRatesData.exchangeRates.length">
<f7-list-item swipeout
:id="getExchangeRateDomId(exchangeRate)"
:after="getFinalConvertedAmount(exchangeRate)"
:key="exchangeRate.currencyCode" v-for="exchangeRate in availableExchangeRates">
:key="baseCurrencyChangedTime + '_' + exchangeRate.currencyCode" v-for="exchangeRate in availableExchangeRates"
@swipeout:closed="onExchangeRateSwipeoutClosed()">
<template #title>
<div class="no-padding no-margin">
<span style="margin-right: 5px">{{ exchangeRate.currencyDisplayName }}</span>
<small class="smaller">{{ exchangeRate.currencyCode }}</small>
</div>
</template>
<f7-swipeout-actions right v-if="exchangeRate.currencyCode !== baseCurrency">
<f7-swipeout-button color="primary" close :text="tt('Set as Base')" @click="setAsBaseline(exchangeRate.currencyCode, getFinalConvertedAmount(exchangeRate))"></f7-swipeout-button>
<f7-swipeout-actions right v-if="exchangeRate.currencyCode !== baseCurrency || (exchangeRate.currencyCode !== defaultCurrency && isUserCustomExchangeRates)">
<f7-swipeout-button color="primary" close
:text="tt('Set as Base')"
:class="{ 'disabled': exchangeRate.currencyCode === baseCurrency }"
@click="setAsBaseline(exchangeRate.currencyCode, getFinalConvertedAmount(exchangeRate)); settingBaseLine = true"
v-if="settingBaseLine || exchangeRate.currencyCode !== baseCurrency"></f7-swipeout-button>
<f7-swipeout-button color="red" class="padding-left padding-right"
@click="remove(exchangeRate, false)"
v-if="exchangeRate.currencyCode !== defaultCurrency && isUserCustomExchangeRates">
<f7-icon f7="trash"></f7-icon>
</f7-swipeout-button>
</f7-swipeout-actions>
</f7-list-item>
</f7-list>
@@ -78,18 +89,34 @@
<f7-list-item>
<small>{{ tt('Data source') }}</small>
<small>
<f7-link external target="_blank" :href="exchangeRatesData.referenceUrl" v-if="exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</f7-link>
<span v-else-if="!exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</span>
<f7-link external target="_blank" :href="exchangeRatesData.referenceUrl" v-if="!isUserCustomExchangeRates && exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</f7-link>
<span v-else-if="!isUserCustomExchangeRates && !exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</span>
<span v-else-if="isUserCustomExchangeRates">{{ tt('User Custom') }}</span>
</small>
</f7-list-item>
</f7-list>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
<f7-actions-group>
<f7-actions-button :class="{ 'disabled': updating }" @click="update(undefined)">
<f7-actions-group v-if="isUserCustomExchangeRates">
<f7-actions-button :class="{ 'disabled': loading }" @click="update()">
<span>{{ tt('Update') }}</span>
</f7-actions-button>
</f7-actions-group>
<f7-actions-group>
<f7-actions-button :class="{ 'disabled': loading }" @click="reload(undefined)">
<span>{{ tt('Refresh') }}</span>
</f7-actions-button>
</f7-actions-group>
<f7-actions-group>
<f7-actions-button bold close>{{ tt('Cancel') }}</f7-actions-button>
</f7-actions-group>
</f7-actions>
<f7-actions close-by-outside-click close-on-escape :opened="showDeleteActionSheet" @actions:closed="showDeleteActionSheet = false">
<f7-actions-group>
<f7-actions-label>{{ tt('Are you sure you want to delete this user custom exchange rate?') }}</f7-actions-label>
<f7-actions-button color="red" @click="remove(customExchangeRateToDelete, true)">{{ tt('Delete') }}</f7-actions-button>
</f7-actions-group>
<f7-actions-group>
<f7-actions-button bold close>{{ tt('Cancel') }}</f7-actions-button>
</f7-actions-group>
@@ -99,9 +126,10 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import type { Router } from 'framework7/types';
import { useI18n } from '@/locales/helpers.ts';
import { useI18nUIComponents, showLoading, hideLoading } from '@/lib/ui/mobile.ts';
import { useI18nUIComponents, showLoading, hideLoading, onSwipeoutDeleted } from '@/lib/ui/mobile.ts';
import { useExchangeRatesPageBase } from '@/views/base/ExchangeRatesPageBase.ts';
import { useExchangeRatesStore } from '@/stores/exchangeRates.ts';
@@ -110,16 +138,38 @@ import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transac
import type { LocalizedLatestExchangeRate } from '@/models/exchange_rate.ts';
import {
getCurrentUnixTime
} from '@/lib/datetime.ts';
const props = defineProps<{
f7router: Router.Router;
}>();
const { tt, getCurrencyName, formatAmount, formatExchangeRateAmount } = useI18n();
const { showToast } = useI18nUIComponents();
const { baseCurrency, baseAmount, exchangeRatesData, exchangeRatesDataUpdateTime, availableExchangeRates, getConvertedAmount, setAsBaseline } = useExchangeRatesPageBase();
const { showAlert, showToast } = useI18nUIComponents();
const {
baseCurrency,
baseAmount,
defaultCurrency,
exchangeRatesData,
isUserCustomExchangeRates,
exchangeRatesDataUpdateTime,
availableExchangeRates,
getConvertedAmount,
setAsBaseline
} = useExchangeRatesPageBase();
const exchangeRatesStore = useExchangeRatesStore();
const updating = ref<boolean>(false);
const loading = ref<boolean>(false);
const baseCurrencyChangedTime = ref<number>(getCurrentUnixTime());
const settingBaseLine = ref<boolean>(false);
const showMoreActionSheet = ref<boolean>(false);
const showBaseCurrencyPopup = ref<boolean>(false);
const showBaseAmountSheet = ref<boolean>(false);
const customExchangeRateToDelete = ref<LocalizedLatestExchangeRate | null>(null);
const showDeleteActionSheet = ref<boolean>(false);
const displayBaseAmount = computed<string>(() => formatAmount(baseAmount.value, baseCurrency.value));
const baseAmountFontSizeClass = computed<string>(() => {
@@ -132,13 +182,17 @@ const baseAmountFontSizeClass = computed<string>(() => {
}
});
function update(done?: () => void): void {
if (updating.value) {
function getExchangeRateDomId(exchangeRate: LocalizedLatestExchangeRate): string {
return 'exchangeRate_' + exchangeRate.currencyCode;
}
function reload(done?: () => void): void {
if (loading.value) {
done?.();
return;
}
updating.value = true;
loading.value = true;
if (!done) {
showLoading();
@@ -150,14 +204,54 @@ function update(done?: () => void): void {
}).then(() => {
done?.();
updating.value = false;
loading.value = false;
hideLoading();
showToast('Exchange rates data has been updated');
}).catch(error => {
done?.();
updating.value = false;
loading.value = false;
hideLoading();
if (!error.processed) {
showToast(error.message || error);
}
});
}
function update(): void {
props.f7router.navigate('/exchange_rates/update');
}
function remove(customExchangeRate: LocalizedLatestExchangeRate | null, confirm: boolean): void {
if (!customExchangeRate) {
showAlert('An error occurred');
return;
}
if (!confirm) {
customExchangeRateToDelete.value = customExchangeRate;
showDeleteActionSheet.value = true;
return;
}
showDeleteActionSheet.value = false;
customExchangeRateToDelete.value = null;
showLoading();
exchangeRatesStore.deleteUserCustomExchangeRate({
currency: customExchangeRate.currencyCode,
beforeResolve: (done) => {
onSwipeoutDeleted(getExchangeRateDomId(customExchangeRate), done);
}
}).then(() => {
if (customExchangeRate.currencyCode === baseCurrency.value) {
baseCurrency.value = defaultCurrency.value;
}
hideLoading();
}).catch(error => {
hideLoading();
if (!error.processed) {
@@ -177,6 +271,15 @@ function getFinalConvertedAmount(toExchangeRate: LocalizedLatestExchangeRate): s
return formatExchangeRateAmount(exchangeRateAmount);
}
function onExchangeRateSwipeoutClosed(): void {
baseCurrencyChangedTime.value = getCurrentUnixTime();
settingBaseLine.value = false;
}
exchangeRatesStore.getLatestExchangeRates({
silent: true,
force: false
}).then(() => {
if (exchangeRatesData.value && exchangeRatesData.value.exchangeRates) {
const exchangeRates = exchangeRatesData.value.exchangeRates;
let hasBaseCurrency = false;
@@ -193,6 +296,7 @@ if (exchangeRatesData.value && exchangeRatesData.value.exchangeRates) {
showToast('There is no exchange rates data for your default currency');
}
}
});
</script>
<style>
@@ -0,0 +1,142 @@
<template>
<f7-page>
<f7-navbar>
<f7-nav-left :back-link="tt('Back')"></f7-nav-left>
<f7-nav-title :title="tt('Update User Custom Exchange Rate')"></f7-nav-title>
<f7-nav-right>
<f7-link :class="{ 'disabled': submitting || !defaultCurrencyAmount || !currency || !targetCurrencyAmount }"
:text="tt('Update')" @click="confirm"></f7-link>
</f7-nav-right>
</f7-navbar>
<f7-list form strong inset dividers class="margin-vertical">
<f7-list-input
type="number"
:disabled="submitting"
:label="tt('Amount')"
:placeholder="tt('Amount')"
v-model:value="defaultCurrencyAmount"
></f7-list-input>
<f7-list-item
class="list-item-with-header-and-title list-item-no-item-after"
link="#"
:class="{ 'disabled': true }"
:header="tt('Currency')"
:no-chevron="true"
>
<template #title>
<div class="no-padding no-margin">
<span>{{ getCurrencyName(defaultCurrency) }}&nbsp;</span>
<small class="smaller">{{ defaultCurrency }}</small>
</div>
</template>
</f7-list-item>
</f7-list>
<f7-block class="display-flex justify-content-center full-line margin-vertical">
<f7-icon class="separate-icon" f7="arrow_up_arrow_down"></f7-icon>
</f7-block>
<f7-list form strong inset dividers class="margin-vertical">
<f7-list-input
type="number"
:disabled="submitting"
:label="tt('Amount')"
:placeholder="tt('Amount')"
v-model:value="targetCurrencyAmount"
></f7-list-input>
<f7-list-item
class="list-item-with-header-and-title list-item-no-item-after"
link="#"
:class="{ 'disabled': submitting }"
:header="tt('Currency')"
@click="showCurrencyPopup = true"
>
<template #title>
<div class="no-padding no-margin">
<span>{{ getCurrencyName(currency) }}&nbsp;</span>
<small class="smaller">{{ currency }}</small>
</div>
</template>
<list-item-selection-popup value-type="item"
key-field="currencyCode" value-field="currencyCode"
title-field="displayName" after-field="currencyCode"
:title="tt('Currency Name')"
:enable-filter="true"
:filter-placeholder="tt('Currency')"
:filter-no-items-text="tt('No results')"
:items="allCurrencies"
v-model:show="showCurrencyPopup"
v-model="currency">
</list-item-selection-popup>
</f7-list-item>
</f7-list>
</f7-page>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import type { Router } from 'framework7/types';
import { useI18n } from '@/locales/helpers.ts';
import { useI18nUIComponents, showLoading, hideLoading } from '@/lib/ui/mobile.ts';
import { useUserStore } from '@/stores/user.ts';
import { useExchangeRatesStore } from '@/stores/exchangeRates.ts';
import type { LocalizedCurrencyInfo } from '@/core/currency.ts';
const props = defineProps<{
f7router: Router.Router;
}>();
const { tt, getAllCurrencies, getCurrencyName } = useI18n();
const { showToast } = useI18nUIComponents();
const userStore = useUserStore();
const exchangeRatesStore = useExchangeRatesStore();
const submitting = ref<boolean>(false);
const defaultCurrency = ref<string>(userStore.currentUserDefaultCurrency);
const defaultCurrencyAmount = ref<number>(1);
const currency = ref<string>(userStore.currentUserDefaultCurrency);
const targetCurrencyAmount = ref<number>(1);
const showCurrencyPopup = ref<boolean>(false);
const allCurrencies = computed<LocalizedCurrencyInfo[]>(() => getAllCurrencies());
function init(): void {
defaultCurrencyAmount.value = 1;
currency.value = userStore.currentUserDefaultCurrency;
targetCurrencyAmount.value = 1;
}
function confirm(): void {
const router = props.f7router;
submitting.value = true;
showLoading(() => submitting.value);
exchangeRatesStore.updateUserCustomExchangeRate({
currency: currency.value,
rate: targetCurrencyAmount.value / defaultCurrencyAmount.value
}).then(() => {
submitting.value = false;
hideLoading();
showToast('You have updated exchange rate');
router.back();
}).catch(error => {
submitting.value = false;
hideLoading();
if (!error.processed) {
showToast(error.message || error);
}
});
}
init();
</script>