mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-19 01:04:25 +08:00
support batch update categories for transactions in insights explorer
This commit is contained in:
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "今月",
|
||||||
|
|||||||
@@ -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": "ಈ ತಿಂಗಳು",
|
||||||
|
|||||||
@@ -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": "이번 달",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "В этом месяце",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "இந்த மாதம்",
|
||||||
|
|||||||
@@ -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": "เดือนนี้",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "Цього місяця",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "本月",
|
||||||
|
|||||||
@@ -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": "本月",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user