diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go
index dc1dbc6b..f9351b4a 100644
--- a/pkg/api/transactions.go
+++ b/pkg/api/transactions.go
@@ -1,6 +1,7 @@
package api
import (
+ "encoding/json"
"sort"
"strings"
@@ -572,7 +573,7 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, *
transactionEditable := transaction.IsEditable(user, utcOffset, accountMap[transaction.AccountId], accountMap[transaction.RelatedAccountId])
transactionTagIds := allTransactionTagIds[transaction.TransactionId]
- transactionResp := transaction.ToTransactionInfoResponse(transactionTagIds, transactionEditable)
+ transactionResp := transaction.ToTransactionInfoResponse(c, transactionTagIds, transactionEditable)
if !transactionGetReq.TrimAccount {
if sourceAccount := accountMap[transaction.AccountId]; sourceAccount != nil {
@@ -664,7 +665,7 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (interface{}
log.InfofWithRequestId(c, "[transactions.TransactionCreateHandler] user \"uid:%d\" has created a new transaction \"id:%d\" successfully", uid, transaction.TransactionId)
- transactionResp := transaction.ToTransactionInfoResponse(tagIds, transactionEditable)
+ transactionResp := transaction.ToTransactionInfoResponse(c, tagIds, transactionEditable)
return transactionResp, nil
}
@@ -722,6 +723,12 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
transactionTagIds = make([]int64, 0, 0)
}
+ var geoLocation []byte
+
+ if transactionModifyReq.GeoLocation != nil {
+ geoLocation, _ = json.Marshal(transactionModifyReq.GeoLocation)
+ }
+
newTransaction := &models.Transaction{
TransactionId: transaction.TransactionId,
Uid: uid,
@@ -732,6 +739,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
Amount: transactionModifyReq.SourceAmount,
HideAmount: transactionModifyReq.HideAmount,
Comment: transactionModifyReq.Comment,
+ GeoLocation: string(geoLocation),
}
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
@@ -748,6 +756,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
(transaction.Type != models.TRANSACTION_DB_TYPE_TRANSFER_OUT || newTransaction.RelatedAccountAmount == transaction.RelatedAccountAmount) &&
newTransaction.HideAmount == transaction.HideAmount &&
newTransaction.Comment == transaction.Comment &&
+ newTransaction.GeoLocation == transaction.GeoLocation &&
utils.Int64SliceEquals(tagIds, transactionTagIds) {
return nil, errs.ErrNothingWillBeUpdated
}
@@ -777,7 +786,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
log.InfofWithRequestId(c, "[transactions.TransactionModifyHandler] user \"uid:%d\" has updated transaction \"id:%d\" successfully", uid, transactionModifyReq.Id)
newTransaction.Type = transaction.Type
- newTransactionResp := newTransaction.ToTransactionInfoResponse(tagIds, transactionEditable)
+ newTransactionResp := newTransaction.ToTransactionInfoResponse(c, tagIds, transactionEditable)
return newTransactionResp, nil
}
@@ -1004,7 +1013,7 @@ func (a *TransactionsApi) getTransactionListResult(c *core.Context, user *models
transactionEditable := transaction.IsEditable(user, utcOffset, allAccounts[transaction.AccountId], allAccounts[transaction.RelatedAccountId])
transactionTagIds := allTransactionTagIds[transaction.TransactionId]
- result[i] = transaction.ToTransactionInfoResponse(transactionTagIds, transactionEditable)
+ result[i] = transaction.ToTransactionInfoResponse(c, transactionTagIds, transactionEditable)
if !trimAccount {
if sourceAccount := allAccounts[transaction.AccountId]; sourceAccount != nil {
@@ -1045,6 +1054,12 @@ func (a *TransactionsApi) createNewTransactionModel(uid int64, transactionCreate
transactionDbType = models.TRANSACTION_DB_TYPE_TRANSFER_OUT
}
+ var geoLocation []byte
+
+ if transactionCreateReq.GeoLocation != nil {
+ geoLocation, _ = json.Marshal(transactionCreateReq.GeoLocation)
+ }
+
transaction := &models.Transaction{
Uid: uid,
Type: transactionDbType,
@@ -1055,6 +1070,7 @@ func (a *TransactionsApi) createNewTransactionModel(uid int64, transactionCreate
Amount: transactionCreateReq.SourceAmount,
HideAmount: transactionCreateReq.HideAmount,
Comment: transactionCreateReq.Comment,
+ GeoLocation: string(geoLocation),
CreatedIp: clientIp,
}
diff --git a/pkg/models/transaction.go b/pkg/models/transaction.go
index ac5bdb78..a074bc73 100644
--- a/pkg/models/transaction.go
+++ b/pkg/models/transaction.go
@@ -1,11 +1,14 @@
package models
import (
+ "encoding/json"
"fmt"
"strings"
"time"
+ "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
+ "github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
@@ -48,40 +51,49 @@ type Transaction struct {
RelatedAccountAmount int64 `xorm:"NOT NULL"`
HideAmount bool `xorm:"NOT NULL"`
Comment string `xorm:"VARCHAR(255) NOT NULL"`
+ GeoLocation string `xorm:"VARCHAR(255)"`
CreatedIp string `xorm:"VARCHAR(39)"`
CreatedUnixTime int64
UpdatedUnixTime int64
DeletedUnixTime int64
}
+// TransactionGeoLocationRequest represents all parameters of transaction geographic location info update request
+type TransactionGeoLocationRequest struct {
+ Latitude float64 `json:"latitude" binding:"required"`
+ Longitude float64 `json:"longitude" binding:"required"`
+}
+
// TransactionCreateRequest represents all parameters of transaction creation request
type TransactionCreateRequest struct {
- Type TransactionType `json:"type" binding:"required"`
- CategoryId int64 `json:"categoryId,string"`
- Time int64 `json:"time" binding:"required,min=1"`
- UtcOffset int16 `json:"utcOffset" binding:"min=-720,max=840"`
- SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
- DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
- SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
- DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
- HideAmount bool `json:"hideAmount"`
- TagIds []string `json:"tagIds"`
- Comment string `json:"comment" binding:"max=255"`
+ Type TransactionType `json:"type" binding:"required"`
+ CategoryId int64 `json:"categoryId,string"`
+ Time int64 `json:"time" binding:"required,min=1"`
+ UtcOffset int16 `json:"utcOffset" binding:"min=-720,max=840"`
+ SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
+ DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
+ SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
+ DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
+ HideAmount bool `json:"hideAmount"`
+ TagIds []string `json:"tagIds"`
+ Comment string `json:"comment" binding:"max=255"`
+ GeoLocation *TransactionGeoLocationRequest `json:"geoLocation" binding:"omitempty"`
}
// TransactionModifyRequest represents all parameters of transaction modification request
type TransactionModifyRequest struct {
- Id int64 `json:"id,string" binding:"required,min=1"`
- CategoryId int64 `json:"categoryId,string"`
- Time int64 `json:"time" binding:"required,min=1"`
- UtcOffset int16 `json:"utcOffset" binding:"min=-720,max=840"`
- SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
- DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
- SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
- DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
- HideAmount bool `json:"hideAmount"`
- TagIds []string `json:"tagIds"`
- Comment string `json:"comment" binding:"max=255"`
+ Id int64 `json:"id,string" binding:"required,min=1"`
+ CategoryId int64 `json:"categoryId,string"`
+ Time int64 `json:"time" binding:"required,min=1"`
+ UtcOffset int16 `json:"utcOffset" binding:"min=-720,max=840"`
+ SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
+ DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
+ SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
+ DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
+ HideAmount bool `json:"hideAmount"`
+ TagIds []string `json:"tagIds"`
+ Comment string `json:"comment" binding:"max=255"`
+ GeoLocation *TransactionGeoLocationRequest `json:"geoLocation" binding:"omitempty"`
}
// TransactionCountRequest represents transaction count request
@@ -170,6 +182,12 @@ type TransactionAccountAmount struct {
TotalExpenseAmount int64
}
+// TransactionGeoLocationResponse represents a view-object of transaction geographic location info
+type TransactionGeoLocationResponse struct {
+ Latitude float64 `json:"latitude"`
+ Longitude float64 `json:"longitude"`
+}
+
// TransactionInfoResponse represents a view-object of transaction
type TransactionInfoResponse struct {
Id int64 `json:"id,string"`
@@ -189,6 +207,7 @@ type TransactionInfoResponse struct {
TagIds []string `json:"tagIds"`
Tags []*TransactionTagInfoResponse `json:"tags,omitempty"`
Comment string `json:"comment"`
+ GeoLocation *TransactionGeoLocationResponse `json:"geoLocation,omitempty"`
Editable bool `json:"editable"`
}
@@ -264,7 +283,7 @@ func (t *Transaction) IsEditable(currentUser *User, utcOffset int16, account *Ac
}
// ToTransactionInfoResponse returns a view-object according to database model
-func (t *Transaction) ToTransactionInfoResponse(tagIds []int64, editable bool) *TransactionInfoResponse {
+func (t *Transaction) ToTransactionInfoResponse(c *core.Context, tagIds []int64, editable bool) *TransactionInfoResponse {
var transactionType TransactionType
if t.Type == TRANSACTION_DB_TYPE_MODIFY_BALANCE {
@@ -298,6 +317,17 @@ func (t *Transaction) ToTransactionInfoResponse(tagIds []int64, editable bool) *
destinationAmount = t.Amount
}
+ geoLocation := &TransactionGeoLocationResponse{}
+
+ if t.GeoLocation != "" {
+ err := json.Unmarshal([]byte(t.GeoLocation), geoLocation)
+ if err != nil {
+ log.WarnfWithRequestId(c, "[transaction.ToTransactionInfoResponse] cannot unmarshal geo location \"%s\", because %s", t.GeoLocation, err.Error())
+ }
+ } else {
+ geoLocation = nil
+ }
+
return &TransactionInfoResponse{
Id: t.TransactionId,
TimeSequenceId: t.TransactionTime,
@@ -312,6 +342,7 @@ func (t *Transaction) ToTransactionInfoResponse(tagIds []int64, editable bool) *
HideAmount: t.HideAmount,
TagIds: utils.Int64ArrayToStringArray(tagIds),
Comment: t.Comment,
+ GeoLocation: geoLocation,
Editable: editable,
}
}
diff --git a/pkg/services/transactions.go b/pkg/services/transactions.go
index b52cdda7..4baabd8b 100644
--- a/pkg/services/transactions.go
+++ b/pkg/services/transactions.go
@@ -542,6 +542,10 @@ func (s *TransactionService) ModifyTransaction(transaction *models.Transaction,
updateCols = append(updateCols, "comment")
}
+ if transaction.GeoLocation != oldTransaction.GeoLocation {
+ updateCols = append(updateCols, "geo_location")
+ }
+
// Get and verify tags
err = s.isTagsValid(sess, transaction, transactionTagIndexs, addTagIds)
@@ -955,6 +959,7 @@ func (s *TransactionService) GetRelatedTransferTransaction(originalTransaction *
RelatedAccountId: originalTransaction.AccountId,
RelatedAccountAmount: originalTransaction.Amount,
Comment: originalTransaction.Comment,
+ GeoLocation: originalTransaction.GeoLocation,
CreatedIp: originalTransaction.CreatedIp,
CreatedUnixTime: originalTransaction.CreatedUnixTime,
UpdatedUnixTime: originalTransaction.UpdatedUnixTime,
diff --git a/src/lib/services.js b/src/lib/services.js
index 48597ad5..bad91bab 100644
--- a/src/lib/services.js
+++ b/src/lib/services.js
@@ -266,7 +266,7 @@ export default {
getTransaction: ({ id }) => {
return axios.get(`v1/transactions/get.json?id=${id}&trim_account=true&trim_category=true&trim_tag=true`);
},
- addTransaction: ({ type, categoryId, time, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, comment, utcOffset }) => {
+ addTransaction: ({ type, categoryId, time, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, comment, geoLocation, utcOffset }) => {
return axios.post('v1/transactions/add.json', {
type,
categoryId,
@@ -278,10 +278,11 @@ export default {
hideAmount,
tagIds,
comment,
+ geoLocation,
utcOffset
});
},
- modifyTransaction: ({ id, type, categoryId, time, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, comment, utcOffset }) => {
+ modifyTransaction: ({ id, type, categoryId, time, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, comment, geoLocation, utcOffset }) => {
return axios.post('v1/transactions/modify.json', {
id,
type,
@@ -294,6 +295,7 @@ export default {
hideAmount,
tagIds,
comment,
+ geoLocation,
utcOffset
});
},
diff --git a/src/lib/settings.js b/src/lib/settings.js
index 1ee19d55..935b9042 100644
--- a/src/lib/settings.js
+++ b/src/lib/settings.js
@@ -13,6 +13,7 @@ const defaultSettings = {
applicationLock: false,
applicationLockWebAuthn: false,
autoUpdateExchangeRatesData: true,
+ autoGetCurrentGeoLocation: false,
thousandsSeparator: true,
currencyDisplayMode: currencyConstants.defaultCurrencyDisplayMode,
showAmountInHomePage: true,
@@ -140,6 +141,8 @@ export default {
setEnableApplicationLockWebAuthn: value => setOption('applicationLockWebAuthn', value),
isAutoUpdateExchangeRatesData: () => getOption('autoUpdateExchangeRatesData'),
setAutoUpdateExchangeRatesData: value => setOption('autoUpdateExchangeRatesData', value),
+ isAutoGetCurrentGeoLocation: () => getOption('autoGetCurrentGeoLocation'),
+ setAutoGetCurrentGeoLocation: value => setOption('autoGetCurrentGeoLocation', value),
isEnableThousandsSeparator: () => getOption('thousandsSeparator'),
setEnableThousandsSeparator: value => setOption('thousandsSeparator', value),
getCurrencyDisplayMode: () => getOption('currencyDisplayMode'),
diff --git a/src/locales/en.js b/src/locales/en.js
index 8ad468da..bf87ee50 100644
--- a/src/locales/en.js
+++ b/src/locales/en.js
@@ -830,6 +830,12 @@ export default {
'Destination Account': 'Destination Account',
'Transaction Time': 'Transaction Time',
'Transaction Time Zone': 'Transaction Time Zone',
+ 'Geographic Location': 'Geographic Location',
+ 'No Location': 'No Location',
+ 'Getting Location...': 'Getting Location...',
+ 'Update Geographic Location': 'Update Geographic Location',
+ 'Clear Geographic Location': 'Clear Geographic Location',
+ 'Unable to get current position': 'Unable to get current position',
'Tags': 'Tags',
'Your transaction description (optional)': 'Your transaction description (optional)',
'Are you sure you want to save this transaction whose amount is 0?': 'Are you sure you want to save this transaction whose amount is 0?',
@@ -881,6 +887,7 @@ export default {
'Timezone': 'Timezone',
'System Default': 'System Default',
'Auto Update Exchange Rates Data': 'Auto Update Exchange Rates Data',
+ 'Auto Get Current Geographic Location': 'Auto Get Current Geographic Location',
'Enable Thousands Separator': 'Enable Thousands Separator',
'Currency Display Mode': 'Currency Display Mode',
'Currency Code': 'Currency Code',
diff --git a/src/locales/zh_Hans.js b/src/locales/zh_Hans.js
index 2562dbac..1a78a470 100644
--- a/src/locales/zh_Hans.js
+++ b/src/locales/zh_Hans.js
@@ -830,6 +830,12 @@ export default {
'Destination Account': '目标账户',
'Transaction Time': '交易时间',
'Transaction Time Zone': '交易时区',
+ 'Geographic Location': '地理位置',
+ 'No Location': '没有位置',
+ 'Getting Location...': '正在获取位置...',
+ 'Update Geographic Location': '更新地理位置',
+ 'Clear Geographic Location': '清除地理位置',
+ 'Unable to get current position': '无法获取当前地理位置',
'Tags': '标签',
'Your transaction description (optional)': '你的交易描述 (可选)',
'Are you sure you want to save this transaction whose amount is 0?': '您确定要保存这个金额为0的交易?',
@@ -881,6 +887,7 @@ export default {
'Timezone': '时区',
'System Default': '系统默认',
'Auto Update Exchange Rates Data': '自动更新汇率数据',
+ 'Auto Get Current Geographic Location': '自动获取当前地理位置',
'Enable Thousands Separator': '启用千位分隔符',
'Currency Display Mode': '货币显示模式',
'Currency Code': '货币代码',
diff --git a/src/views/mobile/SettingsPage.vue b/src/views/mobile/SettingsPage.vue
index 3fa7b2cb..5eb50d1f 100644
--- a/src/views/mobile/SettingsPage.vue
+++ b/src/views/mobile/SettingsPage.vue
@@ -46,6 +46,11 @@
+
+ {{ $t('Auto Get Current Geographic Location') }}
+
+
+
{{ $t('Enable Thousands Separator') }}
@@ -149,6 +154,14 @@ export default {
this.$settings.setAutoUpdateExchangeRatesData(value);
}
},
+ isAutoGetCurrentGeoLocation: {
+ get: function () {
+ return this.$settings.isAutoGetCurrentGeoLocation();
+ },
+ set: function (value) {
+ this.$settings.setAutoGetCurrentGeoLocation(value);
+ }
+ },
isEnableThousandsSeparator: {
get: function () {
return this.$settings.isEnableThousandsSeparator();
diff --git a/src/views/mobile/transactions/EditPage.vue b/src/views/mobile/transactions/EditPage.vue
index 62b5558f..58e7ed02 100644
--- a/src/views/mobile/transactions/EditPage.vue
+++ b/src/views/mobile/transactions/EditPage.vue
@@ -27,6 +27,7 @@
+
@@ -236,6 +237,20 @@
+
+
+
+ {{ `(${transaction.geoLocation.longitude}, ${transaction.geoLocation.latitude})` }}
+ {{ geoLocationStatusInfo }}
+
+
+
+
+
+
+ {{ $t('Update Geographic Location') }}
+ {{ $t('Clear Geographic Location') }}
+
+
+ {{ $t('Cancel') }}
+
+
+
{{ $t('Show Amount') }}
@@ -330,12 +355,16 @@ export default {
destinationAmount: 0,
hideAmount: false,
tagIds: [],
- comment: ''
+ comment: '',
+ geoLocation: null
},
loading: true,
loadingError: null,
+ geoLocationStatus: null,
submitting: false,
+ isSupportGeoLocation: !!navigator.geolocation,
showAccountBalance: self.$settings.isShowAccountBalance(),
+ showGeoLocationActionSheet: false,
showMoreActionSheet: false,
showSourceAmountSheet: false,
showDestinationAmountSheet: false,
@@ -507,6 +536,15 @@ export default {
destinationAmountFontSize() {
return this.getFontSizeByAmount(this.transaction.destinationAmount);
},
+ geoLocationStatusInfo() {
+ if (this.geoLocationStatus === 'success') {
+ return '';
+ } else if (this.geoLocationStatus === 'getting') {
+ return this.$t('Getting Location...');
+ } else {
+ return this.$t('No Location');
+ }
+ },
inputIsEmpty() {
return !!this.inputEmptyProblemMessage;
},
@@ -711,6 +749,10 @@ export default {
self.transaction.hideAmount = transaction.hideAmount;
self.transaction.tagIds = transaction.tagIds || [];
self.transaction.comment = transaction.comment;
+
+ if (self.mode === 'edit' || self.mode === 'view') {
+ self.transaction.geoLocation = transaction.geoLocation;
+ }
}
self.loading = false;
@@ -728,6 +770,11 @@ export default {
methods: {
onPageAfterIn() {
this.$routeBackOnError(this.f7router, 'loadingError');
+
+ if (this.$settings.isAutoGetCurrentGeoLocation() && this.mode === 'add'
+ && !this.geoLocationStatus && !this.transaction.geoLocation) {
+ this.updateGeoLocation(false);
+ }
},
save() {
const self = this;
@@ -747,6 +794,7 @@ export default {
hideAmount: self.transaction.hideAmount,
tagIds: self.transaction.tagIds,
comment: self.transaction.comment,
+ geoLocation: self.transaction.geoLocation,
utcOffset: self.transaction.utcOffset
};
@@ -803,6 +851,51 @@ export default {
doSubmit();
}
},
+ updateGeoLocation(forceUpdate) {
+ const self = this;
+
+ if (!self.isSupportGeoLocation) {
+ self.$logger.warn('this browser does not support geo location');
+
+ if (forceUpdate) {
+ self.$toast('Unable to get current position');
+ }
+ return;
+ }
+
+ navigator.geolocation.getCurrentPosition(function (position) {
+ if (!position || !position.coords) {
+ self.$logger.error('current position is null');
+ self.geoLocationStatus = 'error';
+
+ if (forceUpdate) {
+ self.$toast('Unable to get current position');
+ }
+
+ return;
+ }
+
+ self.geoLocationStatus = 'success';
+
+ self.transaction.geoLocation = {
+ latitude: position.coords.latitude,
+ longitude: position.coords.longitude
+ };
+ }, function (err) {
+ self.$logger.error('cannot get current position', err);
+ self.geoLocationStatus = 'error';
+
+ if (forceUpdate) {
+ self.$toast('Unable to get current position');
+ }
+ });
+
+ self.geoLocationStatus = 'getting';
+ },
+ clearGeoLocation() {
+ this.geoLocationStatus = null;
+ this.transaction.geoLocation = null;
+ },
isCategoryIdAvailable(categories, categoryId) {
if (!categories || !categories.length) {
return false;