diff --git a/cmd/webserver.go b/cmd/webserver.go index e7b783c5..4cc1f706 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -301,6 +301,7 @@ func startWebServer(c *core.CliContext) error { apiV1Route.POST("/accounts/hide.json", bindApi(api.Accounts.AccountHideHandler)) apiV1Route.POST("/accounts/move.json", bindApi(api.Accounts.AccountMoveHandler)) apiV1Route.POST("/accounts/delete.json", bindApi(api.Accounts.AccountDeleteHandler)) + apiV1Route.POST("/accounts/sub_account/delete.json", bindApi(api.Accounts.SubAccountDeleteHandler)) // Transactions apiV1Route.GET("/transactions/count.json", bindApi(api.Transactions.TransactionCountHandler)) diff --git a/pkg/api/accounts.go b/pkg/api/accounts.go index edef3ad9..95340b79 100644 --- a/pkg/api/accounts.go +++ b/pkg/api/accounts.go @@ -508,6 +508,28 @@ func (a *AccountsApi) AccountDeleteHandler(c *core.WebContext) (any, *errs.Error return true, nil } +// SubAccountDeleteHandler deletes an existed sub-account by request parameters for current user +func (a *AccountsApi) SubAccountDeleteHandler(c *core.WebContext) (any, *errs.Error) { + var accountDeleteReq models.AccountDeleteRequest + err := c.ShouldBindJSON(&accountDeleteReq) + + if err != nil { + log.Warnf(c, "[accounts.SubAccountDeleteHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + err = a.accounts.DeleteSubAccount(c, uid, accountDeleteReq.Id) + + if err != nil { + log.Errorf(c, "[accounts.SubAccountDeleteHandler] failed to delete sub-account \"id:%d\" for user \"uid:%d\", because %s", accountDeleteReq.Id, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + log.Infof(c, "[accounts.SubAccountDeleteHandler] user \"uid:%d\" has deleted sub-account \"id:%d\"", uid, accountDeleteReq.Id) + return true, nil +} + func (a *AccountsApi) createNewAccountModel(uid int64, accountCreateReq *models.AccountCreateRequest, isSubAccount bool, order int32) *models.Account { accountExtend := &models.AccountExtend{} diff --git a/pkg/errs/account.go b/pkg/errs/account.go index 4ccc638e..7c42fc04 100644 --- a/pkg/errs/account.go +++ b/pkg/errs/account.go @@ -22,4 +22,6 @@ var ( ErrAccountBalanceTimeNotSet = NewNormalError(NormalSubcategoryAccount, 15, http.StatusBadRequest, "account balance time is not set") ErrCannotSetStatementDateForNonCreditCard = NewNormalError(NormalSubcategoryAccount, 16, http.StatusBadRequest, "cannot set statement date for non credit card account") ErrCannotSetStatementDateForSubAccount = NewNormalError(NormalSubcategoryAccount, 17, http.StatusBadRequest, "cannot set statement date for sub account") + ErrSubAccountNotFound = NewNormalError(NormalSubcategoryAccount, 18, http.StatusBadRequest, "sub-account not found") + ErrSubAccountInUseCannotBeDeleted = NewNormalError(NormalSubcategoryAccount, 19, http.StatusBadRequest, "sub-account is in use and cannot be deleted") ) diff --git a/pkg/services/accounts.go b/pkg/services/accounts.go index 110b7048..6571ac97 100644 --- a/pkg/services/accounts.go +++ b/pkg/services/accounts.go @@ -429,7 +429,7 @@ func (s *AccountService) DeleteAccount(c core.Context, uid int64, accountId int6 return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error { var accountAndSubAccounts []*models.Account - err := sess.Where("uid=? AND deleted=? AND (account_id=? OR parent_account_id=?)", uid, false, accountId, accountId).Find(&accountAndSubAccounts) + err := sess.Where("uid=? AND deleted=? AND ((account_id=? AND parent_account_id=?) OR parent_account_id=?)", uid, false, accountId, models.LevelOneAccountParentId, accountId).Find(&accountAndSubAccounts) if err != nil { return err @@ -499,6 +499,86 @@ func (s *AccountService) DeleteAccount(c core.Context, uid int64, accountId int6 }) } +// DeleteSubAccount deletes an existed sub-account from database +func (s *AccountService) DeleteSubAccount(c core.Context, uid int64, accountId int64) error { + if uid <= 0 { + return errs.ErrUserIdInvalid + } + + now := time.Now().Unix() + + updateModel := &models.Account{ + Balance: 0, + Deleted: true, + DeletedUnixTime: now, + } + + return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error { + account := &models.Account{} + has, err := sess.Cols("account_id", "uid", "deleted", "parent_account_id").Where("uid=? AND deleted=? AND account_id=? AND parent_account_id<>?", uid, false, accountId, models.LevelOneAccountParentId).Limit(1).Get(account) + + if err != nil { + return err + } else if !has { + return errs.ErrSubAccountNotFound + } + + subAccountsCount, err := sess.Where("uid=? AND deleted=? AND parent_account_id=?", uid, false, account.ParentAccountId).Count(&models.Account{}) + + if subAccountsCount <= 1 { + return errs.ErrAccountHaveNoSubAccount + } + + var relatedTransactionsByAccount []*models.Transaction + err = sess.Cols("transaction_id", "uid", "deleted", "account_id", "type").Where("uid=? AND deleted=? AND account_id=?", uid, false, accountId).Limit(2).Find(&relatedTransactionsByAccount) + + if err != nil { + return err + } else if len(relatedTransactionsByAccount) > 1 { + return errs.ErrSubAccountInUseCannotBeDeleted + } else if len(relatedTransactionsByAccount) > 0 { + for i := 0; i < len(relatedTransactionsByAccount); i++ { + transaction := relatedTransactionsByAccount[i] + + if transaction.Type != models.TRANSACTION_DB_TYPE_MODIFY_BALANCE { + return errs.ErrSubAccountInUseCannotBeDeleted + } + } + } + + deletedRows, err := sess.Cols("balance", "deleted", "deleted_unix_time").Where("uid=? AND deleted=? AND account_id=?", uid, false, accountId).Update(updateModel) + + if err != nil { + return err + } else if deletedRows < 1 { + return errs.ErrSubAccountNotFound + } + + if len(relatedTransactionsByAccount) > 0 { + updateTransaction := &models.Transaction{ + Deleted: true, + DeletedUnixTime: now, + } + + transactionIds := make([]int64, len(relatedTransactionsByAccount)) + + for i := 0; i < len(relatedTransactionsByAccount); i++ { + transactionIds[i] = relatedTransactionsByAccount[i].TransactionId + } + + deletedTransactionRows, err := sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).In("transaction_id", transactionIds).Update(updateTransaction) + + if err != nil { + return err + } else if deletedTransactionRows < int64(len(transactionIds)) { + return errs.ErrDatabaseOperationFailed + } + } + + return err + }) +} + // GetAccountMapByList returns an account map by a list func (s *AccountService) GetAccountMapByList(accounts []*models.Account) map[int64]*models.Account { accountMap := make(map[int64]*models.Account) diff --git a/src/lib/services.ts b/src/lib/services.ts index 41c15888..cfff9544 100644 --- a/src/lib/services.ts +++ b/src/lib/services.ts @@ -360,6 +360,9 @@ export default { deleteAccount: (req: AccountDeleteRequest): ApiResponsePromise => { return axios.post>('v1/accounts/delete.json', req); }, + deleteSubAccount: (req: AccountDeleteRequest): ApiResponsePromise => { + return axios.post>('v1/accounts/sub_account/delete.json', req); + }, getTransactions: (req: TransactionListByMaxTimeRequest): ApiResponsePromise => { const amountFilter = encodeURIComponent(req.amountFilter); const keyword = encodeURIComponent(req.keyword); diff --git a/src/locales/de.json b/src/locales/de.json index 4d58b57b..1744943d 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -1067,6 +1067,8 @@ "account balance time is not set": "Kontosaldo-Zeit ist nicht festgelegt", "cannot set statement date for non credit card account": "Abrechnungsdatum kann für kein Kreditkartenkonto festgelegt werden", "cannot set statement date for sub account": "Abrechnungsdatum kann für Teilkonto nicht festgelegt werden", + "sub-account not found": "Sub-account is not found", + "sub-account is in use and cannot be deleted": "Sub-account is in use and it cannot be deleted", "transaction id is invalid": "Transaktions-ID ist ungültig", "transaction not found": "Transaktion nicht gefunden", "transaction type is invalid": "Transaktionstyp ist ungültig", @@ -1576,7 +1578,9 @@ "Unable to unhide this account": "Dieses Konto kann nicht eingeblendet werden", "Unable to move account": "Konto kann nicht verschoben werden", "Are you sure you want to delete this account?": "Sind Sie sicher, dass Sie dieses Konto löschen möchten?", + "Are you sure you want to delete this sub-account?": "Are you sure you want to delete this sub-account?", "Unable to delete this account": "Konto kann nicht gelöscht werden", + "Unable to delete this sub-account": "Unable to delete this sub-account", "Transaction": "Transaktion", "Transactions": "Transaktionen", "Transaction Pictures": "Transaktionsbilder", diff --git a/src/locales/en.json b/src/locales/en.json index a6e74848..19d28f13 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1067,6 +1067,8 @@ "account balance time is not set": "Account balance time is not set", "cannot set statement date for non credit card account": "Cannot set statement date for non credit card account", "cannot set statement date for sub account": "Cannot set statement date for sub-account", + "sub-account not found": "Sub-account is not found", + "sub-account is in use and cannot be deleted": "Sub-account is in use and it cannot be deleted", "transaction id is invalid": "Transaction ID is invalid", "transaction not found": "Transaction is not found", "transaction type is invalid": "Transaction type is invalid", @@ -1576,7 +1578,9 @@ "Unable to unhide this account": "Unable to unhide this account", "Unable to move account": "Unable to move account", "Are you sure you want to delete this account?": "Are you sure you want to delete this account?", + "Are you sure you want to delete this sub-account?": "Are you sure you want to delete this sub-account?", "Unable to delete this account": "Unable to delete this account", + "Unable to delete this sub-account": "Unable to delete this sub-account", "Transaction": "Transaction", "Transactions": "Transactions", "Transaction Pictures": "Transaction Pictures", diff --git a/src/locales/es.json b/src/locales/es.json index 4a39779a..28c44c1a 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1067,6 +1067,8 @@ "account balance time is not set": "La hora del saldo de la cuenta no está establecida", "cannot set statement date for non credit card account": "No se puede establecer la fecha del extracto para una cuenta que no sea de tarjeta de crédito", "cannot set statement date for sub account": "No se puede establecer la fecha del estado de cuenta para la subcuenta", + "sub-account not found": "Sub-account is not found", + "sub-account is in use and cannot be deleted": "Sub-account is in use and it cannot be deleted", "transaction id is invalid": "El ID de transacción no es válido", "transaction not found": "La transacción no se encuentra", "transaction type is invalid": "El tipo de transacción no es válido", @@ -1576,7 +1578,9 @@ "Unable to unhide this account": "No se puede mostrar esta cuenta", "Unable to move account": "No se puede mover la cuenta", "Are you sure you want to delete this account?": "¿Está seguro de que desea eliminar esta cuenta?", + "Are you sure you want to delete this sub-account?": "Are you sure you want to delete this sub-account?", "Unable to delete this account": "No se puede eliminar esta cuenta", + "Unable to delete this sub-account": "Unable to delete this sub-account", "Transaction": "Transacción", "Transactions": "Transacciones", "Transaction Pictures": "Imágenes de transacciones", diff --git a/src/locales/ja.json b/src/locales/ja.json index 322b0ddb..31cb7408 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -1067,6 +1067,8 @@ "account balance time is not set": "口座残高の時間が設定されていません", "cannot set statement date for non credit card account": "クレジットカード以外の口座の明細書の日付を設定できません", "cannot set statement date for sub account": "子口座の明細日を設定できません", + "sub-account not found": "Sub-account is not found", + "sub-account is in use and cannot be deleted": "Sub-account is in use and it cannot be deleted", "transaction id is invalid": "取引IDは無効です", "transaction not found": "取引が見つかりません", "transaction type is invalid": "取引タイプは無効です", @@ -1576,7 +1578,9 @@ "Unable to unhide this account": "この口座は非表示を解除できません", "Unable to move account": "口座を移動できません", "Are you sure you want to delete this account?": "この口座を削除しますか?", + "Are you sure you want to delete this sub-account?": "Are you sure you want to delete this sub-account?", "Unable to delete this account": "この口座を削除できません", + "Unable to delete this sub-account": "Unable to delete this sub-account", "Transaction": "取引", "Transactions": "取引", "Transaction Pictures": "取引の写真", diff --git a/src/locales/ru.json b/src/locales/ru.json index 1b6931a2..f5df8238 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1067,6 +1067,8 @@ "account balance time is not set": "Время баланса счета не установлено", "cannot set statement date for non credit card account": "Нельзя установить дату выписки для счета, не являющегося кредитной картой", "cannot set statement date for sub account": "Нельзя установить дату выписки для субсчета", + "sub-account not found": "Sub-account is not found", + "sub-account is in use and cannot be deleted": "Sub-account is in use and it cannot be deleted", "transaction id is invalid": "ID транзакции недействителен", "transaction not found": "Транзакция не найдена", "transaction type is invalid": "Тип транзакции недействителен", @@ -1576,7 +1578,9 @@ "Unable to unhide this account": "Не удалось отобразить этот счет", "Unable to move account": "Не удалось переместить счет", "Are you sure you want to delete this account?": "Вы уверены, что хотите удалить этот счет?", + "Are you sure you want to delete this sub-account?": "Are you sure you want to delete this sub-account?", "Unable to delete this account": "Не удалось удалить этот счет", + "Unable to delete this sub-account": "Unable to delete this sub-account", "Transaction": "Транзакция", "Transactions": "Транзакции", "Transaction Pictures": "Изображения транзакций", diff --git a/src/locales/vi.json b/src/locales/vi.json index 6ced1edd..2bd1bb29 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -1067,6 +1067,8 @@ "account balance time is not set": "Thời gian số dư tài khoản chưa được đặt", "cannot set statement date for non credit card account": "Cannot set statement date for non credit card account", "cannot set statement date for sub account": "Cannot set statement date for sub-account", + "sub-account not found": "Sub-account is not found", + "sub-account is in use and cannot be deleted": "Sub-account is in use and it cannot be deleted", "transaction id is invalid": "ID giao dịch không hợp lệ", "transaction not found": "Không tìm thấy giao dịch", "transaction type is invalid": "Loại giao dịch không hợp lệ", @@ -1576,7 +1578,9 @@ "Unable to unhide this account": "Không thể hiển thị tài khoản này", "Unable to move account": "Không thể di chuyển tài khoản", "Are you sure you want to delete this account?": "Bạn có chắc chắn muốn xóa tài khoản này không?", + "Are you sure you want to delete this sub-account?": "Are you sure you want to delete this sub-account?", "Unable to delete this account": "Không thể xóa tài khoản này", + "Unable to delete this sub-account": "Unable to delete this sub-account", "Transaction": "Giao dịch", "Transactions": "Giao dịch", "Transaction Pictures": "Ảnh giao dịch", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index 682691e1..f4f5342b 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1067,6 +1067,8 @@ "account balance time is not set": "账户余额时间没有设置", "cannot set statement date for non credit card account": "非信用卡账户不能设置账单日期", "cannot set statement date for sub account": "子账户不能设置账单日期", + "sub-account not found": "子账户不存在", + "sub-account is in use and cannot be deleted": "子账户正在被使用,无法删除", "transaction id is invalid": "交易ID无效", "transaction not found": "交易不存在", "transaction type is invalid": "交易类型无效", @@ -1576,7 +1578,9 @@ "Unable to unhide this account": "无法取消隐藏账户", "Unable to move account": "无法移动账户", "Are you sure you want to delete this account?": "您确定要删除该账户?", + "Are you sure you want to delete this sub-account?": "您确定要删除该子账户?", "Unable to delete this account": "无法删除该账户", + "Unable to delete this sub-account": "无法删除该子账户", "Transaction": "交易", "Transactions": "交易", "Transaction Pictures": "交易图片", diff --git a/src/models/account.ts b/src/models/account.ts index ec7d07d2..b6794c4a 100644 --- a/src/models/account.ts +++ b/src/models/account.ts @@ -240,6 +240,30 @@ export class Account implements AccountInfoResponse { } } + public isAccountOrSubAccountHidden(subAccountId: string): boolean { + if (this.type === AccountType.SingleAccount.type) { + return this.hidden; + } else if (this.type === AccountType.MultiSubAccounts.type && !subAccountId) { + return this.hidden; + } else if (this.type === AccountType.MultiSubAccounts.type && subAccountId) { + if (!this.subAccounts || !this.subAccounts.length) { + return false; + } + + for (let i = 0; i < this.subAccounts.length; i++) { + const subAccount = this.subAccounts[i]; + + if (subAccountId && subAccountId === subAccount.id) { + return subAccount.hidden; + } + } + + return false; + } else { + return false; + } + } + public getAccountOrSubAccountComment(subAccountId: string): string | null { if (this.type === AccountType.SingleAccount.type) { return this.comment; @@ -264,6 +288,46 @@ export class Account implements AccountInfoResponse { } } + public getAccountOrSubAccount(subAccountId: string): Account | null { + if (this.type === AccountType.SingleAccount.type) { + return this; + } else if (this.type === AccountType.MultiSubAccounts.type && !subAccountId) { + return this; + } else if (this.type === AccountType.MultiSubAccounts.type && subAccountId) { + if (!this.subAccounts || !this.subAccounts.length) { + return null; + } + + for (let i = 0; i < this.subAccounts.length; i++) { + const subAccount = this.subAccounts[i]; + + if (subAccountId && subAccountId === subAccount.id) { + return subAccount; + } + } + + return null; + } else { + return null; + } + } + + public getSubAccount(subAccountId: string): Account | null { + if (!this.subAccounts || !this.subAccounts.length) { + return null; + } + + for (let i = 0; i < this.subAccounts.length; i++) { + const subAccount = this.subAccounts[i]; + + if (subAccountId && subAccountId === subAccount.id) { + return subAccount; + } + } + + return null; + } + public getSubAccountCurrencies(showHidden: boolean, subAccountId: string): string[] { if (!this.subAccounts || !this.subAccounts.length) { return []; diff --git a/src/stores/account.ts b/src/stores/account.ts index 1a4d83e3..3b963852 100644 --- a/src/stores/account.ts +++ b/src/stores/account.ts @@ -295,6 +295,46 @@ export const useAccountsStore = defineStore('accounts', () => { } } + function removeSubAccountFromAccountList(subAccount: Account): void { + for (let i = 0; i < allAccounts.value.length; i++) { + if (allAccounts.value[i].type !== AccountType.MultiSubAccounts.type || !allAccounts.value[i].subAccounts) { + continue; + } + + const subAccounts = allAccounts.value[i].subAccounts as Account[]; + + for (let j = 0; j < subAccounts.length; j++) { + if (subAccounts[j].id === subAccount.id) { + subAccounts.splice(j, 1); + break; + } + } + } + + if (allAccountsMap.value[subAccount.id]) { + delete allAccountsMap.value[subAccount.id]; + } + + if (allCategorizedAccountsMap.value[subAccount.category]) { + const accountList = allCategorizedAccountsMap.value[subAccount.category].accounts; + + for (let i = 0; i < accountList.length; i++) { + if (accountList[i].type !== AccountType.MultiSubAccounts.type || !accountList[i].subAccounts) { + continue; + } + + const subAccounts = accountList[i].subAccounts as Account[]; + + for (let j = 0; j < subAccounts.length; j++) { + if (subAccounts[j].id === subAccount.id) { + subAccounts.splice(j, 1); + break; + } + } + } + } + } + function updateAccountListInvalidState(invalidState: boolean): void { accountListStateInvalid.value = invalidState; } @@ -997,6 +1037,41 @@ export const useAccountsStore = defineStore('accounts', () => { }); } + function deleteSubAccount({ subAccount, beforeResolve }: { subAccount: Account, beforeResolve?: BeforeResolveFunction }): Promise { + return new Promise((resolve, reject) => { + services.deleteSubAccount({ + id: subAccount.id + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to delete this sub-account' }); + return; + } + + if (beforeResolve) { + beforeResolve(() => { + removeSubAccountFromAccountList(subAccount); + }); + } else { + removeSubAccountFromAccountList(subAccount); + } + + resolve(data.result); + }).catch(error => { + logger.error('failed to delete sub-account', 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 sub-account' }); + } else { + reject(error); + } + }); + }); + } + return { // states allAccounts, @@ -1029,6 +1104,7 @@ export const useAccountsStore = defineStore('accounts', () => { changeAccountDisplayOrder, updateAccountDisplayOrders, hideAccount, - deleteAccount + deleteAccount, + deleteSubAccount } }); diff --git a/src/views/desktop/accounts/ListPage.vue b/src/views/desktop/accounts/ListPage.vue index b3e9b83d..0cec1e0f 100644 --- a/src/views/desktop/accounts/ListPage.vue +++ b/src/views/desktop/accounts/ListPage.vue @@ -196,24 +196,24 @@ - - {{ accountComment(element) }} + + {{ element.getAccountOrSubAccountComment(activeSubAccount[element.id]) }}