support batch update categories for transactions in insights explorer

This commit is contained in:
MaysWind
2026-04-20 01:01:25 +08:00
parent f56b5c471d
commit 9c87436a36
32 changed files with 1166 additions and 179 deletions
+1
View File
@@ -393,6 +393,7 @@ func startWebServer(c *core.CliContext) error {
apiV1Route.GET("/transactions/get.json", bindApi(api.Transactions.TransactionGetHandler)) apiV1Route.GET("/transactions/get.json", bindApi(api.Transactions.TransactionGetHandler))
apiV1Route.POST("/transactions/add.json", bindApi(api.Transactions.TransactionCreateHandler)) apiV1Route.POST("/transactions/add.json", bindApi(api.Transactions.TransactionCreateHandler))
apiV1Route.POST("/transactions/modify.json", bindApi(api.Transactions.TransactionModifyHandler)) apiV1Route.POST("/transactions/modify.json", bindApi(api.Transactions.TransactionModifyHandler))
apiV1Route.POST("/transactions/batch_update/category.json", bindApi(api.Transactions.TransactionBatchUpdateCategoriesHandler))
apiV1Route.POST("/transactions/move/all.json", bindApi(api.Transactions.TransactionMoveAllBetweenAccountsHandler)) apiV1Route.POST("/transactions/move/all.json", bindApi(api.Transactions.TransactionMoveAllBetweenAccountsHandler))
apiV1Route.POST("/transactions/delete.json", bindApi(api.Transactions.TransactionDeleteHandler)) apiV1Route.POST("/transactions/delete.json", bindApi(api.Transactions.TransactionDeleteHandler))
+99
View File
@@ -1338,6 +1338,105 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.WebContext) (any, *er
return newTransactionResp, nil return newTransactionResp, nil
} }
// TransactionBatchUpdateCategoriesHandler batch updates categories of transactions by request parameters for current user
func (a *TransactionsApi) TransactionBatchUpdateCategoriesHandler(c *core.WebContext) (any, *errs.Error) {
var transactionBatchUpdateReq models.TransactionBatchUpdateCategoryRequest
err := c.ShouldBindJSON(&transactionBatchUpdateReq)
if err != nil {
log.Warnf(c, "[transactions.TransactionBatchUpdateCategoriesHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[transactions.TransactionBatchUpdateCategoriesHandler] cannot get client timezone, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
transactionIds, err := utils.StringArrayToInt64Array(transactionBatchUpdateReq.TransactionIds)
if err != nil {
log.Warnf(c, "[transactions.TransactionBatchUpdateCategoriesHandler] parse transaction ids failed, because %s", err.Error())
return nil, errs.ErrTransactionIdInvalid
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.Errorf(c, "[transactions.TransactionBatchUpdateCategoriesHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
category, err := a.transactionCategories.GetCategoryByCategoryId(c, uid, transactionBatchUpdateReq.CategoryId)
if err != nil {
log.Errorf(c, "[transactions.TransactionBatchUpdateCategoriesHandler] failed to get category \"id:%d\" for user \"uid:%d\", because %s", transactionBatchUpdateReq.CategoryId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if category.ParentCategoryId == models.LevelOneTransactionCategoryParentId {
log.Warnf(c, "[transactions.TransactionBatchUpdateCategoriesHandler] transaction category \"id:%d\" is not a sub category", category.CategoryId)
return nil, errs.ErrCannotUsePrimaryCategoryForTransaction
}
var expectedTransactionType models.TransactionDbType
if category.Type == models.CATEGORY_TYPE_EXPENSE {
expectedTransactionType = models.TRANSACTION_DB_TYPE_EXPENSE
} else if category.Type == models.CATEGORY_TYPE_INCOME {
expectedTransactionType = models.TRANSACTION_DB_TYPE_INCOME
} else if category.Type == models.CATEGORY_TYPE_TRANSFER {
expectedTransactionType = models.TRANSACTION_DB_TYPE_TRANSFER_OUT
}
transactions, err := a.transactions.GetTransactionsByTransactionIds(c, uid, transactionIds)
if err != nil {
log.Errorf(c, "[transactions.TransactionBatchUpdateCategoriesHandler] failed to get transactions for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
allTransactionIds := make([]int64, 0, len(transactions))
for i := 0; i < len(transactions); i++ {
transaction := transactions[i]
if transaction.Type != expectedTransactionType {
log.Warnf(c, "[transactions.TransactionBatchUpdateCategoriesHandler] transaction \"id:%d\" type is not expected type \"%d\" for user \"uid:%d\"", transaction.TransactionId, expectedTransactionType, uid)
return nil, errs.ErrTransactionTypeInvalid
}
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone)
if !transactionEditable {
log.Warnf(c, "[transactions.TransactionBatchUpdateCategoriesHandler] transaction \"id:%d\" is not editable for user \"uid:%d\"", transaction.TransactionId, uid)
return nil, errs.ErrCannotModifyTransactionWithThisTransactionTime
}
allTransactionIds = append(allTransactionIds, transaction.TransactionId)
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
allTransactionIds = append(allTransactionIds, transaction.RelatedId)
}
}
err = a.transactions.BatchUpdateTransactionsCategory(c, uid, allTransactionIds, category.CategoryId)
if err != nil {
log.Errorf(c, "[transactions.TransactionBatchUpdateCategoriesHandler] failed to batch update transactions category for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[transactions.TransactionBatchUpdateCategoriesHandler] user \"uid:%d\" has batch updated category of %d transactions successfully", uid, len(transactionBatchUpdateReq.TransactionIds))
return true, nil
}
// TransactionMoveAllBetweenAccountsHandler moves all transactions from one account to another account for current user // TransactionMoveAllBetweenAccountsHandler moves all transactions from one account to another account for current user
func (a *TransactionsApi) TransactionMoveAllBetweenAccountsHandler(c *core.WebContext) (any, *errs.Error) { func (a *TransactionsApi) TransactionMoveAllBetweenAccountsHandler(c *core.WebContext) (any, *errs.Error) {
var transactionMoveReq models.TransactionMoveBetweenAccountsRequest var transactionMoveReq models.TransactionMoveBetweenAccountsRequest
+6
View File
@@ -325,6 +325,12 @@ type TransactionGetRequest struct {
TrimTag bool `form:"trim_tag"` TrimTag bool `form:"trim_tag"`
} }
// TransactionBatchUpdateCategoryRequest represents all parameters of transaction batch update category request
type TransactionBatchUpdateCategoryRequest struct {
TransactionIds []string `json:"transactionIds,string" binding:"required"`
CategoryId int64 `json:"categoryId,string" binding:"required"`
}
// TransactionMoveBetweenAccountsRequest represents all parameters of moving all transactions between accounts request // TransactionMoveBetweenAccountsRequest represents all parameters of moving all transactions between accounts request
type TransactionMoveBetweenAccountsRequest struct { type TransactionMoveBetweenAccountsRequest struct {
FromAccountId int64 `json:"fromAccountId,string" binding:"required,min=1"` FromAccountId int64 `json:"fromAccountId,string" binding:"required,min=1"`
+52
View File
@@ -434,6 +434,22 @@ func (s *TransactionService) GetTransactionByTransactionId(c core.Context, uid i
return transaction, nil return transaction, nil
} }
// GetTransactionsByTransactionIds returns transaction models according to transaction ids
func (s *TransactionService) GetTransactionsByTransactionIds(c core.Context, uid int64, transactionIds []int64) ([]*models.Transaction, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
if len(transactionIds) <= 0 {
return nil, errs.ErrTransactionIdInvalid
}
var transactions []*models.Transaction
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).In("transaction_id", transactionIds).Find(&transactions)
return transactions, err
}
// GetAllTransactionCount returns total count of transactions // GetAllTransactionCount returns total count of transactions
func (s *TransactionService) GetAllTransactionCount(c core.Context, uid int64) (int64, error) { func (s *TransactionService) GetAllTransactionCount(c core.Context, uid int64) (int64, error) {
return s.GetTransactionCount(c, uid, 0, 0, 0, nil, nil, nil, false, "", "") return s.GetTransactionCount(c, uid, 0, 0, 0, nil, nil, nil, false, "", "")
@@ -1322,6 +1338,42 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
return nil return nil
} }
// BatchUpdateTransactionsCategory batch updates the categories of transactions
func (s *TransactionService) BatchUpdateTransactionsCategory(c core.Context, uid int64, transactionIds []int64, newCategoryId int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
if len(transactionIds) < 1 {
return errs.ErrTransactionIdInvalid
}
if newCategoryId <= 0 {
return errs.ErrTransactionCategoryIdInvalid
}
uniqueTransactionIds := utils.ToUniqueInt64Slice(transactionIds)
now := time.Now().Unix()
updateModel := &models.Transaction{
CategoryId: newCategoryId,
UpdatedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
updatedRows, err := sess.Cols("category_id", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).In("transaction_id", uniqueTransactionIds).Update(updateModel)
if err != nil {
return err
} else if updatedRows < int64(len(uniqueTransactionIds)) {
return errs.ErrTransactionNotFound
}
return err
})
}
// MoveAllTransactionsBetweenAccounts moves all transactions from one account to another account, and combine balance modification transactions if necessary
func (s *TransactionService) MoveAllTransactionsBetweenAccounts(c core.Context, uid int64, fromAccountId int64, toAccountId int64) error { func (s *TransactionService) MoveAllTransactionsBetweenAccounts(c core.Context, uid int64, fromAccountId int64, toAccountId int64) error {
if uid <= 0 { if uid <= 0 {
return errs.ErrUserIdInvalid return errs.ErrUserIdInvalid
+15
View File
@@ -191,6 +191,21 @@ export function getObjectOwnFieldCount(object: object): number {
return count; return count;
} }
export function getObjectOwnFieldWithValueCount(object: object, value: unknown): number {
let count = 0;
if (!object || !isObject(object)) {
return count;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const _ of keysIfValueEquals(object, value)) {
count++;
}
return count;
}
export function replaceAll(value: string, originalValue: string, targetValue: string): string { export function replaceAll(value: string, originalValue: string, targetValue: string): string {
// Escape special characters in originalValue to safely use it in a regex pattern. // Escape special characters in originalValue to safely use it in a regex pattern.
// This ensures that characters like . (dot), * (asterisk), +, ?, etc. are treated literally, // This ensures that characters like . (dot), * (asterisk), +, ?, etc. are treated literally,
+4
View File
@@ -64,6 +64,7 @@ import type {
import type { import type {
TransactionCreateRequest, TransactionCreateRequest,
TransactionModifyRequest, TransactionModifyRequest,
TransactionBatchUpdateCategoryRequest,
TransactionMoveBetweenAccountsRequest, TransactionMoveBetweenAccountsRequest,
TransactionDeleteRequest, TransactionDeleteRequest,
TransactionImportRequest, TransactionImportRequest,
@@ -611,6 +612,9 @@ export default {
modifyTransaction: (req: TransactionModifyRequest): ApiResponsePromise<TransactionInfoResponse> => { modifyTransaction: (req: TransactionModifyRequest): ApiResponsePromise<TransactionInfoResponse> => {
return axios.post<ApiResponse<TransactionInfoResponse>>('v1/transactions/modify.json', req); return axios.post<ApiResponse<TransactionInfoResponse>>('v1/transactions/modify.json', req);
}, },
batchUpdateTransactionCategories: (req: TransactionBatchUpdateCategoryRequest): ApiResponsePromise<boolean> => {
return axios.post<ApiResponse<boolean>>('v1/transactions/batch_update/category.json', req);
},
moveAllTransactionsBetweenAccounts: (req: TransactionMoveBetweenAccountsRequest): ApiResponsePromise<boolean> => { moveAllTransactionsBetweenAccounts: (req: TransactionMoveBetweenAccountsRequest): ApiResponsePromise<boolean> => {
return axios.post<ApiResponse<boolean>>('v1/transactions/move/all.json', req); return axios.post<ApiResponse<boolean>>('v1/transactions/move/all.json', req);
}, },
+6
View File
@@ -1757,6 +1757,8 @@
"Save As New Explorer": "Als neuen Explorer speichern", "Save As New Explorer": "Als neuen Explorer speichern",
"Restore to Last Saved": "Auf letzten Speicherstand zurücksetzen", "Restore to Last Saved": "Auf letzten Speicherstand zurücksetzen",
"Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Sind Sie sicher, dass Sie auf den letzten Speicherstand zurücksetzen möchten? Alle nicht gespeicherten Änderungen gehen verloren.", "Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Sind Sie sicher, dass Sie auf den letzten Speicherstand zurücksetzen möchten? Alle nicht gespeicherten Änderungen gehen verloren.",
"Enter Edit Mode": "Enter Edit Mode",
"Exit Edit Mode": "Exit Edit Mode",
"Set Explorer Name": "Explorer-Name festlegen", "Set Explorer Name": "Explorer-Name festlegen",
"Rename Explorer": "Explorer umbenennen", "Rename Explorer": "Explorer umbenennen",
"Hide Explorer": "Explorer ausblenden", "Hide Explorer": "Explorer ausblenden",
@@ -1843,6 +1845,10 @@
"Coefficient of Variation": "Variationskoeffizient", "Coefficient of Variation": "Variationskoeffizient",
"Skewness": "Skewness", "Skewness": "Skewness",
"Kurtosis": "Kurtosis", "Kurtosis": "Kurtosis",
"Update Categories for Expense Transactions": "Update Categories for Expense Transactions",
"Update Categories for Income Transactions": "Update Categories for Income Transactions",
"Update Categories for Transfer Transactions": "Update Categories for Transfer Transactions",
"Unable to update categories for transactions": "Unable to update categories for transactions",
"Account List": "Kontoliste", "Account List": "Kontoliste",
"This Week": "Diese Woche", "This Week": "Diese Woche",
"This Month": "Dieser Monat", "This Month": "Dieser Monat",
+6
View File
@@ -1757,6 +1757,8 @@
"Save As New Explorer": "Save As New Explorer", "Save As New Explorer": "Save As New Explorer",
"Restore to Last Saved": "Restore to Last Saved", "Restore to Last Saved": "Restore to Last Saved",
"Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.", "Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.",
"Enter Edit Mode": "Enter Edit Mode",
"Exit Edit Mode": "Exit Edit Mode",
"Set Explorer Name": "Set Explorer Name", "Set Explorer Name": "Set Explorer Name",
"Rename Explorer": "Rename Explorer", "Rename Explorer": "Rename Explorer",
"Hide Explorer": "Hide Explorer", "Hide Explorer": "Hide Explorer",
@@ -1843,6 +1845,10 @@
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness", "Skewness": "Skewness",
"Kurtosis": "Kurtosis", "Kurtosis": "Kurtosis",
"Update Categories for Expense Transactions": "Update Categories for Expense Transactions",
"Update Categories for Income Transactions": "Update Categories for Income Transactions",
"Update Categories for Transfer Transactions": "Update Categories for Transfer Transactions",
"Unable to update categories for transactions": "Unable to update categories for transactions",
"Account List": "Account List", "Account List": "Account List",
"This Week": "This Week", "This Week": "This Week",
"This Month": "This Month", "This Month": "This Month",
+6
View File
@@ -1757,6 +1757,8 @@
"Save As New Explorer": "Guardar Como Nueva Exploración", "Save As New Explorer": "Guardar Como Nueva Exploración",
"Restore to Last Saved": "Restaurar al Último Guardado", "Restore to Last Saved": "Restaurar al Último Guardado",
"Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "¿Seguro que quieres restaurar al último estado guardado? Se perderán todos los cambios no guardados.", "Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "¿Seguro que quieres restaurar al último estado guardado? Se perderán todos los cambios no guardados.",
"Enter Edit Mode": "Enter Edit Mode",
"Exit Edit Mode": "Exit Edit Mode",
"Set Explorer Name": "Asignar Nombre a la Exploración", "Set Explorer Name": "Asignar Nombre a la Exploración",
"Rename Explorer": "Renombrar Exploración", "Rename Explorer": "Renombrar Exploración",
"Hide Explorer": "Ocultar Exploración", "Hide Explorer": "Ocultar Exploración",
@@ -1843,6 +1845,10 @@
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness", "Skewness": "Skewness",
"Kurtosis": "Kurtosis", "Kurtosis": "Kurtosis",
"Update Categories for Expense Transactions": "Update Categories for Expense Transactions",
"Update Categories for Income Transactions": "Update Categories for Income Transactions",
"Update Categories for Transfer Transactions": "Update Categories for Transfer Transactions",
"Unable to update categories for transactions": "Unable to update categories for transactions",
"Account List": "Lista de Cuentas", "Account List": "Lista de Cuentas",
"This Week": "Esta Semana", "This Week": "Esta Semana",
"This Month": "Este Mes", "This Month": "Este Mes",
+6
View File
@@ -1757,6 +1757,8 @@
"Save As New Explorer": "Save As New Explorer", "Save As New Explorer": "Save As New Explorer",
"Restore to Last Saved": "Restore to Last Saved", "Restore to Last Saved": "Restore to Last Saved",
"Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.", "Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.",
"Enter Edit Mode": "Enter Edit Mode",
"Exit Edit Mode": "Exit Edit Mode",
"Set Explorer Name": "Set Explorer Name", "Set Explorer Name": "Set Explorer Name",
"Rename Explorer": "Rename Explorer", "Rename Explorer": "Rename Explorer",
"Hide Explorer": "Hide Explorer", "Hide Explorer": "Hide Explorer",
@@ -1843,6 +1845,10 @@
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness", "Skewness": "Skewness",
"Kurtosis": "Kurtosis", "Kurtosis": "Kurtosis",
"Update Categories for Expense Transactions": "Update Categories for Expense Transactions",
"Update Categories for Income Transactions": "Update Categories for Income Transactions",
"Update Categories for Transfer Transactions": "Update Categories for Transfer Transactions",
"Unable to update categories for transactions": "Unable to update categories for transactions",
"Account List": "Liste des comptes", "Account List": "Liste des comptes",
"This Week": "Cette semaine", "This Week": "Cette semaine",
"This Month": "Ce mois", "This Month": "Ce mois",
+6
View File
@@ -1757,6 +1757,8 @@
"Save As New Explorer": "Save As New Explorer", "Save As New Explorer": "Save As New Explorer",
"Restore to Last Saved": "Restore to Last Saved", "Restore to Last Saved": "Restore to Last Saved",
"Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.", "Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.",
"Enter Edit Mode": "Enter Edit Mode",
"Exit Edit Mode": "Exit Edit Mode",
"Set Explorer Name": "Set Explorer Name", "Set Explorer Name": "Set Explorer Name",
"Rename Explorer": "Rename Explorer", "Rename Explorer": "Rename Explorer",
"Hide Explorer": "Hide Explorer", "Hide Explorer": "Hide Explorer",
@@ -1843,6 +1845,10 @@
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness", "Skewness": "Skewness",
"Kurtosis": "Kurtosis", "Kurtosis": "Kurtosis",
"Update Categories for Expense Transactions": "Update Categories for Expense Transactions",
"Update Categories for Income Transactions": "Update Categories for Income Transactions",
"Update Categories for Transfer Transactions": "Update Categories for Transfer Transactions",
"Unable to update categories for transactions": "Unable to update categories for transactions",
"Account List": "Elenco account", "Account List": "Elenco account",
"This Week": "Questa settimana", "This Week": "Questa settimana",
"This Month": "Questo mese", "This Month": "Questo mese",
+6
View File
@@ -1757,6 +1757,8 @@
"Save As New Explorer": "Save As New Explorer", "Save As New Explorer": "Save As New Explorer",
"Restore to Last Saved": "Restore to Last Saved", "Restore to Last Saved": "Restore to Last Saved",
"Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.", "Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.",
"Enter Edit Mode": "Enter Edit Mode",
"Exit Edit Mode": "Exit Edit Mode",
"Set Explorer Name": "Set Explorer Name", "Set Explorer Name": "Set Explorer Name",
"Rename Explorer": "Rename Explorer", "Rename Explorer": "Rename Explorer",
"Hide Explorer": "Hide Explorer", "Hide Explorer": "Hide Explorer",
@@ -1843,6 +1845,10 @@
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness", "Skewness": "Skewness",
"Kurtosis": "Kurtosis", "Kurtosis": "Kurtosis",
"Update Categories for Expense Transactions": "Update Categories for Expense Transactions",
"Update Categories for Income Transactions": "Update Categories for Income Transactions",
"Update Categories for Transfer Transactions": "Update Categories for Transfer Transactions",
"Unable to update categories for transactions": "Unable to update categories for transactions",
"Account List": "口座リスト", "Account List": "口座リスト",
"This Week": "今週", "This Week": "今週",
"This Month": "今月", "This Month": "今月",
+6
View File
@@ -1757,6 +1757,8 @@
"Save As New Explorer": "Save As New Explorer", "Save As New Explorer": "Save As New Explorer",
"Restore to Last Saved": "Restore to Last Saved", "Restore to Last Saved": "Restore to Last Saved",
"Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.", "Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.",
"Enter Edit Mode": "Enter Edit Mode",
"Exit Edit Mode": "Exit Edit Mode",
"Set Explorer Name": "Set Explorer Name", "Set Explorer Name": "Set Explorer Name",
"Rename Explorer": "Rename Explorer", "Rename Explorer": "Rename Explorer",
"Hide Explorer": "Hide Explorer", "Hide Explorer": "Hide Explorer",
@@ -1843,6 +1845,10 @@
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness", "Skewness": "Skewness",
"Kurtosis": "Kurtosis", "Kurtosis": "Kurtosis",
"Update Categories for Expense Transactions": "Update Categories for Expense Transactions",
"Update Categories for Income Transactions": "Update Categories for Income Transactions",
"Update Categories for Transfer Transactions": "Update Categories for Transfer Transactions",
"Unable to update categories for transactions": "Unable to update categories for transactions",
"Account List": "ಖಾತೆಗಳ ಪಟ್ಟಿ", "Account List": "ಖಾತೆಗಳ ಪಟ್ಟಿ",
"This Week": "ಈ ವಾರ", "This Week": "ಈ ವಾರ",
"This Month": "ಈ ತಿಂಗಳು", "This Month": "ಈ ತಿಂಗಳು",
+6
View File
@@ -1757,6 +1757,8 @@
"Save As New Explorer": "새 탐색기로 저장", "Save As New Explorer": "새 탐색기로 저장",
"Restore to Last Saved": "마지막 저장 상태로 복원", "Restore to Last Saved": "마지막 저장 상태로 복원",
"Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "마지막 저장 상태로 복원하시겠습니까? 저장되지 않은 모든 변경 사항이 손실됩니다.", "Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "마지막 저장 상태로 복원하시겠습니까? 저장되지 않은 모든 변경 사항이 손실됩니다.",
"Enter Edit Mode": "Enter Edit Mode",
"Exit Edit Mode": "Exit Edit Mode",
"Set Explorer Name": "탐색기 이름 설정", "Set Explorer Name": "탐색기 이름 설정",
"Rename Explorer": "탐색기 이름 바꾸기", "Rename Explorer": "탐색기 이름 바꾸기",
"Hide Explorer": "탐색기 숨기기", "Hide Explorer": "탐색기 숨기기",
@@ -1843,6 +1845,10 @@
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness", "Skewness": "Skewness",
"Kurtosis": "Kurtosis", "Kurtosis": "Kurtosis",
"Update Categories for Expense Transactions": "Update Categories for Expense Transactions",
"Update Categories for Income Transactions": "Update Categories for Income Transactions",
"Update Categories for Transfer Transactions": "Update Categories for Transfer Transactions",
"Unable to update categories for transactions": "Unable to update categories for transactions",
"Account List": "계좌 목록", "Account List": "계좌 목록",
"This Week": "이번 주", "This Week": "이번 주",
"This Month": "이번 달", "This Month": "이번 달",
+6
View File
@@ -1757,6 +1757,8 @@
"Save As New Explorer": "Save As New Explorer", "Save As New Explorer": "Save As New Explorer",
"Restore to Last Saved": "Restore to Last Saved", "Restore to Last Saved": "Restore to Last Saved",
"Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.", "Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.",
"Enter Edit Mode": "Enter Edit Mode",
"Exit Edit Mode": "Exit Edit Mode",
"Set Explorer Name": "Set Explorer Name", "Set Explorer Name": "Set Explorer Name",
"Rename Explorer": "Rename Explorer", "Rename Explorer": "Rename Explorer",
"Hide Explorer": "Hide Explorer", "Hide Explorer": "Hide Explorer",
@@ -1843,6 +1845,10 @@
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness", "Skewness": "Skewness",
"Kurtosis": "Kurtosis", "Kurtosis": "Kurtosis",
"Update Categories for Expense Transactions": "Update Categories for Expense Transactions",
"Update Categories for Income Transactions": "Update Categories for Income Transactions",
"Update Categories for Transfer Transactions": "Update Categories for Transfer Transactions",
"Unable to update categories for transactions": "Unable to update categories for transactions",
"Account List": "Rekeningenlijst", "Account List": "Rekeningenlijst",
"This Week": "Deze week", "This Week": "Deze week",
"This Month": "Deze maand", "This Month": "Deze maand",
+6
View File
@@ -1757,6 +1757,8 @@
"Save As New Explorer": "Salvar como Novo Explorador", "Save As New Explorer": "Salvar como Novo Explorador",
"Restore to Last Saved": "Restaurar para o Último Salvo", "Restore to Last Saved": "Restaurar para o Último Salvo",
"Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Tem certeza de que deseja restaurar para o último estado salvo? Todas as alterações não salvas serão perdidas.", "Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Tem certeza de que deseja restaurar para o último estado salvo? Todas as alterações não salvas serão perdidas.",
"Enter Edit Mode": "Enter Edit Mode",
"Exit Edit Mode": "Exit Edit Mode",
"Set Explorer Name": "Definir Nome do Explorador", "Set Explorer Name": "Definir Nome do Explorador",
"Rename Explorer": "Renomear Explorador", "Rename Explorer": "Renomear Explorador",
"Hide Explorer": "Ocultar Explorador", "Hide Explorer": "Ocultar Explorador",
@@ -1843,6 +1845,10 @@
"Coefficient of Variation": "Coeficiente de Variação", "Coefficient of Variation": "Coeficiente de Variação",
"Skewness": "Skewness", "Skewness": "Skewness",
"Kurtosis": "Kurtosis", "Kurtosis": "Kurtosis",
"Update Categories for Expense Transactions": "Update Categories for Expense Transactions",
"Update Categories for Income Transactions": "Update Categories for Income Transactions",
"Update Categories for Transfer Transactions": "Update Categories for Transfer Transactions",
"Unable to update categories for transactions": "Unable to update categories for transactions",
"Account List": "Lista de Contas", "Account List": "Lista de Contas",
"This Week": "Esta Semana", "This Week": "Esta Semana",
"This Month": "Este Mês", "This Month": "Este Mês",
+6
View File
@@ -1757,6 +1757,8 @@
"Save As New Explorer": "Сохранить как новое исследование", "Save As New Explorer": "Сохранить как новое исследование",
"Restore to Last Saved": "Восстановить с последнего сохранения", "Restore to Last Saved": "Восстановить с последнего сохранения",
"Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Вы действительно хотите восстановить из последнего сохранения? Вы несохранённые изминения будут потеряны.", "Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Вы действительно хотите восстановить из последнего сохранения? Вы несохранённые изминения будут потеряны.",
"Enter Edit Mode": "Enter Edit Mode",
"Exit Edit Mode": "Exit Edit Mode",
"Set Explorer Name": "Установить название исследования", "Set Explorer Name": "Установить название исследования",
"Rename Explorer": "Переименовать исследование", "Rename Explorer": "Переименовать исследование",
"Hide Explorer": "Скрыть исследование", "Hide Explorer": "Скрыть исследование",
@@ -1843,6 +1845,10 @@
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness", "Skewness": "Skewness",
"Kurtosis": "Kurtosis", "Kurtosis": "Kurtosis",
"Update Categories for Expense Transactions": "Update Categories for Expense Transactions",
"Update Categories for Income Transactions": "Update Categories for Income Transactions",
"Update Categories for Transfer Transactions": "Update Categories for Transfer Transactions",
"Unable to update categories for transactions": "Unable to update categories for transactions",
"Account List": "Список счетов", "Account List": "Список счетов",
"This Week": "На этой неделе", "This Week": "На этой неделе",
"This Month": "В этом месяце", "This Month": "В этом месяце",
+6
View File
@@ -1757,6 +1757,8 @@
"Save As New Explorer": "Shrani kot novo raziskovanje", "Save As New Explorer": "Shrani kot novo raziskovanje",
"Restore to Last Saved": "Povrni na zadnje shranjeno stanje", "Restore to Last Saved": "Povrni na zadnje shranjeno stanje",
"Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Ali ste prepričani, da želite povrniti na zadnje shranjeno stanje? Vse neshranjene spremembe bodo izgubljene.", "Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Ali ste prepričani, da želite povrniti na zadnje shranjeno stanje? Vse neshranjene spremembe bodo izgubljene.",
"Enter Edit Mode": "Enter Edit Mode",
"Exit Edit Mode": "Exit Edit Mode",
"Set Explorer Name": "Nastavi ime raziskovanja", "Set Explorer Name": "Nastavi ime raziskovanja",
"Rename Explorer": "Preimenuj raziskovanje", "Rename Explorer": "Preimenuj raziskovanje",
"Hide Explorer": "Skrij raziskovanje", "Hide Explorer": "Skrij raziskovanje",
@@ -1843,6 +1845,10 @@
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness", "Skewness": "Skewness",
"Kurtosis": "Kurtosis", "Kurtosis": "Kurtosis",
"Update Categories for Expense Transactions": "Update Categories for Expense Transactions",
"Update Categories for Income Transactions": "Update Categories for Income Transactions",
"Update Categories for Transfer Transactions": "Update Categories for Transfer Transactions",
"Unable to update categories for transactions": "Unable to update categories for transactions",
"Account List": "Seznam računov", "Account List": "Seznam računov",
"This Week": "Ta teden", "This Week": "Ta teden",
"This Month": "Ta mesec", "This Month": "Ta mesec",
+6
View File
@@ -1757,6 +1757,8 @@
"Save As New Explorer": "புதிய ஆய்வுக்கருவியாக சேமி", "Save As New Explorer": "புதிய ஆய்வுக்கருவியாக சேமி",
"Restore to Last Saved": "கடைசி சேமிப்புக்கு மீட்டமை", "Restore to Last Saved": "கடைசி சேமிப்புக்கு மீட்டமை",
"Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "கடைசி சேமிக்கப்பட்ட நிலைக்கு மீட்டமைக்க விரும்புகிறீர்களா? சேமிக்கப்படாத அனைத்து மாற்றங்களும் இழக்கப்படும்.", "Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "கடைசி சேமிக்கப்பட்ட நிலைக்கு மீட்டமைக்க விரும்புகிறீர்களா? சேமிக்கப்படாத அனைத்து மாற்றங்களும் இழக்கப்படும்.",
"Enter Edit Mode": "Enter Edit Mode",
"Exit Edit Mode": "Exit Edit Mode",
"Set Explorer Name": "ஆய்வுக்கருவி பெயர் அமை", "Set Explorer Name": "ஆய்வுக்கருவி பெயர் அமை",
"Rename Explorer": "ஆய்வுக்கருவி மறுபெயரிடு", "Rename Explorer": "ஆய்வுக்கருவி மறுபெயரிடு",
"Hide Explorer": "ஆய்வுக்கருவி மறை", "Hide Explorer": "ஆய்வுக்கருவி மறை",
@@ -1843,6 +1845,10 @@
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness", "Skewness": "Skewness",
"Kurtosis": "Kurtosis", "Kurtosis": "Kurtosis",
"Update Categories for Expense Transactions": "Update Categories for Expense Transactions",
"Update Categories for Income Transactions": "Update Categories for Income Transactions",
"Update Categories for Transfer Transactions": "Update Categories for Transfer Transactions",
"Unable to update categories for transactions": "Unable to update categories for transactions",
"Account List": "கணக்குகளின் பட்டியல்", "Account List": "கணக்குகளின் பட்டியல்",
"This Week": "இந்த வாரம்", "This Week": "இந்த வாரம்",
"This Month": "இந்த மாதம்", "This Month": "இந்த மாதம்",
+6
View File
@@ -1757,6 +1757,8 @@
"Save As New Explorer": "Save As New Explorer", "Save As New Explorer": "Save As New Explorer",
"Restore to Last Saved": "Restore to Last Saved", "Restore to Last Saved": "Restore to Last Saved",
"Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.", "Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.",
"Enter Edit Mode": "Enter Edit Mode",
"Exit Edit Mode": "Exit Edit Mode",
"Set Explorer Name": "Set Explorer Name", "Set Explorer Name": "Set Explorer Name",
"Rename Explorer": "Rename Explorer", "Rename Explorer": "Rename Explorer",
"Hide Explorer": "Hide Explorer", "Hide Explorer": "Hide Explorer",
@@ -1843,6 +1845,10 @@
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness", "Skewness": "Skewness",
"Kurtosis": "Kurtosis", "Kurtosis": "Kurtosis",
"Update Categories for Expense Transactions": "Update Categories for Expense Transactions",
"Update Categories for Income Transactions": "Update Categories for Income Transactions",
"Update Categories for Transfer Transactions": "Update Categories for Transfer Transactions",
"Unable to update categories for transactions": "Unable to update categories for transactions",
"Account List": "รายการบัญชี", "Account List": "รายการบัญชี",
"This Week": "สัปดาห์นี้", "This Week": "สัปดาห์นี้",
"This Month": "เดือนนี้", "This Month": "เดือนนี้",
+6
View File
@@ -1757,6 +1757,8 @@
"Save As New Explorer": "Save As New Explorer", "Save As New Explorer": "Save As New Explorer",
"Restore to Last Saved": "Restore to Last Saved", "Restore to Last Saved": "Restore to Last Saved",
"Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.", "Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.",
"Enter Edit Mode": "Enter Edit Mode",
"Exit Edit Mode": "Exit Edit Mode",
"Set Explorer Name": "Set Explorer Name", "Set Explorer Name": "Set Explorer Name",
"Rename Explorer": "Rename Explorer", "Rename Explorer": "Rename Explorer",
"Hide Explorer": "Hide Explorer", "Hide Explorer": "Hide Explorer",
@@ -1843,6 +1845,10 @@
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness", "Skewness": "Skewness",
"Kurtosis": "Kurtosis", "Kurtosis": "Kurtosis",
"Update Categories for Expense Transactions": "Update Categories for Expense Transactions",
"Update Categories for Income Transactions": "Update Categories for Income Transactions",
"Update Categories for Transfer Transactions": "Update Categories for Transfer Transactions",
"Unable to update categories for transactions": "Unable to update categories for transactions",
"Account List": "Hesap Listesi", "Account List": "Hesap Listesi",
"This Week": "Bu Hafta", "This Week": "Bu Hafta",
"This Month": "Bu Ay", "This Month": "Bu Ay",
+6
View File
@@ -1757,6 +1757,8 @@
"Save As New Explorer": "Save As New Explorer", "Save As New Explorer": "Save As New Explorer",
"Restore to Last Saved": "Restore to Last Saved", "Restore to Last Saved": "Restore to Last Saved",
"Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.", "Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.",
"Enter Edit Mode": "Enter Edit Mode",
"Exit Edit Mode": "Exit Edit Mode",
"Set Explorer Name": "Set Explorer Name", "Set Explorer Name": "Set Explorer Name",
"Rename Explorer": "Rename Explorer", "Rename Explorer": "Rename Explorer",
"Hide Explorer": "Hide Explorer", "Hide Explorer": "Hide Explorer",
@@ -1843,6 +1845,10 @@
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness", "Skewness": "Skewness",
"Kurtosis": "Kurtosis", "Kurtosis": "Kurtosis",
"Update Categories for Expense Transactions": "Update Categories for Expense Transactions",
"Update Categories for Income Transactions": "Update Categories for Income Transactions",
"Update Categories for Transfer Transactions": "Update Categories for Transfer Transactions",
"Unable to update categories for transactions": "Unable to update categories for transactions",
"Account List": "Список рахунків", "Account List": "Список рахунків",
"This Week": "Цього тижня", "This Week": "Цього тижня",
"This Month": "Цього місяця", "This Month": "Цього місяця",
+6
View File
@@ -1757,6 +1757,8 @@
"Save As New Explorer": "Save As New Explorer", "Save As New Explorer": "Save As New Explorer",
"Restore to Last Saved": "Restore to Last Saved", "Restore to Last Saved": "Restore to Last Saved",
"Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.", "Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.",
"Enter Edit Mode": "Enter Edit Mode",
"Exit Edit Mode": "Exit Edit Mode",
"Set Explorer Name": "Set Explorer Name", "Set Explorer Name": "Set Explorer Name",
"Rename Explorer": "Rename Explorer", "Rename Explorer": "Rename Explorer",
"Hide Explorer": "Hide Explorer", "Hide Explorer": "Hide Explorer",
@@ -1843,6 +1845,10 @@
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness", "Skewness": "Skewness",
"Kurtosis": "Kurtosis", "Kurtosis": "Kurtosis",
"Update Categories for Expense Transactions": "Update Categories for Expense Transactions",
"Update Categories for Income Transactions": "Update Categories for Income Transactions",
"Update Categories for Transfer Transactions": "Update Categories for Transfer Transactions",
"Unable to update categories for transactions": "Unable to update categories for transactions",
"Account List": "Danh sách tài khoản", "Account List": "Danh sách tài khoản",
"This Week": "Tuần này", "This Week": "Tuần này",
"This Month": "Tháng này", "This Month": "Tháng này",
+6
View File
@@ -1757,6 +1757,8 @@
"Save As New Explorer": "另存为新的探索", "Save As New Explorer": "另存为新的探索",
"Restore to Last Saved": "恢复到上次保存", "Restore to Last Saved": "恢复到上次保存",
"Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "您确定要恢复到上次保存的状态吗?所有未保存的更改将会丢失。", "Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "您确定要恢复到上次保存的状态吗?所有未保存的更改将会丢失。",
"Enter Edit Mode": "进入编辑模式",
"Exit Edit Mode": "退出编辑模式",
"Set Explorer Name": "设置探索名称", "Set Explorer Name": "设置探索名称",
"Rename Explorer": "重命名探索", "Rename Explorer": "重命名探索",
"Hide Explorer": "隐藏探索", "Hide Explorer": "隐藏探索",
@@ -1843,6 +1845,10 @@
"Coefficient of Variation": "变异系数", "Coefficient of Variation": "变异系数",
"Skewness": "偏度", "Skewness": "偏度",
"Kurtosis": "峰度", "Kurtosis": "峰度",
"Update Categories for Expense Transactions": "更新支出交易的分类",
"Update Categories for Income Transactions": "更新收入交易的分类",
"Update Categories for Transfer Transactions": "更新转账交易的分类",
"Unable to update categories for transactions": "无法更新交易的分类",
"Account List": "账户列表", "Account List": "账户列表",
"This Week": "本周", "This Week": "本周",
"This Month": "本月", "This Month": "本月",
+6
View File
@@ -1757,6 +1757,8 @@
"Save As New Explorer": "另存新探索", "Save As New Explorer": "另存新探索",
"Restore to Last Saved": "還原到上次儲存", "Restore to Last Saved": "還原到上次儲存",
"Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "您確定要還原到上次儲存的狀態嗎?所有未儲存的更改將會遺失。", "Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "您確定要還原到上次儲存的狀態嗎?所有未儲存的更改將會遺失。",
"Enter Edit Mode": "進入編輯模式",
"Exit Edit Mode": "退出編輯模式",
"Set Explorer Name": "設定探索名稱", "Set Explorer Name": "設定探索名稱",
"Rename Explorer": "重新命名探索", "Rename Explorer": "重新命名探索",
"Hide Explorer": "隱藏探索", "Hide Explorer": "隱藏探索",
@@ -1843,6 +1845,10 @@
"Coefficient of Variation": "變異係數", "Coefficient of Variation": "變異係數",
"Skewness": "偏度", "Skewness": "偏度",
"Kurtosis": "峰度", "Kurtosis": "峰度",
"Update Categories for Expense Transactions": "更新支出交易的分類",
"Update Categories for Income Transactions": "更新收入交易的分類",
"Update Categories for Transfer Transactions": "更新轉帳交易的分類",
"Unable to update categories for transactions": "無法更新交易的分類",
"Account List": "帳戶清單", "Account List": "帳戶清單",
"This Week": "本週", "This Week": "本週",
"This Month": "本月", "This Month": "本月",
+5
View File
@@ -558,6 +558,11 @@ export interface TransactionModifyRequest {
readonly geoLocation?: TransactionGeoLocationRequest; readonly geoLocation?: TransactionGeoLocationRequest;
} }
export interface TransactionBatchUpdateCategoryRequest {
readonly transactionIds: string[];
readonly categoryId: string;
}
export interface TransactionMoveBetweenAccountsRequest { export interface TransactionMoveBetweenAccountsRequest {
readonly fromAccountId: string; readonly fromAccountId: string;
readonly toAccountId: string; readonly toAccountId: string;
+46
View File
@@ -1117,6 +1117,51 @@ export const useTransactionsStore = defineStore('transactions', () => {
}); });
} }
function batchUpdateTransactionCategories({ transactionIds, categoryId }: { transactionIds: string[], categoryId: string }): Promise<boolean> {
return new Promise((resolve, reject) => {
services.batchUpdateTransactionCategories({ transactionIds, categoryId }).then(response => {
const data = response.data;
if (!data || !data.success || !data.result) {
reject({ message: 'Unable to update categories for transactions' });
return;
}
if (!transactionListStateInvalid.value) {
updateTransactionListInvalidState(true);
}
if (!transactionReconciliationStatementStateInvalid.value) {
updateTransactionReconciliationStatementInvalidState(true);
}
if (!overviewStore.transactionOverviewStateInvalid) {
overviewStore.updateTransactionOverviewInvalidState(true);
}
if (!statisticsStore.transactionStatisticsStateInvalid) {
statisticsStore.updateTransactionStatisticsInvalidState(true);
}
if (!explorersStore.transactionExplorerStateInvalid) {
explorersStore.updateTransactionExplorerInvalidState(true);
}
resolve(data.result);
}).catch(error => {
logger.error('failed to update categories for transactions', 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 categories for transactions' });
} else {
reject(error);
}
});
});
}
function moveAllTransactionsBetweenAccounts({ fromAccountId, toAccountId }: { fromAccountId: string, toAccountId: string }): Promise<boolean> { function moveAllTransactionsBetweenAccounts({ fromAccountId, toAccountId }: { fromAccountId: string, toAccountId: string }): Promise<boolean> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
services.moveAllTransactionsBetweenAccounts({ fromAccountId, toAccountId }).then(response => { services.moveAllTransactionsBetweenAccounts({ fromAccountId, toAccountId }).then(response => {
@@ -1472,6 +1517,7 @@ export const useTransactionsStore = defineStore('transactions', () => {
getReconciliationStatements, getReconciliationStatements,
getTransaction, getTransaction,
saveTransaction, saveTransaction,
batchUpdateTransactionCategories,
moveAllTransactionsBetweenAccounts, moveAllTransactionsBetweenAccounts,
deleteTransaction, deleteTransaction,
recognizeReceiptImage, recognizeReceiptImage,
@@ -0,0 +1,208 @@
import { ref, computed } from 'vue';
import { useI18n } from '@/locales/helpers.ts';
import { useSettingsStore } from '@/stores/setting.ts';
import { useUserStore } from '@/stores/user.ts';
import { useExplorersStore } from '@/stores/explorer.ts';
import { type NameValue, type NameNumeralValue, itemAndIndex } from '@/core/base.ts';
import type { NumeralSystem } from '@/core/numeral.ts';
import { TransactionType } from '@/core/transaction.ts';
import type { TransactionInsightDataItem } from '@/models/transaction.ts';
import type { InsightsExplorer} from '@/models/explorer.ts';
import {
getUtcOffsetByUtcOffsetMinutes,
getTimezoneOffsetMinutes,
parseDateTimeFromUnixTimeWithTimezoneOffset
} from '@/lib/datetime.ts';
export function useExplorerDataTablePageBase() {
const {
tt,
getCurrentNumeralSystemType,
formatDateTimeToLongDateTime,
formatAmountToLocalizedNumeralsWithCurrency
} = useI18n();
const settingsStore = useSettingsStore();
const userStore = useUserStore();
const explorersStore = useExplorersStore();
const currentPage = ref<number>(1);
const numeralSystem = computed<NumeralSystem>(() => getCurrentNumeralSystemType());
const defaultCurrency = computed<string>(() => userStore.currentUserDefaultCurrency);
const currentExplorer = computed<InsightsExplorer>(() => explorersStore.currentInsightsExplorer);
const filteredTransactions = computed<TransactionInsightDataItem[]>(() => explorersStore.filteredTransactionsInDataTable);
const allDataTableQuerySources = computed<NameValue[]>(() => {
const sources: NameValue[] = [];
sources.push({
name: tt('All Queries'),
value: ''
});
for (const [query, index] of itemAndIndex(currentExplorer.value.queries)) {
if (query.name) {
sources.push({
name: query.name,
value: query.id
});
} else {
sources.push({
name: tt('format.misc.queryIndex', { index: index + 1 }),
value: query.id
});
}
}
return sources;
});
const allPageCounts = computed<NameNumeralValue[]>(() => {
const pageCounts: NameNumeralValue[] = [];
const availableCountPerPage: number[] = [ 5, 10, 15, 20, 25, 30, 50 ];
for (const count of availableCountPerPage) {
pageCounts.push({ value: count, name: numeralSystem.value.formatNumber(count) });
}
pageCounts.push({ value: -1, name: tt('All') });
return pageCounts;
});
const skeletonData = computed<number[]>(() => {
const data: number[] = [];
for (let i = 0; i < currentExplorer.value.countPerPage; i++) {
data.push(i);
}
return data;
});
const totalPageCount = computed<number>(() => {
if (!filteredTransactions.value || filteredTransactions.value.length < 1) {
return 1;
}
const count = filteredTransactions.value.length;
return Math.ceil(count / currentExplorer.value.countPerPage);
});
const dataTableHeaders = computed<object[]>(() => {
const headers: object[] = [];
headers.push({ key: 'time', value: 'time', title: tt('Transaction Time'), sortable: true, nowrap: true });
headers.push({ key: 'type', value: 'type', title: tt('Type'), sortable: true, nowrap: true });
headers.push({ key: 'secondaryCategoryName', value: 'secondaryCategoryName', title: tt('Category'), sortable: true, nowrap: true });
headers.push({ key: 'sourceAmount', value: 'sourceAmount', title: tt('Amount'), sortable: true, nowrap: true });
headers.push({ key: 'sourceAccountName', value: 'sourceAccountName', title: tt('Account'), sortable: true, nowrap: true });
if (settingsStore.appSettings.showTagInInsightsExplorerPage) {
headers.push({ key: 'tags', value: 'tags', title: tt('Tags'), sortable: true, nowrap: true });
}
headers.push({ key: 'comment', value: 'comment', title: tt('Description'), sortable: true, nowrap: true });
headers.push({ key: 'operation', title: tt('Operation'), sortable: false, nowrap: true, align: 'center' });
return headers;
});
function getDisplayDateTime(transaction: TransactionInsightDataItem): string {
const dateTime = parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.time, transaction.utcOffset);
return formatDateTimeToLongDateTime(dateTime);
}
function isSameAsDefaultTimezoneOffsetMinutes(transaction: TransactionInsightDataItem): boolean {
return transaction.utcOffset === getTimezoneOffsetMinutes(transaction.time);
}
function getDisplayTimezone(transaction: TransactionInsightDataItem): string {
return `UTC${getUtcOffsetByUtcOffsetMinutes(transaction.utcOffset)}`;
}
function getDisplayTimeInDefaultTimezone(transaction: TransactionInsightDataItem): string {
const timezoneOffsetMinutes = getTimezoneOffsetMinutes(transaction.time);
const dateTime = parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.time, timezoneOffsetMinutes);
const utcOffset = numeralSystem.value.replaceWesternArabicDigitsToLocalizedDigits(getUtcOffsetByUtcOffsetMinutes(timezoneOffsetMinutes));
return `${formatDateTimeToLongDateTime(dateTime)} (UTC${utcOffset})`;
}
function getDisplayTransactionType(transaction: TransactionInsightDataItem): string {
if (transaction.type === TransactionType.ModifyBalance) {
return tt('Modify Balance');
} else if (transaction.type === TransactionType.Income) {
return tt('Income');
} else if (transaction.type === TransactionType.Expense) {
return tt('Expense');
} else if (transaction.type === TransactionType.Transfer) {
return tt('Transfer');
} else {
return tt('Unknown');
}
}
function getTransactionTypeColor(transaction: TransactionInsightDataItem): string | undefined {
if (transaction.type === TransactionType.ModifyBalance) {
return 'secondary';
} else if (transaction.type === TransactionType.Income) {
return undefined;
} else if (transaction.type === TransactionType.Expense) {
return undefined;
} else if (transaction.type === TransactionType.Transfer) {
return 'primary';
} else {
return 'default';
}
}
function getDisplaySourceAmount(transaction: TransactionInsightDataItem): string {
let currency = defaultCurrency.value;
if (transaction.sourceAccount) {
currency = transaction.sourceAccount.currency;
}
return formatAmountToLocalizedNumeralsWithCurrency(transaction.sourceAmount, currency);
}
function getDisplayDestinationAmount(transaction: TransactionInsightDataItem): string {
let currency = defaultCurrency.value;
if (transaction.destinationAccount) {
currency = transaction.destinationAccount.currency;
}
return formatAmountToLocalizedNumeralsWithCurrency(transaction.destinationAmount, currency);
}
return {
// states
currentPage,
// computed states
currentExplorer,
filteredTransactions,
allDataTableQuerySources,
allPageCounts,
skeletonData,
totalPageCount,
dataTableHeaders,
// functions
getDisplayDateTime,
isSameAsDefaultTimezoneOffsetMinutes,
getDisplayTimezone,
getDisplayTimeInDefaultTimezone,
getDisplayTransactionType,
getTransactionTypeColor,
getDisplaySourceAmount,
getDisplayDestinationAmount
};
}
+43 -17
View File
@@ -5,7 +5,7 @@
<v-layout> <v-layout>
<v-navigation-drawer :permanent="alwaysShowNav" v-model="showNav"> <v-navigation-drawer :permanent="alwaysShowNav" v-model="showNav">
<div class="mx-6 my-4"> <div class="mx-6 my-4">
<btn-vertical-group :disabled="loading || updating" :buttons="allTabs" v-model="activeTab" /> <btn-vertical-group :disabled="loading || updating || isCurrentDataTableEditable" :buttons="allTabs" v-model="activeTab" />
</div> </div>
<v-divider /> <v-divider />
<v-tabs show-arrows <v-tabs show-arrows
@@ -13,13 +13,13 @@
style="max-height: calc(100% - 150px)" style="max-height: calc(100% - 150px)"
direction="vertical" direction="vertical"
:prev-icon="mdiMenuUp" :next-icon="mdiMenuDown" :prev-icon="mdiMenuUp" :next-icon="mdiMenuDown"
:key="currentExplorer.id" :disabled="loading || updating" :key="currentExplorer.id" :disabled="loading || updating || isCurrentDataTableEditable"
:model-value="currentExplorer.id"> :model-value="currentExplorer.id">
<v-tab class="tab-text-truncate" key="new" value="" @click="createNewExplorer"> <v-tab class="tab-text-truncate" key="new" value="" @click="createNewExplorer">
<span class="text-truncate">{{ tt('New Explorer') }}</span> <span class="text-truncate">{{ tt('New Explorer') }}</span>
</v-tab> </v-tab>
<v-tab class="tab-text-truncate" :key="explorer.id" :value="explorer.id" <v-tab class="tab-text-truncate" :key="explorer.id" :value="explorer.id"
:disabled="loading || updating" :disabled="loading || updating || isCurrentDataTableEditable"
v-for="explorer in allVisibleExplorers" v-for="explorer in allVisibleExplorers"
@click="loadExplorer(explorer.id)"> @click="loadExplorer(explorer.id)">
<span class="text-truncate">{{ explorer.name || tt('Untitled Explorer') }}</span> <span class="text-truncate">{{ explorer.name || tt('Untitled Explorer') }}</span>
@@ -41,11 +41,11 @@
<span>{{ tt('Insights Explorer') }}</span> <span>{{ tt('Insights Explorer') }}</span>
<v-btn-group class="ms-4" color="default" density="comfortable" variant="outlined" divided> <v-btn-group class="ms-4" color="default" density="comfortable" variant="outlined" divided>
<v-btn class="button-icon-with-direction" :icon="mdiArrowLeft" <v-btn class="button-icon-with-direction" :icon="mdiArrowLeft"
:disabled="loading || updating || !canShiftDateRange" :disabled="loading || updating || !canShiftDateRange || isCurrentDataTableEditable"
@click="shiftDateRange(-1)"/> @click="shiftDateRange(-1)"/>
<v-menu location="bottom" max-height="500"> <v-menu location="bottom" max-height="500">
<template #activator="{ props }"> <template #activator="{ props }">
<v-btn :disabled="loading || updating" <v-btn :disabled="loading || updating || isCurrentDataTableEditable"
v-bind="props">{{ displayQueryDateRangeName }}</v-btn> v-bind="props">{{ displayQueryDateRangeName }}</v-btn>
</template> </template>
<v-list :selected="[currentFilter.dateRangeType]"> <v-list :selected="[currentFilter.dateRangeType]">
@@ -68,7 +68,7 @@
</v-list> </v-list>
</v-menu> </v-menu>
<v-btn class="button-icon-with-direction" :icon="mdiArrowRight" <v-btn class="button-icon-with-direction" :icon="mdiArrowRight"
:disabled="loading || updating || !canShiftDateRange" :disabled="loading || updating || !canShiftDateRange || isCurrentDataTableEditable"
@click="shiftDateRange(1)"/> @click="shiftDateRange(1)"/>
</v-btn-group> </v-btn-group>
@@ -84,7 +84,7 @@
<v-btn class="ms-3" <v-btn class="ms-3"
:color="isCurrentExplorerModified ? 'primary' : 'default'" :color="isCurrentExplorerModified ? 'primary' : 'default'"
:variant="isCurrentExplorerModified ? 'elevated' : 'outlined'" :variant="isCurrentExplorerModified ? 'elevated' : 'outlined'"
:disabled="loading || updating" @click="saveExplorer(false)"> :disabled="loading || updating || isCurrentDataTableEditable" @click="saveExplorer(false)">
{{ tt('Save Explorer') }} {{ tt('Save Explorer') }}
<v-progress-circular indeterminate size="22" class="ms-2" v-if="updating"></v-progress-circular> <v-progress-circular indeterminate size="22" class="ms-2" v-if="updating"></v-progress-circular>
<v-menu activator="parent" :open-on-hover="true"> <v-menu activator="parent" :open-on-hover="true">
@@ -113,29 +113,41 @@
v-for="timezoneType in allTimezoneTypesUsedForDateRange" v-for="timezoneType in allTimezoneTypesUsedForDateRange"
@click="currentExplorer.timezoneUsedForDateRange = timezoneType.type"></v-list-item> @click="currentExplorer.timezoneUsedForDateRange = timezoneType.type"></v-list-item>
</template> </template>
<v-list-item :prepend-icon="mdiTableEdit"
:title="tt('Enter Edit Mode')"
:disabled="loading || updating || filteredTransactionsInDataTable.length < 1"
@click="isCurrentDataTableEditable = true"
v-if="activeTab === 'table' && !isCurrentDataTableEditable"></v-list-item>
<v-list-item :prepend-icon="mdiTableCheck"
:title="tt('Exit Edit Mode')"
:disabled="loading || updating"
@click="isCurrentDataTableEditable = false"
v-if="activeTab === 'table' && isCurrentDataTableEditable"></v-list-item>
<v-divider class="my-2" v-if="activeTab === 'table' && !isCurrentDataTableEditable"/>
<v-list-item :prepend-icon="mdiExport" <v-list-item :prepend-icon="mdiExport"
:title="tt('Export Results')" :title="tt('Export Results')"
:disabled="loading || updating || (activeTab === 'table' && (!filteredTransactionsInDataTable || filteredTransactionsInDataTable.length < 1))" :disabled="loading || updating || (activeTab === 'table' && (!filteredTransactionsInDataTable || filteredTransactionsInDataTable.length < 1))"
@click="exportResults" @click="exportResults"
v-if="activeTab === 'table' || activeTab === 'chart'"></v-list-item> v-if="(activeTab === 'table' || activeTab === 'chart') && !isCurrentDataTableEditable"></v-list-item>
<v-divider class="my-2" v-if="currentExplorer.id" /> <v-divider class="my-2" v-if="currentExplorer.id && !isCurrentDataTableEditable" />
<v-list-item :prepend-icon="mdiPencilOutline" @click="setExplorerName" v-if="currentExplorer.id"> <v-list-item :prepend-icon="mdiPencilOutline" @click="setExplorerName" v-if="currentExplorer.id && !isCurrentDataTableEditable">
<v-list-item-title>{{ tt('Rename Explorer') }}</v-list-item-title> <v-list-item-title>{{ tt('Rename Explorer') }}</v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item :prepend-icon="mdiEyeOffOutline" @click="hideExplorer(true)" v-if="currentExplorer.id && !currentExplorer.hidden"> <v-list-item :prepend-icon="mdiEyeOffOutline" @click="hideExplorer(true)" v-if="currentExplorer.id && !currentExplorer.hidden && !isCurrentDataTableEditable">
<v-list-item-title>{{ tt('Hide Explorer') }}</v-list-item-title> <v-list-item-title>{{ tt('Hide Explorer') }}</v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item :prepend-icon="mdiEyeOutline" @click="hideExplorer(false)" v-if="currentExplorer.id && currentExplorer.hidden"> <v-list-item :prepend-icon="mdiEyeOutline" @click="hideExplorer(false)" v-if="currentExplorer.id && currentExplorer.hidden && !isCurrentDataTableEditable">
<v-list-item-title>{{ tt('Unhide Explorer') }}</v-list-item-title> <v-list-item-title>{{ tt('Unhide Explorer') }}</v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item :prepend-icon="mdiDeleteOutline" @click="removeExplorer" v-if="currentExplorer.id"> <v-list-item :prepend-icon="mdiDeleteOutline" @click="removeExplorer" v-if="currentExplorer.id && !isCurrentDataTableEditable">
<v-list-item-title>{{ tt('Delete Explorer') }}</v-list-item-title> <v-list-item-title>{{ tt('Delete Explorer') }}</v-list-item-title>
</v-list-item> </v-list-item>
<v-divider class="my-2"/> <v-divider class="my-2" v-if="!isCurrentDataTableEditable"/>
<v-list-item :prepend-icon="mdiSort" <v-list-item :prepend-icon="mdiSort"
:disabled="!allExplorers || allExplorers.length < 2" :disabled="!allExplorers || allExplorers.length < 2"
:title="tt('Change Explorer Display Order')" :title="tt('Change Explorer Display Order')"
@click="showChangeExplorerDisplayOrderDialog"></v-list-item> @click="showChangeExplorerDisplayOrderDialog"
v-if="!isCurrentDataTableEditable"></v-list-item>
</v-list> </v-list>
</v-menu> </v-menu>
</v-btn> </v-btn>
@@ -149,7 +161,13 @@
<v-window-item value="table"> <v-window-item value="table">
<explorer-data-table-tab ref="explorerDataTableTab" <explorer-data-table-tab ref="explorerDataTableTab"
:loading="loading" :disabled="loading || updating" :loading="loading" :disabled="loading || updating"
@click:transaction="onShowTransaction" /> @click:transaction="onShowTransaction"
v-if="!isCurrentDataTableEditable" />
<explorer-editable-data-table-tab ref="explorerEditableDataTableTab"
:loading="loading" :disabled="loading || updating"
@click:transaction="onShowTransaction"
@update:transactions="onUpdateTransactions"
v-if="isCurrentDataTableEditable" />
</v-window-item> </v-window-item>
<v-window-item value="chart"> <v-window-item value="chart">
<explorer-chart-tab ref="explorerChartTab" <explorer-chart-tab ref="explorerChartTab"
@@ -187,6 +205,7 @@ import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue';
import SnackBar from '@/components/desktop/SnackBar.vue'; import SnackBar from '@/components/desktop/SnackBar.vue';
import ExplorerQueryTab from '@/views/desktop/insights/tabs/ExplorerQueryTab.vue'; import ExplorerQueryTab from '@/views/desktop/insights/tabs/ExplorerQueryTab.vue';
import ExplorerDataTableTab from '@/views/desktop/insights/tabs/ExplorerDataTableTab.vue'; import ExplorerDataTableTab from '@/views/desktop/insights/tabs/ExplorerDataTableTab.vue';
import ExplorerEditableDataTableTab from '@/views/desktop/insights/tabs/ExplorerEditableDataTableTab.vue';
import ExplorerChartTab from '@/views/desktop/insights/tabs/ExplorerChartTab.vue'; import ExplorerChartTab from '@/views/desktop/insights/tabs/ExplorerChartTab.vue';
import ExplorerChangeDisplayOrderDialog from '@/views/desktop/insights/dialogs/ExplorerChangeDisplayOrderDialog.vue'; import ExplorerChangeDisplayOrderDialog from '@/views/desktop/insights/dialogs/ExplorerChangeDisplayOrderDialog.vue';
import EditDialog from '@/views/desktop/transactions/list/dialogs/EditDialog.vue'; import EditDialog from '@/views/desktop/transactions/list/dialogs/EditDialog.vue';
@@ -238,7 +257,9 @@ import {
mdiSort, mdiSort,
mdiHomeClockOutline, mdiHomeClockOutline,
mdiInvoiceTextClockOutline, mdiInvoiceTextClockOutline,
mdiExport mdiExport,
mdiTableEdit,
mdiTableCheck
} from '@mdi/js'; } from '@mdi/js';
interface InsightsExplorerProps { interface InsightsExplorerProps {
@@ -298,6 +319,7 @@ const initing = ref<boolean>(true);
const updating = ref<boolean>(false); const updating = ref<boolean>(false);
const clientSessionId = ref<string>(''); const clientSessionId = ref<string>('');
const isCurrentExplorerModified = ref<boolean>(false); const isCurrentExplorerModified = ref<boolean>(false);
const isCurrentDataTableEditable = ref<boolean>(false);
const alwaysShowNav = ref<boolean>(display.mdAndUp.value); const alwaysShowNav = ref<boolean>(display.mdAndUp.value);
const showNav = ref<boolean>(display.mdAndUp.value); const showNav = ref<boolean>(display.mdAndUp.value);
const activeTab = ref<ExplorerPageTabType>('query'); const activeTab = ref<ExplorerPageTabType>('query');
@@ -726,6 +748,10 @@ function onShowTransaction(transaction: TransactionInsightDataItem): void {
}); });
} }
function onUpdateTransactions(): void {
reload(false);
}
function onShowDateRangeError(message: string): void { function onShowDateRangeError(message: string): void {
snackbar.value?.showError(message); snackbar.value?.showError(message);
} }
@@ -0,0 +1,193 @@
<template>
<v-dialog width="600" :persistent="true" v-model="showState">
<v-card class="pa-sm-1 pa-md-2">
<template #title>
<div class="d-flex flex-wrap align-center">
<h4 class="text-h4 text-wrap" v-if="type === CategoryType.Expense">{{ tt('Update Categories for Expense Transactions') }}</h4>
<h4 class="text-h4 text-wrap" v-if="type === CategoryType.Income">{{ tt('Update Categories for Income Transactions') }}</h4>
<h4 class="text-h4 text-wrap" v-if="type === CategoryType.Transfer">{{ tt('Update Categories for Transfer Transactions') }}</h4>
<v-btn class="ms-2" density="compact" color="default" variant="text" size="24"
:icon="true" :disabled="loading || submitting" :loading="loading"
@click="reload">
<template #loader>
<v-progress-circular indeterminate size="20"/>
</template>
<v-icon :icon="mdiRefresh" size="24" />
<v-tooltip activator="parent">{{ tt('Refresh') }}</v-tooltip>
</v-btn>
</div>
</template>
<v-card-text class="w-100 d-flex justify-center">
<v-row>
<v-col cols="12">
<two-column-select primary-key-field="id" primary-value-field="id" primary-title-field="name"
primary-icon-field="icon" primary-icon-type="category" primary-color-field="color"
primary-hidden-field="hidden" primary-sub-items-field="subCategories"
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:disabled="loading || submitting || !hasVisibleExpenseCategories"
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
:show-selection-primary-text="true"
:custom-selection-primary-text="getTransactionPrimaryCategoryName(categoryId, allCategories[CategoryType.Expense])"
:custom-selection-secondary-text="getTransactionSecondaryCategoryName(categoryId, allCategories[CategoryType.Expense])"
:label="tt('Target Category')"
:placeholder="tt('Target Category')"
:items="allCategories[CategoryType.Expense]"
v-model="categoryId"
v-if="type === CategoryType.Expense">
</two-column-select>
<two-column-select primary-key-field="id" primary-value-field="id" primary-title-field="name"
primary-icon-field="icon" primary-icon-type="category" primary-color-field="color"
primary-hidden-field="hidden" primary-sub-items-field="subCategories"
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:disabled="loading || submitting || !hasVisibleIncomeCategories"
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
:show-selection-primary-text="true"
:custom-selection-primary-text="getTransactionPrimaryCategoryName(categoryId, allCategories[CategoryType.Income])"
:custom-selection-secondary-text="getTransactionSecondaryCategoryName(categoryId, allCategories[CategoryType.Income])"
:label="tt('Target Category')"
:placeholder="tt('Target Category')"
:items="allCategories[CategoryType.Income]"
v-model="categoryId"
v-if="type === CategoryType.Income">
</two-column-select>
<two-column-select primary-key-field="id" primary-value-field="id" primary-title-field="name"
primary-icon-field="icon" primary-icon-type="category" primary-color-field="color"
primary-hidden-field="hidden" primary-sub-items-field="subCategories"
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:disabled="loading || submitting || !hasVisibleTransferCategories"
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
:show-selection-primary-text="true"
:custom-selection-primary-text="getTransactionPrimaryCategoryName(categoryId, allCategories[CategoryType.Transfer])"
:custom-selection-secondary-text="getTransactionSecondaryCategoryName(categoryId, allCategories[CategoryType.Transfer])"
:label="tt('Target Category')"
:placeholder="tt('Target Category')"
:items="allCategories[CategoryType.Transfer]"
v-model="categoryId"
v-if="type === CategoryType.Transfer">
</two-column-select>
</v-col>
</v-row>
</v-card-text>
<v-card-text>
<div class="w-100 d-flex justify-center flex-wrap mt-sm-1 mt-md-2 gap-4">
<v-btn :disabled="loading || submitting || updateIds.length < 1 || !categoryId" @click="confirm">
{{ tt('OK') }}
<v-progress-circular indeterminate size="22" class="ms-2" v-if="submitting"></v-progress-circular>
</v-btn>
<v-btn color="secondary" variant="tonal" :disabled="loading || 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, computed, useTemplateRef } from 'vue';
import { useI18n } from '@/locales/helpers.ts';
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
import { useTransactionsStore } from '@/stores/transaction.ts';
import { CategoryType } from '@/core/category.ts';
import type { TransactionCategory } from '@/models/transaction_category.ts';
import {
getTransactionPrimaryCategoryName,
getTransactionSecondaryCategoryName
} from '@/lib/category.ts';
import {
mdiRefresh
} from '@mdi/js';
type SnackBarType = InstanceType<typeof SnackBar>;
const {
tt
} = useI18n();
const transactionCategoriesStore = useTransactionCategoriesStore();
const transactionsStore = useTransactionsStore();
const snackbar = useTemplateRef<SnackBarType>('snackbar');
const showState = ref<boolean>(false);
const loading = ref<boolean>(false);
const submitting = ref<boolean>(false);
const type = ref<CategoryType>(CategoryType.Expense);
const updateIds = ref<string[]>([]);
const categoryId = ref<string>('');
let resolveFunc: ((response: number) => void) | null = null;
let rejectFunc: ((reason?: unknown) => void) | null = null;
const allCategories = computed<Record<number, TransactionCategory[]>>(() => transactionCategoriesStore.allTransactionCategories);
const hasVisibleExpenseCategories = computed<boolean>(() => transactionCategoriesStore.hasVisibleExpenseCategories);
const hasVisibleIncomeCategories = computed<boolean>(() => transactionCategoriesStore.hasVisibleIncomeCategories);
const hasVisibleTransferCategories = computed<boolean>(() => transactionCategoriesStore.hasVisibleTransferCategories);
function open(options: { type: CategoryType; updateIds: string[] }): Promise<number> {
type.value = options.type;
updateIds.value = options.updateIds;
categoryId.value = '';
showState.value = true;
return new Promise((resolve, reject) => {
resolveFunc = resolve;
rejectFunc = reject;
});
}
function reload(): void {
transactionCategoriesStore.loadAllCategories({ force: true }).then(() => {
loading.value = false;
}).catch(error => {
loading.value = false;
if (!error.processed) {
snackbar.value?.showError(error);
}
});
}
function confirm(): void {
submitting.value = true;
transactionsStore.batchUpdateTransactionCategories({
transactionIds: updateIds.value,
categoryId: categoryId.value
}).then(() => {
submitting.value = false;
showState.value = false;
resolveFunc?.(updateIds.value.length);
}).catch(error => {
submitting.value = false;
if (!error.processed) {
snackbar.value?.showError(error);
}
});
}
function cancel(): void {
rejectFunc?.();
showState.value = false;
}
defineExpose({
open
});
</script>
@@ -207,26 +207,20 @@
<script setup lang="ts"> <script setup lang="ts">
import PaginationButtons from '@/components/desktop/PaginationButtons.vue'; import PaginationButtons from '@/components/desktop/PaginationButtons.vue';
import { ref, computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from '@/locales/helpers.ts'; import { useI18n } from '@/locales/helpers.ts';
import { useExplorerDataTablePageBase } from '@/views/base/explorer/ExplorerDataTablePageBase.ts';
import { useSettingsStore } from '@/stores/setting.ts'; import { useSettingsStore } from '@/stores/setting.ts';
import { useUserStore } from '@/stores/user.ts';
import { type InsightsExplorerTransactionStatisticData, useExplorersStore } from '@/stores/explorer.ts'; import { type InsightsExplorerTransactionStatisticData, useExplorersStore } from '@/stores/explorer.ts';
import { type NameValue, type NameNumeralValue, itemAndIndex } from '@/core/base.ts';
import type { NumeralSystem } from '@/core/numeral.ts';
import { TransactionType } from '@/core/transaction.ts'; import { TransactionType } from '@/core/transaction.ts';
import type { TransactionInsightDataItem } from '@/models/transaction.ts'; import type { TransactionInsightDataItem } from '@/models/transaction.ts';
import type { InsightsExplorer} from '@/models/explorer.ts';
import { isDefined, replaceAll } from '@/lib/common.ts'; import { isDefined, replaceAll } from '@/lib/common.ts';
import { import {
getUtcOffsetByUtcOffsetMinutes,
getTimezoneOffsetMinutes,
parseDateTimeFromUnixTimeWithTimezoneOffset parseDateTimeFromUnixTimeWithTimezoneOffset
} from '@/lib/datetime.ts'; } from '@/lib/datetime.ts';
@@ -249,8 +243,6 @@ const emit = defineEmits<{
const { const {
tt, tt,
getCurrentNumeralSystemType,
formatDateTimeToLongDateTime,
formatDateTimeToGregorianDefaultDateTime, formatDateTimeToGregorianDefaultDateTime,
formatAmountToWesternArabicNumeralsWithoutDigitGrouping, formatAmountToWesternArabicNumeralsWithoutDigitGrouping,
formatAmountToLocalizedNumeralsWithCurrency, formatAmountToLocalizedNumeralsWithCurrency,
@@ -258,163 +250,30 @@ const {
formatPercentToLocalizedNumerals formatPercentToLocalizedNumerals
} = useI18n(); } = useI18n();
const {
currentPage,
currentExplorer,
filteredTransactions,
allDataTableQuerySources,
allPageCounts,
skeletonData,
totalPageCount,
dataTableHeaders,
getDisplayDateTime,
isSameAsDefaultTimezoneOffsetMinutes,
getDisplayTimezone,
getDisplayTimeInDefaultTimezone,
getDisplayTransactionType,
getTransactionTypeColor,
getDisplaySourceAmount,
getDisplayDestinationAmount
} = useExplorerDataTablePageBase();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const userStore = useUserStore();
const explorersStore = useExplorersStore(); const explorersStore = useExplorersStore();
const currentPage = ref<number>(1);
const numeralSystem = computed<NumeralSystem>(() => getCurrentNumeralSystemType());
const defaultCurrency = computed<string>(() => userStore.currentUserDefaultCurrency);
const currentExplorer = computed<InsightsExplorer>(() => explorersStore.currentInsightsExplorer);
const filteredTransactions = computed<TransactionInsightDataItem[]>(() => explorersStore.filteredTransactionsInDataTable);
const filteredTransactionsStatistic = computed<InsightsExplorerTransactionStatisticData | undefined>(() => explorersStore.filteredTransactionsInDataTableStatistic); const filteredTransactionsStatistic = computed<InsightsExplorerTransactionStatisticData | undefined>(() => explorersStore.filteredTransactionsInDataTableStatistic);
const allDataTableQuerySources = computed<NameValue[]>(() => {
const sources: NameValue[] = [];
sources.push({
name: tt('All Queries'),
value: ''
});
for (const [query, index] of itemAndIndex(currentExplorer.value.queries)) {
if (query.name) {
sources.push({
name: query.name,
value: query.id
});
} else {
sources.push({
name: tt('format.misc.queryIndex', { index: index + 1 }),
value: query.id
});
}
}
return sources;
});
const allPageCounts = computed<NameNumeralValue[]>(() => {
const pageCounts: NameNumeralValue[] = [];
const availableCountPerPage: number[] = [ 5, 10, 15, 20, 25, 30, 50 ];
for (const count of availableCountPerPage) {
pageCounts.push({ value: count, name: numeralSystem.value.formatNumber(count) });
}
pageCounts.push({ value: -1, name: tt('All') });
return pageCounts;
});
const skeletonData = computed<number[]>(() => {
const data: number[] = [];
for (let i = 0; i < currentExplorer.value.countPerPage; i++) {
data.push(i);
}
return data;
});
const totalPageCount = computed<number>(() => {
if (!filteredTransactions.value || filteredTransactions.value.length < 1) {
return 1;
}
const count = filteredTransactions.value.length;
return Math.ceil(count / currentExplorer.value.countPerPage);
});
const dataTableHeaders = computed<object[]>(() => {
const headers: object[] = [];
headers.push({ key: 'time', value: 'time', title: tt('Transaction Time'), sortable: true, nowrap: true });
headers.push({ key: 'type', value: 'type', title: tt('Type'), sortable: true, nowrap: true });
headers.push({ key: 'secondaryCategoryName', value: 'secondaryCategoryName', title: tt('Category'), sortable: true, nowrap: true });
headers.push({ key: 'sourceAmount', value: 'sourceAmount', title: tt('Amount'), sortable: true, nowrap: true });
headers.push({ key: 'sourceAccountName', value: 'sourceAccountName', title: tt('Account'), sortable: true, nowrap: true });
if (settingsStore.appSettings.showTagInInsightsExplorerPage) {
headers.push({ key: 'tags', value: 'tags', title: tt('Tags'), sortable: true, nowrap: true });
}
headers.push({ key: 'comment', value: 'comment', title: tt('Description'), sortable: true, nowrap: true });
headers.push({ key: 'operation', title: tt('Operation'), sortable: false, nowrap: true, align: 'center' });
return headers;
});
function getDisplayDateTime(transaction: TransactionInsightDataItem): string {
const dateTime = parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.time, transaction.utcOffset);
return formatDateTimeToLongDateTime(dateTime);
}
function isSameAsDefaultTimezoneOffsetMinutes(transaction: TransactionInsightDataItem): boolean {
return transaction.utcOffset === getTimezoneOffsetMinutes(transaction.time);
}
function getDisplayTimezone(transaction: TransactionInsightDataItem): string {
return `UTC${getUtcOffsetByUtcOffsetMinutes(transaction.utcOffset)}`;
}
function getDisplayTimeInDefaultTimezone(transaction: TransactionInsightDataItem): string {
const timezoneOffsetMinutes = getTimezoneOffsetMinutes(transaction.time);
const dateTime = parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.time, timezoneOffsetMinutes);
const utcOffset = numeralSystem.value.replaceWesternArabicDigitsToLocalizedDigits(getUtcOffsetByUtcOffsetMinutes(timezoneOffsetMinutes));
return `${formatDateTimeToLongDateTime(dateTime)} (UTC${utcOffset})`;
}
function getDisplayTransactionType(transaction: TransactionInsightDataItem): string {
if (transaction.type === TransactionType.ModifyBalance) {
return tt('Modify Balance');
} else if (transaction.type === TransactionType.Income) {
return tt('Income');
} else if (transaction.type === TransactionType.Expense) {
return tt('Expense');
} else if (transaction.type === TransactionType.Transfer) {
return tt('Transfer');
} else {
return tt('Unknown');
}
}
function getTransactionTypeColor(transaction: TransactionInsightDataItem): string | undefined {
if (transaction.type === TransactionType.ModifyBalance) {
return 'secondary';
} else if (transaction.type === TransactionType.Income) {
return undefined;
} else if (transaction.type === TransactionType.Expense) {
return undefined;
} else if (transaction.type === TransactionType.Transfer) {
return 'primary';
} else {
return 'default';
}
}
function getDisplaySourceAmount(transaction: TransactionInsightDataItem): string {
let currency = defaultCurrency.value;
if (transaction.sourceAccount) {
currency = transaction.sourceAccount.currency;
}
return formatAmountToLocalizedNumeralsWithCurrency(transaction.sourceAmount, currency);
}
function getDisplayDestinationAmount(transaction: TransactionInsightDataItem): string {
let currency = defaultCurrency.value;
if (transaction.destinationAccount) {
currency = transaction.destinationAccount.currency;
}
return formatAmountToLocalizedNumeralsWithCurrency(transaction.destinationAmount, currency);
}
function showTransaction(transaction: TransactionInsightDataItem): void { function showTransaction(transaction: TransactionInsightDataItem): void {
emit('click:transaction', transaction); emit('click:transaction', transaction);
} }
@@ -0,0 +1,359 @@
<template>
<v-card-text class="px-5 py-0 mb-4">
<v-row>
<v-col cols="12">
<div class="d-flex overflow-x-auto align-center gap-2 pt-2">
<v-select
class="flex-0-0"
min-width="150"
item-title="name"
item-value="value"
density="compact"
:disabled="true"
:label="tt('Data Source')"
:items="allDataTableQuerySources"
:model-value="currentExplorer.datatableQuerySource"
/>
<v-select
class="flex-0-0"
min-width="150"
item-title="name"
item-value="value"
density="compact"
:disabled="loading || disabled"
:label="tt('Transactions Per Page')"
:items="allPageCounts"
v-model="currentExplorer.countPerPage"
/>
<v-spacer/>
<div class="d-flex align-center">
<span class="text-subtitle-1">
{{ tt('format.misc.selectedCount', { count: formatNumberToLocalizedNumerals(selectedTransactionCount), totalCount: formatNumberToLocalizedNumerals(filteredTransactions.length) }) }}
</span>
</div>
</div>
</v-col>
</v-row>
</v-card-text>
<v-data-table
fixed-header
fixed-footer
multi-sort
item-value="index"
:class="{ 'insights-editable-explorer-table': true, 'text-sm': true, 'disabled': loading || disabled, 'loading-skeleton': loading }"
:headers="editableDataTableHeaders"
:items="filteredTransactions"
:hover="true"
v-model:items-per-page="currentExplorer.countPerPage"
v-model:page="currentPage"
>
<template #header.data-table-select>
<v-checkbox readonly class="always-cursor-pointer"
density="compact" width="28"
:disabled="!!disabled"
:indeterminate="anyButNotAllTransactionSelected"
v-model="allTransactionSelected"
>
<v-menu activator="parent" location="bottom">
<v-list>
<v-list-item :prepend-icon="mdiSelectAll"
:title="tt('Select All')"
:disabled="loading || disabled"
@click="selectAll"></v-list-item>
<v-list-item :prepend-icon="mdiSelect"
:title="tt('Select None')"
:disabled="loading || disabled"
@click="selectNone"></v-list-item>
<v-list-item :prepend-icon="mdiSelectInverse"
:title="tt('Invert Selection')"
:disabled="loading || disabled"
@click="selectInvert"></v-list-item>
</v-list>
</v-menu>
</v-checkbox>
</template>
<template #header.operation>
<div>
<span>{{ tt('Operation') }}</span>
<v-icon :icon="mdiMenuDown" size="20" />
<v-menu activator="parent" location="bottom">
<v-list>
<v-list-item :prepend-icon="mdiTextBoxEditOutline"
:title="tt('Update Categories for Expense Transactions')"
:disabled="!isAllSelectedTransactionsExpense"
@click="batchUpdateTransactionCategories(CategoryType.Expense)"></v-list-item>
<v-list-item :prepend-icon="mdiTextBoxEditOutline"
:title="tt('Update Categories for Income Transactions')"
:disabled="!isAllSelectedTransactionsIncome"
@click="batchUpdateTransactionCategories(CategoryType.Income)"></v-list-item>
<v-list-item :prepend-icon="mdiTextBoxEditOutline"
:title="tt('Update Categories for Transfer Transactions')"
:disabled="!isAllSelectedTransactionsTransfer"
@click="batchUpdateTransactionCategories(CategoryType.Transfer)"></v-list-item>
</v-list>
</v-menu>
</div>
</template>
<template #item.data-table-select="{ item }">
<v-checkbox density="compact" :disabled="loading || disabled"
v-model="selectedTransactions[item.id]"></v-checkbox>
</template>
<template #item.time="{ item }">
<span>{{ getDisplayDateTime(item) }}</span>
<v-chip class="ms-1" variant="flat" color="grey" size="x-small"
v-if="!isSameAsDefaultTimezoneOffsetMinutes(item)">{{ getDisplayTimezone(item) }}</v-chip>
<v-tooltip activator="parent" v-if="!isSameAsDefaultTimezoneOffsetMinutes(item)">{{ getDisplayTimeInDefaultTimezone(item) }}</v-tooltip>
</template>
<template #item.type="{ item }">
<v-chip label variant="outlined" size="x-small"
:class="{ 'text-income' : item.type === TransactionType.Income, 'text-expense': item.type === TransactionType.Expense }"
:color="getTransactionTypeColor(item)">{{ getDisplayTransactionType(item) }}</v-chip>
</template>
<template #item.secondaryCategoryName="{ item }">
<div class="d-flex align-center">
<ItemIcon size="24px" icon-type="category"
:icon-id="item.secondaryCategory?.icon ?? ''"
:color="item.secondaryCategory?.color ?? ''"
v-if="item.secondaryCategory?.color"></ItemIcon>
<v-icon size="24" :icon="mdiPencilBoxOutline" v-else-if="!item.secondaryCategory || !item.secondaryCategory?.color" />
<span class="ms-2" v-if="item.type === TransactionType.ModifyBalance">
{{ tt('Modify Balance') }}
</span>
<span class="ms-2" v-else-if="item.type !== TransactionType.ModifyBalance && item.secondaryCategory">
{{ item.secondaryCategory?.name }}
</span>
</div>
</template>
<template #item.sourceAmount="{ item }">
<span :class="{ 'text-expense': item.type === TransactionType.Expense, 'text-income': item.type === TransactionType.Income }">{{ getDisplaySourceAmount(item) }}</span>
<v-icon class="icon-with-direction mx-1" size="13" :icon="mdiArrowRight" v-if="item.type === TransactionType.Transfer && item.sourceAccount?.id !== item.destinationAccount?.id && getDisplaySourceAmount(item) !== getDisplayDestinationAmount(item)"></v-icon>
<span v-if="item.type === TransactionType.Transfer && item.sourceAccount?.id !== item.destinationAccount?.id && getDisplaySourceAmount(item) !== getDisplayDestinationAmount(item)">{{ getDisplayDestinationAmount(item) }}</span>
</template>
<template #item.sourceAccountName="{ item }">
<div class="d-flex align-center">
<span v-if="item.sourceAccount">{{ item.sourceAccount?.name }}</span>
<v-icon class="icon-with-direction mx-1" size="13" :icon="mdiArrowRight" v-if="item.type === TransactionType.Transfer"></v-icon>
<span v-if="item.type === TransactionType.Transfer && item.destinationAccount">{{ item.destinationAccount?.name }}</span>
</div>
</template>
<template #item.tags="{ item }">
<div class="d-flex">
<v-chip class="transaction-tag" size="small"
:key="tag.id" :prepend-icon="mdiPound"
:text="tag.name"
v-for="tag in item.tags"/>
<v-chip class="transaction-tag" size="small"
:text="tt('None')"
v-if="!item.tagIds || !item.tagIds.length"/>
</div>
</template>
<template #item.operation="{ item }">
<v-btn density="compact" variant="text" color="default" :disabled="loading || disabled"
@click="showTransaction(item)">
{{ tt('View') }}
</v-btn>
</template>
<template #no-data>
<div v-if="loading && (!filteredTransactions || filteredTransactions.length < 1)">
<div class="ms-1" style="padding-top: 3px; padding-bottom: 3px" :key="itemIdx" v-for="itemIdx in skeletonData">
<v-skeleton-loader type="text" :loading="true"></v-skeleton-loader>
</div>
</div>
<div v-else>
{{ tt('No transaction data') }}
</div>
</template>
<template #bottom>
<div class="title-and-toolbar d-flex align-center justify-center text-no-wrap mt-2 mb-4">
<pagination-buttons :disabled="loading || disabled"
:totalPageCount="totalPageCount"
v-model="currentPage">
</pagination-buttons>
</div>
</template>
</v-data-table>
<batch-update-category-dialog ref="batchUpdateCategoryDialog" />
<snack-bar ref="snackbar" />
</template>
<script setup lang="ts">
import SnackBar from '@/components/desktop/SnackBar.vue';
import PaginationButtons from '@/components/desktop/PaginationButtons.vue';
import BatchUpdateCategoryDialog from '@/views/desktop/insights/dialogs/BatchUpdateCategoryDialog.vue';
import { ref, computed, useTemplateRef } from 'vue';
import { useI18n } from '@/locales/helpers.ts';
import { useExplorerDataTablePageBase } from '@/views/base/explorer/ExplorerDataTablePageBase.ts';
import { CategoryType } from '@/core/category.ts';
import { TransactionType } from '@/core/transaction.ts';
import type { TransactionInsightDataItem } from '@/models/transaction.ts';
import { getObjectOwnFieldWithValueCount } from '@/lib/common.ts';
import {
mdiArrowRight,
mdiPencilBoxOutline,
mdiPound,
mdiSelect,
mdiSelectAll,
mdiSelectInverse,
mdiMenuDown,
mdiTextBoxEditOutline
} from '@mdi/js';
type SnackBarType = InstanceType<typeof SnackBar>;
type BatchUpdateCategoryDialogType = InstanceType<typeof BatchUpdateCategoryDialog>;
interface InsightsExplorerDataTableTabProps {
loading?: boolean;
disabled?: boolean;
}
defineProps<InsightsExplorerDataTableTabProps>();
const emit = defineEmits<{
(e: 'click:transaction', value: TransactionInsightDataItem): void;
(e: 'update:transactions'): void;
}>();
const snackbar = useTemplateRef<SnackBarType>('snackbar');
const batchUpdateCategoryDialog = useTemplateRef<BatchUpdateCategoryDialogType>('batchUpdateCategoryDialog');
const {
tt,
formatNumberToLocalizedNumerals
} = useI18n();
const {
currentPage,
currentExplorer,
filteredTransactions,
allDataTableQuerySources,
allPageCounts,
skeletonData,
totalPageCount,
dataTableHeaders,
getDisplayDateTime,
isSameAsDefaultTimezoneOffsetMinutes,
getDisplayTimezone,
getDisplayTimeInDefaultTimezone,
getDisplayTransactionType,
getTransactionTypeColor,
getDisplaySourceAmount,
getDisplayDestinationAmount
} = useExplorerDataTablePageBase();
const selectedTransactions = ref<Record<string, boolean>>({});
const selectedTransactionCount = computed<number>(() => getObjectOwnFieldWithValueCount(selectedTransactions.value, true));
const allTransactionSelected = computed<boolean>(() => selectedTransactionCount.value > 0 && selectedTransactionCount.value === filteredTransactions.value.length);
const anyButNotAllTransactionSelected = computed<boolean>(() => selectedTransactionCount.value > 0 && selectedTransactionCount.value < filteredTransactions.value.length);
const isAllSelectedTransactionsExpense = computed<boolean>(() => isAllSelectedTransactionsSpecificType(TransactionType.Expense));
const isAllSelectedTransactionsIncome = computed<boolean>(() => isAllSelectedTransactionsSpecificType(TransactionType.Income));
const isAllSelectedTransactionsTransfer = computed<boolean>(() => isAllSelectedTransactionsSpecificType(TransactionType.Transfer));
const editableDataTableHeaders = computed<object[]>(() => {
const headers: object[] = [
{ key: 'data-table-select', fixed: true }
];
headers.push(...dataTableHeaders.value);
return headers;
});
function isAllSelectedTransactionsSpecificType(type: TransactionType): boolean {
for (const transaction of filteredTransactions.value) {
if (selectedTransactions.value[transaction.id] && transaction.type !== type) {
return false;
}
}
return selectedTransactionCount.value > 0;
}
function getAllSelectedTransactionIds(): string[] {
const selectedIds: string[] = [];
for (const transaction of filteredTransactions.value) {
if (selectedTransactions.value[transaction.id]) {
selectedIds.push(transaction.id);
}
}
return selectedIds;
}
function selectAll(): void {
for (const transaction of filteredTransactions.value) {
selectedTransactions.value[transaction.id] = true;
}
}
function selectNone(): void {
for (const transaction of filteredTransactions.value) {
selectedTransactions.value[transaction.id] = false;
}
}
function selectInvert(): void {
for (const transaction of filteredTransactions.value) {
selectedTransactions.value[transaction.id] = !selectedTransactions.value[transaction.id];
}
}
function batchUpdateTransactionCategories(type: CategoryType): void {
batchUpdateCategoryDialog.value?.open({
type: type,
updateIds: getAllSelectedTransactionIds() }
).then(updatedCount => {
if (updatedCount > 0) {
snackbar.value?.showMessage('format.misc.youHaveUpdatedTransactions', {
count: formatNumberToLocalizedNumerals(updatedCount)
});
}
selectedTransactions.value = {};
emit('update:transactions');
}).catch(error => {
if (!error.processed) {
snackbar.value?.showError(error);
}
});
}
function showTransaction(transaction: TransactionInsightDataItem): void {
emit('click:transaction', transaction);
}
</script>
<style>
.v-table.insights-editable-explorer-table > .v-table__wrapper > table {
th:not(:nth-last-child(2)),
td:not(:nth-last-child(2)) {
width: auto !important;
white-space: nowrap;
}
th:nth-last-child(2),
td:nth-last-child(2) {
width: 100% !important;
}
}
.v-table.insights-editable-explorer-table.loading-skeleton tr.v-data-table-rows-no-data > td {
padding: 0;
}
.v-table.insights-editable-explorer-table .v-chip.transaction-tag {
margin-inline-end: 4px;
margin-top: 2px;
margin-bottom: 2px;
}
.v-table.insights-editable-explorer-table .v-chip.transaction-tag > .v-chip__content {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
</style>