support user custom exchange rates data
This commit is contained in:
@@ -133,5 +133,13 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error {
|
|||||||
|
|
||||||
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction picture table maintained successfully")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -359,6 +359,8 @@ func startWebServer(c *core.CliContext) error {
|
|||||||
|
|
||||||
// Exchange Rates
|
// Exchange Rates
|
||||||
apiV1Route.GET("/exchange_rates/latest.json", bindApi(api.ExchangeRates.LatestExchangeRateHandler))
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -363,6 +363,7 @@ custom_map_tile_server_default_zoom_level = 14
|
|||||||
# "national_bank_of_ukraine": https://bank.gov.ua/ua/markets/exchangerates
|
# "national_bank_of_ukraine": https://bank.gov.ua/ua/markets/exchangerates
|
||||||
# "central_bank_of_uzbekistan": https://cbu.uz/en/arkhiv-kursov-valyut/
|
# "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
|
# "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
|
data_source = euro_central_bank
|
||||||
|
|
||||||
# Requesting exchange rates data timeout (0 - 4294967295 milliseconds)
|
# Requesting exchange rates data timeout (0 - 4294967295 milliseconds)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type DataManagementsApi struct {
|
|||||||
tags *services.TransactionTagService
|
tags *services.TransactionTagService
|
||||||
pictures *services.TransactionPictureService
|
pictures *services.TransactionPictureService
|
||||||
templates *services.TransactionTemplateService
|
templates *services.TransactionTemplateService
|
||||||
|
userCustomExchangeRates *services.UserCustomExchangeRatesService
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a data management api singleton instance
|
// Initialize a data management api singleton instance
|
||||||
@@ -44,6 +45,7 @@ var (
|
|||||||
tags: services.TransactionTags,
|
tags: services.TransactionTags,
|
||||||
pictures: services.TransactionPictures,
|
pictures: services.TransactionPictures,
|
||||||
templates: services.TransactionTemplates,
|
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)
|
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)
|
log.Infof(c, "[data_managements.ClearDataHandler] user \"uid:%d\" has cleared all data", uid)
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,17 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/exchangerates"
|
"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"
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExchangeRatesApi represents exchange rate api
|
// ExchangeRatesApi represents exchange rate api
|
||||||
type ExchangeRatesApi struct {
|
type ExchangeRatesApi struct {
|
||||||
ApiUsingConfig
|
ApiUsingConfig
|
||||||
|
users *services.UserService
|
||||||
|
userCustomExchangeRates *services.UserCustomExchangeRatesService
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a exchange rate api singleton instance
|
// Initialize a exchange rate api singleton instance
|
||||||
@@ -18,6 +23,8 @@ var (
|
|||||||
ApiUsingConfig: ApiUsingConfig{
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
container: settings.Container,
|
container: settings.Container,
|
||||||
},
|
},
|
||||||
|
users: services.Users,
|
||||||
|
userCustomExchangeRates: services.UserCustomExchangeRates,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,3 +44,69 @@ func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.WebContext) (any, *
|
|||||||
|
|
||||||
return exchangeRateResponse, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const (
|
|||||||
NormalSubcategoryTemplate = 10
|
NormalSubcategoryTemplate = 10
|
||||||
NormalSubcategoryPicture = 11
|
NormalSubcategoryPicture = 11
|
||||||
NormalSubcategoryConverter = 12
|
NormalSubcategoryConverter = 12
|
||||||
|
NormalSubcategoryUserCustomExchangeRate = 13
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error represents the specific error returned to user
|
// Error represents the specific error returned to user
|
||||||
|
|||||||
@@ -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 {
|
} else if config.ExchangeRatesDataSource == settings.InternationalMonetaryFundDataSource {
|
||||||
Container.Current = newCommonHttpExchangeRatesDataSource(&InternationalMonetaryFundDataSource{})
|
Container.Current = newCommonHttpExchangeRatesDataSource(&InternationalMonetaryFundDataSource{})
|
||||||
return nil
|
return nil
|
||||||
|
} else if config.ExchangeRatesDataSource == settings.UserCustomExchangeRatesDataSource {
|
||||||
|
Container.Current = newUserCustomExchangeRatesDataSource()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return errs.ErrInvalidExchangeRatesDataSource
|
return errs.ErrInvalidExchangeRatesDataSource
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,39 @@
|
|||||||
package models
|
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
|
// LatestExchangeRateResponse returns a view-object which contains latest exchange rate
|
||||||
type LatestExchangeRateResponse struct {
|
type LatestExchangeRateResponse struct {
|
||||||
@@ -17,6 +50,53 @@ type LatestExchangeRate struct {
|
|||||||
Rate string `json:"rate"`
|
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
|
// LatestExchangeRateSlice represents the slice data structure of LatestExchangeRate
|
||||||
type LatestExchangeRateSlice []*LatestExchangeRate
|
type LatestExchangeRateSlice []*LatestExchangeRate
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -117,6 +117,7 @@ const (
|
|||||||
NationalBankOfUkraineDataSource string = "national_bank_of_ukraine"
|
NationalBankOfUkraineDataSource string = "national_bank_of_ukraine"
|
||||||
CentralBankOfUzbekistanDataSource string = "central_bank_of_uzbekistan"
|
CentralBankOfUzbekistanDataSource string = "central_bank_of_uzbekistan"
|
||||||
InternationalMonetaryFundDataSource string = "international_monetary_fund"
|
InternationalMonetaryFundDataSource string = "international_monetary_fund"
|
||||||
|
UserCustomExchangeRatesDataSource string = "user_custom"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -914,7 +915,8 @@ func loadExchangeRatesConfiguration(config *Config, configFile *ini.File, sectio
|
|||||||
dataSource == SwissNationalBankDataSource ||
|
dataSource == SwissNationalBankDataSource ||
|
||||||
dataSource == NationalBankOfUkraineDataSource ||
|
dataSource == NationalBankOfUkraineDataSource ||
|
||||||
dataSource == CentralBankOfUzbekistanDataSource ||
|
dataSource == CentralBankOfUzbekistanDataSource ||
|
||||||
dataSource == InternationalMonetaryFundDataSource {
|
dataSource == InternationalMonetaryFundDataSource ||
|
||||||
|
dataSource == UserCustomExchangeRatesDataSource {
|
||||||
config.ExchangeRatesDataSource = dataSource
|
config.ExchangeRatesDataSource = dataSource
|
||||||
} else {
|
} else {
|
||||||
return errs.ErrInvalidExchangeRatesDataSource
|
return errs.ErrInvalidExchangeRatesDataSource
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ import type {
|
|||||||
DataStatisticsResponse
|
DataStatisticsResponse
|
||||||
} from '@/models/data_management.ts';
|
} from '@/models/data_management.ts';
|
||||||
import type {
|
import type {
|
||||||
|
UserCustomExchangeRateUpdateRequest,
|
||||||
|
UserCustomExchangeRateDeleteRequest,
|
||||||
|
UserCustomExchangeRateUpdateResponse,
|
||||||
LatestExchangeRateResponse
|
LatestExchangeRateResponse
|
||||||
} from '@/models/exchange_rate.ts';
|
} from '@/models/exchange_rate.ts';
|
||||||
import type {
|
import type {
|
||||||
@@ -574,6 +577,12 @@ export default {
|
|||||||
timeout: getExchangeRatesRequestTimeout() || DEFAULT_API_TIMEOUT
|
timeout: getExchangeRatesRequestTimeout() || DEFAULT_API_TIMEOUT
|
||||||
} as ApiRequestConfig);
|
} 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 => {
|
generateQrCodeUrl: (qrCodeName: string): string => {
|
||||||
return `${getBasePath()}${BASE_QRCODE_PATH}/${qrCodeName}.png`;
|
return `${getBasePath()}${BASE_QRCODE_PATH}/${qrCodeName}.png`;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1171,6 +1171,9 @@
|
|||||||
"invalid beancount file": "Invalid Beancount file",
|
"invalid beancount file": "Invalid Beancount file",
|
||||||
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
|
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
|
||||||
"invalid amount expression": "Amount expression is invalid",
|
"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 cannot be blank": "Abfrageelemente dürfen nicht leer sein",
|
||||||
"query items too much": "Zu viele Abfrageelemente",
|
"query items too much": "Zu viele Abfrageelemente",
|
||||||
"query items have invalid item": "Ungültiges Element in Abfrageelementen",
|
"query items have invalid item": "Ungültiges Element in Abfrageelementen",
|
||||||
@@ -2039,6 +2042,7 @@
|
|||||||
"WebAuthn is not enabled": "WebAuthn ist nicht aktiviert",
|
"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?",
|
"Are you sure you want to re-login?": "Sind Sie sicher, dass Sie sich erneut anmelden möchten?",
|
||||||
"Exchange Rates Data": "Wechselkursdaten",
|
"Exchange Rates Data": "Wechselkursdaten",
|
||||||
|
"User Custom": "User Custom",
|
||||||
"Base Currency": "Basiswährung",
|
"Base Currency": "Basiswährung",
|
||||||
"Base Amount": "Basisbetrag",
|
"Base Amount": "Basisbetrag",
|
||||||
"Set as Base": "Als Basis festlegen",
|
"Set as Base": "Als Basis festlegen",
|
||||||
@@ -2049,6 +2053,11 @@
|
|||||||
"Exchange rates data has been updated": "Wechselkursdaten wurden aktualisiert",
|
"Exchange rates data has been updated": "Wechselkursdaten wurden aktualisiert",
|
||||||
"Exchange rates data is up to date": "Wechselkursdaten sind aktuell",
|
"Exchange rates data is up to date": "Wechselkursdaten sind aktuell",
|
||||||
"Unable to retrieve exchange rates data": "Wechselkursdaten können nicht abgerufen werden",
|
"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",
|
"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.",
|
"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",
|
"Switch to Mobile Version": "Zur mobilen Version wechseln",
|
||||||
|
|||||||
@@ -1171,6 +1171,9 @@
|
|||||||
"invalid beancount file": "Invalid Beancount file",
|
"invalid beancount file": "Invalid Beancount file",
|
||||||
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
|
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
|
||||||
"invalid amount expression": "Amount expression is invalid",
|
"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 cannot be blank": "There are no query items",
|
||||||
"query items too much": "There are too many query items",
|
"query items too much": "There are too many query items",
|
||||||
"query items have invalid item": "There is invalid item in 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",
|
"WebAuthn is not enabled": "WebAuthn is not enabled",
|
||||||
"Are you sure you want to re-login?": "Are you sure you want to re-login?",
|
"Are you sure you want to re-login?": "Are you sure you want to re-login?",
|
||||||
"Exchange Rates Data": "Exchange Rates Data",
|
"Exchange Rates Data": "Exchange Rates Data",
|
||||||
|
"User Custom": "User Custom",
|
||||||
"Base Currency": "Base Currency",
|
"Base Currency": "Base Currency",
|
||||||
"Base Amount": "Base Amount",
|
"Base Amount": "Base Amount",
|
||||||
"Set as Base": "Set as Base",
|
"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 has been updated": "Exchange rates data has been updated",
|
||||||
"Exchange rates data is up to date": "Exchange rates data is up to date",
|
"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",
|
"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",
|
"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.",
|
"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",
|
"Switch to Mobile Version": "Switch to Mobile Version",
|
||||||
|
|||||||
@@ -1170,6 +1170,9 @@
|
|||||||
"invalid beancount file": "Invalid Beancount file",
|
"invalid beancount file": "Invalid Beancount file",
|
||||||
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
|
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
|
||||||
"invalid amount expression": "Amount expression is invalid",
|
"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 cannot be blank": "--",
|
||||||
"query items too much": "--",
|
"query items too much": "--",
|
||||||
"query items have invalid item": "Hay un elemento no válido en los elementos de consulta",
|
"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",
|
"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?",
|
"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",
|
"Exchange Rates Data": "Datos de tipos de cambio",
|
||||||
|
"User Custom": "User Custom",
|
||||||
"Base Currency": "Moneda base",
|
"Base Currency": "Moneda base",
|
||||||
"Base Amount": "Importe base",
|
"Base Amount": "Importe base",
|
||||||
"Set as Base": "Establecer como 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 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.",
|
"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",
|
"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",
|
"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.",
|
"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",
|
"Switch to Mobile Version": "Cambiar a la versión móvil",
|
||||||
|
|||||||
@@ -1171,6 +1171,9 @@
|
|||||||
"invalid beancount file": "File Beancount non valido",
|
"invalid beancount file": "File Beancount non valido",
|
||||||
"not support include directive for beancount file": "Direttiva \"include\" non supportata per il file Beancount",
|
"not support include directive for beancount file": "Direttiva \"include\" non supportata per il file Beancount",
|
||||||
"invalid amount expression": "Espressione dell'importo non valida",
|
"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 cannot be blank": "Non ci sono elementi di query",
|
||||||
"query items too much": "Ci sono troppi 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",
|
"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",
|
"WebAuthn is not enabled": "WebAuthn non è abilitato",
|
||||||
"Are you sure you want to re-login?": "Sei sicuro di voler accedere di nuovo?",
|
"Are you sure you want to re-login?": "Sei sicuro di voler accedere di nuovo?",
|
||||||
"Exchange Rates Data": "Dati tassi di cambio",
|
"Exchange Rates Data": "Dati tassi di cambio",
|
||||||
|
"User Custom": "User Custom",
|
||||||
"Base Currency": "Valuta di base",
|
"Base Currency": "Valuta di base",
|
||||||
"Base Amount": "Importo di base",
|
"Base Amount": "Importo di base",
|
||||||
"Set as Base": "Imposta come 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 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",
|
"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",
|
"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",
|
"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.",
|
"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",
|
"Switch to Mobile Version": "Passa alla versione mobile",
|
||||||
|
|||||||
@@ -1171,6 +1171,9 @@
|
|||||||
"invalid beancount file": "Invalid Beancount file",
|
"invalid beancount file": "Invalid Beancount file",
|
||||||
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
|
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
|
||||||
"invalid amount expression": "Amount expression is invalid",
|
"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 cannot be blank": "クエリ項目がありません",
|
||||||
"query items too much": "クエリ項目が多すぎます",
|
"query items too much": "クエリ項目が多すぎます",
|
||||||
"query items have invalid item": "クエリ項目に無効な項目があります",
|
"query items have invalid item": "クエリ項目に無効な項目があります",
|
||||||
@@ -2039,6 +2042,7 @@
|
|||||||
"WebAuthn is not enabled": "WebAuthnが有効になっていません",
|
"WebAuthn is not enabled": "WebAuthnが有効になっていません",
|
||||||
"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.": "以下のQRコードをモバイルデバイスでスキャンできます。",
|
"You can scan the QR code below on your mobile device.": "以下のQRコードをモバイルデバイスでスキャンできます。",
|
||||||
"Switch to Mobile Version": "モバイルバージョンに切り替え",
|
"Switch to Mobile Version": "モバイルバージョンに切り替え",
|
||||||
|
|||||||
@@ -1171,6 +1171,9 @@
|
|||||||
"invalid beancount file": "Invalid Beancount file",
|
"invalid beancount file": "Invalid Beancount file",
|
||||||
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
|
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
|
||||||
"invalid amount expression": "Amount expression is invalid",
|
"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 cannot be blank": "Нет элементов запроса",
|
||||||
"query items too much": "Слишком много элементов запроса",
|
"query items too much": "Слишком много элементов запроса",
|
||||||
"query items have invalid item": "В элементах запроса присутствует недопустимый элемент",
|
"query items have invalid item": "В элементах запроса присутствует недопустимый элемент",
|
||||||
@@ -2039,6 +2042,7 @@
|
|||||||
"WebAuthn is not enabled": "WebAuthn не включен",
|
"WebAuthn is not enabled": "WebAuthn не включен",
|
||||||
"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.": "Вы можете отсканировать QR-код ниже на своем мобильном устройстве.",
|
"You can scan the QR code below on your mobile device.": "Вы можете отсканировать QR-код ниже на своем мобильном устройстве.",
|
||||||
"Switch to Mobile Version": "Переключиться на мобильную версию",
|
"Switch to Mobile Version": "Переключиться на мобильную версию",
|
||||||
|
|||||||
@@ -1171,6 +1171,9 @@
|
|||||||
"invalid beancount file": "Недійсний файл Beancount",
|
"invalid beancount file": "Недійсний файл Beancount",
|
||||||
"not support include directive for beancount file": "Не підтримується директива \"include\" у файлі Beancount",
|
"not support include directive for beancount file": "Не підтримується директива \"include\" у файлі Beancount",
|
||||||
"invalid amount expression": "Недійсний вираз суми",
|
"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 cannot be blank": "Елементи запиту не можуть бути порожніми",
|
||||||
"query items too much": "Занадто багато елементів запиту",
|
"query items too much": "Занадто багато елементів запиту",
|
||||||
"query items have invalid item": "Запит містить недійсний елемент",
|
"query items have invalid item": "Запит містить недійсний елемент",
|
||||||
@@ -2039,6 +2042,7 @@
|
|||||||
"WebAuthn is not enabled": "WebAuthn не увімкнено",
|
"WebAuthn is not enabled": "WebAuthn не увімкнено",
|
||||||
"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.": "Ви можете відсканувати наведений нижче QR-код на своєму мобільному пристрої.",
|
"You can scan the QR code below on your mobile device.": "Ви можете відсканувати наведений нижче QR-код на своєму мобільному пристрої.",
|
||||||
"Switch to Mobile Version": "Переключитися на мобільну версію",
|
"Switch to Mobile Version": "Переключитися на мобільну версію",
|
||||||
|
|||||||
@@ -1171,6 +1171,9 @@
|
|||||||
"invalid beancount file": "Invalid Beancount file",
|
"invalid beancount file": "Invalid Beancount file",
|
||||||
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
|
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
|
||||||
"invalid amount expression": "Amount expression is invalid",
|
"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 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 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",
|
"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",
|
"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?",
|
"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",
|
"Exchange Rates Data": "Dữ liệu tỷ giá hối đoái",
|
||||||
|
"User Custom": "User Custom",
|
||||||
"Base Currency": "Tiền tệ cơ sở",
|
"Base Currency": "Tiền tệ cơ sở",
|
||||||
"Base Amount": "Số tiền cơ sở",
|
"Base Amount": "Số tiền cơ sở",
|
||||||
"Set as Base": "Đặt làm 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 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",
|
"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",
|
"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",
|
"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.",
|
"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",
|
"Switch to Mobile Version": "Chuyển sang phiên bản di động",
|
||||||
|
|||||||
@@ -1171,6 +1171,9 @@
|
|||||||
"invalid beancount file": "无效的 Beancount 文件",
|
"invalid beancount file": "无效的 Beancount 文件",
|
||||||
"not support include directive for beancount file": "不支持 Beancount 文件的 \"include\" 指令",
|
"not support include directive for beancount file": "不支持 Beancount 文件的 \"include\" 指令",
|
||||||
"invalid amount expression": "金额表达式无效",
|
"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 cannot be blank": "请求项目不能为空",
|
||||||
"query items too much": "请求项目过多",
|
"query items too much": "请求项目过多",
|
||||||
"query items have invalid item": "请求项目中有非法项目",
|
"query items have invalid item": "请求项目中有非法项目",
|
||||||
@@ -2039,6 +2042,7 @@
|
|||||||
"WebAuthn is not enabled": "WebAuthn 没有启用",
|
"WebAuthn is not enabled": "WebAuthn 没有启用",
|
||||||
"Are you sure you want to re-login?": "您确定要重新登录?",
|
"Are you sure you want to re-login?": "您确定要重新登录?",
|
||||||
"Exchange Rates Data": "汇率数据",
|
"Exchange Rates Data": "汇率数据",
|
||||||
|
"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": "更新用户自定义汇率",
|
||||||
|
"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": "在移动设备使用",
|
"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": "切换到移动版",
|
||||||
|
|||||||
@@ -1171,6 +1171,9 @@
|
|||||||
"invalid beancount file": "無效的 Beancount 檔案",
|
"invalid beancount file": "無效的 Beancount 檔案",
|
||||||
"not support include directive for beancount file": "不支援 Beancount 檔案的 \"include\" 指令",
|
"not support include directive for beancount file": "不支援 Beancount 檔案的 \"include\" 指令",
|
||||||
"invalid amount expression": "金額表達式無效",
|
"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 cannot be blank": "查詢項目不能為空",
|
||||||
"query items too much": "查詢項目過多",
|
"query items too much": "查詢項目過多",
|
||||||
"query items have invalid item": "查詢項目中有非法項目",
|
"query items have invalid item": "查詢項目中有非法項目",
|
||||||
@@ -2039,6 +2042,7 @@
|
|||||||
"WebAuthn is not enabled": "WebAuthn 沒有啟用",
|
"WebAuthn is not enabled": "WebAuthn 沒有啟用",
|
||||||
"Are you sure you want to re-login?": "您確定要重新登入?",
|
"Are you sure you want to re-login?": "您確定要重新登入?",
|
||||||
"Exchange Rates Data": "匯率資料",
|
"Exchange Rates Data": "匯率資料",
|
||||||
|
"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": "更新使用者自訂匯率",
|
||||||
|
"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": "在行動裝置使用",
|
"Use on Mobile Device": "在行動裝置使用",
|
||||||
"You can scan the QR code below on your mobile device.": "您可以在您的行動裝置上掃描下方的 QR Code。",
|
"You can scan the QR code below on your mobile device.": "您可以在您的行動裝置上掃描下方的 QR Code。",
|
||||||
"Switch to Mobile Version": "切換到行動版",
|
"Switch to Mobile Version": "切換到行動版",
|
||||||
|
|||||||
@@ -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 {
|
export interface LatestExchangeRate {
|
||||||
readonly currency: string;
|
readonly currency: string;
|
||||||
readonly rate: string;
|
readonly rate: string;
|
||||||
@@ -6,7 +19,7 @@ export interface LatestExchangeRate {
|
|||||||
export interface LatestExchangeRateResponse {
|
export interface LatestExchangeRateResponse {
|
||||||
readonly dataSource: string;
|
readonly dataSource: string;
|
||||||
readonly referenceUrl: string;
|
readonly referenceUrl: string;
|
||||||
readonly updateTime: number;
|
updateTime: number;
|
||||||
readonly baseCurrency: string;
|
readonly baseCurrency: string;
|
||||||
readonly exchangeRates: LatestExchangeRate[];
|
readonly exchangeRates: LatestExchangeRate[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import TransactionTemplateListPage from '@/views/desktop/templates/ListPage.vue'
|
|||||||
import UserSettingsPage from '@/views/desktop/user/UserSettingsPage.vue';
|
import UserSettingsPage from '@/views/desktop/user/UserSettingsPage.vue';
|
||||||
import AppSettingsPage from '@/views/desktop/app/AppSettingsPage.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';
|
import AboutPage from '@/views/desktop/AboutPage.vue';
|
||||||
|
|
||||||
function checkLogin(): NavigationGuardReturn {
|
function checkLogin(): NavigationGuardReturn {
|
||||||
@@ -168,7 +168,7 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/exchange_rates',
|
path: '/exchange_rates',
|
||||||
component: ExchangeRatesPage,
|
component: ExchangeRatesListPage,
|
||||||
beforeEnter: checkLogin
|
beforeEnter: checkLogin
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ import TransactionTagFilterSettingsPage from '@/views/mobile/settings/Transactio
|
|||||||
|
|
||||||
import SettingsPage from '@/views/mobile/SettingsPage.vue';
|
import SettingsPage from '@/views/mobile/SettingsPage.vue';
|
||||||
import ApplicationLockPage from '@/views/mobile/ApplicationLockPage.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 AboutPage from '@/views/mobile/AboutPage.vue';
|
||||||
|
|
||||||
import UserProfilePage from '@/views/mobile/users/UserProfilePage.vue';
|
import UserProfilePage from '@/views/mobile/users/UserProfilePage.vue';
|
||||||
@@ -236,7 +237,12 @@ const routes: Router.RouteParameters[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/exchange_rates',
|
path: '/exchange_rates',
|
||||||
async: asyncResolve(ExchangeRatesPage),
|
async: asyncResolve(ExchangeRatesListPage),
|
||||||
|
beforeEnter: [checkLogin]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/exchange_rates/update',
|
||||||
|
async: asyncResolve(ExchangeRatesUpdatePage),
|
||||||
beforeEnter: [checkLogin]
|
beforeEnter: [checkLogin]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+138
-1
@@ -1,7 +1,13 @@
|
|||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
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 { isEquals } from '@/lib/common.ts';
|
||||||
import { getCurrentUnixTime, formatUnixTime } from '@/lib/datetime.ts';
|
import { getCurrentUnixTime, formatUnixTime } from '@/lib/datetime.ts';
|
||||||
@@ -11,6 +17,7 @@ import logger from '@/lib/logger.ts';
|
|||||||
import services from '@/lib/services.ts';
|
import services from '@/lib/services.ts';
|
||||||
|
|
||||||
const exchangeRatesLocalStorageKey = 'ebk_app_exchange_rates';
|
const exchangeRatesLocalStorageKey = 'ebk_app_exchange_rates';
|
||||||
|
const userDataSourceType = 'user_custom';
|
||||||
|
|
||||||
interface LatestExchangeRates {
|
interface LatestExchangeRates {
|
||||||
readonly time?: number;
|
readonly time?: number;
|
||||||
@@ -34,6 +41,14 @@ function clearExchangeRatesFromLocalStorage(): void {
|
|||||||
export const useExchangeRatesStore = defineStore('exchangeRates', () => {
|
export const useExchangeRatesStore = defineStore('exchangeRates', () => {
|
||||||
const latestExchangeRates = ref<LatestExchangeRates>(getExchangeRatesFromLocalStorage());
|
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 exchangeRatesLastUpdateTime = computed<number | null>(() => {
|
||||||
const exchangeRates = latestExchangeRates.value || {};
|
const exchangeRates = latestExchangeRates.value || {};
|
||||||
return exchangeRates && exchangeRates.data ? exchangeRates.data.updateTime : null;
|
return exchangeRates && exchangeRates.data ? exchangeRates.data.updateTime : null;
|
||||||
@@ -54,6 +69,55 @@ export const useExchangeRatesStore = defineStore('exchangeRates', () => {
|
|||||||
return exchangeRateMap;
|
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 {
|
function resetLatestExchangeRates(): void {
|
||||||
latestExchangeRates.value = {};
|
latestExchangeRates.value = {};
|
||||||
clearExchangeRatesFromLocalStorage();
|
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 {
|
function getExchangedAmount(amount: number, fromCurrency: string, toCurrency: string): number | null {
|
||||||
if (amount === 0) {
|
if (amount === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -145,11 +279,14 @@ export const useExchangeRatesStore = defineStore('exchangeRates', () => {
|
|||||||
// states
|
// states
|
||||||
latestExchangeRates,
|
latestExchangeRates,
|
||||||
// computed states
|
// computed states
|
||||||
|
isUserCustomExchangeRates,
|
||||||
exchangeRatesLastUpdateTime,
|
exchangeRatesLastUpdateTime,
|
||||||
latestExchangeRateMap,
|
latestExchangeRateMap,
|
||||||
// functions
|
// functions
|
||||||
resetLatestExchangeRates,
|
resetLatestExchangeRates,
|
||||||
getLatestExchangeRates,
|
getLatestExchangeRates,
|
||||||
|
updateUserCustomExchangeRate,
|
||||||
|
deleteUserCustomExchangeRate,
|
||||||
getExchangedAmount
|
getExchangedAmount
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -378,6 +378,8 @@ export const useRootStore = defineStore('root', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateUserProfile(req: UserProfileUpdateRequest): Promise<UserProfileUpdateResponse> {
|
function updateUserProfile(req: UserProfileUpdateRequest): Promise<UserProfileUpdateResponse> {
|
||||||
|
const userDefaultCurrency = userStore.currentUserDefaultCurrency;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
services.updateProfile(req).then(response => {
|
services.updateProfile(req).then(response => {
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
@@ -407,6 +409,10 @@ export const useRootStore = defineStore('root', () => {
|
|||||||
statisticsStore.updateTransactionStatisticsInvalidState(true);
|
statisticsStore.updateTransactionStatisticsInvalidState(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.result.user && data.result.user.defaultCurrency !== userDefaultCurrency) {
|
||||||
|
exchangeRatesStore.resetLatestExchangeRates();
|
||||||
|
}
|
||||||
|
|
||||||
resolve(data.result);
|
resolve(data.result);
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
logger.error('failed to save user profile', error);
|
logger.error('failed to save user profile', error);
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
--ebk-icon-font-size: 28px;
|
--ebk-icon-font-size: 28px;
|
||||||
--ebk-icon-text-margin: 2px;
|
--ebk-icon-text-margin: 2px;
|
||||||
--ebk-hide-icon-font-size: 18px;
|
--ebk-hide-icon-font-size: 18px;
|
||||||
|
--ebk-separate-icon-font-size: 24px;
|
||||||
--ebk-big-icon-button-size: 42px;
|
--ebk-big-icon-button-size: 42px;
|
||||||
--ebk-icon-after-text-font-size: 16px;
|
--ebk-icon-after-text-font-size: 16px;
|
||||||
--ebk-right-bottom-icon-font-size: 13px;
|
--ebk-right-bottom-icon-font-size: 13px;
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
--ebk-icon-font-size: 29px;
|
--ebk-icon-font-size: 29px;
|
||||||
--ebk-icon-text-margin: 3px;
|
--ebk-icon-text-margin: 3px;
|
||||||
--ebk-hide-icon-font-size: 19px;
|
--ebk-hide-icon-font-size: 19px;
|
||||||
|
--ebk-separate-icon-font-size: 28px;
|
||||||
--ebk-big-icon-button-size: 44px;
|
--ebk-big-icon-button-size: 44px;
|
||||||
--ebk-icon-after-text-font-size: 17px;
|
--ebk-icon-after-text-font-size: 17px;
|
||||||
--ebk-right-bottom-icon-font-size: 13px;
|
--ebk-right-bottom-icon-font-size: 13px;
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
--ebk-icon-font-size: 28px;
|
--ebk-icon-font-size: 28px;
|
||||||
--ebk-icon-text-margin: 2px;
|
--ebk-icon-text-margin: 2px;
|
||||||
--ebk-hide-icon-font-size: 18px;
|
--ebk-hide-icon-font-size: 18px;
|
||||||
|
--ebk-separate-icon-font-size: 20px;
|
||||||
--ebk-big-icon-button-size: 42px;
|
--ebk-big-icon-button-size: 42px;
|
||||||
--ebk-icon-after-text-font-size: 15px;
|
--ebk-icon-after-text-font-size: 15px;
|
||||||
--ebk-right-bottom-icon-font-size: 13px;
|
--ebk-right-bottom-icon-font-size: 13px;
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
--ebk-icon-font-size: 30px;
|
--ebk-icon-font-size: 30px;
|
||||||
--ebk-icon-text-margin: 3px;
|
--ebk-icon-text-margin: 3px;
|
||||||
--ebk-hide-icon-font-size: 20px;
|
--ebk-hide-icon-font-size: 20px;
|
||||||
|
--ebk-separate-icon-font-size: 32px;
|
||||||
--ebk-big-icon-button-size: 48px;
|
--ebk-big-icon-button-size: 48px;
|
||||||
--ebk-icon-after-text-font-size: 17px;
|
--ebk-icon-after-text-font-size: 17px;
|
||||||
--ebk-right-bottom-icon-font-size: 14px;
|
--ebk-right-bottom-icon-font-size: 14px;
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
--ebk-icon-font-size: 32px;
|
--ebk-icon-font-size: 32px;
|
||||||
--ebk-icon-text-margin: 4px;
|
--ebk-icon-text-margin: 4px;
|
||||||
--ebk-hide-icon-font-size: 24px;
|
--ebk-hide-icon-font-size: 24px;
|
||||||
|
--ebk-separate-icon-font-size: 36px;
|
||||||
--ebk-big-icon-button-size: 54px;
|
--ebk-big-icon-button-size: 54px;
|
||||||
--ebk-icon-after-text-font-size: 18px;
|
--ebk-icon-after-text-font-size: 18px;
|
||||||
--ebk-right-bottom-icon-font-size: 15px;
|
--ebk-right-bottom-icon-font-size: 15px;
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
--ebk-icon-font-size: 34px;
|
--ebk-icon-font-size: 34px;
|
||||||
--ebk-icon-text-margin: 4px;
|
--ebk-icon-text-margin: 4px;
|
||||||
--ebk-hide-icon-font-size: 26px;
|
--ebk-hide-icon-font-size: 26px;
|
||||||
|
--ebk-separate-icon-font-size: 40px;
|
||||||
--ebk-big-icon-button-size: 58px;
|
--ebk-big-icon-button-size: 58px;
|
||||||
--ebk-icon-after-text-font-size: 20px;
|
--ebk-icon-after-text-font-size: 20px;
|
||||||
--ebk-right-bottom-icon-font-size: 17px;
|
--ebk-right-bottom-icon-font-size: 17px;
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
--ebk-icon-font-size: 36px;
|
--ebk-icon-font-size: 36px;
|
||||||
--ebk-icon-text-margin: 4px;
|
--ebk-icon-text-margin: 4px;
|
||||||
--ebk-hide-icon-font-size: 28px;
|
--ebk-hide-icon-font-size: 28px;
|
||||||
|
--ebk-separate-icon-font-size: 44px;
|
||||||
--ebk-big-icon-button-size: 64px;
|
--ebk-big-icon-button-size: 64px;
|
||||||
--ebk-icon-after-text-font-size: 22px;
|
--ebk-icon-after-text-font-size: 22px;
|
||||||
--ebk-right-bottom-icon-font-size: 19px;
|
--ebk-right-bottom-icon-font-size: 19px;
|
||||||
|
|||||||
@@ -152,6 +152,10 @@ i.icon.la, i.icon.las, i.icon.lab {
|
|||||||
font-size: var(--ebk-hide-icon-font-size);
|
font-size: var(--ebk-hide-icon-font-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.separate-icon {
|
||||||
|
font-size: var(--ebk-separate-icon-font-size);
|
||||||
|
}
|
||||||
|
|
||||||
.transaction-tag-icon {
|
.transaction-tag-icon {
|
||||||
font-size: var(--ebk-transaction-tag-icon-font-size);
|
font-size: var(--ebk-transaction-tag-icon-font-size);
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ export function useExchangeRatesPageBase() {
|
|||||||
const baseCurrency = ref<string>(userStore.currentUserDefaultCurrency);
|
const baseCurrency = ref<string>(userStore.currentUserDefaultCurrency);
|
||||||
const baseAmount = ref<number>(100);
|
const baseAmount = ref<number>(100);
|
||||||
|
|
||||||
|
const defaultCurrency = computed<string>(() => userStore.currentUserDefaultCurrency);
|
||||||
const exchangeRatesData = computed<LatestExchangeRateResponse | undefined>(() => exchangeRatesStore.latestExchangeRates.data);
|
const exchangeRatesData = computed<LatestExchangeRateResponse | undefined>(() => exchangeRatesStore.latestExchangeRates.data);
|
||||||
|
const isUserCustomExchangeRates = computed<boolean>(() => exchangeRatesStore.isUserCustomExchangeRates);
|
||||||
|
|
||||||
const exchangeRatesDataUpdateTime = computed<string>(() => {
|
const exchangeRatesDataUpdateTime = computed<string>(() => {
|
||||||
const exchangeRatesLastUpdateTime = exchangeRatesStore.exchangeRatesLastUpdateTime;
|
const exchangeRatesLastUpdateTime = exchangeRatesStore.exchangeRatesLastUpdateTime;
|
||||||
@@ -55,7 +57,9 @@ export function useExchangeRatesPageBase() {
|
|||||||
baseCurrency,
|
baseCurrency,
|
||||||
baseAmount,
|
baseAmount,
|
||||||
// computed states
|
// computed states
|
||||||
|
defaultCurrency,
|
||||||
exchangeRatesData,
|
exchangeRatesData,
|
||||||
|
isUserCustomExchangeRates,
|
||||||
exchangeRatesDataUpdateTime,
|
exchangeRatesDataUpdateTime,
|
||||||
availableExchangeRates,
|
availableExchangeRates,
|
||||||
// functions
|
// functions
|
||||||
|
|||||||
+84
-6
@@ -7,8 +7,9 @@
|
|||||||
<div class="mx-6 my-4">
|
<div class="mx-6 my-4">
|
||||||
<span class="text-subtitle-2">{{ tt('Data source') }}</span>
|
<span class="text-subtitle-2">{{ tt('Data source') }}</span>
|
||||||
<p class="text-body-1 mt-1 mb-3">
|
<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>
|
<a tabindex="-1" target="_blank" :href="exchangeRatesData.referenceUrl" v-if="!loading && exchangeRatesData && !isUserCustomExchangeRates && exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</a>
|
||||||
<span v-else-if="!loading && exchangeRatesData && !exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</span>
|
<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 && !exchangeRatesData">{{ tt('None') }}</span>
|
||||||
<span v-else-if="loading">
|
<span v-else-if="loading">
|
||||||
<v-skeleton-loader class="skeleton-no-margin mt-3 mb-4" type="text" :loading="true"></v-skeleton-loader>
|
<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-icon :icon="mdiMenu" size="24" />
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<span>{{ tt('Exchange Rates Data') }}</span>
|
<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"
|
<v-btn density="compact" color="default" variant="text" size="24"
|
||||||
class="ml-2" :icon="true" :loading="loading" @click="reload(true)">
|
class="ml-2" :icon="true" :loading="loading" @click="reload(true)">
|
||||||
<template #loader>
|
<template #loader>
|
||||||
@@ -107,15 +111,30 @@
|
|||||||
<div class="d-flex align-center">
|
<div class="d-flex align-center">
|
||||||
<span class="text-sm">{{ exchangeRate.currencyDisplayName }}</span>
|
<span class="text-sm">{{ exchangeRate.currencyDisplayName }}</span>
|
||||||
<span class="text-caption ml-1">{{ exchangeRate.currencyCode }}</span>
|
<span class="text-caption ml-1">{{ exchangeRate.currencyCode }}</span>
|
||||||
|
|
||||||
<v-spacer/>
|
<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"
|
density="comfortable" variant="text"
|
||||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||||
v-if="exchangeRate.currencyCode !== baseCurrency"
|
v-if="exchangeRate.currencyCode !== baseCurrency"
|
||||||
@click="setAsBaseline(exchangeRate.currencyCode, getFinalConvertedAmount(exchangeRate))">
|
@click="setAsBaseline(exchangeRate.currencyCode, getFinalConvertedAmount(exchangeRate))">
|
||||||
{{ tt('Set as Base') }}
|
{{ tt('Set as Base') }}
|
||||||
</v-btn>
|
</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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -130,11 +149,16 @@
|
|||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
|
<update-dialog ref="updateDialog" />
|
||||||
|
|
||||||
|
<confirm-dialog ref="confirmDialog"/>
|
||||||
<snack-bar ref="snackbar" />
|
<snack-bar ref="snackbar" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue';
|
||||||
import SnackBar from '@/components/desktop/SnackBar.vue';
|
import SnackBar from '@/components/desktop/SnackBar.vue';
|
||||||
|
import UpdateDialog from './list/dialogs/UpdateDialog.vue';
|
||||||
|
|
||||||
import { ref, useTemplateRef, watch } from 'vue';
|
import { ref, useTemplateRef, watch } from 'vue';
|
||||||
import { useDisplay } from 'vuetify';
|
import { useDisplay } from 'vuetify';
|
||||||
@@ -150,22 +174,39 @@ import logger from '@/lib/logger.ts';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
mdiRefresh,
|
mdiRefresh,
|
||||||
mdiMenu
|
mdiMenu,
|
||||||
|
mdiDeleteOutline
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
|
|
||||||
|
type ConfirmDialogType = InstanceType<typeof ConfirmDialog>;
|
||||||
type SnackBarType = InstanceType<typeof SnackBar>;
|
type SnackBarType = InstanceType<typeof SnackBar>;
|
||||||
|
type UpdateDialogType = InstanceType<typeof UpdateDialog>;
|
||||||
|
|
||||||
const { mdAndUp } = useDisplay();
|
const { mdAndUp } = useDisplay();
|
||||||
|
|
||||||
const { tt, formatExchangeRateAmount } = useI18n();
|
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 exchangeRatesStore = useExchangeRatesStore();
|
||||||
|
|
||||||
|
const confirmDialog = useTemplateRef<ConfirmDialogType>('confirmDialog');
|
||||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||||
|
const updateDialog = useTemplateRef<UpdateDialogType>('updateDialog');
|
||||||
|
|
||||||
const activeTab = ref<string>('exchangeRatesPage');
|
const activeTab = ref<string>('exchangeRatesPage');
|
||||||
const loading = ref<boolean>(true);
|
const loading = ref<boolean>(true);
|
||||||
|
const updating = ref<boolean>(false);
|
||||||
|
const customExchangeRateRemoving = ref<Record<string, boolean>>({});
|
||||||
const alwaysShowNav = ref<boolean>(mdAndUp.value);
|
const alwaysShowNav = ref<boolean>(mdAndUp.value);
|
||||||
const showNav = 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 {
|
function getFinalConvertedAmount(toExchangeRate: LocalizedLatestExchangeRate): string {
|
||||||
if (!baseCurrency.value) {
|
if (!baseCurrency.value) {
|
||||||
return '0';
|
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>
|
||||||
+123
-19
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<f7-page ptr @ptr:refresh="update">
|
<f7-page ptr @ptr:refresh="reload">
|
||||||
<f7-navbar>
|
<f7-navbar>
|
||||||
<f7-nav-left :back-link="tt('Back')"></f7-nav-left>
|
<f7-nav-left :back-link="tt('Back')"></f7-nav-left>
|
||||||
<f7-nav-title :title="tt('Exchange Rates Data')"></f7-nav-title>
|
<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 strong inset dividers class="margin-vertical" v-if="exchangeRatesData && exchangeRatesData.exchangeRates && exchangeRatesData.exchangeRates.length">
|
||||||
<f7-list-item swipeout
|
<f7-list-item swipeout
|
||||||
|
:id="getExchangeRateDomId(exchangeRate)"
|
||||||
:after="getFinalConvertedAmount(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>
|
<template #title>
|
||||||
<div class="no-padding no-margin">
|
<div class="no-padding no-margin">
|
||||||
<span style="margin-right: 5px">{{ exchangeRate.currencyDisplayName }}</span>
|
<span style="margin-right: 5px">{{ exchangeRate.currencyDisplayName }}</span>
|
||||||
<small class="smaller">{{ exchangeRate.currencyCode }}</small>
|
<small class="smaller">{{ exchangeRate.currencyCode }}</small>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<f7-swipeout-actions right v-if="exchangeRate.currencyCode !== baseCurrency">
|
<f7-swipeout-actions right v-if="exchangeRate.currencyCode !== baseCurrency || (exchangeRate.currencyCode !== defaultCurrency && isUserCustomExchangeRates)">
|
||||||
<f7-swipeout-button color="primary" close :text="tt('Set as Base')" @click="setAsBaseline(exchangeRate.currencyCode, getFinalConvertedAmount(exchangeRate))"></f7-swipeout-button>
|
<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-swipeout-actions>
|
||||||
</f7-list-item>
|
</f7-list-item>
|
||||||
</f7-list>
|
</f7-list>
|
||||||
@@ -78,18 +89,34 @@
|
|||||||
<f7-list-item>
|
<f7-list-item>
|
||||||
<small>{{ tt('Data source') }}</small>
|
<small>{{ tt('Data source') }}</small>
|
||||||
<small>
|
<small>
|
||||||
<f7-link external target="_blank" :href="exchangeRatesData.referenceUrl" v-if="exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</f7-link>
|
<f7-link external target="_blank" :href="exchangeRatesData.referenceUrl" v-if="!isUserCustomExchangeRates && exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</f7-link>
|
||||||
<span v-else-if="!exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</span>
|
<span v-else-if="!isUserCustomExchangeRates && !exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</span>
|
||||||
|
<span v-else-if="isUserCustomExchangeRates">{{ tt('User Custom') }}</span>
|
||||||
</small>
|
</small>
|
||||||
</f7-list-item>
|
</f7-list-item>
|
||||||
</f7-list>
|
</f7-list>
|
||||||
|
|
||||||
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
|
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
|
||||||
<f7-actions-group>
|
<f7-actions-group v-if="isUserCustomExchangeRates">
|
||||||
<f7-actions-button :class="{ 'disabled': updating }" @click="update(undefined)">
|
<f7-actions-button :class="{ 'disabled': loading }" @click="update()">
|
||||||
<span>{{ tt('Update') }}</span>
|
<span>{{ tt('Update') }}</span>
|
||||||
</f7-actions-button>
|
</f7-actions-button>
|
||||||
</f7-actions-group>
|
</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-group>
|
||||||
<f7-actions-button bold close>{{ tt('Cancel') }}</f7-actions-button>
|
<f7-actions-button bold close>{{ tt('Cancel') }}</f7-actions-button>
|
||||||
</f7-actions-group>
|
</f7-actions-group>
|
||||||
@@ -99,9 +126,10 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
|
import type { Router } from 'framework7/types';
|
||||||
|
|
||||||
import { useI18n } from '@/locales/helpers.ts';
|
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 { useExchangeRatesPageBase } from '@/views/base/ExchangeRatesPageBase.ts';
|
||||||
|
|
||||||
import { useExchangeRatesStore } from '@/stores/exchangeRates.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 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 { tt, getCurrencyName, formatAmount, formatExchangeRateAmount } = useI18n();
|
||||||
const { showToast } = useI18nUIComponents();
|
const { showAlert, showToast } = useI18nUIComponents();
|
||||||
const { baseCurrency, baseAmount, exchangeRatesData, exchangeRatesDataUpdateTime, availableExchangeRates, getConvertedAmount, setAsBaseline } = useExchangeRatesPageBase();
|
const {
|
||||||
|
baseCurrency,
|
||||||
|
baseAmount,
|
||||||
|
defaultCurrency,
|
||||||
|
exchangeRatesData,
|
||||||
|
isUserCustomExchangeRates,
|
||||||
|
exchangeRatesDataUpdateTime,
|
||||||
|
availableExchangeRates,
|
||||||
|
getConvertedAmount,
|
||||||
|
setAsBaseline
|
||||||
|
} = useExchangeRatesPageBase();
|
||||||
|
|
||||||
const exchangeRatesStore = useExchangeRatesStore();
|
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 showMoreActionSheet = ref<boolean>(false);
|
||||||
const showBaseCurrencyPopup = ref<boolean>(false);
|
const showBaseCurrencyPopup = ref<boolean>(false);
|
||||||
const showBaseAmountSheet = 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 displayBaseAmount = computed<string>(() => formatAmount(baseAmount.value, baseCurrency.value));
|
||||||
const baseAmountFontSizeClass = computed<string>(() => {
|
const baseAmountFontSizeClass = computed<string>(() => {
|
||||||
@@ -132,13 +182,17 @@ const baseAmountFontSizeClass = computed<string>(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function update(done?: () => void): void {
|
function getExchangeRateDomId(exchangeRate: LocalizedLatestExchangeRate): string {
|
||||||
if (updating.value) {
|
return 'exchangeRate_' + exchangeRate.currencyCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reload(done?: () => void): void {
|
||||||
|
if (loading.value) {
|
||||||
done?.();
|
done?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updating.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
if (!done) {
|
if (!done) {
|
||||||
showLoading();
|
showLoading();
|
||||||
@@ -150,14 +204,54 @@ function update(done?: () => void): void {
|
|||||||
}).then(() => {
|
}).then(() => {
|
||||||
done?.();
|
done?.();
|
||||||
|
|
||||||
updating.value = false;
|
loading.value = false;
|
||||||
hideLoading();
|
hideLoading();
|
||||||
|
|
||||||
showToast('Exchange rates data has been updated');
|
showToast('Exchange rates data has been updated');
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
done?.();
|
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();
|
hideLoading();
|
||||||
|
|
||||||
if (!error.processed) {
|
if (!error.processed) {
|
||||||
@@ -177,7 +271,16 @@ function getFinalConvertedAmount(toExchangeRate: LocalizedLatestExchangeRate): s
|
|||||||
return formatExchangeRateAmount(exchangeRateAmount);
|
return formatExchangeRateAmount(exchangeRateAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exchangeRatesData.value && exchangeRatesData.value.exchangeRates) {
|
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;
|
const exchangeRates = exchangeRatesData.value.exchangeRates;
|
||||||
let hasBaseCurrency = false;
|
let hasBaseCurrency = false;
|
||||||
|
|
||||||
@@ -192,7 +295,8 @@ if (exchangeRatesData.value && exchangeRatesData.value.exchangeRates) {
|
|||||||
if (!hasBaseCurrency) {
|
if (!hasBaseCurrency) {
|
||||||
showToast('There is no exchange rates data for your default currency');
|
showToast('There is no exchange rates data for your default currency');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<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) }} </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) }} </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>
|
||||||
Reference in New Issue
Block a user