add account list page and account add page
This commit is contained in:
@@ -31,6 +31,7 @@ func updateDatabaseStructure(c *cli.Context) error {
|
||||
|
||||
_ = datastore.Container.UserStore.SyncStructs(new(models.User), new(models.TwoFactor), new(models.TwoFactorRecoveryCode))
|
||||
_ = datastore.Container.TokenStore.SyncStructs(new(models.TokenRecord))
|
||||
_ = datastore.Container.UserDataStore.SyncStructs(new(models.Account))
|
||||
|
||||
log.BootInfof("[database.updateDatabaseStructure] maintained successfully")
|
||||
|
||||
|
||||
@@ -157,6 +157,10 @@ func startWebServer(c *cli.Context) error {
|
||||
apiV1Route.POST("/users/2fa/disable.json", bindApi(api.TwoFactorAuthorizations.TwoFactorDisableHandler))
|
||||
apiV1Route.POST("/users/2fa/recovery/regenerate.json", bindApi(api.TwoFactorAuthorizations.TwoFactorRecoveryCodeRegenerateHandler))
|
||||
}
|
||||
|
||||
// Accounts
|
||||
apiV1Route.GET("/accounts/list.json", bindApi(api.Accounts.AccountListHandler))
|
||||
apiV1Route.POST("/accounts/add.json", bindApi(api.Accounts.AccountCreateHandler))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/mayswind/lab/pkg/core"
|
||||
"github.com/mayswind/lab/pkg/errs"
|
||||
"github.com/mayswind/lab/pkg/log"
|
||||
"github.com/mayswind/lab/pkg/models"
|
||||
"github.com/mayswind/lab/pkg/services"
|
||||
)
|
||||
|
||||
type AccountsApi struct {
|
||||
accounts *services.AccountService
|
||||
}
|
||||
|
||||
var (
|
||||
Accounts = &AccountsApi{
|
||||
accounts: services.Accounts,
|
||||
}
|
||||
)
|
||||
|
||||
func (a *AccountsApi) AccountListHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
accounts, err := a.accounts.GetAllAccountsByUid(uid)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[accounts.AccountListHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
userAllAccountResps := make([]*models.AccountInfoResponse, len(accounts))
|
||||
userAllAccountRespMap := make(map[int64]*models.AccountInfoResponse)
|
||||
|
||||
for i := 0; i < len(accounts); i++ {
|
||||
userAllAccountResps[i] = accounts[i].ToAccountInfoResponse()
|
||||
userAllAccountRespMap[userAllAccountResps[i].Id] = userAllAccountResps[i]
|
||||
}
|
||||
|
||||
for i := 0; i < len(userAllAccountResps); i++ {
|
||||
userAccountResp := userAllAccountResps[i]
|
||||
|
||||
if userAccountResp.ParentId <= models.ACCOUNT_PARENT_ID_LEVEL_ONE {
|
||||
continue
|
||||
}
|
||||
|
||||
parentAccount, parentExists := userAllAccountRespMap[userAccountResp.ParentId]
|
||||
|
||||
if !parentExists || parentAccount == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
parentAccount.SubAccounts = append(parentAccount.SubAccounts, userAccountResp)
|
||||
}
|
||||
|
||||
userFinalAccountResps := make(models.AccountInfoResponseSlice, 0)
|
||||
|
||||
for i := 0; i < len(userAllAccountResps); i++ {
|
||||
if userAllAccountResps[i].ParentId == models.ACCOUNT_PARENT_ID_LEVEL_ONE {
|
||||
sort.Sort(userAllAccountResps[i].SubAccounts)
|
||||
userFinalAccountResps = append(userFinalAccountResps, userAllAccountResps[i])
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(userFinalAccountResps)
|
||||
|
||||
return userFinalAccountResps, nil
|
||||
}
|
||||
|
||||
func (a *AccountsApi) AccountCreateHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||
var accountCreateReq models.AccountCreateRequest
|
||||
err := c.ShouldBindJSON(&accountCreateReq)
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
if accountCreateReq.Type == models.ACCOUNT_TYPE_SINGLE_ACCOUNT {
|
||||
if len(accountCreateReq.SubAccounts) > 0 {
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account cannot have any sub accounts")
|
||||
return nil, errs.ErrAccountCannotHaveSubAccounts
|
||||
}
|
||||
} else if accountCreateReq.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS {
|
||||
if len(accountCreateReq.SubAccounts) < 1 {
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account does not have any sub accounts")
|
||||
return nil, errs.ErrAccountHaveNoSubAccount
|
||||
}
|
||||
} else {
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account type invalid, type is %d", accountCreateReq.Type)
|
||||
return nil, errs.ErrAccountTypeInvalid
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
maxOrderId, err := a.accounts.GetMaxDisplayOrder(uid)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[accounts.AccountCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
mainAccount := a.createNewAccount(uid, &accountCreateReq, maxOrderId+1)
|
||||
childrenAccounts := a.createSubAccounts(uid, &accountCreateReq)
|
||||
|
||||
err = a.accounts.CreateAccounts(mainAccount, childrenAccounts)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[accounts.AccountCreateHandler] failed to create account \"id:%d\" for user \"uid:%d\", because %s", mainAccount.AccountId, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.InfofWithRequestId(c, "[accounts.AccountCreateHandler] user \"uid:%d\" has created a new account \"id:%d\" successfully", uid, mainAccount.AccountId)
|
||||
|
||||
accountInfoResp := mainAccount.ToAccountInfoResponse()
|
||||
|
||||
if len(childrenAccounts) > 0 {
|
||||
accountInfoResp.SubAccounts = make([]*models.AccountInfoResponse, len(childrenAccounts))
|
||||
|
||||
for i := 0; i < len(childrenAccounts); i++ {
|
||||
accountInfoResp.SubAccounts[i] = childrenAccounts[i].ToAccountInfoResponse()
|
||||
}
|
||||
}
|
||||
|
||||
return accountInfoResp, nil
|
||||
}
|
||||
|
||||
func (a *AccountsApi) createNewAccount(uid int64, accountCreateReq *models.AccountCreateRequest, order int) *models.Account {
|
||||
return &models.Account{
|
||||
Uid: uid,
|
||||
Name: accountCreateReq.Name,
|
||||
DisplayOrder: order,
|
||||
Category: accountCreateReq.Category,
|
||||
Icon: accountCreateReq.Icon,
|
||||
Currency: accountCreateReq.Currency,
|
||||
Comment: accountCreateReq.Comment,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AccountsApi) createSubAccounts(uid int64, accountCreateReq *models.AccountCreateRequest) []*models.Account {
|
||||
if len(accountCreateReq.SubAccounts) <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
childrenAccounts := make([]*models.Account, len(accountCreateReq.SubAccounts))
|
||||
|
||||
for i := 0; i < len(accountCreateReq.SubAccounts); i++ {
|
||||
childrenAccounts[i] = a.createNewAccount(uid, accountCreateReq.SubAccounts[i], i+1)
|
||||
}
|
||||
|
||||
return childrenAccounts
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package errs
|
||||
|
||||
import "net/http"
|
||||
|
||||
var (
|
||||
ErrAccountIdInvalid = NewNormalError(NORMAL_SUBCATEGORY_ACCOUNT, 0, http.StatusBadRequest, "account id is invalid")
|
||||
ErrAccountNotFound = NewNormalError(NORMAL_SUBCATEGORY_ACCOUNT, 1, http.StatusBadRequest, "account not found")
|
||||
ErrAccountTypeInvalid = NewNormalError(NORMAL_SUBCATEGORY_ACCOUNT, 2, http.StatusBadRequest, "account type is invalid")
|
||||
ErrAccountHaveNoSubAccount = NewNormalError(NORMAL_SUBCATEGORY_ACCOUNT, 3, http.StatusBadRequest, "account must have at least one sub account")
|
||||
ErrAccountCannotHaveSubAccounts = NewNormalError(NORMAL_SUBCATEGORY_ACCOUNT, 4, http.StatusBadRequest, "account cannot have sub accounts")
|
||||
)
|
||||
@@ -0,0 +1,97 @@
|
||||
package models
|
||||
|
||||
// Level-One Account
|
||||
const ACCOUNT_PARENT_ID_LEVEL_ONE = 0
|
||||
|
||||
type AccountCategory byte
|
||||
|
||||
const (
|
||||
ACCOUNT_CATEGORY_CASH AccountCategory = 1
|
||||
ACCOUNT_CATEGORY_DEBIT_CARD AccountCategory = 2
|
||||
ACCOUNT_CATEGORY_CREDIT_CARD AccountCategory = 3
|
||||
ACCOUNT_CATEGORY_VIRTUAL AccountCategory = 4
|
||||
ACCOUNT_CATEGORY_DEBT AccountCategory = 5
|
||||
ACCOUNT_CATEGORY_RECEIVABLES AccountCategory = 6
|
||||
ACCOUNT_CATEGORY_INVESTMENT AccountCategory = 7
|
||||
)
|
||||
|
||||
type AccountType byte
|
||||
|
||||
const (
|
||||
ACCOUNT_TYPE_SINGLE_ACCOUNT AccountType = 1
|
||||
ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS AccountType = 2
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
AccountId int64 `xorm:"PK"`
|
||||
Uid int64 `xorm:"INDEX(IDX_account_uid_deleted_parent_account_id_order) NOT NULL"`
|
||||
Deleted bool `xorm:"INDEX(IDX_account_uid_deleted_parent_account_id_order) NOT NULL"`
|
||||
Category AccountCategory `xorm:"NOT NULL"`
|
||||
Type AccountType `xorm:"NOT NULL"`
|
||||
ParentAccountId int64 `xorm:"INDEX(IDX_account_uid_deleted_parent_account_id_order) NOT NULL"`
|
||||
Name string `xorm:"VARCHAR(32) NOT NULL"`
|
||||
DisplayOrder int `xorm:"INDEX(IDX_account_uid_deleted_parent_account_id_order) NOT NULL"`
|
||||
Icon int64 `xorm:"NOT NULL"`
|
||||
Currency string `xorm:"VARCHAR(3) NOT NULL"`
|
||||
Balance int64 `xorm:"NOT NULL"`
|
||||
Comment string `xorm:"VARCHAR(255) NOT NULL"`
|
||||
Hidden bool `xorm:"NOT NULL"`
|
||||
CreatedUnixTime int64
|
||||
UpdatedUnixTime int64
|
||||
DeletedUnixTime int64
|
||||
}
|
||||
|
||||
type AccountCreateRequest struct {
|
||||
Name string `json:"name" binding:"required,notBlank,max=32"`
|
||||
Category AccountCategory `json:"category" binding:"required"`
|
||||
Type AccountType `json:"type" binding:"required"`
|
||||
Icon int64 `json:"icon,string" binding:"required,min=1"`
|
||||
Currency string `json:"currency" binding:"required,len=3,validCurrency"`
|
||||
Comment string `json:"comment" binding:"max=255"`
|
||||
SubAccounts []*AccountCreateRequest `json:"subAccounts" binding:"omitempty"`
|
||||
}
|
||||
|
||||
type AccountInfoResponse struct {
|
||||
Id int64 `json:"id,string"`
|
||||
Name string `json:"name"`
|
||||
ParentId int64 `json:"parentId,string"`
|
||||
Category AccountCategory `json:"category"`
|
||||
Type AccountType `json:"type"`
|
||||
Icon int64 `json:"icon,string"`
|
||||
Currency string `json:"currency"`
|
||||
Balance int64 `json:"balance"`
|
||||
Comment string `json:"comment"`
|
||||
DisplayOrder int `json:"displayOrder"`
|
||||
Hidden bool `json:"hidden"`
|
||||
SubAccounts AccountInfoResponseSlice `json:"subAccounts,omitempty"`
|
||||
}
|
||||
|
||||
func (a *Account) ToAccountInfoResponse() *AccountInfoResponse {
|
||||
return &AccountInfoResponse{
|
||||
Id: a.AccountId,
|
||||
Name: a.Name,
|
||||
ParentId: a.ParentAccountId,
|
||||
Category: a.Category,
|
||||
Type: a.Type,
|
||||
Icon: a.Icon,
|
||||
Currency: a.Currency,
|
||||
Balance: a.Balance,
|
||||
Comment: a.Comment,
|
||||
DisplayOrder: a.DisplayOrder,
|
||||
Hidden: a.Hidden,
|
||||
}
|
||||
}
|
||||
|
||||
type AccountInfoResponseSlice []*AccountInfoResponse
|
||||
|
||||
func (a AccountInfoResponseSlice) Len() int {
|
||||
return len(a)
|
||||
}
|
||||
|
||||
func (a AccountInfoResponseSlice) Swap(i, j int) {
|
||||
a[i], a[j] = a[j], a[i]
|
||||
}
|
||||
|
||||
func (a AccountInfoResponseSlice) Less(i, j int) bool {
|
||||
return a[i].DisplayOrder < a[j].DisplayOrder
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/mayswind/lab/pkg/datastore"
|
||||
"github.com/mayswind/lab/pkg/errs"
|
||||
"github.com/mayswind/lab/pkg/models"
|
||||
"github.com/mayswind/lab/pkg/uuid"
|
||||
)
|
||||
|
||||
type AccountService struct {
|
||||
ServiceUsingDB
|
||||
ServiceUsingUuid
|
||||
}
|
||||
|
||||
var (
|
||||
Accounts = &AccountService{
|
||||
ServiceUsingDB: ServiceUsingDB{
|
||||
container: datastore.Container,
|
||||
},
|
||||
ServiceUsingUuid: ServiceUsingUuid{
|
||||
container: uuid.Container,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func (s *AccountService) GetAllAccountsByUid(uid int64) ([]*models.Account, error) {
|
||||
if uid <= 0 {
|
||||
return nil, errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
var accounts []*models.Account
|
||||
err := s.UserDataDB(uid).Where("uid=? AND deleted=?", uid, false).OrderBy("parent_account_id asc, display_order asc").Find(&accounts)
|
||||
|
||||
return accounts, err
|
||||
}
|
||||
|
||||
func (s *AccountService) GetMaxDisplayOrder(uid int64) (int, error) {
|
||||
if uid <= 0 {
|
||||
return 0, errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
account := &models.Account{}
|
||||
has, err := s.UserDataDB(uid).Cols("uid", "deleted", "parent_account_id", "display_order").Where("uid=? AND deleted=? AND parent_account_id=?", uid, false, models.ACCOUNT_PARENT_ID_LEVEL_ONE).OrderBy("display_order desc").Limit(1).Get(account)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if has {
|
||||
return account.DisplayOrder, nil
|
||||
} else {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AccountService) GetMaxSubAccountDisplayOrder(uid int64, parentAccountId int64) (int, error) {
|
||||
if uid <= 0 {
|
||||
return 0, errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
if parentAccountId <= 0 {
|
||||
return 0, errs.ErrAccountIdInvalid
|
||||
}
|
||||
|
||||
account := &models.Account{}
|
||||
has, err := s.UserDataDB(uid).Cols("uid", "deleted", "parent_account_id", "display_order").Where("uid=? AND deleted=? AND parent_account_id=?", uid, false, parentAccountId).OrderBy("display_order desc").Limit(1).Get(account)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if has {
|
||||
return account.DisplayOrder, nil
|
||||
} else {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AccountService) CreateAccounts(mainAccount *models.Account, childrenAccounts []*models.Account) error {
|
||||
if mainAccount.Uid <= 0 {
|
||||
return errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
allAccounts := make([]*models.Account, len(childrenAccounts)+1)
|
||||
|
||||
mainAccount.AccountId = s.GenerateUuid(uuid.UUID_TYPE_ACCOUNT)
|
||||
allAccounts[0] = mainAccount
|
||||
|
||||
if mainAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS {
|
||||
for i := 0; i < len(childrenAccounts); i++ {
|
||||
childAccount := childrenAccounts[i]
|
||||
childAccount.AccountId = s.GenerateUuid(uuid.UUID_TYPE_ACCOUNT)
|
||||
childAccount.ParentAccountId = mainAccount.AccountId
|
||||
childAccount.Uid = mainAccount.Uid
|
||||
childAccount.Type = models.ACCOUNT_TYPE_SINGLE_ACCOUNT
|
||||
|
||||
allAccounts[i+1] = childrenAccounts[i]
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(allAccounts); i++ {
|
||||
allAccounts[i].Deleted = false
|
||||
allAccounts[i].CreatedUnixTime = time.Now().Unix()
|
||||
allAccounts[i].UpdatedUnixTime = time.Now().Unix()
|
||||
}
|
||||
|
||||
return s.UserDataDB(mainAccount.Uid).DoTransaction(func(sess *xorm.Session) error {
|
||||
for i := 0; i < len(allAccounts); i++ {
|
||||
account := allAccounts[i]
|
||||
_, err := sess.Insert(account)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
const allAccountCategories = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Cash'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Debit Card'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Credit Card'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Virtual Account'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Debt Account'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Receivables'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Investment Account'
|
||||
}
|
||||
];
|
||||
|
||||
export default {
|
||||
allCategories: allAccountCategories
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
import settings from "../lib/settings.js";
|
||||
import utils from "../lib/utils.js";
|
||||
|
||||
export default function ({ i18n }, value, currencyCode) {
|
||||
if (!value) {
|
||||
if (!utils.isNumber(value) && !utils.isString(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
value = value / 100;
|
||||
const currencyDisplayMode = settings.getCurrencyDisplayMode();
|
||||
|
||||
|
||||
@@ -160,4 +160,18 @@ export default {
|
||||
password
|
||||
});
|
||||
},
|
||||
getAllAccounts: () => {
|
||||
return axios.get('v1/accounts/list.json');
|
||||
},
|
||||
addAccount: ({ category, type, name, icon, currency, comment, subAccounts }) => {
|
||||
return axios.post('v1/accounts/add.json', {
|
||||
category,
|
||||
type,
|
||||
name,
|
||||
icon,
|
||||
currency,
|
||||
comment,
|
||||
subAccounts
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -204,6 +204,11 @@ export default {
|
||||
'two factor is not enabled': 'Two factor is not enabled',
|
||||
'two factor has already been enabled': 'Two factor has already been enabled',
|
||||
'two factor backup code does not exist': 'Two factor backup code does not exist',
|
||||
'account id is invalid': 'Account id is invalid',
|
||||
'account not found': 'Account is not found',
|
||||
'account type is invalid': 'Account type is invalid',
|
||||
'account must have at least one sub account': 'Account must have at least one sub account',
|
||||
'account cannot have sub accounts': 'Account cannot have sub accounts',
|
||||
},
|
||||
'parameter': {
|
||||
'username': 'Username',
|
||||
@@ -228,6 +233,7 @@ export default {
|
||||
'OK': 'OK',
|
||||
'Cancel': 'Cancel',
|
||||
'Close': 'Close',
|
||||
'Submit': 'Submit',
|
||||
'Update': 'Update',
|
||||
'None': 'None',
|
||||
'Done': 'Done',
|
||||
@@ -286,6 +292,30 @@ export default {
|
||||
'Expense': 'Expense',
|
||||
'Income': 'Income',
|
||||
'Transfer': 'Transfer',
|
||||
'Cash': 'Cash',
|
||||
'Debit Card': 'Debit Card',
|
||||
'Credit Card': 'Credit Card',
|
||||
'Virtual Account': 'Virtual Account',
|
||||
'Debt Account': 'Debt Account',
|
||||
'Receivables': 'Receivables',
|
||||
'Investment Account': 'Investment Account',
|
||||
'Unable to get account list': 'Unable to get account list',
|
||||
'Add Account': 'Add Account',
|
||||
'Account Category': 'Account Category',
|
||||
'Single Account': 'Single Account',
|
||||
'Multi Sub Accounts': 'Multi Sub Accounts',
|
||||
'Account Type': 'Account Type',
|
||||
'Account Name': 'Account Name',
|
||||
'Your account name': 'Your account name',
|
||||
'Currency': 'Currency',
|
||||
'Description': 'Description',
|
||||
'Your account description (optional)': 'Your account description (optional)',
|
||||
'Account category cannot be empty': 'Account category cannot be empty',
|
||||
'Account type cannot be empty': 'Account type cannot be empty',
|
||||
'Account name cannot be empty': 'Account name cannot be empty',
|
||||
'Account currency cannot be empty': 'Account currency cannot be empty',
|
||||
'You have added a new account': 'You have added a new account',
|
||||
'Unable to add account': 'Unable to add account',
|
||||
'User Profile': 'User Profile',
|
||||
'Language': 'Language',
|
||||
'Currency Display Mode': 'Currency Display Mode',
|
||||
|
||||
@@ -204,6 +204,11 @@ export default {
|
||||
'two factor is not enabled': '两步验证没有启用',
|
||||
'two factor has already been enabled': '两步验证已经启用',
|
||||
'two factor backup code does not exist': '两步验证备用码不存在',
|
||||
'account id is invalid': '账户ID无效',
|
||||
'account not found': '账户不存在',
|
||||
'account type is invalid': '账户类型无效',
|
||||
'account must have at least one sub account': '账户必须包含至少一个子账户',
|
||||
'account cannot have sub accounts': '账户不能包含子账户',
|
||||
},
|
||||
'parameter': {
|
||||
'username': '用户名',
|
||||
@@ -228,6 +233,7 @@ export default {
|
||||
'OK': '确定',
|
||||
'Cancel': '取消',
|
||||
'Close': '关闭',
|
||||
'Submit': '提交',
|
||||
'Update': '更新',
|
||||
'None': '无',
|
||||
'Done': '完成',
|
||||
@@ -286,6 +292,30 @@ export default {
|
||||
'Expense': '支出',
|
||||
'Income': '收入',
|
||||
'Transfer': '转账',
|
||||
'Cash': '现金',
|
||||
'Debit Card': '借记卡',
|
||||
'Credit Card': '信用卡',
|
||||
'Virtual Account': '虚拟账户',
|
||||
'Debt Account': '负债账户',
|
||||
'Receivables': '应收款项',
|
||||
'Investment Account': '投资账户',
|
||||
'Unable to get account list': '无法获取账户列表',
|
||||
'Add Account': '添加账户',
|
||||
'Account Category': '账户分类',
|
||||
'Single Account': '单一账户',
|
||||
'Multi Sub Accounts': '多个子账户',
|
||||
'Account Type': '账户类型',
|
||||
'Account Name': '账户名称',
|
||||
'Your account name': '你的账户名称',
|
||||
'Currency': '货币',
|
||||
'Description': '描述',
|
||||
'Your account description (optional)': '你的账户描述 (可选)',
|
||||
'Account category cannot be empty': '账户分类不能为空',
|
||||
'Account type cannot be empty': '账户类型不能为空',
|
||||
'Account name cannot be empty': '账户名称不能为空',
|
||||
'Account currency cannot be empty': '账户货币不能为空',
|
||||
'You have added a new account': '您已经添加新账户',
|
||||
'Unable to add account': '无法添加账户',
|
||||
'User Profile': '用户信息',
|
||||
'Language': '语言',
|
||||
'Currency Display Mode': '货币显示模式',
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'framework7-icons';
|
||||
|
||||
import { getAllLanguages, getLanguage, getDefaultLanguage, getI18nOptions, getLocalizedError } from './lib/i18n.js';
|
||||
import currency from './consts/currency.js';
|
||||
import account from './consts/account.js';
|
||||
import version from './lib/version.js';
|
||||
import settings from './lib/settings.js';
|
||||
import services from './lib/services.js';
|
||||
@@ -31,6 +32,9 @@ Framework7.use(Framework7Vue);
|
||||
const i18n = new VueI18n(getI18nOptions());
|
||||
|
||||
Vue.prototype.$version = version.getVersion;
|
||||
Vue.prototype.$constants = {
|
||||
account: account
|
||||
};
|
||||
Vue.prototype.$utils = utils;
|
||||
Vue.prototype.$settings = settings;
|
||||
Vue.prototype.$getDefaultLanguage = getDefaultLanguage;
|
||||
|
||||
@@ -8,6 +8,7 @@ import TransactionDetailPage from '../views/mobile/transactions/Detail.vue'
|
||||
import TransactionNewPage from '../views/mobile/transactions/New.vue'
|
||||
|
||||
import AccountListPage from '../views/mobile/accounts/AccountList.vue'
|
||||
import AccountAddPage from '../views/mobile/accounts/AccountAdd.vue'
|
||||
|
||||
import StatisticsOverviewPage from '../views/mobile/statistics/Overview.vue'
|
||||
|
||||
@@ -72,6 +73,11 @@ const routes = [
|
||||
component: AccountListPage,
|
||||
beforeEnter: checkLogin
|
||||
},
|
||||
{
|
||||
path: '/account/add',
|
||||
component: AccountAddPage,
|
||||
beforeEnter: checkLogin
|
||||
},
|
||||
{
|
||||
path: '/statistic/overview',
|
||||
component: StatisticsOverviewPage,
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<f7-page>
|
||||
<f7-navbar :title="$t('Add Account')" :back-link="$t('Back')"></f7-navbar>
|
||||
|
||||
<f7-list no-hairlines-md>
|
||||
<f7-list-input
|
||||
type="select"
|
||||
:label="$t('Account Category')"
|
||||
:value="category"
|
||||
@input="category = $event.target.value"
|
||||
>
|
||||
<option v-for="accountCategory in allAccountCategories"
|
||||
:key="accountCategory.id"
|
||||
:value="accountCategory.id">{{ $t(accountCategory.name) }}</option>
|
||||
</f7-list-input>
|
||||
|
||||
<f7-list-input
|
||||
type="select"
|
||||
disabled
|
||||
:label="$t('Account Type')"
|
||||
:value="type"
|
||||
@input="type = $event.target.value"
|
||||
>
|
||||
<option value="1">{{ $t('Single Account') }}</option>
|
||||
<option value="2">{{ $t('Multi Sub Accounts') }}</option>
|
||||
</f7-list-input>
|
||||
|
||||
<f7-list-input
|
||||
type="text"
|
||||
clear-button
|
||||
:label="$t('Account Name')"
|
||||
:placeholder="$t('Your account name')"
|
||||
:value="name"
|
||||
@input="name = $event.target.value"
|
||||
></f7-list-input>
|
||||
|
||||
<f7-list-input
|
||||
type="select"
|
||||
:label="$t('Currency')"
|
||||
:value="currency"
|
||||
@input="currency = $event.target.value"
|
||||
>
|
||||
<option v-for="currency in allCurrencies"
|
||||
:key="currency.code"
|
||||
:value="currency.code">{{ currency.displayName }}</option>
|
||||
</f7-list-input>
|
||||
|
||||
<f7-list-input
|
||||
type="textarea"
|
||||
:label="$t('Description')"
|
||||
:placeholder="$t('Your account description (optional)')"
|
||||
:value="comment"
|
||||
@input="comment = $event.target.value"
|
||||
></f7-list-input>
|
||||
|
||||
<f7-list-item class="lab-list-item-error-info" v-if="inputIsInvalid" :footer="$t(inputInvalidProblemMessage)"></f7-list-item>
|
||||
</f7-list>
|
||||
|
||||
<f7-button large fill :class="{ 'disabled': inputIsEmpty || submitting }" :text="$t('Submit')" @click="submit"></f7-button>
|
||||
</f7-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
const self = this;
|
||||
|
||||
return {
|
||||
category: 1,
|
||||
type: 1,
|
||||
name: '',
|
||||
icon: "1",
|
||||
currency: self.$user.getUserInfo() ? self.$user.getUserInfo().defaultCurrency : self.$t('default.currency'),
|
||||
comment: '',
|
||||
submitting: false,
|
||||
allAccountCategories: self.$constants.account.allCategories,
|
||||
allCurrencies: self.$getAllCurrencies()
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
inputIsEmpty() {
|
||||
return !!this.inputEmptyProblemMessage;
|
||||
},
|
||||
inputIsInvalid() {
|
||||
return !!this.inputInvalidProblemMessage;
|
||||
},
|
||||
inputEmptyProblemMessage() {
|
||||
if (!this.category) {
|
||||
return 'Account category cannot be empty';
|
||||
} else if (!this.type) {
|
||||
return 'Account type cannot be empty';
|
||||
} else if (!this.name) {
|
||||
return 'Account name cannot be empty';
|
||||
} else if (!this.currency) {
|
||||
return 'Account currency cannot be empty';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
inputInvalidProblemMessage() {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
const self = this;
|
||||
const router = self.$f7router;
|
||||
|
||||
let problemMessage = self.inputEmptyProblemMessage || self.inputInvalidProblemMessage;
|
||||
|
||||
if (problemMessage) {
|
||||
self.$alert(problemMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
self.submitting = true;
|
||||
self.$showLoading(() => self.signuping);
|
||||
|
||||
self.$services.addAccount({
|
||||
category: parseInt(self.category),
|
||||
type: parseInt(self.type),
|
||||
name: self.name,
|
||||
icon: self.icon,
|
||||
currency: self.currency,
|
||||
comment: self.comment
|
||||
}).then(response => {
|
||||
self.submitting = false;
|
||||
self.$hideLoading();
|
||||
const data = response.data;
|
||||
|
||||
if (!data || !data.success || !data.result) {
|
||||
self.$alert('Unable to add account');
|
||||
return;
|
||||
}
|
||||
|
||||
self.$toast('You have added a new account');
|
||||
router.back('/account/list', { force: true });
|
||||
}).catch(error => {
|
||||
self.submitting = false;
|
||||
self.$hideLoading();
|
||||
|
||||
if (error.response && error.response.data && error.response.data.errorMessage) {
|
||||
self.$alert({ error: error.response.data });
|
||||
} else if (!error.processed) {
|
||||
self.$alert('Unable to add account');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,9 +1,99 @@
|
||||
<template>
|
||||
<f7-page>
|
||||
<f7-navbar :title="$t('Account List')" :back-link="$t('Back')"></f7-navbar>
|
||||
<f7-navbar>
|
||||
<f7-nav-left :back-link="$t('Back')"></f7-nav-left>
|
||||
<f7-nav-title :title="$t('Account List')" :back-link="$t('Back')"></f7-nav-title>
|
||||
<f7-nav-right>
|
||||
<f7-link href="/account/add" icon-f7="plus"></f7-link>
|
||||
</f7-nav-right>
|
||||
</f7-navbar>
|
||||
|
||||
<f7-list media-list class="skeleton-text">
|
||||
<f7-list-item title="Placeholder"></f7-list-item>
|
||||
</f7-list>
|
||||
<f7-block class="skeleton-text" v-if="loading">
|
||||
<f7-block-title>Account Category</f7-block-title>
|
||||
<f7-list media-list>
|
||||
<f7-list-item title="Account Name" after="0.00 USD"></f7-list-item>
|
||||
</f7-list>
|
||||
</f7-block>
|
||||
|
||||
<f7-block v-for="accountCategory in usedAccountCategories" :key="accountCategory.id">
|
||||
<f7-block-title>{{ $t(accountCategory.name) }}</f7-block-title>
|
||||
<f7-list media-list>
|
||||
<f7-list-item v-for="account in accounts[accountCategory.id]" :key="account.id"
|
||||
:title="account.name" :after="account.balance | currency(account.currency)"></f7-list-item>
|
||||
</f7-list>
|
||||
</f7-block>
|
||||
</f7-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
accounts: {},
|
||||
loading: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
usedAccountCategories() {
|
||||
const allAccountCategories = this.$constants.account.allCategories;
|
||||
const usedAccountCategories = [];
|
||||
|
||||
for (let i = 0; i < allAccountCategories.length; i++) {
|
||||
const accountCategory = allAccountCategories[i];
|
||||
|
||||
if (this.$utils.isArray(this.accounts[accountCategory.id]) && this.accounts[accountCategory.id].length) {
|
||||
usedAccountCategories.push(accountCategory);
|
||||
}
|
||||
}
|
||||
|
||||
return usedAccountCategories;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const self = this;
|
||||
const router = self.$f7router;
|
||||
|
||||
self.loading = true;
|
||||
|
||||
self.$services.getAllAccounts().then(response => {
|
||||
self.loading = false;
|
||||
const data = response.data;
|
||||
|
||||
if (!data || !data.success || !data.result) {
|
||||
self.$alert('Unable to get account list', () => {
|
||||
router.back();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
self.accounts = {};
|
||||
|
||||
for (let i = 0; i < data.result.length; i++) {
|
||||
const account = data.result[i];
|
||||
|
||||
if (!self.accounts[account.category]) {
|
||||
self.accounts[account.category] = [];
|
||||
}
|
||||
|
||||
const accountList = self.accounts[account.category];
|
||||
accountList.push(account);
|
||||
}
|
||||
}).catch(error => {
|
||||
self.loading = false;
|
||||
|
||||
if (error.response && error.response.data && error.response.data.errorMessage) {
|
||||
self.$alert({ error: error.response.data }, () => {
|
||||
router.back();
|
||||
});
|
||||
} else if (!error.processed) {
|
||||
self.$alert('Unable to get account list', () => {
|
||||
router.back();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user