add transaction gallery mode in transaction list page

This commit is contained in:
MaysWind
2026-05-11 00:25:55 +08:00
parent 11f2c9fff7
commit 563bef69cf
33 changed files with 453 additions and 87 deletions
+1 -1
View File
@@ -419,7 +419,7 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(exportTransactionDataReq.MinTime) minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(exportTransactionDataReq.MinTime)
} }
allTransactions, err := a.transactions.GetAllSpecifiedTransactions(c, uid, maxTransactionTime, minTransactionTime, exportTransactionDataReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, exportTransactionDataReq.AmountFilter, exportTransactionDataReq.Keyword, pageCountForDataExport, true) allTransactions, err := a.transactions.GetAllSpecifiedTransactions(c, uid, maxTransactionTime, minTransactionTime, exportTransactionDataReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, exportTransactionDataReq.AmountFilter, exportTransactionDataReq.Keyword, false, pageCountForDataExport, true)
if err != nil { if err != nil {
log.Errorf(c, "[data_managements.getExportedFileContent] failed to all transactions user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[data_managements.getExportedFileContent] failed to all transactions user \"uid:%d\", because %s", uid, err.Error())
+5 -5
View File
@@ -97,7 +97,7 @@ func (a *TransactionsApi) TransactionCountHandler(c *core.WebContext) (any, *err
} }
} }
totalCount, err := a.transactions.GetTransactionCount(c, uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionCountReq.AmountFilter, transactionCountReq.Keyword) totalCount, err := a.transactions.GetTransactionCount(c, uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionCountReq.AmountFilter, transactionCountReq.Keyword, transactionCountReq.MustHavePictures)
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionCountHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.TransactionCountHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
@@ -168,7 +168,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs
var totalCount int64 var totalCount int64
if transactionListReq.WithCount { if transactionListReq.WithCount {
totalCount, err = a.transactions.GetTransactionCount(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword) totalCount, err = a.transactions.GetTransactionCount(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword, transactionListReq.MustHavePictures)
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionListHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.TransactionListHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
@@ -176,7 +176,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs
} }
} }
transactions, err := a.transactions.GetTransactionsByMaxTime(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword, transactionListReq.Page, transactionListReq.Count, true, true) transactions, err := a.transactions.GetTransactionsByMaxTime(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword, transactionListReq.MustHavePictures, transactionListReq.Page, transactionListReq.Count, true, true)
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionListHandler] failed to get transactions earlier than \"%d\" for user \"uid:%d\", because %s", transactionListReq.MaxTime, uid, err.Error()) log.Errorf(c, "[transactions.TransactionListHandler] failed to get transactions earlier than \"%d\" for user \"uid:%d\", because %s", transactionListReq.MaxTime, uid, err.Error())
@@ -276,7 +276,7 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.WebContext) (any,
} }
} }
transactions, err := a.transactions.GetTransactionsInMonthByPage(c, uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword) transactions, err := a.transactions.GetTransactionsInMonthByPage(c, uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword, transactionListReq.MustHavePictures)
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionMonthListHandler] failed to get transactions in month \"%d-%d\" for user \"uid:%d\", because %s", transactionListReq.Year, transactionListReq.Month, uid, err.Error()) log.Errorf(c, "[transactions.TransactionMonthListHandler] failed to get transactions in month \"%d-%d\" for user \"uid:%d\", because %s", transactionListReq.Year, transactionListReq.Month, uid, err.Error())
@@ -371,7 +371,7 @@ func (a *TransactionsApi) TransactionListAllHandler(c *core.WebContext) (any, *e
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(transactionAllListReq.StartTime) minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(transactionAllListReq.StartTime)
} }
allTransactions, err := a.transactions.GetAllSpecifiedTransactions(c, uid, maxTransactionTime, minTransactionTime, transactionAllListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionAllListReq.AmountFilter, transactionAllListReq.Keyword, pageCountForDataExport, true) allTransactions, err := a.transactions.GetAllSpecifiedTransactions(c, uid, maxTransactionTime, minTransactionTime, transactionAllListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionAllListReq.AmountFilter, transactionAllListReq.Keyword, transactionAllListReq.MustHavePictures, pageCountForDataExport, true)
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionListAllHandler] failed to get all transactions for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.TransactionListAllHandler] failed to get all transactions for user \"uid:%d\", because %s", uid, err.Error())
+2 -2
View File
@@ -153,14 +153,14 @@ func (h *mcpQueryTransactionsToolHandler) Handle(c *core.WebContext, callToolReq
} }
} }
totalCount, err := services.GetTransactionService().GetTransactionCount(c, uid, maxTransactionTime, minTransactionTime, transactionType, filterCategoryIds, filterAccountIds, nil, false, "", queryTransactionsRequest.Keyword) totalCount, err := services.GetTransactionService().GetTransactionCount(c, uid, maxTransactionTime, minTransactionTime, transactionType, filterCategoryIds, filterAccountIds, nil, false, "", queryTransactionsRequest.Keyword, false)
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionListHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.TransactionListHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
return nil, nil, err return nil, nil, err
} }
transactions, err := services.GetTransactionService().GetTransactionsByMaxTime(c, uid, maxTransactionTime, minTransactionTime, transactionType, filterCategoryIds, filterAccountIds, nil, false, "", queryTransactionsRequest.Keyword, queryTransactionsRequest.Page, queryTransactionsRequest.Count, false, true) transactions, err := services.GetTransactionService().GetTransactionsByMaxTime(c, uid, maxTransactionTime, minTransactionTime, transactionType, filterCategoryIds, filterAccountIds, nil, false, "", queryTransactionsRequest.Keyword, false, queryTransactionsRequest.Page, queryTransactionsRequest.Count, false, true)
structuredResponse, response, err := h.createNewMCPQueryTransactionsResponse(c, &queryTransactionsRequest, transactions, totalCount, services.GetAccountService().GetAccountMapByList(allAccounts), services.GetTransactionCategoryService().GetCategoryMapByList(allCategories)) structuredResponse, response, err := h.createNewMCPQueryTransactionsResponse(c, &queryTransactionsRequest, transactions, totalCount, services.GetAccountService().GetAccountMapByList(allAccounts), services.GetTransactionCategoryService().GetCategoryMapByList(allCategories))
if err != nil { if err != nil {
+51 -47
View File
@@ -210,65 +210,69 @@ type TransactionTagFilter struct {
// TransactionCountRequest represents transaction count request // TransactionCountRequest represents transaction count request
type TransactionCountRequest struct { type TransactionCountRequest struct {
Type TransactionType `form:"type" binding:"min=0,max=4"` Type TransactionType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"` CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"` AccountIds string `form:"account_ids"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"` TagFilter string `form:"tag_filter" binding:"validTagFilter"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"` AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"` Keyword string `form:"keyword"`
MaxTime int64 `form:"max_time" binding:"min=0"` // Transaction time sequence id MustHavePictures bool `form:"must_have_pictures"`
MinTime int64 `form:"min_time" binding:"min=0"` // Transaction time sequence id MaxTime int64 `form:"max_time" binding:"min=0"` // Transaction time sequence id
MinTime int64 `form:"min_time" binding:"min=0"` // Transaction time sequence id
} }
// TransactionListByMaxTimeRequest represents all parameters of transaction listing by max time request // TransactionListByMaxTimeRequest represents all parameters of transaction listing by max time request
type TransactionListByMaxTimeRequest struct { type TransactionListByMaxTimeRequest struct {
Type TransactionType `form:"type" binding:"min=0,max=4"` Type TransactionType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"` CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"` AccountIds string `form:"account_ids"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"` TagFilter string `form:"tag_filter" binding:"validTagFilter"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"` AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"` Keyword string `form:"keyword"`
MaxTime int64 `form:"max_time" binding:"min=0"` // Transaction time sequence id MustHavePictures bool `form:"must_have_pictures"`
MinTime int64 `form:"min_time" binding:"min=0"` // Transaction time sequence id MaxTime int64 `form:"max_time" binding:"min=0"` // Transaction time sequence id
Page int32 `form:"page" binding:"min=0"` MinTime int64 `form:"min_time" binding:"min=0"` // Transaction time sequence id
Count int32 `form:"count" binding:"required,min=1,max=50"` Page int32 `form:"page" binding:"min=0"`
WithCount bool `form:"with_count"` Count int32 `form:"count" binding:"required,min=1,max=50"`
WithPictures bool `form:"with_pictures"` WithCount bool `form:"with_count"`
TrimAccount bool `form:"trim_account"` WithPictures bool `form:"with_pictures"`
TrimCategory bool `form:"trim_category"` TrimAccount bool `form:"trim_account"`
TrimTag bool `form:"trim_tag"` TrimCategory bool `form:"trim_category"`
TrimTag bool `form:"trim_tag"`
} }
// TransactionListInMonthByPageRequest represents all parameters of transaction listing by month request // TransactionListInMonthByPageRequest represents all parameters of transaction listing by month request
type TransactionListInMonthByPageRequest struct { type TransactionListInMonthByPageRequest struct {
Year int32 `form:"year" binding:"required,min=1"` Year int32 `form:"year" binding:"required,min=1"`
Month int32 `form:"month" binding:"required,min=1"` Month int32 `form:"month" binding:"required,min=1"`
Type TransactionType `form:"type" binding:"min=0,max=4"` Type TransactionType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"` CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"` AccountIds string `form:"account_ids"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"` TagFilter string `form:"tag_filter" binding:"validTagFilter"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"` AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"` Keyword string `form:"keyword"`
WithPictures bool `form:"with_pictures"` MustHavePictures bool `form:"must_have_pictures"`
TrimAccount bool `form:"trim_account"` WithPictures bool `form:"with_pictures"`
TrimCategory bool `form:"trim_category"` TrimAccount bool `form:"trim_account"`
TrimTag bool `form:"trim_tag"` TrimCategory bool `form:"trim_category"`
TrimTag bool `form:"trim_tag"`
} }
// TransactionAllListRequest represents all parameters of all transaction listing request // TransactionAllListRequest represents all parameters of all transaction listing request
type TransactionAllListRequest struct { type TransactionAllListRequest struct {
Type TransactionType `form:"type" binding:"min=0,max=4"` Type TransactionType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"` CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"` AccountIds string `form:"account_ids"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"` TagFilter string `form:"tag_filter" binding:"validTagFilter"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"` AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"` Keyword string `form:"keyword"`
StartTime int64 `form:"start_time" binding:"min=0"` MustHavePictures bool `form:"must_have_pictures"`
EndTime int64 `form:"end_time" binding:"min=0"` StartTime int64 `form:"start_time" binding:"min=0"`
WithPictures bool `form:"with_pictures"` EndTime int64 `form:"end_time" binding:"min=0"`
TrimAccount bool `form:"trim_account"` WithPictures bool `form:"with_pictures"`
TrimCategory bool `form:"trim_category"` TrimAccount bool `form:"trim_account"`
TrimTag bool `form:"trim_tag"` TrimCategory bool `form:"trim_category"`
TrimTag bool `form:"trim_tag"`
} }
// TransactionReconciliationStatementRequest represents all parameters of transaction reconciliation statement request // TransactionReconciliationStatementRequest represents all parameters of transaction reconciliation statement request
+24 -10
View File
@@ -76,11 +76,11 @@ func (s *TransactionService) GetAllTransactions(c core.Context, uid int64, pageC
// GetAllTransactionsByMaxTime returns all transactions before given time // GetAllTransactionsByMaxTime returns all transactions before given time
func (s *TransactionService) GetAllTransactionsByMaxTime(c core.Context, uid int64, maxTransactionTime int64, count int32, noDuplicated bool) ([]*models.Transaction, error) { func (s *TransactionService) GetAllTransactionsByMaxTime(c core.Context, uid int64, maxTransactionTime int64, count int32, noDuplicated bool) ([]*models.Transaction, error) {
return s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, nil, false, "", "", 1, count, false, noDuplicated) return s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, nil, false, "", "", false, 1, count, false, noDuplicated)
} }
// GetAllSpecifiedTransactions returns all transactions that match given conditions // GetAllSpecifiedTransactions returns all transactions that match given conditions
func (s *TransactionService) GetAllSpecifiedTransactions(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, noTags bool, amountFilter string, keyword string, pageCount int32, noDuplicated bool) ([]*models.Transaction, error) { func (s *TransactionService) GetAllSpecifiedTransactions(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, noTags bool, amountFilter string, keyword string, mustHavePictures bool, pageCount int32, noDuplicated bool) ([]*models.Transaction, error) {
if maxTransactionTime <= 0 { if maxTransactionTime <= 0 {
maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(time.Now().Unix()) maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(time.Now().Unix())
} }
@@ -88,7 +88,7 @@ func (s *TransactionService) GetAllSpecifiedTransactions(c core.Context, uid int
var allTransactions []*models.Transaction var allTransactions []*models.Transaction
for maxTransactionTime > 0 { for maxTransactionTime > 0 {
transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, tagFilters, noTags, amountFilter, keyword, 1, pageCount, false, noDuplicated) transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, tagFilters, noTags, amountFilter, keyword, mustHavePictures, 1, pageCount, false, noDuplicated)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -116,7 +116,7 @@ func (s *TransactionService) GetAllTransactionsInOneAccountWithAccountBalanceByM
var allTransactions []*models.Transaction var allTransactions []*models.Transaction
for maxTransactionTime > 0 { for maxTransactionTime > 0 {
transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, []int64{accountId}, nil, false, "", "", 1, pageCount, false, true) transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, []int64{accountId}, nil, false, "", "", false, 1, pageCount, false, true)
if err != nil { if err != nil {
return nil, 0, 0, 0, 0, err return nil, 0, 0, 0, 0, err
@@ -206,7 +206,7 @@ func (s *TransactionService) GetAllAccountsDailyOpeningAndClosingBalance(c core.
var allTransactions []*models.Transaction var allTransactions []*models.Transaction
for maxTransactionTime > 0 { for maxTransactionTime > 0 {
transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, nil, false, "", "", 1, pageCountForLoadTransactionAmounts, false, false) transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, nil, false, "", "", false, 1, pageCountForLoadTransactionAmounts, false, false)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -323,7 +323,7 @@ func (s *TransactionService) GetAllAccountsDailyOpeningAndClosingBalance(c core.
} }
// GetTransactionsByMaxTime returns transactions before given time // GetTransactionsByMaxTime returns transactions before given time
func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, noTags bool, amountFilter string, keyword string, page int32, count int32, needOneMoreItem bool, noDuplicated bool) ([]*models.Transaction, error) { func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, noTags bool, amountFilter string, keyword string, mustHavePictures bool, page int32, count int32, needOneMoreItem bool, noDuplicated bool) ([]*models.Transaction, error) {
if uid <= 0 { if uid <= 0 {
return nil, errs.ErrUserIdInvalid return nil, errs.ErrUserIdInvalid
} }
@@ -360,6 +360,7 @@ func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64,
condition, conditionParams := s.buildTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionDbType, categoryIds, accountIds, tagFilters, amountFilter, keyword, noDuplicated) condition, conditionParams := s.buildTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionDbType, categoryIds, accountIds, tagFilters, amountFilter, keyword, noDuplicated)
sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...) sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...)
sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagFilters, noTags) sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagFilters, noTags)
sess = s.appendFilterPicturesConditionToQuery(sess, uid, mustHavePictures)
err = sess.Limit(int(actualCount), int(count*(page-1))).OrderBy("transaction_time desc").Find(&transactions) err = sess.Limit(int(actualCount), int(count*(page-1))).OrderBy("transaction_time desc").Find(&transactions)
@@ -367,7 +368,7 @@ func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64,
} }
// GetTransactionsInMonthByPage returns all transactions in given year and month // GetTransactionsInMonthByPage returns all transactions in given year and month
func (s *TransactionService) GetTransactionsInMonthByPage(c core.Context, uid int64, year int32, month int32, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, noTags bool, amountFilter string, keyword string) ([]*models.Transaction, error) { func (s *TransactionService) GetTransactionsInMonthByPage(c core.Context, uid int64, year int32, month int32, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, noTags bool, amountFilter string, keyword string, mustHavePictures bool) ([]*models.Transaction, error) {
if uid <= 0 { if uid <= 0 {
return nil, errs.ErrUserIdInvalid return nil, errs.ErrUserIdInvalid
} }
@@ -394,6 +395,7 @@ func (s *TransactionService) GetTransactionsInMonthByPage(c core.Context, uid in
condition, conditionParams := s.buildTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionDbType, categoryIds, accountIds, tagFilters, amountFilter, keyword, true) condition, conditionParams := s.buildTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionDbType, categoryIds, accountIds, tagFilters, amountFilter, keyword, true)
sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...) sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...)
sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagFilters, noTags) sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagFilters, noTags)
sess = s.appendFilterPicturesConditionToQuery(sess, uid, mustHavePictures)
err = sess.OrderBy("transaction_time desc").Find(&transactions) err = sess.OrderBy("transaction_time desc").Find(&transactions)
@@ -452,11 +454,11 @@ func (s *TransactionService) GetTransactionsByTransactionIds(c core.Context, uid
// 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, "", "", false)
} }
// GetTransactionCount returns count of transactions // GetTransactionCount returns count of transactions
func (s *TransactionService) GetTransactionCount(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, noTags bool, amountFilter string, keyword string) (int64, error) { func (s *TransactionService) GetTransactionCount(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, noTags bool, amountFilter string, keyword string, mustHavePictures bool) (int64, error) {
if uid <= 0 { if uid <= 0 {
return 0, errs.ErrUserIdInvalid return 0, errs.ErrUserIdInvalid
} }
@@ -475,6 +477,7 @@ func (s *TransactionService) GetTransactionCount(c core.Context, uid int64, maxT
condition, conditionParams := s.buildTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionDbType, categoryIds, accountIds, tagFilters, amountFilter, keyword, true) condition, conditionParams := s.buildTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionDbType, categoryIds, accountIds, tagFilters, amountFilter, keyword, true)
sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...) sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...)
sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagFilters, noTags) sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagFilters, noTags)
sess = s.appendFilterPicturesConditionToQuery(sess, uid, mustHavePictures)
return sess.Count(&models.Transaction{}) return sess.Count(&models.Transaction{})
} }
@@ -1964,7 +1967,7 @@ func (s *TransactionService) DeleteAllTransactionsOfAccount(c core.Context, uid
return errs.ErrAccountIdInvalid return errs.ErrAccountIdInvalid
} }
transactions, err := s.GetAllSpecifiedTransactions(c, uid, 0, 0, 0, nil, []int64{accountId}, nil, false, "", "", pageCount, true) transactions, err := s.GetAllSpecifiedTransactions(c, uid, 0, 0, 0, nil, []int64{accountId}, nil, false, "", "", false, pageCount, true)
if err != nil { if err != nil {
return err return err
@@ -2897,6 +2900,17 @@ func (s *TransactionService) appendFilterTagIdsConditionToQuery(sess *xorm.Sessi
return sess return sess
} }
func (s *TransactionService) appendFilterPicturesConditionToQuery(sess *xorm.Session, uid int64, mustHavePictures bool) *xorm.Session {
if !mustHavePictures {
return sess
}
subQuery := builder.Select("transaction_id").From("transaction_picture_info").Where(builder.And(builder.Eq{"uid": uid}, builder.Eq{"deleted": false}, builder.Neq{"transaction_id": models.TransactionPictureNewPictureTransactionId}))
sess.And(builder.Or(builder.In("transaction_id", subQuery), builder.In("related_id", subQuery)))
return sess
}
func (s *TransactionService) isAccountIdValid(transaction *models.Transaction) error { func (s *TransactionService) isAccountIdValid(transaction *models.Transaction) error {
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE { if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
if transaction.RelatedAccountId != 0 && transaction.RelatedAccountId != transaction.AccountId { if transaction.RelatedAccountId != 0 && transaction.RelatedAccountId != transaction.AccountId {
+97
View File
@@ -0,0 +1,97 @@
<template>
<div :class="imageBoxClass" :style="style">
<img class="image-with-placeholder" :class="{ 'image-loading': loading }"
:src="src" :alt="alt" v-if="!link && !loadError"
@load="onLoad" @error="onError"/>
<f7-link class="image-link" :class="{ 'image-loading': loading }"
:href="link" v-if="link && !loadError">
<img class="image-with-placeholder" :src="src" :alt="alt"
@load="onLoad" @error="onError"/>
</f7-link>
<div class="image-loading-hint" v-if="loading && !loadError">
<f7-preloader size="28" />
</div>
<div class="image-error-hint" v-if="!link && !loading && loadError">
<slot name="error"></slot>
</div>
<f7-link class="image-error-hint" :href="link" v-if="link && !loading && loadError">
<slot name="error"></slot>
</f7-link>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
const props = defineProps<{
src: string;
class?: string;
style?: string
alt?: string;
link?: string;
}>();
const loading = ref<boolean>(true);
const loadError = ref<boolean>(false);
const imageBoxClass = computed<string>(() => {
let classes = 'image-box';
if (props.class) {
classes += ` ${props.class}`;
}
return classes;
});
function onLoad(): void {
loading.value = false;
}
function onError(): void {
loading.value = false;
loadError.value = true;
}
watch(() => props.src, () => {
loading.value = true;
loadError.value = false;
});
</script>
<style scoped>
.image-box > .image-with-placeholder,
.image-box > .image-link,
.image-box > .image-link > .image-with-placeholder {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
.image-box > .image-with-placeholder.image-loading,
.image-box > .image-link.image-loading {
display: none !important;
}
.image-box > .image-loading-hint {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.image-box > .image-error-hint {
display: flex;
position: absolute;
inset: 0;
align-items: center;
justify-content: center;
text-align: center;
font-size: var(--f7-list-item-footer-font-size);
color: var(--f7-list-item-footer-text-color);
padding: 4px;
overflow: hidden;
}
</style>
+2 -2
View File
@@ -526,13 +526,13 @@ export default {
const tagFilter = encodeURIComponent(req.tagFilter); const tagFilter = encodeURIComponent(req.tagFilter);
const amountFilter = encodeURIComponent(req.amountFilter); const amountFilter = encodeURIComponent(req.amountFilter);
const keyword = encodeURIComponent(req.keyword); const keyword = encodeURIComponent(req.keyword);
return axios.get<ApiResponse<TransactionInfoPageWrapperResponse>>(`v1/transactions/list.json?max_time=${req.maxTime}&min_time=${req.minTime}&type=${req.type}&category_ids=${req.categoryIds}&account_ids=${req.accountIds}&tag_filter=${tagFilter}&amount_filter=${amountFilter}&keyword=${keyword}&count=${req.count}&page=${req.page}&with_count=${req.withCount}&trim_account=true&trim_category=true&trim_tag=true`); return axios.get<ApiResponse<TransactionInfoPageWrapperResponse>>(`v1/transactions/list.json?max_time=${req.maxTime}&min_time=${req.minTime}&type=${req.type}&category_ids=${req.categoryIds}&account_ids=${req.accountIds}&tag_filter=${tagFilter}&amount_filter=${amountFilter}&keyword=${keyword}&must_have_pictures=${!!req.mustHavePictures}&count=${req.count}&page=${req.page}&with_count=${req.withCount}&with_pictures=${!!req.withPictures}&trim_account=true&trim_category=true&trim_tag=true`);
}, },
getAllTransactionsByMonth: (req: TransactionListInMonthByPageRequest): ApiResponsePromise<TransactionInfoPageWrapperResponse2> => { getAllTransactionsByMonth: (req: TransactionListInMonthByPageRequest): ApiResponsePromise<TransactionInfoPageWrapperResponse2> => {
const tagFilter = encodeURIComponent(req.tagFilter); const tagFilter = encodeURIComponent(req.tagFilter);
const amountFilter = encodeURIComponent(req.amountFilter); const amountFilter = encodeURIComponent(req.amountFilter);
const keyword = encodeURIComponent(req.keyword); const keyword = encodeURIComponent(req.keyword);
return axios.get<ApiResponse<TransactionInfoPageWrapperResponse2>>(`v1/transactions/list/by_month.json?year=${req.year}&month=${req.month}&type=${req.type}&category_ids=${req.categoryIds}&account_ids=${req.accountIds}&tag_filter=${tagFilter}&amount_filter=${amountFilter}&keyword=${keyword}&trim_account=true&trim_category=true&trim_tag=true`); return axios.get<ApiResponse<TransactionInfoPageWrapperResponse2>>(`v1/transactions/list/by_month.json?year=${req.year}&month=${req.month}&type=${req.type}&category_ids=${req.categoryIds}&account_ids=${req.accountIds}&tag_filter=${tagFilter}&amount_filter=${amountFilter}&keyword=${keyword}&must_have_pictures=${!!req.mustHavePictures}&with_pictures=${!!req.withPictures}&trim_account=true&trim_category=true&trim_tag=true`);
}, },
getAllTransactions: (req: TransactionAllListRequest): ApiResponsePromise<TransactionInfoResponse[]> => { getAllTransactions: (req: TransactionAllListRequest): ApiResponsePromise<TransactionInfoResponse[]> => {
return axios.get<ApiResponse<TransactionInfoResponse[]>>(`v1/transactions/list/all.json?trim_account=true&with_pictures=${!!req.withPictures}&trim_category=true&trim_tag=true&start_time=${req.startTime}&end_time=${req.endTime}`); return axios.get<ApiResponse<TransactionInfoResponse[]>>(`v1/transactions/list/all.json?trim_account=true&with_pictures=${!!req.withPictures}&trim_category=true&trim_tag=true&start_time=${req.startTime}&end_time=${req.endTime}`);
+11 -1
View File
@@ -3,7 +3,7 @@ import { TransactionType } from '@/core/transaction.ts';
import { Account } from '@/models/account.ts'; import { Account } from '@/models/account.ts';
import { TransactionCategory } from '@/models/transaction_category.ts'; import { TransactionCategory } from '@/models/transaction_category.ts';
import { TransactionTag } from '@/models/transaction_tag.ts'; import { TransactionTag } from '@/models/transaction_tag.ts';
import { TransactionPicture } from '@/models/transaction_picture_info.ts'; import { TransactionPicture, type TransactionPictureInfoBasicResponse } from '@/models/transaction_picture_info.ts';
import { Transaction } from '@/models/transaction.ts'; import { Transaction } from '@/models/transaction.ts';
import { import {
@@ -32,6 +32,16 @@ export interface SetTransactionOptions {
comment?: string; comment?: string;
} }
export function* allTransactionPictures(transactions: Transaction[]): Iterable<[Transaction, TransactionPictureInfoBasicResponse]> {
for (const transaction of transactions) {
if (transaction.pictures) {
for (const pictureInfo of transaction.pictures) {
yield [transaction, pictureInfo];
}
}
}
}
export function setTransactionModelByTransaction(transaction: Transaction, transaction2: Transaction | null | undefined, allCategories: Record<number, TransactionCategory[]>, allCategoriesMap: Record<string, TransactionCategory>, allVisibleAccounts: Account[], allAccountsMap: Record<string, Account>, allTagsMap: Record<string, TransactionTag>, defaultAccountId: string, options: SetTransactionOptions, setContextData: boolean): void { export function setTransactionModelByTransaction(transaction: Transaction, transaction2: Transaction | null | undefined, allCategories: Record<number, TransactionCategory[]>, allCategoriesMap: Record<string, TransactionCategory>, allVisibleAccounts: Account[], allAccountsMap: Record<string, Account>, allTagsMap: Record<string, TransactionTag>, defaultAccountId: string, options: SetTransactionOptions, setContextData: boolean): void {
if (isDefined(options.time)) { if (isDefined(options.time)) {
transaction.time = options.time; transaction.time = options.time;
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Details anzeigen", "View Details": "Details anzeigen",
"Transaction List": "Transaktionsliste", "Transaction List": "Transaktionsliste",
"Transaction Calendar": "Transaktionskalender", "Transaction Calendar": "Transaktionskalender",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Transaktionsdetails", "Transaction Details": "Transaktionsdetails",
"Statistics & Analysis": "Statistiken & Analysen", "Statistics & Analysis": "Statistiken & Analysen",
"Insights Explorer": "Daten-Explorer", "Insights Explorer": "Daten-Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "View Details", "View Details": "View Details",
"Transaction List": "Transaction List", "Transaction List": "Transaction List",
"Transaction Calendar": "Transaction Calendar", "Transaction Calendar": "Transaction Calendar",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Transaction Details", "Transaction Details": "Transaction Details",
"Statistics & Analysis": "Statistics & Analysis", "Statistics & Analysis": "Statistics & Analysis",
"Insights Explorer": "Insights Explorer", "Insights Explorer": "Insights Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Ver Detalles", "View Details": "Ver Detalles",
"Transaction List": "Listado de Transacciones", "Transaction List": "Listado de Transacciones",
"Transaction Calendar": "Calendario de Transacciones", "Transaction Calendar": "Calendario de Transacciones",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Detalles", "Transaction Details": "Detalles",
"Statistics & Analysis": "Estadísticas y Análisis", "Statistics & Analysis": "Estadísticas y Análisis",
"Insights Explorer": "Explorador de Datos", "Insights Explorer": "Explorador de Datos",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Voir les détails", "View Details": "Voir les détails",
"Transaction List": "Liste des transactions", "Transaction List": "Liste des transactions",
"Transaction Calendar": "Calendrier des transactions", "Transaction Calendar": "Calendrier des transactions",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Détails de la transaction", "Transaction Details": "Détails de la transaction",
"Statistics & Analysis": "Statistiques et analyse", "Statistics & Analysis": "Statistiques et analyse",
"Insights Explorer": "Insights Explorer", "Insights Explorer": "Insights Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Visualizza dettagli", "View Details": "Visualizza dettagli",
"Transaction List": "Elenco transazioni", "Transaction List": "Elenco transazioni",
"Transaction Calendar": "Transaction Calendar", "Transaction Calendar": "Transaction Calendar",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Dettagli transazione", "Transaction Details": "Dettagli transazione",
"Statistics & Analysis": "Statistiche e analisi", "Statistics & Analysis": "Statistiche e analisi",
"Insights Explorer": "Insights Explorer", "Insights Explorer": "Insights Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "詳細を表示", "View Details": "詳細を表示",
"Transaction List": "取引リスト", "Transaction List": "取引リスト",
"Transaction Calendar": "Transaction Calendar", "Transaction Calendar": "Transaction Calendar",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "取引の詳細", "Transaction Details": "取引の詳細",
"Statistics & Analysis": "統計と分析", "Statistics & Analysis": "統計と分析",
"Insights Explorer": "Insights Explorer", "Insights Explorer": "Insights Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "ವಿವರಗಳನ್ನು ನೋಡಿ", "View Details": "ವಿವರಗಳನ್ನು ನೋಡಿ",
"Transaction List": "ವಹಿವಾಟು ಪಟ್ಟಿ", "Transaction List": "ವಹಿವಾಟು ಪಟ್ಟಿ",
"Transaction Calendar": "ವಹಿವಾಟು ಕ್ಯಾಲೆಂಡರ್", "Transaction Calendar": "ವಹಿವಾಟು ಕ್ಯಾಲೆಂಡರ್",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "ವಹಿವಾಟಿನ ವಿವರಗಳು", "Transaction Details": "ವಹಿವಾಟಿನ ವಿವರಗಳು",
"Statistics & Analysis": "ಸಂಖ್ಯಾಶಾಸ್ತ್ರ ಮತ್ತು ವಿಶ್ಲೇಷಣೆ", "Statistics & Analysis": "ಸಂಖ್ಯಾಶಾಸ್ತ್ರ ಮತ್ತು ವಿಶ್ಲೇಷಣೆ",
"Insights Explorer": "Insights Explorer", "Insights Explorer": "Insights Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "세부사항 보기", "View Details": "세부사항 보기",
"Transaction List": "거래 목록", "Transaction List": "거래 목록",
"Transaction Calendar": "거래 달력", "Transaction Calendar": "거래 달력",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "거래 세부사항", "Transaction Details": "거래 세부사항",
"Statistics & Analysis": "통계 및 분석", "Statistics & Analysis": "통계 및 분석",
"Insights Explorer": "인사이트 탐색기", "Insights Explorer": "인사이트 탐색기",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Details bekijken", "View Details": "Details bekijken",
"Transaction List": "Transactielijst", "Transaction List": "Transactielijst",
"Transaction Calendar": "Transactiekalender", "Transaction Calendar": "Transactiekalender",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Transactiedetails", "Transaction Details": "Transactiedetails",
"Statistics & Analysis": "Statistieken & Analyse", "Statistics & Analysis": "Statistieken & Analyse",
"Insights Explorer": "Insights Explorer", "Insights Explorer": "Insights Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Ver Detalhes", "View Details": "Ver Detalhes",
"Transaction List": "Lista de Transações", "Transaction List": "Lista de Transações",
"Transaction Calendar": "Calendário de Transações", "Transaction Calendar": "Calendário de Transações",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Detalhes da Transação", "Transaction Details": "Detalhes da Transação",
"Statistics & Analysis": "Estatísticas e Análise", "Statistics & Analysis": "Estatísticas e Análise",
"Insights Explorer": "Explorador de Insights", "Insights Explorer": "Explorador de Insights",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Просмотреть детали", "View Details": "Просмотреть детали",
"Transaction List": "Список транзакций", "Transaction List": "Список транзакций",
"Transaction Calendar": "Календарь транзакций", "Transaction Calendar": "Календарь транзакций",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Детали транзакции", "Transaction Details": "Детали транзакции",
"Statistics & Analysis": "Статистика и анализ", "Statistics & Analysis": "Статистика и анализ",
"Insights Explorer": "Исследователь идей", "Insights Explorer": "Исследователь идей",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Ogled podrobnosti", "View Details": "Ogled podrobnosti",
"Transaction List": "Seznam transakcij", "Transaction List": "Seznam transakcij",
"Transaction Calendar": "Koledar transakcij", "Transaction Calendar": "Koledar transakcij",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Podrobnosti transakcije", "Transaction Details": "Podrobnosti transakcije",
"Statistics & Analysis": "Statistika in analiza", "Statistics & Analysis": "Statistika in analiza",
"Insights Explorer": "Vpogledi in raziskovanje", "Insights Explorer": "Vpogledi in raziskovanje",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "விவரங்களை பார்க்கவும்", "View Details": "விவரங்களை பார்க்கவும்",
"Transaction List": "பரிவர்த்தனை பட்டியல்", "Transaction List": "பரிவர்த்தனை பட்டியல்",
"Transaction Calendar": "பரிவர்த்தனை நாட்காட்டி", "Transaction Calendar": "பரிவர்த்தனை நாட்காட்டி",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "பரிவர்த்தனையின் விவரங்கள்", "Transaction Details": "பரிவர்த்தனையின் விவரங்கள்",
"Statistics & Analysis": "புள்ளியியல் மற்றும் பகுப்பாய்வு", "Statistics & Analysis": "புள்ளியியல் மற்றும் பகுப்பாய்வு",
"Insights Explorer": "நுண்ணறிவு ஆய்வுக்கருவி", "Insights Explorer": "நுண்ணறிவு ஆய்வுக்கருவி",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "ดูรายละเอียด", "View Details": "ดูรายละเอียด",
"Transaction List": "รายการธุรกรรม", "Transaction List": "รายการธุรกรรม",
"Transaction Calendar": "ปฏิทินธุรกรรม", "Transaction Calendar": "ปฏิทินธุรกรรม",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "รายละเอียดธุรกรรม", "Transaction Details": "รายละเอียดธุรกรรม",
"Statistics & Analysis": "สถิติ & การวิเคราะห์", "Statistics & Analysis": "สถิติ & การวิเคราะห์",
"Insights Explorer": "Insights Explorer", "Insights Explorer": "Insights Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Detayları Gör", "View Details": "Detayları Gör",
"Transaction List": "İşlem Listesi", "Transaction List": "İşlem Listesi",
"Transaction Calendar": "İşlem Takvimi", "Transaction Calendar": "İşlem Takvimi",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "İşlem Detayları", "Transaction Details": "İşlem Detayları",
"Statistics & Analysis": "İstatistik & Analiz", "Statistics & Analysis": "İstatistik & Analiz",
"Insights Explorer": "Insights Explorer", "Insights Explorer": "Insights Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Переглянути деталі", "View Details": "Переглянути деталі",
"Transaction List": "Список транзакцій", "Transaction List": "Список транзакцій",
"Transaction Calendar": "Transaction Calendar", "Transaction Calendar": "Transaction Calendar",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Деталі по транзакціях", "Transaction Details": "Деталі по транзакціях",
"Statistics & Analysis": "Статистика та аналіз", "Statistics & Analysis": "Статистика та аналіз",
"Insights Explorer": "Insights Explorer", "Insights Explorer": "Insights Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Xem chi tiết", "View Details": "Xem chi tiết",
"Transaction List": "Danh sách giao dịch", "Transaction List": "Danh sách giao dịch",
"Transaction Calendar": "Transaction Calendar", "Transaction Calendar": "Transaction Calendar",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Chi tiết giao dịch", "Transaction Details": "Chi tiết giao dịch",
"Statistics & Analysis": "Thống kê & Phân tích", "Statistics & Analysis": "Thống kê & Phân tích",
"Insights Explorer": "Insights Explorer", "Insights Explorer": "Insights Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "查看详情", "View Details": "查看详情",
"Transaction List": "交易列表", "Transaction List": "交易列表",
"Transaction Calendar": "交易日历", "Transaction Calendar": "交易日历",
"Transaction Gallery": "交易相册",
"Transaction Details": "交易详情", "Transaction Details": "交易详情",
"Statistics & Analysis": "统计分析", "Statistics & Analysis": "统计分析",
"Insights Explorer": "洞察探索", "Insights Explorer": "洞察探索",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "查看詳情", "View Details": "查看詳情",
"Transaction List": "交易清單", "Transaction List": "交易清單",
"Transaction Calendar": "交易日曆", "Transaction Calendar": "交易日曆",
"Transaction Gallery": "交易相簿",
"Transaction Details": "交易詳情", "Transaction Details": "交易詳情",
"Statistics & Analysis": "統計分析", "Statistics & Analysis": "統計分析",
"Insights Explorer": "洞察探索", "Insights Explorer": "洞察探索",
+2
View File
@@ -55,6 +55,7 @@ import MonthPicker from '@/components/common/MonthPicker.vue';
import TransactionCalendar from '@/components/common/TransactionCalendar.vue'; import TransactionCalendar from '@/components/common/TransactionCalendar.vue';
import ItemIcon from '@/components/mobile/ItemIcon.vue'; import ItemIcon from '@/components/mobile/ItemIcon.vue';
import ImageBox from '@/components/mobile/ImageBox.vue';
import LanguageSelectButton from '@/components/mobile/LanguageSelectButton.vue'; import LanguageSelectButton from '@/components/mobile/LanguageSelectButton.vue';
import PieChart from '@/components/mobile/PieChart.vue'; import PieChart from '@/components/mobile/PieChart.vue';
import TrendsBarChart from '@/components/mobile/TrendsBarChart.vue'; import TrendsBarChart from '@/components/mobile/TrendsBarChart.vue';
@@ -150,6 +151,7 @@ app.component('MonthPicker', MonthPicker);
app.component('TransactionCalendar', TransactionCalendar); app.component('TransactionCalendar', TransactionCalendar);
app.component('ItemIcon', ItemIcon); app.component('ItemIcon', ItemIcon);
app.component('ImageBox', ImageBox);
app.component('LanguageSelectButton', LanguageSelectButton); app.component('LanguageSelectButton', LanguageSelectButton);
app.component('PieChart', PieChart); app.component('PieChart', PieChart);
app.component('TrendsBarChart', TrendsBarChart); app.component('TrendsBarChart', TrendsBarChart);
+4
View File
@@ -614,6 +614,8 @@ export interface TransactionListByMaxTimeRequest {
readonly tagFilter: string; readonly tagFilter: string;
readonly amountFilter: string; readonly amountFilter: string;
readonly keyword: string; readonly keyword: string;
readonly mustHavePictures?: boolean;
readonly withPictures?: boolean;
} }
export interface TransactionListInMonthByPageRequest { export interface TransactionListInMonthByPageRequest {
@@ -625,6 +627,8 @@ export interface TransactionListInMonthByPageRequest {
readonly tagFilter: string; readonly tagFilter: string;
readonly amountFilter: string; readonly amountFilter: string;
readonly keyword: string; readonly keyword: string;
readonly mustHavePictures?: boolean;
readonly withPictures?: boolean;
} }
export interface TransactionAllListRequest { export interface TransactionAllListRequest {
+7 -3
View File
@@ -827,7 +827,7 @@ export const useTransactionsStore = defineStore('transactions', () => {
}; };
} }
function loadTransactions({ reload, count, page, withCount, autoExpand, defaultCurrency }: { reload?: boolean, count?: number, page?: number, withCount?: boolean, autoExpand: boolean, defaultCurrency: string }): Promise<TransactionPageWrapper> { function loadTransactions({ reload, count, page, mustHavePictures, withCount, withPictures, autoExpand, defaultCurrency }: { reload?: boolean, count?: number, page?: number, mustHavePictures?: boolean, withCount?: boolean, withPictures?: boolean, autoExpand: boolean, defaultCurrency: string }): Promise<TransactionPageWrapper> {
let actualMaxTime = transactionsNextTimeId.value; let actualMaxTime = transactionsNextTimeId.value;
if (reload && transactionsFilter.value.maxTime > 0) { if (reload && transactionsFilter.value.maxTime > 0) {
@@ -843,6 +843,8 @@ export const useTransactionsStore = defineStore('transactions', () => {
count: count || 50, count: count || 50,
page: page || 1, page: page || 1,
withCount: !!withCount, withCount: !!withCount,
withPictures: !!withPictures,
mustHavePictures: !!mustHavePictures,
type: transactionsFilter.value.type, type: transactionsFilter.value.type,
categoryIds: transactionsFilter.value.categoryIds, categoryIds: transactionsFilter.value.categoryIds,
accountIds: transactionsFilter.value.accountIds, accountIds: transactionsFilter.value.accountIds,
@@ -917,7 +919,7 @@ export const useTransactionsStore = defineStore('transactions', () => {
}); });
} }
function loadMonthlyAllTransactions({ year, month, autoExpand, defaultCurrency }: { year: number, month: number, autoExpand: boolean, defaultCurrency: string }): Promise<TransactionPageWrapper> { function loadMonthlyAllTransactions({ year, month, mustHavePictures, withPictures, autoExpand, defaultCurrency }: { year: number, month: number, mustHavePictures?: boolean, withPictures?: boolean, autoExpand: boolean, defaultCurrency: string }): Promise<TransactionPageWrapper> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
services.getAllTransactionsByMonth({ services.getAllTransactionsByMonth({
year: year, year: year,
@@ -927,7 +929,9 @@ export const useTransactionsStore = defineStore('transactions', () => {
accountIds: transactionsFilter.value.accountIds, accountIds: transactionsFilter.value.accountIds,
tagFilter: transactionsFilter.value.tagFilter, tagFilter: transactionsFilter.value.tagFilter,
amountFilter: transactionsFilter.value.amountFilter, amountFilter: transactionsFilter.value.amountFilter,
keyword: transactionsFilter.value.keyword keyword: transactionsFilter.value.keyword,
mustHavePictures: !!mustHavePictures,
withPictures: !!withPictures
}).then(response => { }).then(response => {
const data = response.data; const data = response.data;
@@ -22,6 +22,7 @@ import type { TransactionCategory } from '@/models/transaction_category.ts';
import { TransactionTagGroup } from '@/models/transaction_tag_group.ts'; import { TransactionTagGroup } from '@/models/transaction_tag_group.ts';
import type { TransactionTag } from '@/models/transaction_tag.ts'; import type { TransactionTag } from '@/models/transaction_tag.ts';
import { type Transaction, TransactionTagFilter } from '@/models/transaction.ts'; import { type Transaction, TransactionTagFilter } from '@/models/transaction.ts';
import type { TransactionPictureInfoBasicResponse } from '@/models/transaction_picture_info.ts';
import { import {
getUtcOffsetByUtcOffsetMinutes, getUtcOffsetByUtcOffsetMinutes,
@@ -49,6 +50,7 @@ export class TransactionListPageType implements TypeAndName {
public static readonly List = new TransactionListPageType(0, 'Transaction List'); public static readonly List = new TransactionListPageType(0, 'Transaction List');
public static readonly Calendar = new TransactionListPageType(1, 'Transaction Calendar'); public static readonly Calendar = new TransactionListPageType(1, 'Transaction Calendar');
public static readonly Gallery = new TransactionListPageType(2, 'Transaction Gallery');
public static readonly Default = TransactionListPageType.List; public static readonly Default = TransactionListPageType.List;
@@ -401,6 +403,10 @@ export function useTransactionListPageBase() {
} }
} }
function getTransactionPictureUrl(pictureInfo?: TransactionPictureInfoBasicResponse | null): string | undefined {
return transactionsStore.getTransactionPictureUrl(pictureInfo);
}
return { return {
// states // states
pageType, pageType,
@@ -458,5 +464,6 @@ export function useTransactionListPageBase() {
getDisplayAmount, getDisplayAmount,
getDisplayMonthTotalAmount, getDisplayMonthTotalAmount,
getTransactionTypeName, getTransactionTypeName,
getTransactionPictureUrl
}; };
} }
+81 -6
View File
@@ -31,7 +31,7 @@
v-model="queryType" v-model="queryType"
/> />
</div> </div>
<div class="mx-6 mt-4" v-if="pageType === TransactionListPageType.List.type"> <div class="mx-6 mt-4" v-if="pageType === TransactionListPageType.List.type || pageType === TransactionListPageType.Gallery.type">
<span class="text-subtitle-2">{{ tt('Transactions Per Page') }}</span> <span class="text-subtitle-2">{{ tt('Transactions Per Page') }}</span>
<v-select class="mt-2" density="compact" <v-select class="mt-2" density="compact"
item-title="name" item-title="name"
@@ -182,7 +182,7 @@
v-model="currentCalendarDate"></transaction-calendar> v-model="currentCalendarDate"></transaction-calendar>
</v-card-text> </v-card-text>
<v-table class="transaction-table" :hover="!loading"> <v-table class="transaction-table" :hover="!loading" v-if="pageType !== TransactionListPageType.Gallery.type">
<thead> <thead>
<tr> <tr>
<th class="transaction-table-column-time text-no-wrap"> <th class="transaction-table-column-time text-no-wrap">
@@ -595,7 +595,53 @@
</tbody> </tbody>
</v-table> </v-table>
<div class="mt-2 mb-4" v-if="pageType === TransactionListPageType.List.type"> <v-card-text class="transaction-gallery-container" v-if="pageType === TransactionListPageType.Gallery.type">
<div v-if="loading && (!transactions || !transactions.length || transactions.length < 1)">
<v-skeleton-loader class="skeleton-no-margin mt-2" type="text" :loading="true"></v-skeleton-loader>
</div>
<div v-if="!loading && (!transactions || !transactions.length || transactions.length < 1)">
{{ tt('No transaction data') }}
</div>
<div :key="date" :class="{ 'disabled': loading }"
v-for="(transactions, date) in transactionsByDay">
<div class="text-sm text-body-2 font-weight-bold">
<div class="d-flex align-center">
<span>{{ getDisplayLongDate(transactions[0] as Transaction) }}</span>
<v-chip class="ms-1" color="default" size="x-small"
v-if="(transactions[0] as Transaction).displayDayOfWeek">
{{ getWeekdayLongName((transactions[0] as Transaction).displayDayOfWeek as WeekDay) }}
</v-chip>
</div>
</div>
<div class="d-flex flex-wrap gap-2 py-2">
<v-avatar rounded="lg" variant="tonal" size="160"
class="cursor-pointer transaction-picture" color="rgba(0,0,0,0)"
:key="pictureInfo.pictureId"
v-for="[transaction, pictureInfo] in allTransactionPictures(transactions)"
@click="show(transaction)">
<v-img :src="getTransactionPictureUrl(pictureInfo)">
<template #placeholder>
<div class="d-flex align-center justify-center fill-height bg-light-primary">
<v-progress-circular color="grey-500" indeterminate size="48"></v-progress-circular>
</div>
</template>
<template #error>
<div class="d-flex align-center justify-center fill-height bg-light-primary">
<span class="text-body-1">{{ tt('Failed to load image, please check whether the config "domain" and "root_url" are set correctly.') }}</span>
</div>
</template>
</v-img>
<div class="picture-control-icon">
<v-icon size="64" :icon="mdiTextBoxEditOutline"/>
</div>
</v-avatar>
</div>
</div>
</v-card-text>
<div class="mt-2 mb-4" v-if="pageType === TransactionListPageType.List.type || pageType === TransactionListPageType.Gallery.type">
<pagination-buttons :totalPageCount="totalPageCount" <pagination-buttons :totalPageCount="totalPageCount"
v-model="paginationCurrentPage"></pagination-buttons> v-model="paginationCurrentPage"></pagination-buttons>
</div> </div>
@@ -681,6 +727,7 @@ import {
type Year0BasedMonth, type Year0BasedMonth,
type LocalizedRecentMonthDateRange, type LocalizedRecentMonthDateRange,
type TimeRangeAndDateType, type TimeRangeAndDateType,
type WeekDay,
DateRangeScene, DateRangeScene,
DateRange DateRange
} from '@/core/datetime.ts'; } from '@/core/datetime.ts';
@@ -720,6 +767,7 @@ import {
categoryTypeToTransactionType, categoryTypeToTransactionType,
transactionTypeToCategoryType transactionTypeToCategoryType
} from '@/lib/category.ts'; } from '@/lib/category.ts';
import { allTransactionPictures } from '@/lib/transaction.ts';
import { isDataExportingEnabled, isDataImportingEnabled, isTransactionFromAIImageRecognitionEnabled } from '@/lib/server_settings.ts'; import { isDataExportingEnabled, isDataImportingEnabled, isTransactionFromAIImageRecognitionEnabled } from '@/lib/server_settings.ts';
import { scrollToSelectedItem, startDownloadFile } from '@/lib/ui/common.ts'; import { scrollToSelectedItem, startDownloadFile } from '@/lib/ui/common.ts';
import logger from '@/lib/logger.ts'; import logger from '@/lib/logger.ts';
@@ -739,7 +787,8 @@ import {
mdiArrowRight, mdiArrowRight,
mdiPound, mdiPound,
mdiMagicStaff, mdiMagicStaff,
mdiTextBoxOutline mdiTextBoxOutline,
mdiTextBoxEditOutline
} from '@mdi/js'; } from '@mdi/js';
interface TransactionListProps { interface TransactionListProps {
@@ -830,6 +879,7 @@ const {
getDisplayAmount, getDisplayAmount,
getDisplayMonthTotalAmount, getDisplayMonthTotalAmount,
getTransactionTypeName, getTransactionTypeName,
getTransactionPictureUrl
} = useTransactionListPageBase(); } = useTransactionListPageBase();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
@@ -886,7 +936,7 @@ const allPageCounts = computed<NameNumeralValue[]>(() => {
return pageCounts; return pageCounts;
}); });
const recentMonthDateRanges = computed<LocalizedRecentMonthDateRange[]>(() => getAllRecentMonthDateRanges(pageType.value === TransactionListPageType.List.type, true)); const recentMonthDateRanges = computed<LocalizedRecentMonthDateRange[]>(() => getAllRecentMonthDateRanges(pageType.value === TransactionListPageType.List.type || pageType.value === TransactionListPageType.Gallery.type, true));
const allTransactionTemplates = computed<TransactionTemplate[]>(() => { const allTransactionTemplates = computed<TransactionTemplate[]>(() => {
const allTemplates = transactionTemplatesStore.allVisibleTemplates; const allTemplates = transactionTemplatesStore.allVisibleTemplates;
@@ -902,7 +952,7 @@ const allowCategoryTypes = computed<string>(() => {
}); });
const transactions = computed<Transaction[]>(() => { const transactions = computed<Transaction[]>(() => {
if (pageType.value === TransactionListPageType.List.type) { if (pageType.value === TransactionListPageType.List.type || pageType.value === TransactionListPageType.Gallery.type) {
if (queryMonthlyData.value) { if (queryMonthlyData.value) {
const transactionData = currentMonthTransactionData.value; const transactionData = currentMonthTransactionData.value;
@@ -942,6 +992,22 @@ const transactions = computed<Transaction[]>(() => {
} }
}); });
const transactionsByDay = computed<Record<string, Transaction[]>>(() => {
const transactionsByDay: Record<string, Transaction[]> = {};
for (const transaction of transactions.value) {
if (!transaction.gregorianCalendarYearDashMonthDashDay) {
continue;
}
const transactions: Transaction[] = transactionsByDay[transaction.gregorianCalendarYearDashMonthDashDay] ?? [];
transactions.push(transaction);
transactionsByDay[transaction.gregorianCalendarYearDashMonthDashDay] = transactions;
}
return transactionsByDay;
});
const recentDateRangeIndex = computed<number>({ const recentDateRangeIndex = computed<number>({
get: () => getRecentDateRangeIndex(recentMonthDateRanges.value, query.value.dateType, query.value.minTime, query.value.maxTime, firstDayOfWeek.value, fiscalYearStart.value), get: () => getRecentDateRangeIndex(recentMonthDateRanges.value, query.value.dateType, query.value.minTime, query.value.maxTime, firstDayOfWeek.value, fiscalYearStart.value),
set: (value) => { set: (value) => {
@@ -1166,6 +1232,7 @@ function init(initProps: TransactionListProps): void {
function reload(force: boolean, init: boolean): void { function reload(force: boolean, init: boolean): void {
loading.value = true; loading.value = true;
const isGalleryMode = pageType.value === TransactionListPageType.Gallery.type;
const page = currentPage.value; const page = currentPage.value;
Promise.all([ Promise.all([
@@ -1188,6 +1255,8 @@ function reload(force: boolean, init: boolean): void {
return transactionsStore.loadMonthlyAllTransactions({ return transactionsStore.loadMonthlyAllTransactions({
year: currentYear, year: currentYear,
month: currentMonth, month: currentMonth,
mustHavePictures: isGalleryMode,
withPictures: isGalleryMode,
autoExpand: true, autoExpand: true,
defaultCurrency: defaultCurrency.value defaultCurrency: defaultCurrency.value
}); });
@@ -1196,7 +1265,9 @@ function reload(force: boolean, init: boolean): void {
reload: true, reload: true,
count: countPerPage.value, count: countPerPage.value,
page: page, page: page,
mustHavePictures: isGalleryMode,
withCount: page <= 1, withCount: page <= 1,
withPictures: isGalleryMode,
autoExpand: true, autoExpand: true,
defaultCurrency: defaultCurrency.value defaultCurrency: defaultCurrency.value
}); });
@@ -1896,4 +1967,8 @@ init(props);
.transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row > .dp__calendar_item .transaction-calendar-daily-amounts > span.transaction-calendar-daily-amount { .transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row > .dp__calendar_item .transaction-calendar-daily-amounts > span.transaction-calendar-daily-amount {
font-size: 0.95rem; font-size: 0.95rem;
} }
.transaction-gallery-container {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
}
</style> </style>
+140 -10
View File
@@ -80,10 +80,10 @@
</f7-block> </f7-block>
<div class="skeleton-text" v-if="loading"> <div class="skeleton-text" v-if="loading">
<f7-block class="combination-list-wrapper margin-vertical" :class="{ 'no-accordion-toggle': pageType !== TransactionListPageType.List.type }" <f7-block class="combination-list-wrapper margin-vertical" :class="{ 'no-accordion-toggle': pageType !== TransactionListPageType.List.type && pageType !== TransactionListPageType.Gallery.type }"
:key="blockIdx" v-for="blockIdx in (pageType === TransactionListPageType.List.type ? [ 1, 2 ] : [ 1 ])"> :key="blockIdx" v-for="blockIdx in (pageType === TransactionListPageType.List.type ? [ 1, 2 ] : [ 1 ])">
<f7-accordion-item> <f7-accordion-item>
<f7-block-title v-if="pageType === TransactionListPageType.List.type"> <f7-block-title v-if="pageType === TransactionListPageType.List.type || pageType === TransactionListPageType.Gallery.type">
<f7-accordion-toggle> <f7-accordion-toggle>
<f7-list strong inset dividers media-list <f7-list strong inset dividers media-list
class="transaction-amount-list combination-list-header combination-list-opened"> class="transaction-amount-list combination-list-header combination-list-opened">
@@ -101,7 +101,8 @@
</f7-accordion-toggle> </f7-accordion-toggle>
</f7-block-title> </f7-block-title>
<f7-accordion-content style="height: auto"> <f7-accordion-content style="height: auto">
<f7-list strong inset dividers media-list accordion-list class="transaction-info-list combination-list-content"> <f7-list strong inset dividers media-list accordion-list class="transaction-info-list combination-list-content"
v-if="pageType === TransactionListPageType.List.type || pageType === TransactionListPageType.Calendar.type">
<f7-list-item link="#" chevron-center class="transaction-info" <f7-list-item link="#" chevron-center class="transaction-info"
:key="itemIdx" v-for="itemIdx in (pageType === TransactionListPageType.List.type && blockIdx === 1 ? [ 1, 2, 3, 4, 5, 6, 7 ] : [ 1, 2, 3 ])"> :key="itemIdx" v-for="itemIdx in (pageType === TransactionListPageType.List.type && blockIdx === 1 ? [ 1, 2, 3, 4, 5, 6, 7 ] : [ 1, 2, 3 ])">
<template #media> <template #media>
@@ -147,6 +148,21 @@
</template> </template>
</f7-list-item> </f7-list-item>
</f7-list> </f7-list>
<f7-list strong inset dividers media-list accordion-list class="transaction-info-list combination-list-content transaction-gallery-list"
v-if="pageType === TransactionListPageType.Gallery.type">
<f7-list-item class="transaction-gallery-container">
<template #default>
<div class="transaction-gallery-grid">
<div class="transaction-picture-cell"
:key="itemIdx" v-for="itemIdx in [ 1, 2, 3, 4, 5 ]">
<div class="display-flex justify-content-center align-items-center" style="height: 100%">
<f7-skeleton-block class="transaction-picture-img" style="width: 40px; height: 40px; border-radius: 50%" />
</div>
</div>
</div>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-content> </f7-accordion-content>
</f7-accordion-item> </f7-accordion-item>
</f7-block> </f7-block>
@@ -156,14 +172,14 @@
<f7-list-item :title="tt('No transaction data')"></f7-list-item> <f7-list-item :title="tt('No transaction data')"></f7-list-item>
</f7-list> </f7-list>
<f7-block class="combination-list-wrapper margin-vertical" :class="{ 'no-accordion-toggle': pageType !== TransactionListPageType.List.type }" <f7-block class="combination-list-wrapper margin-vertical" :class="{ 'no-accordion-toggle': pageType !== TransactionListPageType.List.type && pageType !== TransactionListPageType.Gallery.type }"
:key="transactionMonthList.yearDashMonth" v-for="(transactionMonthList) in transactions"> :key="transactionMonthList.yearDashMonth" v-for="(transactionMonthList) in transactions">
<f7-accordion-item :opened="transactionMonthList.opened" <f7-accordion-item :opened="transactionMonthList.opened"
@accordion:open="collapseTransactionMonthList(transactionMonthList, false)" @accordion:open="collapseTransactionMonthList(transactionMonthList, false)"
@accordion:opened="onTransactionMonthListCollapseStateChanged" @accordion:opened="onTransactionMonthListCollapseStateChanged"
@accordion:close="collapseTransactionMonthList(transactionMonthList, true)" @accordion:close="collapseTransactionMonthList(transactionMonthList, true)"
@accordion:closed="onTransactionMonthListCollapseStateChanged"> @accordion:closed="onTransactionMonthListCollapseStateChanged">
<f7-block-title :id="getTransactionMonthTitleDomId(transactionMonthList.yearDashMonth)" v-if="pageType === TransactionListPageType.List.type"> <f7-block-title :id="getTransactionMonthTitleDomId(transactionMonthList.yearDashMonth)" v-if="pageType === TransactionListPageType.List.type || pageType === TransactionListPageType.Gallery.type">
<f7-accordion-toggle> <f7-accordion-toggle>
<f7-list strong inset dividers media-list <f7-list strong inset dividers media-list
class="transaction-amount-list combination-list-header" class="transaction-amount-list combination-list-header"
@@ -193,7 +209,7 @@
<f7-list strong inset dividers media-list accordion-list <f7-list strong inset dividers media-list accordion-list
class="transaction-info-list transaction-month-list combination-list-content" class="transaction-info-list transaction-month-list combination-list-content"
:id="getTransactionMonthListDomId(transactionMonthList.yearDashMonth)" :id="getTransactionMonthListDomId(transactionMonthList.yearDashMonth)"
v-if="!isTransactionMonthListInvisible(transactionMonthList)" v-if="!isTransactionMonthListInvisible(transactionMonthList) && (pageType === TransactionListPageType.List.type || pageType === TransactionListPageType.Calendar.type)"
> >
<f7-list-item swipeout chevron-center accordion-item <f7-list-item swipeout chevron-center accordion-item
class="transaction-info" class="transaction-info"
@@ -293,12 +309,33 @@
</f7-swipeout-actions> </f7-swipeout-actions>
</f7-list-item> </f7-list-item>
</f7-list> </f7-list>
<f7-list strong inset dividers media-list accordion-list
class="transaction-info-list transaction-month-list combination-list-content transaction-gallery-list"
:id="getTransactionMonthListDomId(transactionMonthList.yearDashMonth)"
v-if="!isTransactionMonthListInvisible(transactionMonthList) && (pageType === TransactionListPageType.Gallery.type)">
<f7-list-item class="transaction-gallery-container">
<template #default>
<div class="transaction-gallery-grid">
<div class="transaction-picture-cell" :key="pictureInfo.pictureId"
v-for="[transaction, pictureInfo] in allTransactionPictures(transactionMonthList.items)">
<image-box class="transaction-picture-img" alt="picture"
:link="`/transaction/detail?id=${transaction.id}&type=${transaction.type}`"
:src="getTransactionPictureUrl(pictureInfo)">
<template #error>
{{ tt('Failed to load image, please check whether the config "domain" and "root_url" are set correctly.') }}
</template>
</image-box>
</div>
</div>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-content> </f7-accordion-content>
</f7-accordion-item> </f7-accordion-item>
</f7-block> </f7-block>
<f7-block class="text-align-center" :class="{ 'disabled': loadingMore }" v-show="!loading && hasMoreTransaction" <f7-block class="text-align-center" :class="{ 'disabled': loadingMore }" v-show="!loading && hasMoreTransaction"
v-if="pageType === TransactionListPageType.List.type"> v-if="pageType === TransactionListPageType.List.type || pageType === TransactionListPageType.Gallery.type">
<f7-link href="#" @click="loadMore(false)">{{ tt('Load More') }}</f7-link> <f7-link href="#" @click="loadMore(false)">{{ tt('Load More') }}</f7-link>
</f7-block> </f7-block>
@@ -309,7 +346,7 @@
:class="{ 'list-item-selected': query.dateType === dateRange.type }" :class="{ 'list-item-selected': query.dateType === dateRange.type }"
:key="dateRange.type" :key="dateRange.type"
v-for="dateRange in allDateRanges" v-for="dateRange in allDateRanges"
v-show="pageType === TransactionListPageType.List.type || dateRange.type === DateRange.ThisMonth.type || dateRange.type === DateRange.LastMonth.type || dateRange.type === DateRange.Custom.type" v-show="pageType === TransactionListPageType.List.type || pageType === TransactionListPageType.Gallery.type || dateRange.type === DateRange.ThisMonth.type || dateRange.type === DateRange.LastMonth.type || dateRange.type === DateRange.Custom.type"
@click="changeDateFilter(dateRange.type)"> @click="changeDateFilter(dateRange.type)">
<template #after> <template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.dateType === dateRange.type"></f7-icon> <f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.dateType === dateRange.type"></f7-icon>
@@ -661,6 +698,7 @@ import {
categoryTypeToTransactionType, categoryTypeToTransactionType,
transactionTypeToCategoryType transactionTypeToCategoryType
} from '@/lib/category.ts'; } from '@/lib/category.ts';
import { allTransactionPictures } from '@/lib/transaction.ts';
const props = defineProps<{ const props = defineProps<{
f7route: Router.Route; f7route: Router.Route;
@@ -727,6 +765,7 @@ const {
getDisplayAmount, getDisplayAmount,
getDisplayMonthTotalAmount, getDisplayMonthTotalAmount,
getTransactionTypeName, getTransactionTypeName,
getTransactionPictureUrl
} = useTransactionListPageBase(); } = useTransactionListPageBase();
const environmentsStore = useEnvironmentsStore(); const environmentsStore = useEnvironmentsStore();
@@ -753,7 +792,7 @@ const transactions = computed<TransactionMonthList[]>(() => {
return []; return [];
} }
if (pageType.value === TransactionListPageType.List.type) { if (pageType.value === TransactionListPageType.List.type || pageType.value === TransactionListPageType.Gallery.type) {
return transactionsStore.transactions; return transactionsStore.transactions;
} else if (pageType.value === TransactionListPageType.Calendar.type) { } else if (pageType.value === TransactionListPageType.Calendar.type) {
if (queryMonthlyData.value) { if (queryMonthlyData.value) {
@@ -796,7 +835,7 @@ const transactions = computed<TransactionMonthList[]>(() => {
}); });
const noTransaction = computed<boolean>(() => { const noTransaction = computed<boolean>(() => {
if (pageType.value === TransactionListPageType.List.type) { if (pageType.value === TransactionListPageType.List.type || pageType.value === TransactionListPageType.Gallery.type) {
return transactionsStore.noTransaction; return transactionsStore.noTransaction;
} else if (pageType.value === TransactionListPageType.Calendar.type) { } else if (pageType.value === TransactionListPageType.Calendar.type) {
return !transactions.value || !transactions.value.length || !transactions.value[0]!.items || !transactions.value[0]!.items.length; return !transactions.value || !transactions.value.length || !transactions.value[0]!.items || !transactions.value[0]!.items.length;
@@ -962,6 +1001,8 @@ function reload(done?: () => void): void {
loading.value = true; loading.value = true;
} }
const isGalleryMode = pageType.value === TransactionListPageType.Gallery.type;
transactionInvisibleYearMonths.value = {}; transactionInvisibleYearMonths.value = {};
transactionYearMonthListHeights.value = {}; transactionYearMonthListHeights.value = {};
@@ -978,12 +1019,16 @@ function reload(done?: () => void): void {
return transactionsStore.loadMonthlyAllTransactions({ return transactionsStore.loadMonthlyAllTransactions({
year: currentYear, year: currentYear,
month: currentMonth, month: currentMonth,
mustHavePictures: isGalleryMode,
withPictures: isGalleryMode,
autoExpand: true, autoExpand: true,
defaultCurrency: defaultCurrency.value defaultCurrency: defaultCurrency.value
}); });
} else { } else {
return transactionsStore.loadTransactions({ return transactionsStore.loadTransactions({
reload: true, reload: true,
mustHavePictures: isGalleryMode,
withPictures: isGalleryMode,
autoExpand: true, autoExpand: true,
defaultCurrency: defaultCurrency.value defaultCurrency: defaultCurrency.value
}); });
@@ -1023,10 +1068,14 @@ function loadMore(autoExpand: boolean): void {
return; return;
} }
const isGalleryMode = pageType.value === TransactionListPageType.Gallery.type;
loadingMore.value = true; loadingMore.value = true;
transactionsStore.loadTransactions({ transactionsStore.loadTransactions({
reload: false, reload: false,
mustHavePictures: isGalleryMode,
withPictures: isGalleryMode,
autoExpand: autoExpand, autoExpand: autoExpand,
defaultCurrency: defaultCurrency.value defaultCurrency: defaultCurrency.value
}).then(() => { }).then(() => {
@@ -1060,6 +1109,8 @@ function changePageType(type: number): void {
reload(); reload();
} }
} }
} else {
reload();
} }
} }
@@ -1702,4 +1753,83 @@ html[dir="rtl"] .list.transaction-info-list li.transaction-info .transaction-foo
.transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row > .dp__calendar_item .transaction-calendar-daily-amounts > span.transaction-calendar-daily-amount { .transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row > .dp__calendar_item .transaction-calendar-daily-amounts > span.transaction-calendar-daily-amount {
font-size: var(--ebk-transaction-calendar-amount-font-size); font-size: var(--ebk-transaction-calendar-amount-font-size);
} }
.transaction-gallery-list.list > ul {
overflow: hidden;
}
.transaction-gallery-list.list > ul::before,
.transaction-gallery-list.list > ul::after {
display: none;
}
.transaction-gallery-container > .item-content {
padding: 0;
}
.transaction-gallery-container > .item-content > .item-inner {
padding: 0;
min-height: 0;
}
.transaction-gallery-container > .item-content > .item-inner::after {
display: none;
}
.transaction-gallery-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2px;
width: 100%;
}
@media (min-width: 480px) {
.transaction-gallery-grid {
grid-template-columns: repeat(4, 1fr);
}
}
@media (min-width: 640px) {
.transaction-gallery-grid {
grid-template-columns: repeat(5, 1fr);
}
}
@media (min-width: 800px) {
.transaction-gallery-grid {
grid-template-columns: repeat(6, 1fr);
}
}
@media (min-width: 960px) {
.transaction-gallery-grid {
grid-template-columns: repeat(7, 1fr);
}
}
@media (min-width: 1024px) {
.transaction-gallery-grid {
grid-template-columns: repeat(8, 1fr);
}
}
@media (min-width: 1280px) {
.transaction-gallery-grid {
grid-template-columns: repeat(9, 1fr);
}
}
.transaction-picture-cell {
position: relative;
display: block;
aspect-ratio: 1;
overflow: hidden;
background-color: var(--f7-list-bg-color);
}
.transaction-picture-img {
width: 100%;
height: 100%;
display: block;
}
</style> </style>