mirror of
https://github.com/mayswind/ezbookkeeping
synced 2026-04-21 13:37:43 +00:00
support batch update categories for transactions in insights explorer
Some checks are pending
Docker Snapshot / build (push) Waiting to run
Some checks are pending
Docker Snapshot / build (push) Waiting to run
This commit is contained in:
parent
f56b5c471d
commit
9c87436a36
32 changed files with 1166 additions and 179 deletions
|
|
@ -393,6 +393,7 @@ func startWebServer(c *core.CliContext) error {
|
|||
apiV1Route.GET("/transactions/get.json", bindApi(api.Transactions.TransactionGetHandler))
|
||||
apiV1Route.POST("/transactions/add.json", bindApi(api.Transactions.TransactionCreateHandler))
|
||||
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/delete.json", bindApi(api.Transactions.TransactionDeleteHandler))
|
||||
|
||||
|
|
|
|||
|
|
@ -1338,6 +1338,105 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.WebContext) (any, *er
|
|||
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
|
||||
func (a *TransactionsApi) TransactionMoveAllBetweenAccountsHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
var transactionMoveReq models.TransactionMoveBetweenAccountsRequest
|
||||
|
|
|
|||
|
|
@ -325,6 +325,12 @@ type TransactionGetRequest struct {
|
|||
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
|
||||
type TransactionMoveBetweenAccountsRequest struct {
|
||||
FromAccountId int64 `json:"fromAccountId,string" binding:"required,min=1"`
|
||||
|
|
|
|||
|
|
@ -434,6 +434,22 @@ func (s *TransactionService) GetTransactionByTransactionId(c core.Context, uid i
|
|||
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
|
||||
func (s *TransactionService) GetAllTransactionCount(c core.Context, uid int64) (int64, error) {
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if uid <= 0 {
|
||||
return errs.ErrUserIdInvalid
|
||||
|
|
|
|||
|
|
@ -191,6 +191,21 @@ export function getObjectOwnFieldCount(object: object): number {
|
|||
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 {
|
||||
// Escape special characters in originalValue to safely use it in a regex pattern.
|
||||
// This ensures that characters like . (dot), * (asterisk), +, ?, etc. are treated literally,
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ import type {
|
|||
import type {
|
||||
TransactionCreateRequest,
|
||||
TransactionModifyRequest,
|
||||
TransactionBatchUpdateCategoryRequest,
|
||||
TransactionMoveBetweenAccountsRequest,
|
||||
TransactionDeleteRequest,
|
||||
TransactionImportRequest,
|
||||
|
|
@ -611,6 +612,9 @@ export default {
|
|||
modifyTransaction: (req: TransactionModifyRequest): ApiResponsePromise<TransactionInfoResponse> => {
|
||||
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> => {
|
||||
return axios.post<ApiResponse<boolean>>('v1/transactions/move/all.json', req);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1757,6 +1757,8 @@
|
|||
"Save As New Explorer": "Als neuen Explorer speichern",
|
||||
"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.",
|
||||
"Enter Edit Mode": "Enter Edit Mode",
|
||||
"Exit Edit Mode": "Exit Edit Mode",
|
||||
"Set Explorer Name": "Explorer-Name festlegen",
|
||||
"Rename Explorer": "Explorer umbenennen",
|
||||
"Hide Explorer": "Explorer ausblenden",
|
||||
|
|
@ -1843,6 +1845,10 @@
|
|||
"Coefficient of Variation": "Variationskoeffizient",
|
||||
"Skewness": "Skewness",
|
||||
"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",
|
||||
"This Week": "Diese Woche",
|
||||
"This Month": "Dieser Monat",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
"Skewness": "Skewness",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -1757,6 +1757,8 @@
|
|||
"Save As New Explorer": "Guardar Como Nueva Exploración",
|
||||
"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.",
|
||||
"Enter Edit Mode": "Enter Edit Mode",
|
||||
"Exit Edit Mode": "Exit Edit Mode",
|
||||
"Set Explorer Name": "Asignar Nombre a la Exploración",
|
||||
"Rename Explorer": "Renombrar Exploración",
|
||||
"Hide Explorer": "Ocultar Exploración",
|
||||
|
|
@ -1843,6 +1845,10 @@
|
|||
"Coefficient of Variation": "Coefficient of Variation",
|
||||
"Skewness": "Skewness",
|
||||
"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",
|
||||
"This Week": "Esta Semana",
|
||||
"This Month": "Este Mes",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
"Skewness": "Skewness",
|
||||
"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",
|
||||
"This Week": "Cette semaine",
|
||||
"This Month": "Ce mois",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
"Skewness": "Skewness",
|
||||
"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",
|
||||
"This Week": "Questa settimana",
|
||||
"This Month": "Questo mese",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
"Skewness": "Skewness",
|
||||
"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": "口座リスト",
|
||||
"This Week": "今週",
|
||||
"This Month": "今月",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
"Skewness": "Skewness",
|
||||
"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": "ಖಾತೆಗಳ ಪಟ್ಟಿ",
|
||||
"This Week": "ಈ ವಾರ",
|
||||
"This Month": "ಈ ತಿಂಗಳು",
|
||||
|
|
|
|||
|
|
@ -1757,6 +1757,8 @@
|
|||
"Save As New Explorer": "새 탐색기로 저장",
|
||||
"Restore to Last Saved": "마지막 저장 상태로 복원",
|
||||
"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": "탐색기 이름 설정",
|
||||
"Rename 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 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": "계좌 목록",
|
||||
"This Week": "이번 주",
|
||||
"This Month": "이번 달",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
"Skewness": "Skewness",
|
||||
"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",
|
||||
"This Week": "Deze week",
|
||||
"This Month": "Deze maand",
|
||||
|
|
|
|||
|
|
@ -1757,6 +1757,8 @@
|
|||
"Save As New Explorer": "Salvar como Novo Explorador",
|
||||
"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.",
|
||||
"Enter Edit Mode": "Enter Edit Mode",
|
||||
"Exit Edit Mode": "Exit Edit Mode",
|
||||
"Set Explorer Name": "Definir Nome do Explorador",
|
||||
"Rename Explorer": "Renomear Explorador",
|
||||
"Hide Explorer": "Ocultar Explorador",
|
||||
|
|
@ -1843,6 +1845,10 @@
|
|||
"Coefficient of Variation": "Coeficiente de Variação",
|
||||
"Skewness": "Skewness",
|
||||
"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",
|
||||
"This Week": "Esta Semana",
|
||||
"This Month": "Este Mês",
|
||||
|
|
|
|||
|
|
@ -1757,6 +1757,8 @@
|
|||
"Save As New Explorer": "Сохранить как новое исследование",
|
||||
"Restore to Last Saved": "Восстановить с последнего сохранения",
|
||||
"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": "Установить название исследования",
|
||||
"Rename 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 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": "Список счетов",
|
||||
"This Week": "На этой неделе",
|
||||
"This Month": "В этом месяце",
|
||||
|
|
|
|||
|
|
@ -1757,6 +1757,8 @@
|
|||
"Save As New Explorer": "Shrani kot novo raziskovanje",
|
||||
"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.",
|
||||
"Enter Edit Mode": "Enter Edit Mode",
|
||||
"Exit Edit Mode": "Exit Edit Mode",
|
||||
"Set Explorer Name": "Nastavi ime raziskovanja",
|
||||
"Rename Explorer": "Preimenuj raziskovanje",
|
||||
"Hide Explorer": "Skrij raziskovanje",
|
||||
|
|
@ -1843,6 +1845,10 @@
|
|||
"Coefficient of Variation": "Coefficient of Variation",
|
||||
"Skewness": "Skewness",
|
||||
"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",
|
||||
"This Week": "Ta teden",
|
||||
"This Month": "Ta mesec",
|
||||
|
|
|
|||
|
|
@ -1757,6 +1757,8 @@
|
|||
"Save As New Explorer": "புதிய ஆய்வுக்கருவியாக சேமி",
|
||||
"Restore to Last Saved": "கடைசி சேமிப்புக்கு மீட்டமை",
|
||||
"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": "ஆய்வுக்கருவி பெயர் அமை",
|
||||
"Rename 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 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": "கணக்குகளின் பட்டியல்",
|
||||
"This Week": "இந்த வாரம்",
|
||||
"This Month": "இந்த மாதம்",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
"Skewness": "Skewness",
|
||||
"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": "รายการบัญชี",
|
||||
"This Week": "สัปดาห์นี้",
|
||||
"This Month": "เดือนนี้",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
"Skewness": "Skewness",
|
||||
"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",
|
||||
"This Week": "Bu Hafta",
|
||||
"This Month": "Bu Ay",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
"Skewness": "Skewness",
|
||||
"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": "Список рахунків",
|
||||
"This Week": "Цього тижня",
|
||||
"This Month": "Цього місяця",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
"Skewness": "Skewness",
|
||||
"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",
|
||||
"This Week": "Tuần này",
|
||||
"This Month": "Tháng này",
|
||||
|
|
|
|||
|
|
@ -1757,6 +1757,8 @@
|
|||
"Save As New Explorer": "另存为新的探索",
|
||||
"Restore to Last Saved": "恢复到上次保存",
|
||||
"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": "设置探索名称",
|
||||
"Rename Explorer": "重命名探索",
|
||||
"Hide Explorer": "隐藏探索",
|
||||
|
|
@ -1843,6 +1845,10 @@
|
|||
"Coefficient of Variation": "变异系数",
|
||||
"Skewness": "偏度",
|
||||
"Kurtosis": "峰度",
|
||||
"Update Categories for Expense Transactions": "更新支出交易的分类",
|
||||
"Update Categories for Income Transactions": "更新收入交易的分类",
|
||||
"Update Categories for Transfer Transactions": "更新转账交易的分类",
|
||||
"Unable to update categories for transactions": "无法更新交易的分类",
|
||||
"Account List": "账户列表",
|
||||
"This Week": "本周",
|
||||
"This Month": "本月",
|
||||
|
|
|
|||
|
|
@ -1757,6 +1757,8 @@
|
|||
"Save As New Explorer": "另存新探索",
|
||||
"Restore to Last Saved": "還原到上次儲存",
|
||||
"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": "設定探索名稱",
|
||||
"Rename Explorer": "重新命名探索",
|
||||
"Hide Explorer": "隱藏探索",
|
||||
|
|
@ -1843,6 +1845,10 @@
|
|||
"Coefficient of Variation": "變異係數",
|
||||
"Skewness": "偏度",
|
||||
"Kurtosis": "峰度",
|
||||
"Update Categories for Expense Transactions": "更新支出交易的分類",
|
||||
"Update Categories for Income Transactions": "更新收入交易的分類",
|
||||
"Update Categories for Transfer Transactions": "更新轉帳交易的分類",
|
||||
"Unable to update categories for transactions": "無法更新交易的分類",
|
||||
"Account List": "帳戶清單",
|
||||
"This Week": "本週",
|
||||
"This Month": "本月",
|
||||
|
|
|
|||
|
|
@ -558,6 +558,11 @@ export interface TransactionModifyRequest {
|
|||
readonly geoLocation?: TransactionGeoLocationRequest;
|
||||
}
|
||||
|
||||
export interface TransactionBatchUpdateCategoryRequest {
|
||||
readonly transactionIds: string[];
|
||||
readonly categoryId: string;
|
||||
}
|
||||
|
||||
export interface TransactionMoveBetweenAccountsRequest {
|
||||
readonly fromAccountId: string;
|
||||
readonly toAccountId: string;
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
return new Promise((resolve, reject) => {
|
||||
services.moveAllTransactionsBetweenAccounts({ fromAccountId, toAccountId }).then(response => {
|
||||
|
|
@ -1472,6 +1517,7 @@ export const useTransactionsStore = defineStore('transactions', () => {
|
|||
getReconciliationStatements,
|
||||
getTransaction,
|
||||
saveTransaction,
|
||||
batchUpdateTransactionCategories,
|
||||
moveAllTransactionsBetweenAccounts,
|
||||
deleteTransaction,
|
||||
recognizeReceiptImage,
|
||||
|
|
|
|||
208
src/views/base/explorer/ExplorerDataTablePageBase.ts
Normal file
208
src/views/base/explorer/ExplorerDataTablePageBase.ts
Normal file
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
<v-layout>
|
||||
<v-navigation-drawer :permanent="alwaysShowNav" v-model="showNav">
|
||||
<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>
|
||||
<v-divider />
|
||||
<v-tabs show-arrows
|
||||
|
|
@ -13,13 +13,13 @@
|
|||
style="max-height: calc(100% - 150px)"
|
||||
direction="vertical"
|
||||
:prev-icon="mdiMenuUp" :next-icon="mdiMenuDown"
|
||||
:key="currentExplorer.id" :disabled="loading || updating"
|
||||
:key="currentExplorer.id" :disabled="loading || updating || isCurrentDataTableEditable"
|
||||
:model-value="currentExplorer.id">
|
||||
<v-tab class="tab-text-truncate" key="new" value="" @click="createNewExplorer">
|
||||
<span class="text-truncate">{{ tt('New Explorer') }}</span>
|
||||
</v-tab>
|
||||
<v-tab class="tab-text-truncate" :key="explorer.id" :value="explorer.id"
|
||||
:disabled="loading || updating"
|
||||
:disabled="loading || updating || isCurrentDataTableEditable"
|
||||
v-for="explorer in allVisibleExplorers"
|
||||
@click="loadExplorer(explorer.id)">
|
||||
<span class="text-truncate">{{ explorer.name || tt('Untitled Explorer') }}</span>
|
||||
|
|
@ -41,11 +41,11 @@
|
|||
<span>{{ tt('Insights Explorer') }}</span>
|
||||
<v-btn-group class="ms-4" color="default" density="comfortable" variant="outlined" divided>
|
||||
<v-btn class="button-icon-with-direction" :icon="mdiArrowLeft"
|
||||
:disabled="loading || updating || !canShiftDateRange"
|
||||
:disabled="loading || updating || !canShiftDateRange || isCurrentDataTableEditable"
|
||||
@click="shiftDateRange(-1)"/>
|
||||
<v-menu location="bottom" max-height="500">
|
||||
<template #activator="{ props }">
|
||||
<v-btn :disabled="loading || updating"
|
||||
<v-btn :disabled="loading || updating || isCurrentDataTableEditable"
|
||||
v-bind="props">{{ displayQueryDateRangeName }}</v-btn>
|
||||
</template>
|
||||
<v-list :selected="[currentFilter.dateRangeType]">
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
</v-list>
|
||||
</v-menu>
|
||||
<v-btn class="button-icon-with-direction" :icon="mdiArrowRight"
|
||||
:disabled="loading || updating || !canShiftDateRange"
|
||||
:disabled="loading || updating || !canShiftDateRange || isCurrentDataTableEditable"
|
||||
@click="shiftDateRange(1)"/>
|
||||
</v-btn-group>
|
||||
|
||||
|
|
@ -84,7 +84,7 @@
|
|||
<v-btn class="ms-3"
|
||||
:color="isCurrentExplorerModified ? 'primary' : 'default'"
|
||||
:variant="isCurrentExplorerModified ? 'elevated' : 'outlined'"
|
||||
:disabled="loading || updating" @click="saveExplorer(false)">
|
||||
:disabled="loading || updating || isCurrentDataTableEditable" @click="saveExplorer(false)">
|
||||
{{ tt('Save Explorer') }}
|
||||
<v-progress-circular indeterminate size="22" class="ms-2" v-if="updating"></v-progress-circular>
|
||||
<v-menu activator="parent" :open-on-hover="true">
|
||||
|
|
@ -113,29 +113,41 @@
|
|||
v-for="timezoneType in allTimezoneTypesUsedForDateRange"
|
||||
@click="currentExplorer.timezoneUsedForDateRange = timezoneType.type"></v-list-item>
|
||||
</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"
|
||||
:title="tt('Export Results')"
|
||||
:disabled="loading || updating || (activeTab === 'table' && (!filteredTransactionsInDataTable || filteredTransactionsInDataTable.length < 1))"
|
||||
@click="exportResults"
|
||||
v-if="activeTab === 'table' || activeTab === 'chart'"></v-list-item>
|
||||
<v-divider class="my-2" v-if="currentExplorer.id" />
|
||||
<v-list-item :prepend-icon="mdiPencilOutline" @click="setExplorerName" v-if="currentExplorer.id">
|
||||
v-if="(activeTab === 'table' || activeTab === 'chart') && !isCurrentDataTableEditable"></v-list-item>
|
||||
<v-divider class="my-2" v-if="currentExplorer.id && !isCurrentDataTableEditable" />
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<v-divider class="my-2"/>
|
||||
<v-divider class="my-2" v-if="!isCurrentDataTableEditable"/>
|
||||
<v-list-item :prepend-icon="mdiSort"
|
||||
:disabled="!allExplorers || allExplorers.length < 2"
|
||||
:title="tt('Change Explorer Display Order')"
|
||||
@click="showChangeExplorerDisplayOrderDialog"></v-list-item>
|
||||
@click="showChangeExplorerDisplayOrderDialog"
|
||||
v-if="!isCurrentDataTableEditable"></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
|
|
@ -149,7 +161,13 @@
|
|||
<v-window-item value="table">
|
||||
<explorer-data-table-tab ref="explorerDataTableTab"
|
||||
: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 value="chart">
|
||||
<explorer-chart-tab ref="explorerChartTab"
|
||||
|
|
@ -187,6 +205,7 @@ import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue';
|
|||
import SnackBar from '@/components/desktop/SnackBar.vue';
|
||||
import ExplorerQueryTab from '@/views/desktop/insights/tabs/ExplorerQueryTab.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 ExplorerChangeDisplayOrderDialog from '@/views/desktop/insights/dialogs/ExplorerChangeDisplayOrderDialog.vue';
|
||||
import EditDialog from '@/views/desktop/transactions/list/dialogs/EditDialog.vue';
|
||||
|
|
@ -238,7 +257,9 @@ import {
|
|||
mdiSort,
|
||||
mdiHomeClockOutline,
|
||||
mdiInvoiceTextClockOutline,
|
||||
mdiExport
|
||||
mdiExport,
|
||||
mdiTableEdit,
|
||||
mdiTableCheck
|
||||
} from '@mdi/js';
|
||||
|
||||
interface InsightsExplorerProps {
|
||||
|
|
@ -298,6 +319,7 @@ const initing = ref<boolean>(true);
|
|||
const updating = ref<boolean>(false);
|
||||
const clientSessionId = ref<string>('');
|
||||
const isCurrentExplorerModified = ref<boolean>(false);
|
||||
const isCurrentDataTableEditable = ref<boolean>(false);
|
||||
const alwaysShowNav = ref<boolean>(display.mdAndUp.value);
|
||||
const showNav = ref<boolean>(display.mdAndUp.value);
|
||||
const activeTab = ref<ExplorerPageTabType>('query');
|
||||
|
|
@ -726,6 +748,10 @@ function onShowTransaction(transaction: TransactionInsightDataItem): void {
|
|||
});
|
||||
}
|
||||
|
||||
function onUpdateTransactions(): void {
|
||||
reload(false);
|
||||
}
|
||||
|
||||
function onShowDateRangeError(message: string): void {
|
||||
snackbar.value?.showError(message);
|
||||
}
|
||||
|
|
|
|||
193
src/views/desktop/insights/dialogs/BatchUpdateCategoryDialog.vue
Normal file
193
src/views/desktop/insights/dialogs/BatchUpdateCategoryDialog.vue
Normal file
|
|
@ -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">
|
||||
import PaginationButtons from '@/components/desktop/PaginationButtons.vue';
|
||||
|
||||
import { ref, computed } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
import { useExplorerDataTablePageBase } from '@/views/base/explorer/ExplorerDataTablePageBase.ts';
|
||||
|
||||
import { useSettingsStore } from '@/stores/setting.ts';
|
||||
import { useUserStore } from '@/stores/user.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 type { TransactionInsightDataItem } from '@/models/transaction.ts';
|
||||
import type { InsightsExplorer} from '@/models/explorer.ts';
|
||||
|
||||
import { isDefined, replaceAll } from '@/lib/common.ts';
|
||||
|
||||
import {
|
||||
getUtcOffsetByUtcOffsetMinutes,
|
||||
getTimezoneOffsetMinutes,
|
||||
parseDateTimeFromUnixTimeWithTimezoneOffset
|
||||
} from '@/lib/datetime.ts';
|
||||
|
||||
|
|
@ -249,8 +243,6 @@ const emit = defineEmits<{
|
|||
|
||||
const {
|
||||
tt,
|
||||
getCurrentNumeralSystemType,
|
||||
formatDateTimeToLongDateTime,
|
||||
formatDateTimeToGregorianDefaultDateTime,
|
||||
formatAmountToWesternArabicNumeralsWithoutDigitGrouping,
|
||||
formatAmountToLocalizedNumeralsWithCurrency,
|
||||
|
|
@ -258,163 +250,30 @@ const {
|
|||
formatPercentToLocalizedNumerals
|
||||
} = useI18n();
|
||||
|
||||
const {
|
||||
currentPage,
|
||||
currentExplorer,
|
||||
filteredTransactions,
|
||||
allDataTableQuerySources,
|
||||
allPageCounts,
|
||||
skeletonData,
|
||||
totalPageCount,
|
||||
dataTableHeaders,
|
||||
getDisplayDateTime,
|
||||
isSameAsDefaultTimezoneOffsetMinutes,
|
||||
getDisplayTimezone,
|
||||
getDisplayTimeInDefaultTimezone,
|
||||
getDisplayTransactionType,
|
||||
getTransactionTypeColor,
|
||||
getDisplaySourceAmount,
|
||||
getDisplayDestinationAmount
|
||||
} = useExplorerDataTablePageBase();
|
||||
|
||||
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 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 {
|
||||
emit('click:transaction', transaction);
|
||||
}
|
||||
|
|
|
|||
359
src/views/desktop/insights/tabs/ExplorerEditableDataTableTab.vue
Normal file
359
src/views/desktop/insights/tabs/ExplorerEditableDataTableTab.vue
Normal file
|
|
@ -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>
|
||||
Loading…
Reference in a new issue