From b7973772b34bb4a3a47c1015183e3c269f0bf7a2 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Thu, 1 May 2025 01:00:11 +0800 Subject: [PATCH] show process when importing a lot of transactions --- cmd/webserver.go | 1 + pkg/api/transactions.go | 75 +++++++++++++++++-- pkg/cli/user_data.go | 2 +- pkg/core/task_process.go | 4 + pkg/errs/global.go | 1 + pkg/models/transaction.go | 5 ++ pkg/services/transactions.go | 12 ++- src/lib/services.ts | 5 ++ src/locales/de.json | 5 +- src/locales/en.json | 5 +- src/locales/es.json | 5 +- src/locales/it.json | 5 +- src/locales/ja.json | 5 +- src/locales/ru.json | 5 +- src/locales/uk.json | 5 +- src/locales/vi.json | 5 +- src/locales/zh_Hans.json | 5 +- src/locales/zh_Hant.json | 5 +- src/stores/transaction.ts | 26 +++++++ .../transactions/import/ImportDialog.vue | 48 +++++++++++- 20 files changed, 210 insertions(+), 19 deletions(-) create mode 100644 pkg/core/task_process.go diff --git a/cmd/webserver.go b/cmd/webserver.go index 5d5143b7..cd12561d 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -319,6 +319,7 @@ func startWebServer(c *core.CliContext) error { apiV1Route.POST("/transactions/parse_dsv_file.json", bindApi(api.Transactions.TransactionParseImportDsvFileDataHandler)) apiV1Route.POST("/transactions/parse_import.json", bindApi(api.Transactions.TransactionParseImportFileHandler)) apiV1Route.POST("/transactions/import.json", bindApi(api.Transactions.TransactionImportHandler)) + apiV1Route.GET("/transactions/import/process.json", bindApi(api.Transactions.TransactionImportProcessHandler)) } // Transaction Pictures diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index defd5552..defd8c49 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "fmt" "io" "sort" "strings" @@ -1344,11 +1345,21 @@ func (a *TransactionsApi) TransactionImportHandler(c *core.WebContext) (any, *er found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_IMPORT_TRANSACTIONS, uid, transactionImportReq.ClientSessionId) if found { - log.Infof(c, "[transactions.TransactionImportHandler] another \"%s\" transactions has been imported for user \"uid:%d\"", remark, uid) - count, err := utils.StringToInt(remark) + items := strings.Split(remark, ":") - if err == nil { - return count, nil + if len(items) >= 2 { + if items[0] == "finished" { + log.Infof(c, "[transactions.TransactionImportHandler] another \"%s\" transactions has been imported for user \"uid:%d\"", items[1], uid) + count, err := utils.StringToInt(items[1]) + + if err == nil { + return count, nil + } + } else if items[0] == "processing" { + return nil, errs.ErrRepeatedRequest + } + } else { + log.Warnf(c, "[transactions.TransactionImportHandler] another transaction import task may be executing, but remark \"%s\" is invalid", remark) } } } @@ -1422,7 +1433,9 @@ func (a *TransactionsApi) TransactionImportHandler(c *core.WebContext) (any, *er newTransactions[i] = transaction } - err = a.transactions.BatchCreateTransactions(c, user.Uid, newTransactions, newTransactionTagIdsMap) + err = a.transactions.BatchCreateTransactions(c, user.Uid, newTransactions, newTransactionTagIdsMap, func(currentProcess float64) { + a.SetSubmissionRemarkIfEnable(duplicatechecker.DUPLICATE_CHECKER_TYPE_IMPORT_TRANSACTIONS, uid, transactionImportReq.ClientSessionId, fmt.Sprintf("processing:%.2f", currentProcess)) + }) count := len(newTransactions) if err != nil { @@ -1432,11 +1445,61 @@ func (a *TransactionsApi) TransactionImportHandler(c *core.WebContext) (any, *er log.Infof(c, "[transactions.TransactionImportHandler] user \"uid:%d\" has imported %d transactions successfully", uid, count) - a.SetSubmissionRemarkIfEnable(duplicatechecker.DUPLICATE_CHECKER_TYPE_IMPORT_TRANSACTIONS, uid, transactionImportReq.ClientSessionId, utils.IntToString(count)) + a.SetSubmissionRemarkIfEnable(duplicatechecker.DUPLICATE_CHECKER_TYPE_IMPORT_TRANSACTIONS, uid, transactionImportReq.ClientSessionId, fmt.Sprintf("finished:%d", count)) return count, nil } +// TransactionImportProcessHandler returns the process of specified transaction import task by request parameters for current user +func (a *TransactionsApi) TransactionImportProcessHandler(c *core.WebContext) (any, *errs.Error) { + var transactionImportProcessReq models.TransactionImportProcessRequest + err := c.ShouldBindQuery(&transactionImportProcessReq) + + if err != nil { + log.Warnf(c, "[transactions.TransactionImportProcessHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + + if !a.CurrentConfig().EnableDuplicateSubmissionsCheck { + return nil, nil + } + + found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_IMPORT_TRANSACTIONS, uid, transactionImportProcessReq.ClientSessionId) + + if !found { + return nil, nil + } + + items := strings.Split(remark, ":") + + if len(items) < 2 { + return nil, nil + } + + if items[0] == "finished" { + return 100, nil + } else if items[0] != "processing" { + return nil, nil + } + + process, err := utils.StringToFloat64(items[1]) + + if err != nil { + log.Warnf(c, "[transactions.TransactionImportProcessHandler] parse process failed, because %s", err.Error()) + return nil, nil + } + + if process < 0 { + return nil, nil + } else if process >= 100 { + process = 100 + } + + return process, nil +} + func (a *TransactionsApi) filterTransactions(c *core.WebContext, uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account) []*models.Transaction { finalTransactions := make([]*models.Transaction, 0, len(transactions)) diff --git a/pkg/cli/user_data.go b/pkg/cli/user_data.go index a9819ead..9c719c1a 100644 --- a/pkg/cli/user_data.go +++ b/pkg/cli/user_data.go @@ -810,7 +810,7 @@ func (l *UserDataCli) ImportTransaction(c *core.CliContext, username string, fil return errs.ErrOperationFailed } - err = l.transactions.BatchCreateTransactions(c, user.Uid, newTransactions, newTransactionTagIdsMap) + err = l.transactions.BatchCreateTransactions(c, user.Uid, newTransactions, newTransactionTagIdsMap, nil) if err != nil { log.CliErrorf(c, "[user_data.ImportTransaction] failed to create transaction, because %s", err.Error()) diff --git a/pkg/core/task_process.go b/pkg/core/task_process.go new file mode 100644 index 00000000..126685e5 --- /dev/null +++ b/pkg/core/task_process.go @@ -0,0 +1,4 @@ +package core + +// TaskProcessUpdateHandler represents the task process update handler +type TaskProcessUpdateHandler func(currentProcess float64) diff --git a/pkg/errs/global.go b/pkg/errs/global.go index 114b54d7..4b0cd616 100644 --- a/pkg/errs/global.go +++ b/pkg/errs/global.go @@ -26,6 +26,7 @@ var ( ErrUploadedFileEmpty = NewNormalError(NormalSubcategoryGlobal, 16, http.StatusBadRequest, "uploaded file is empty") ErrExceedMaxUploadFileSize = NewNormalError(NormalSubcategoryGlobal, 17, http.StatusBadRequest, "uploaded file size exceeds the maximum allowed size") ErrFailureCountLimitReached = NewNormalError(NormalSubcategoryGlobal, 18, http.StatusBadRequest, "failure count exceeded maximum limit") + ErrRepeatedRequest = NewNormalError(NormalSubcategoryGlobal, 19, http.StatusBadRequest, "repeated request") ) // GetParameterInvalidMessage returns specific error message for invalid parameter error diff --git a/pkg/models/transaction.go b/pkg/models/transaction.go index 5923fc44..c3d27d99 100644 --- a/pkg/models/transaction.go +++ b/pkg/models/transaction.go @@ -149,6 +149,11 @@ type TransactionImportRequest struct { ClientSessionId string `json:"clientSessionId"` } +// TransactionImportProcessRequest represents all parameters of transaction import process request +type TransactionImportProcessRequest struct { + ClientSessionId string `form:"client_session_id"` +} + // TransactionCountRequest represents transaction count request type TransactionCountRequest struct { Type TransactionDbType `form:"type" binding:"min=0,max=4"` diff --git a/pkg/services/transactions.go b/pkg/services/transactions.go index 05215766..06aaf338 100644 --- a/pkg/services/transactions.go +++ b/pkg/services/transactions.go @@ -2,6 +2,7 @@ package services import ( "fmt" + "math" "strings" "time" @@ -260,8 +261,11 @@ func (s *TransactionService) CreateTransaction(c core.Context, transaction *mode } // BatchCreateTransactions saves new transactions to database -func (s *TransactionService) BatchCreateTransactions(c core.Context, uid int64, transactions []*models.Transaction, allTagIds map[int][]int64) error { +func (s *TransactionService) BatchCreateTransactions(c core.Context, uid int64, transactions []*models.Transaction, allTagIds map[int][]int64, processHandler core.TaskProcessUpdateHandler) error { now := time.Now().Unix() + currentProcess := float64(0) + processUpdateStep := int(math.Max(100.0, float64(len(transactions)/100.0))) + needTransactionUuidCount := uint16(0) needTagIndexUuidCount := uint16(0) @@ -366,6 +370,12 @@ func (s *TransactionService) BatchCreateTransactions(c core.Context, uid int64, transactionTagIds := allTransactionTagIds[transaction.TransactionId] err := s.doCreateTransaction(c, userDataDb, sess, transaction, transactionTagIndexes, transactionTagIds, nil, nil) + currentProcess = float64(i) / float64(len(transactions)) * 100 + + if processHandler != nil && i%processUpdateStep == 0 { + processHandler(currentProcess) + } + if err != nil { transactionUnixTime := utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime) transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60) diff --git a/src/lib/services.ts b/src/lib/services.ts index cfff9544..87783d1c 100644 --- a/src/lib/services.ts +++ b/src/lib/services.ts @@ -483,6 +483,11 @@ export default { timeout: DEFAULT_IMPORT_API_TIMEOUT } as ApiRequestConfig); }, + getImportTransactionsProcess: (clientSessionId: string): ApiResponsePromise => { + return axios.get>('v1/transactions/import/process.json?client_session_id=' + clientSessionId, { + ignoreError: true + } as ApiRequestConfig); + }, uploadTransactionPicture: ({ pictureFile, clientSessionId }: { pictureFile: File, clientSessionId?: string }): ApiResponsePromise => { return axios.postForm>('v1/transaction/pictures/upload.json', { picture: pictureFile, diff --git a/src/locales/de.json b/src/locales/de.json index d6fcfb76..f8f97d09 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -102,6 +102,7 @@ "selectedCount": "{count} von {totalCount} ausgewählt", "youHaveUpdatedTransactions": "Sie haben {count} Transaktionen aktualisiert", "confirmImportTransactions": "Sind Sie sicher, dass Sie {count} Transaktionen importieren möchten?", + "importingTransactions": "Importing ({process}%)", "importTransactionResult": "Sie haben {count} Transaktionen erfolgreich importiert.", "accountActivationAndResendValidationEmailTip": "Ein Aktivierungslink wurde an Ihre E-Mail-Adresse gesendet: {email}. Wenn Sie die E-Mail nicht erhalten haben, geben Sie bitte das Passwort erneut ein und klicken Sie auf die Schaltfläche unten, um die Bestätigungs-E-Mail erneut zu senden.", "resendValidationEmailTip": "Wenn Sie die E-Mail nicht erhalten haben, geben Sie bitte das Passwort erneut ein und klicken Sie auf die Schaltfläche unten, um die Bestätigungs-E-Mail an: {email} erneut zu senden." @@ -1179,7 +1180,8 @@ "no files uploaded": "Keine Dateien hochgeladen", "uploaded file is empty": "Hochgeladene Datei ist leer", "uploaded file size exceeds the maximum allowed size": "Hochgeladene Datei überschreitet die maximal zulässige Größe", - "failure count exceeded maximum limit": "Failure count exceeded maximum limit, please try again after some time" + "failure count exceeded maximum limit": "Failure count exceeded maximum limit, please try again after some time", + "repeated request": "Repeated Request" }, "parameter": { "id": "ID", @@ -1689,6 +1691,7 @@ "Transaction amount format is not set": "Transaction amount format is not set", "Cannot import invalid transactions": "Ungültige Transaktionen können nicht importiert werden", "Unable to parse import file": "Importdatei kann nicht geparst werden", + "Unable to import transactions": "Unable to import transactions", "Batch Replace Selected Expense Categories": "Ausgewählte Ausgabenkategorien im Batch ersetzen", "Batch Replace Selected Income Categories": "Ausgewählte Einnahmenkategorien im Batch ersetzen", "Batch Replace Selected Transfer Categories": "Ausgewählte Überweisungskategorien im Batch ersetzen", diff --git a/src/locales/en.json b/src/locales/en.json index ec99e22a..bb45b0e1 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -102,6 +102,7 @@ "selectedCount": "Selected {count} of {totalCount}", "youHaveUpdatedTransactions": "You have updated {count} transactions", "confirmImportTransactions": "Are you sure you want to import {count} transactions?", + "importingTransactions": "Importing ({process}%)", "importTransactionResult": "You have imported {count} transactions successfully.", "accountActivationAndResendValidationEmailTip": "Account activation link has been sent to your email address: {email}, If you don't receive the mail, please fill password again and click the button below to resend the validation mail.", "resendValidationEmailTip": "If you don't receive the mail, please fill password again and click the button below to resend the validation mail to: {email}" @@ -1179,7 +1180,8 @@ "no files uploaded": "No files uploaded", "uploaded file is empty": "Uploaded file is empty", "uploaded file size exceeds the maximum allowed size": "Uploaded file size exceeds the maximum allowed size", - "failure count exceeded maximum limit": "Failure count exceeded maximum limit, please try again after some time" + "failure count exceeded maximum limit": "Failure count exceeded maximum limit, please try again after some time", + "repeated request": "Repeated Request" }, "parameter": { "id": "ID", @@ -1689,6 +1691,7 @@ "Transaction amount format is not set": "Transaction amount format is not set", "Cannot import invalid transactions": "Cannot import invalid transactions", "Unable to parse import file": "Unable to parse import file", + "Unable to import transactions": "Unable to import transactions", "Batch Replace Selected Expense Categories": "Batch Replace Selected Expense Categories", "Batch Replace Selected Income Categories": "Batch Replace Selected Income Categories", "Batch Replace Selected Transfer Categories": "Batch Replace Selected Transfer Categories", diff --git a/src/locales/es.json b/src/locales/es.json index 66eb1a22..e5db0f90 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -101,6 +101,7 @@ "selectedCount": "Seleccionado {count} de {totalCount}", "youHaveUpdatedTransactions": "Has actualizado {count} transacciones", "confirmImportTransactions": "¿Está seguro de que desea importar {count} transacciones?", + "importingTransactions": "Importing ({process}%)", "importTransactionResult": "Ha importado {count} transacciones correctamente.", "accountActivationAndResendValidationEmailTip": "El enlace de activación de la cuenta se envió a su dirección de correo electrónico: {email}. Si no recibe el correo, ingrese la contraseña nuevamente y haga clic en el botón a continuación para reenviar el correo de validación.", "resendValidationEmailTip": "Si no recibe el correo, complete nuevamente la contraseña y haga clic en el botón a continuación para reenviar el correo de validación a: {email}" @@ -1178,7 +1179,8 @@ "no files uploaded": "No se subieron archivos", "uploaded file is empty": "El archivo subido está vacío", "uploaded file size exceeds the maximum allowed size": "El tamaño del archivo cargado excede el tamaño máximo permitido", - "failure count exceeded maximum limit": "Failure count exceeded maximum limit, please try again after some time" + "failure count exceeded maximum limit": "Failure count exceeded maximum limit, please try again after some time", + "repeated request": "Repeated Request" }, "parameter": { "id": "IDENTIFICACIÓN", @@ -1688,6 +1690,7 @@ "Transaction amount format is not set": "Transaction amount format is not set", "Cannot import invalid transactions": "No se pueden importar transacciones no válidas", "Unable to parse import file": "No se puede analizar el archivo de importación", + "Unable to import transactions": "Unable to import transactions", "Batch Replace Selected Expense Categories": "Reemplazar por lotes categorías de gastos seleccionadas", "Batch Replace Selected Income Categories": "Reemplazo por lotes de categorías de ingresos seleccionadas", "Batch Replace Selected Transfer Categories": "Reemplazar por lotes las categorías de transferencia seleccionadas", diff --git a/src/locales/it.json b/src/locales/it.json index ec3d9259..a990dbdc 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -102,6 +102,7 @@ "selectedCount": "{count} selezionati su {totalCount}", "youHaveUpdatedTransactions": "Hai aggiornato {count} transazioni", "confirmImportTransactions": "Sei sicuro di voler importare {count} transazioni?", + "importingTransactions": "Importing ({process}%)", "importTransactionResult": "Hai importato {count} transazioni.", "accountActivationAndResendValidationEmailTip": "Abbiamo inviato un link per l'attivazione del tuo account all'indirizzo {email}. Se non hai ricevuto la mail, inserisci nuovamente la password e premi il bottone per ritentare l'invio.", "resendValidationEmailTip": "Se non hai ricevuto la mail, inserisci nuovamente la password e premi il bottone per ritentare l'invio all'indirizzo: {email}" @@ -1179,7 +1180,8 @@ "no files uploaded": "Nessun file caricato", "uploaded file is empty": "Il file caricato è vuoto", "uploaded file size exceeds the maximum allowed size": "La dimensione del file caricato supera la dimensione massima consentita", - "failure count exceeded maximum limit": "Il conteggio dei fallimenti ha superato il limite massimo, riprova più tardi" + "failure count exceeded maximum limit": "Il conteggio dei fallimenti ha superato il limite massimo, riprova più tardi", + "repeated request": "Repeated Request" }, "parameter": { "id": "ID", @@ -1689,6 +1691,7 @@ "Transaction amount format is not set": "Formato importo transazione non impostato", "Cannot import invalid transactions": "Impossibile importare transazioni non valide", "Unable to parse import file": "Impossibile analizzare il file di importazione", + "Unable to import transactions": "Unable to import transactions", "Batch Replace Selected Expense Categories": "Sostituisci in blocco categorie di spesa selezionate", "Batch Replace Selected Income Categories": "Sostituisci in blocco categorie di entrata selezionate", "Batch Replace Selected Transfer Categories": "Sostituisci in blocco categorie di trasferimento selezionate", diff --git a/src/locales/ja.json b/src/locales/ja.json index 47a94083..08968ddd 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -102,6 +102,7 @@ "selectedCount": "{count} / {totalCount}を選択", "youHaveUpdatedTransactions": "{count}件の取引を更新しました", "confirmImportTransactions": "本当に{count}件の取引をインポートしますか?", + "importingTransactions": "Importing ({process}%)", "importTransactionResult": "{count}件の取引を正常にインポートしました。", "accountActivationAndResendValidationEmailTip": "アカウントの有効化リンクがメールアドレスに送信されました:{email}、メールが届かない場合はパスワードをもう一度入力して下のボタンをクリックして認証メールを再送信してください。", "resendValidationEmailTip": "メールが届かない場合は、パスワードをもう一度入力の上、以下のボタンをクリックして検証メールを再送信してください: {email}" @@ -1179,7 +1180,8 @@ "no files uploaded": "アップロードされたファイルはありません", "uploaded file is empty": "アップロードされたファイルは空です", "uploaded file size exceeds the maximum allowed size": "アップロードされたファイルが最大許容サイズを超えています", - "failure count exceeded maximum limit": "Failure count exceeded maximum limit, please try again after some time" + "failure count exceeded maximum limit": "Failure count exceeded maximum limit, please try again after some time", + "repeated request": "Repeated Request" }, "parameter": { "id": "ID", @@ -1689,6 +1691,7 @@ "Transaction amount format is not set": "Transaction amount format is not set", "Cannot import invalid transactions": "無効な取引をインポートできません", "Unable to parse import file": "インポートファイルを解析できません", + "Unable to import transactions": "Unable to import transactions", "Batch Replace Selected Expense Categories": "バッチは選択した支出カテゴリを置き換えます", "Batch Replace Selected Income Categories": "バッチは選択した収入カテゴリを置き換えます", "Batch Replace Selected Transfer Categories": "バッチは選択した振替カテゴリを置き換えます", diff --git a/src/locales/ru.json b/src/locales/ru.json index 611baaca..6e41e0ad 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -102,6 +102,7 @@ "selectedCount": "Выбрано {count} из {totalCount}", "youHaveUpdatedTransactions": "Вы обновили {count} транзакций", "confirmImportTransactions": "Вы уверены, что хотите импортировать {count} транзакций?", + "importingTransactions": "Importing ({process}%)", "importTransactionResult": "Вы успешно импортировали {count} транзакций.", "accountActivationAndResendValidationEmailTip": "Ссылка для активации учетной записи была отправлена на ваш электронный адрес: {email}. Если вы не получили письмо, заполните пароль снова и нажмите кнопку ниже, чтобы отправить письмо повторно.", "resendValidationEmailTip": "Если вы не получили письмо, заполните пароль снова и нажмите кнопку ниже, чтобы отправить письмо повторно на: {email}" @@ -1179,7 +1180,8 @@ "no files uploaded": "Файлы не загружены", "uploaded file is empty": "Загруженный файл пуст", "uploaded file size exceeds the maximum allowed size": "Размер загруженного файла превышает максимально допустимый размер", - "failure count exceeded maximum limit": "Failure count exceeded maximum limit, please try again after some time" + "failure count exceeded maximum limit": "Failure count exceeded maximum limit, please try again after some time", + "repeated request": "Repeated Request" }, "parameter": { "id": "ID", @@ -1689,6 +1691,7 @@ "Transaction amount format is not set": "Transaction amount format is not set", "Cannot import invalid transactions": "Невозможно импортировать недействительные транзакции", "Unable to parse import file": "Не удалось обработать файл импорта", + "Unable to import transactions": "Unable to import transactions", "Batch Replace Selected Expense Categories": "Пакетная замена выбранных категорий расходов", "Batch Replace Selected Income Categories": "Пакетная замена выбранных категорий доходов", "Batch Replace Selected Transfer Categories": "Пакетная замена выбранных категорий переводов", diff --git a/src/locales/uk.json b/src/locales/uk.json index 591ce040..f6ab7c2d 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -102,6 +102,7 @@ "selectedCount": "Вибрано {count} з {totalCount}", "youHaveUpdatedTransactions": "Ви оновили {count} транзакцій", "confirmImportTransactions": "Ви впевнені, що хочете імпортувати {count} транзакцій?", + "importingTransactions": "Importing ({process}%)", "importTransactionResult": "Ви успішно імпортували {count} транзакцій.", "accountActivationAndResendValidationEmailTip": "Посилання для активації облікового запису було надіслано на вашу електронну адресу: {email}. Якщо ви не отримали лист, введіть пароль ще раз і натисніть кнопку нижче, щоб надіслати лист повторно.", "resendValidationEmailTip": "Якщо ви не отримали лист, введіть пароль ще раз і натисніть кнопку нижче, щоб надіслати лист повторно на адресу: {email}" @@ -1179,7 +1180,8 @@ "no files uploaded": "Файли не завантажено", "uploaded file is empty": "Завантажений файл порожній", "uploaded file size exceeds the maximum allowed size": "Розмір завантаженого файлу перевищує максимально допустимий", - "failure count exceeded maximum limit": "Кількість невдали спроб перевищила допустимий ліміт, спробуйте пізніше" + "failure count exceeded maximum limit": "Кількість невдали спроб перевищила допустимий ліміт, спробуйте пізніше", + "repeated request": "Repeated Request" }, "parameter": { "id": "ID", @@ -1689,6 +1691,7 @@ "Transaction amount format is not set": "Не вказано формат суми транзакцій", "Cannot import invalid transactions": "Неможливо імпортувати недійсні транзакції", "Unable to parse import file": "Не вдалося обробити файл імпорту", + "Unable to import transactions": "Unable to import transactions", "Batch Replace Selected Expense Categories": "Пакетна заміна вибраних категорій витрат", "Batch Replace Selected Income Categories": "Пакетна заміна вибраних категорій доходів", "Batch Replace Selected Transfer Categories": "Пакетна заміна вибраних категорій переказів", diff --git a/src/locales/vi.json b/src/locales/vi.json index 3c669b9b..83b4a1ce 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -102,6 +102,7 @@ "selectedCount": "Đã chọn {count} trên {totalCount}", "youHaveUpdatedTransactions": "Bạn đã cập nhật {count} giao dịch", "confirmImportTransactions": "Bạn có chắc chắn muốn nhập {count} giao dịch không?", + "importingTransactions": "Importing ({process}%)", "importTransactionResult": "Bạn đã nhập thành công {count} giao dịch.", "accountActivationAndResendValidationEmailTip": "Liên kết kích hoạt tài khoản đã được gửi tới email của bạn: {email}. Nếu bạn không nhận được email, vui lòng nhập lại mật khẩu và nhấp nút bên dưới để gửi lại email xác nhận.", "resendValidationEmailTip": "Nếu bạn không nhận được email, vui lòng nhập lại mật khẩu và nhấp nút bên dưới để gửi lại email xác nhận tới: {email}" @@ -1179,7 +1180,8 @@ "no files uploaded": "Không có tệp nào được tải lên", "uploaded file is empty": "Tệp đã tải lên trống", "uploaded file size exceeds the maximum allowed size": "Kích thước tệp đã tải lên vượt quá kích thước tối đa cho phép", - "failure count exceeded maximum limit": "Failure count exceeded maximum limit, please try again after some time" + "failure count exceeded maximum limit": "Failure count exceeded maximum limit, please try again after some time", + "repeated request": "Repeated Request" }, "parameter": { "id": "ID", @@ -1689,6 +1691,7 @@ "Transaction amount format is not set": "Transaction amount format is not set", "Cannot import invalid transactions": "Không thể nhập giao dịch không hợp lệ", "Unable to parse import file": "Không thể phân tích tệp nhập", + "Unable to import transactions": "Unable to import transactions", "Batch Replace Selected Expense Categories": "Thay thế hàng loạt các danh mục chi phí đã chọn", "Batch Replace Selected Income Categories": "Thay thế hàng loạt các danh mục thu nhập đã chọn", "Batch Replace Selected Transfer Categories": "Thay thế hàng loạt các danh mục chuyển khoản đã chọn", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index ad1b2344..413bd9aa 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -102,6 +102,7 @@ "selectedCount": "已选择 {count} / {totalCount}", "youHaveUpdatedTransactions": "您已经更新 {count} 个交易", "confirmImportTransactions": "您确定要导入 {count} 个交易?", + "importingTransactions": "正在导入 ({process}%)", "importTransactionResult": "您已经成功导入 {count} 个交易。", "accountActivationAndResendValidationEmailTip": "账号激活链接已经发送到您的邮箱地址:{email},如果您没有收到邮件,请再次输入密码并点击下方的按钮重新发送验证邮件。", "resendValidationEmailTip": "如果您没有收到邮件,请再次输入密码并点击下方的按钮重新发送验证邮件到:{email}" @@ -1179,7 +1180,8 @@ "no files uploaded": "没有上传文件", "uploaded file is empty": "上传的文件为空", "uploaded file size exceeds the maximum allowed size": "上传的文件大小超出了允许的最大大小", - "failure count exceeded maximum limit": "失败次数超出最大限制,请稍后重试" + "failure count exceeded maximum limit": "失败次数超出最大限制,请稍后重试", + "repeated request": "重复的请求" }, "parameter": { "id": "ID", @@ -1689,6 +1691,7 @@ "Transaction amount format is not set": "交易金额格式没有设置", "Cannot import invalid transactions": "不能导入无效的交易", "Unable to parse import file": "无法解析导入的文件", + "Unable to import transactions": "无法导入交易", "Batch Replace Selected Expense Categories": "批量替换选中的支出分类", "Batch Replace Selected Income Categories": "批量替换选中的收入分类", "Batch Replace Selected Transfer Categories": "批量替换选中的转账分类", diff --git a/src/locales/zh_Hant.json b/src/locales/zh_Hant.json index 878a0e69..55ddb8d9 100644 --- a/src/locales/zh_Hant.json +++ b/src/locales/zh_Hant.json @@ -102,6 +102,7 @@ "selectedCount": "已選擇 {count} / {totalCount}", "youHaveUpdatedTransactions": "您已經更新 {count} 個交易", "confirmImportTransactions": "您確定要匯入 {count} 個交易?", + "importingTransactions": "正在匯入 ({process}%)", "importTransactionResult": "您已經成功匯入 {count} 個交易。", "accountActivationAndResendValidationEmailTip": "帳號啟用連結已經傳送到您的信箱地址:{email},如果您沒有收到郵件,請再次輸入密碼並點擊下方的按鈕重新發送驗證郵件。", "resendValidationEmailTip": "如果您沒有收到郵件,請再次輸入密碼並點擊下方的按鈕重新發送驗證郵件到:{email}" @@ -1179,7 +1180,8 @@ "no files uploaded": "沒有上傳檔案", "uploaded file is empty": "上傳的檔案為空", "uploaded file size exceeds the maximum allowed size": "上傳的檔案大小超出了允許的最大大小", - "failure count exceeded maximum limit": "失敗次數超出最大限制,請稍後重試" + "failure count exceeded maximum limit": "失敗次數超出最大限制,請稍後重試", + "repeated request": "重複的請求" }, "parameter": { "id": "ID", @@ -1689,6 +1691,7 @@ "Transaction amount format is not set": "交易金額格式沒有設定", "Cannot import invalid transactions": "無法匯入無效的交易", "Unable to parse import file": "無法解析匯入的檔案", + "Unable to import transactions": "無法匯入交易", "Batch Replace Selected Expense Categories": "批次替換選中的支出分類", "Batch Replace Selected Income Categories": "批次替換選中的收入分類", "Batch Replace Selected Transfer Categories": "批次替換選中的轉帳分類", diff --git a/src/stores/transaction.ts b/src/stores/transaction.ts index 6a9e0be3..fd481839 100644 --- a/src/stores/transaction.ts +++ b/src/stores/transaction.ts @@ -1143,6 +1143,31 @@ export const useTransactionsStore = defineStore('transactions', () => { }); } + function getImportTransactionsProcess({ clientSessionId }: { clientSessionId: string }): Promise { + return new Promise((resolve, reject) => { + services.getImportTransactionsProcess(clientSessionId).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to get transactions import process' }); + return; + } + + resolve(data.result); + }).catch(error => { + logger.error('Unable to get transactions import process', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to get transactions import process' }); + } else { + reject(error); + } + }); + }); + } + function uploadTransactionPicture({ pictureFile, clientSessionId }: { pictureFile: File, clientSessionId?: string }): Promise { return new Promise((resolve, reject) => { services.uploadTransactionPicture({ pictureFile, clientSessionId }).then(response => { @@ -1243,6 +1268,7 @@ export const useTransactionsStore = defineStore('transactions', () => { parseImportDsvFile, parseImportTransaction, importTransactions, + getImportTransactionsProcess, uploadTransactionPicture, removeUnusedTransactionPicture, getTransactionPictureUrl, diff --git a/src/views/desktop/transactions/import/ImportDialog.vue b/src/views/desktop/transactions/import/ImportDialog.vue index 1155b195..80d4bd02 100644 --- a/src/views/desktop/transactions/import/ImportDialog.vue +++ b/src/views/desktop/transactions/import/ImportDialog.vue @@ -786,7 +786,7 @@ - {{ tt('Import') }} + {{ (submitting && importProcess > 0 ? tt('format.misc.importingTransactions', { process: importProcess.toFixed(2) }) : tt('Import')) }} ('fileInput'); const showState = ref(false); const clientSessionId = ref(''); const currentStep = ref('uploadFile'); +const importProcess = ref(0); const fileType = ref('ezbookkeeping'); const fileSubType = ref('ezbookkeeping_csv'); const fileEncoding = ref('utf-8'); @@ -1927,6 +1928,7 @@ function open(): Promise { fileSubType.value = 'ezbookkeeping_csv'; fileEncoding.value = 'utf-8'; currentStep.value = 'uploadFile'; + importProcess.value = 0; importFile.value = null; importData.value = ''; parsedFileData.value = undefined; @@ -2221,10 +2223,48 @@ function submit(): void { editingTags.value = []; submitting.value = true; + let showProcessTimer : number | undefined = undefined; + + if (transactions.length > 100) { + setTimeout(() => { + if (!submitting.value) { + logger.warn('transaction import is not submitting'); + return; + } + + // @ts-expect-error the return value of setInterval is number, but lint shows it as NodeJS.Timer + showProcessTimer = setInterval(() => { + if (submitting.value) { + transactionsStore.getImportTransactionsProcess({ + clientSessionId: clientSessionId.value + }).then(response => { + if (isNumber(response) && 0 <= response && response < 100) { + importProcess.value = response; + } else { + importProcess.value = 0; + clearInterval(showProcessTimer); + showProcessTimer = undefined; + } + }).catch(() => { + importProcess.value = 0; + clearInterval(showProcessTimer); + showProcessTimer = undefined; + }); + } + }, 2000); + }, 2000); + } + transactionsStore.importTransactions({ transactions: transactions, clientSessionId: clientSessionId.value }).then(response => { + if (showProcessTimer) { + importProcess.value = 0; + clearInterval(showProcessTimer); + showProcessTimer = undefined; + } + importedCount.value = response; currentStep.value = 'finalResult'; @@ -2235,6 +2275,12 @@ function submit(): void { submitting.value = false; }).catch(error => { + if (showProcessTimer) { + importProcess.value = 0; + clearInterval(showProcessTimer); + showProcessTimer = undefined; + } + submitting.value = false; if (!error.processed) {