From a0bc9db9a69886fadf0c27a7863181e9a7719b8a Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 3 Nov 2025 21:40:24 +0200 Subject: [PATCH 1/8] feat: bulk transcations delete --- .../server/src/common/dtos/BulkDelete.dto.ts | 43 +++++++++++ .../modules/Accounts/Accounts.controller.ts | 36 +++++++++ .../src/modules/Accounts/Accounts.module.ts | 6 +- .../Accounts/AccountsApplication.service.ts | 25 ++++++- .../Accounts/BulkDeleteAccounts.service.ts | 33 +++++++++ .../ValidateBulkDeleteAccounts.service.ts | 63 ++++++++++++++++ .../modules/Bills/BulkDeleteBills.service.ts | 28 +++++++ .../Bills/ValidateBulkDeleteBills.service.ts | 51 +++++++++++++ .../BulkDeleteCreditNotes.service.ts | 30 ++++++++ .../ValidateBulkDeleteCreditNotes.service.ts | 51 +++++++++++++ .../Expenses/BulkDeleteExpenses.service.ts | 28 +++++++ .../ValidateBulkDeleteExpenses.service.ts | 51 +++++++++++++ .../BulkDeleteItemCategories.service.ts | 30 ++++++++ ...alidateBulkDeleteItemCategories.service.ts | 51 +++++++++++++ .../modules/Items/BulkDeleteItems.service.ts | 38 ++++++++++ .../src/modules/Items/Item.controller.ts | 36 +++++++++ .../server/src/modules/Items/Items.module.ts | 8 +- .../modules/Items/ItemsApplication.service.ts | 29 +++++++- .../Items/ValidateBulkDeleteItems.service.ts | 74 +++++++++++++++++++ .../modules/Items/dtos/BulkDeleteItems.dto.ts | 43 +++++++++++ .../BulkDeleteManualJournals.service.ts | 32 ++++++++ ...alidateBulkDeleteManualJournals.service.ts | 55 ++++++++++++++ .../BulkDeletePaymentReceived.service.ts | 32 ++++++++ ...lidateBulkDeletePaymentReceived.service.ts | 55 ++++++++++++++ .../BulkDeleteSaleEstimates.service.ts | 28 +++++++ ...ValidateBulkDeleteSaleEstimates.service.ts | 51 +++++++++++++ .../BulkDeleteSaleInvoices.service.ts | 28 +++++++ .../ValidateBulkDeleteSaleInvoices.service.ts | 51 +++++++++++++ .../BulkDeleteVendorCredits.service.ts | 30 ++++++++ ...ValidateBulkDeleteVendorCredits.service.ts | 55 ++++++++++++++ .../JournalsLanding/ManualJournalsAlerts.tsx | 5 ++ .../Accounting/JournalsLanding/components.tsx | 4 +- .../Accounts/AccountsActionsBar.tsx | 1 + .../containers/Accounts/AccountsAlerts.tsx | 12 +++ .../containers/Accounts/AccountsDataTable.tsx | 12 +++ .../src/containers/Accounts/withAccounts.tsx | 1 + .../Accounts/withAccountsTableActions.tsx | 3 + .../Accounts/AccountBulkActivateAlert.tsx | 5 +- .../Accounts/AccountBulkDeleteAlert.tsx | 25 ++----- .../Alerts/Bills/BillBulkDeleteAlert.tsx | 69 +++++++++++++++++ .../CreditNotes/CreditNoteBulkDeleteAlert.tsx | 69 +++++++++++++++++ .../Estimates/EstimateBulkDeleteAlert.tsx | 69 +++++++++++++++++ .../Expenses/ExpenseBulkDeleteAlert.tsx | 45 +++++------ .../Invoices/InvoiceBulkDeleteAlert.tsx | 69 +++++++++++++++++ .../Alerts/Items/ItemBulkDeleteAlert.tsx | 23 ++---- .../ManualJournals/JournalBulkDeleteAlert.tsx | 73 +++++++++++------- .../PaymentReceivedBulkDeleteAlert.tsx | 69 +++++++++++++++++ .../VendorCreditBulkDeleteAlert.tsx | 69 +++++++++++++++++ .../Expenses/ExpenseForm/ExpenseForm.tsx | 13 ++-- .../containers/Expenses/ExpensesAlerts.tsx | 5 ++ .../Expenses/ExpensesLanding/components.tsx | 2 +- .../src/containers/Items/ItemsDataTable.tsx | 14 +++- .../src/containers/Items/components.tsx | 2 +- .../src/containers/Items/withItemsActions.tsx | 2 + .../Bills/BillsLanding/BillsAlerts.tsx | 5 ++ .../Bills/BillsLanding/components.tsx | 10 +-- .../CreditNotesLanding/components.tsx | 6 +- .../CreditNotes/VendorCreditNotesAlerts.tsx | 9 +++ .../Sales/CreditNotes/CreditNotesAlerts.tsx | 8 ++ .../CreditNotesDataTable.tsx | 12 +++ .../CreditNotesLanding/components.tsx | 6 +- .../CreditNotesLanding/withCreditNotes.tsx | 1 + .../withCreditNotesActions.tsx | 2 + .../Sales/Estimates/EstimatesAlerts.tsx | 5 ++ .../EstimatesLanding/EstimatesActionsBar.tsx | 19 ++++- .../EstimatesLanding/EstimatesDataTable.tsx | 12 +++ .../Estimates/EstimatesLanding/components.tsx | 14 ++-- .../EstimatesLanding/withEstimates.tsx | 1 + .../EstimatesLanding/withEstimatesActions.tsx | 2 + .../Sales/Invoices/InvoicesAlerts.tsx | 5 ++ .../InvoicesLanding/InvoicesActionsBar.tsx | 16 +++- .../InvoicesLanding/InvoicesDataTable.tsx | 12 +++ .../Invoices/InvoicesLanding/components.tsx | 49 +++++------- .../InvoicesLanding/withInvoiceActions.tsx | 4 +- .../Invoices/InvoicesLanding/withInvoices.tsx | 1 + .../PaymentsReceivedAlerts.tsx | 9 +++ .../Receipts/ReceiptsLanding/components.tsx | 4 +- packages/webapp/src/hooks/query/accounts.tsx | 34 +++++++++ packages/webapp/src/hooks/query/bills.tsx | 19 +++++ .../webapp/src/hooks/query/creditNote.tsx | 19 +++++ packages/webapp/src/hooks/query/estimates.tsx | 19 +++++ packages/webapp/src/hooks/query/expenses.tsx | 19 +++++ packages/webapp/src/hooks/query/invoices.tsx | 19 +++++ packages/webapp/src/hooks/query/items.tsx | 19 +++++ .../webapp/src/hooks/query/manualJournals.tsx | 19 +++++ .../src/hooks/query/paymentReceives.tsx | 19 +++++ .../webapp/src/hooks/query/vendorCredit.tsx | 19 +++++ .../webapp/src/store/Bills/bills.actions.tsx | 6 ++ .../webapp/src/store/Bills/bills.reducer.tsx | 5 ++ .../store/CreditNote/creditNote.actions.tsx | 7 +- .../store/CreditNote/creditNote.reducer.tsx | 5 ++ .../src/store/Estimate/estimates.actions.tsx | 7 ++ .../src/store/Estimate/estimates.reducer.tsx | 5 ++ .../src/store/Invoice/invoices.actions.tsx | 7 +- .../src/store/Invoice/invoices.reducer.tsx | 5 ++ .../paymentReceives.actions.tsx | 7 ++ .../paymentReceives.reducer.tsx | 5 ++ .../VendorCredit/VendorCredit.reducer.tsx | 5 ++ .../VendorCredit/vendorCredit.actions.tsx | 7 +- .../src/store/accounts/accounts.actions.tsx | 10 +++ .../src/store/accounts/accounts.reducer.tsx | 5 ++ .../src/store/expenses/expenses.actions.tsx | 6 ++ .../src/store/expenses/expenses.reducer.tsx | 5 ++ .../webapp/src/store/items/items.actions.tsx | 7 +- .../webapp/src/store/items/items.reducer.tsx | 4 + .../manualJournals/manualJournals.actions.tsx | 7 ++ .../manualJournals.reducers.tsx | 5 ++ 107 files changed, 2213 insertions(+), 156 deletions(-) create mode 100644 packages/server/src/common/dtos/BulkDelete.dto.ts create mode 100644 packages/server/src/modules/Accounts/BulkDeleteAccounts.service.ts create mode 100644 packages/server/src/modules/Accounts/ValidateBulkDeleteAccounts.service.ts create mode 100644 packages/server/src/modules/Bills/BulkDeleteBills.service.ts create mode 100644 packages/server/src/modules/Bills/ValidateBulkDeleteBills.service.ts create mode 100644 packages/server/src/modules/CreditNotes/BulkDeleteCreditNotes.service.ts create mode 100644 packages/server/src/modules/CreditNotes/ValidateBulkDeleteCreditNotes.service.ts create mode 100644 packages/server/src/modules/Expenses/BulkDeleteExpenses.service.ts create mode 100644 packages/server/src/modules/Expenses/ValidateBulkDeleteExpenses.service.ts create mode 100644 packages/server/src/modules/ItemCategories/BulkDeleteItemCategories.service.ts create mode 100644 packages/server/src/modules/ItemCategories/ValidateBulkDeleteItemCategories.service.ts create mode 100644 packages/server/src/modules/Items/BulkDeleteItems.service.ts create mode 100644 packages/server/src/modules/Items/ValidateBulkDeleteItems.service.ts create mode 100644 packages/server/src/modules/Items/dtos/BulkDeleteItems.dto.ts create mode 100644 packages/server/src/modules/ManualJournals/BulkDeleteManualJournals.service.ts create mode 100644 packages/server/src/modules/ManualJournals/ValidateBulkDeleteManualJournals.service.ts create mode 100644 packages/server/src/modules/PaymentReceived/BulkDeletePaymentReceived.service.ts create mode 100644 packages/server/src/modules/PaymentReceived/ValidateBulkDeletePaymentReceived.service.ts create mode 100644 packages/server/src/modules/SaleEstimates/BulkDeleteSaleEstimates.service.ts create mode 100644 packages/server/src/modules/SaleEstimates/ValidateBulkDeleteSaleEstimates.service.ts create mode 100644 packages/server/src/modules/SaleInvoices/BulkDeleteSaleInvoices.service.ts create mode 100644 packages/server/src/modules/SaleInvoices/ValidateBulkDeleteSaleInvoices.service.ts create mode 100644 packages/server/src/modules/VendorCredit/BulkDeleteVendorCredits.service.ts create mode 100644 packages/server/src/modules/VendorCredit/ValidateBulkDeleteVendorCredits.service.ts create mode 100644 packages/webapp/src/containers/Alerts/Bills/BillBulkDeleteAlert.tsx create mode 100644 packages/webapp/src/containers/Alerts/CreditNotes/CreditNoteBulkDeleteAlert.tsx create mode 100644 packages/webapp/src/containers/Alerts/Estimates/EstimateBulkDeleteAlert.tsx create mode 100644 packages/webapp/src/containers/Alerts/Invoices/InvoiceBulkDeleteAlert.tsx create mode 100644 packages/webapp/src/containers/Alerts/PaymentReceived/PaymentReceivedBulkDeleteAlert.tsx create mode 100644 packages/webapp/src/containers/Alerts/VendorCeditNotes/VendorCreditBulkDeleteAlert.tsx diff --git a/packages/server/src/common/dtos/BulkDelete.dto.ts b/packages/server/src/common/dtos/BulkDelete.dto.ts new file mode 100644 index 000000000..6b1415499 --- /dev/null +++ b/packages/server/src/common/dtos/BulkDelete.dto.ts @@ -0,0 +1,43 @@ +import { IsArray, IsInt, ArrayNotEmpty } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class BulkDeleteDto { + @IsArray() + @ArrayNotEmpty() + @IsInt({ each: true }) + @ApiProperty({ + description: 'Array of IDs to delete', + type: [Number], + example: [1, 2, 3], + }) + ids: number[]; +} + +export class ValidateBulkDeleteResponseDto { + @ApiProperty({ + description: 'Number of items that can be deleted', + example: 2, + }) + deletableCount: number; + + @ApiProperty({ + description: 'Number of items that cannot be deleted', + example: 1, + }) + nonDeletableCount: number; + + @ApiProperty({ + description: 'IDs of items that can be deleted', + type: [Number], + example: [1, 2], + }) + deletableIds: number[]; + + @ApiProperty({ + description: 'IDs of items that cannot be deleted', + type: [Number], + example: [3], + }) + nonDeletableIds: number[]; +} + diff --git a/packages/server/src/modules/Accounts/Accounts.controller.ts b/packages/server/src/modules/Accounts/Accounts.controller.ts index 5d0413f6d..7a9f26163 100644 --- a/packages/server/src/modules/Accounts/Accounts.controller.ts +++ b/packages/server/src/modules/Accounts/Accounts.controller.ts @@ -26,12 +26,17 @@ import { GetAccountTransactionResponseDto } from './dtos/GetAccountTransactionRe import { GetAccountTransactionsQueryDto } from './dtos/GetAccountTransactionsQuery.dto'; import { GetAccountsQueryDto } from './dtos/GetAccountsQuery.dto'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; +import { + BulkDeleteDto, + ValidateBulkDeleteResponseDto, +} from '@/common/dtos/BulkDelete.dto'; @Controller('accounts') @ApiTags('Accounts') @ApiExtraModels(AccountResponseDto) @ApiExtraModels(AccountTypeResponseDto) @ApiExtraModels(GetAccountTransactionResponseDto) +@ApiExtraModels(ValidateBulkDeleteResponseDto) @ApiCommonHeaders() export class AccountsController { constructor(private readonly accountsApplication: AccountsApplication) { } @@ -83,6 +88,37 @@ export class AccountsController { return this.accountsApplication.deleteAccount(id); } + @Post('validate-bulk-delete') + @ApiOperation({ + summary: + 'Validates which accounts can be deleted and returns counts of deletable and non-deletable accounts.', + }) + @ApiResponse({ + status: 200, + description: + 'Validation completed. Returns counts and IDs of deletable and non-deletable accounts.', + schema: { + $ref: getSchemaPath(ValidateBulkDeleteResponseDto), + }, + }) + async validateBulkDeleteAccounts( + @Body() bulkDeleteDto: BulkDeleteDto, + ): Promise { + return this.accountsApplication.validateBulkDeleteAccounts( + bulkDeleteDto.ids, + ); + } + + @Post('bulk-delete') + @ApiOperation({ summary: 'Deletes multiple accounts in bulk.' }) + @ApiResponse({ + status: 200, + description: 'The accounts have been successfully deleted.', + }) + async bulkDeleteAccounts(@Body() bulkDeleteDto: BulkDeleteDto): Promise { + return this.accountsApplication.bulkDeleteAccounts(bulkDeleteDto.ids); + } + @Post(':id/activate') @ApiOperation({ summary: 'Activate the given account.' }) @ApiResponse({ diff --git a/packages/server/src/modules/Accounts/Accounts.module.ts b/packages/server/src/modules/Accounts/Accounts.module.ts index 42abb8fcf..85f0c6488 100644 --- a/packages/server/src/modules/Accounts/Accounts.module.ts +++ b/packages/server/src/modules/Accounts/Accounts.module.ts @@ -19,6 +19,8 @@ import { GetAccountsService } from './GetAccounts.service'; import { DynamicListModule } from '../DynamicListing/DynamicList.module'; import { AccountsExportable } from './AccountsExportable.service'; import { AccountsImportable } from './AccountsImportable.service'; +import { BulkDeleteAccountsService } from './BulkDeleteAccounts.service'; +import { ValidateBulkDeleteAccountsService } from './ValidateBulkDeleteAccounts.service'; const models = [RegisterTenancyModel(BankAccount)]; @@ -40,7 +42,9 @@ const models = [RegisterTenancyModel(BankAccount)]; GetAccountTransactionsService, GetAccountsService, AccountsExportable, - AccountsImportable + AccountsImportable, + BulkDeleteAccountsService, + ValidateBulkDeleteAccountsService, ], exports: [ AccountRepository, diff --git a/packages/server/src/modules/Accounts/AccountsApplication.service.ts b/packages/server/src/modules/Accounts/AccountsApplication.service.ts index a34cb8bb2..b1a8b60e8 100644 --- a/packages/server/src/modules/Accounts/AccountsApplication.service.ts +++ b/packages/server/src/modules/Accounts/AccountsApplication.service.ts @@ -15,6 +15,9 @@ import { GetAccountsService } from './GetAccounts.service'; import { IFilterMeta } from '@/interfaces/Model'; import { GetAccountTransactionResponseDto } from './dtos/GetAccountTransactionResponse.dto'; import { GetAccountsQueryDto } from './dtos/GetAccountsQuery.dto'; +import { BulkDeleteAccountsService } from './BulkDeleteAccounts.service'; +import { ValidateBulkDeleteAccountsService } from './ValidateBulkDeleteAccounts.service'; +import { ValidateBulkDeleteResponseDto } from '@/common/dtos/BulkDelete.dto'; @Injectable() export class AccountsApplication { @@ -37,7 +40,9 @@ export class AccountsApplication { private readonly getAccountService: GetAccount, private readonly getAccountTransactionsService: GetAccountTransactionsService, private readonly getAccountsService: GetAccountsService, - ) { } + private readonly bulkDeleteAccountsService: BulkDeleteAccountsService, + private readonly validateBulkDeleteAccountsService: ValidateBulkDeleteAccountsService, + ) {} /** * Creates a new account. @@ -128,4 +133,22 @@ export class AccountsApplication { ): Promise> => { return this.getAccountTransactionsService.getAccountsTransactions(filter); }; + + /** + * Validates which accounts can be deleted in bulk. + */ + public validateBulkDeleteAccounts = ( + accountIds: number[], + ): Promise => { + return this.validateBulkDeleteAccountsService.validateBulkDeleteAccounts( + accountIds, + ); + }; + + /** + * Deletes multiple accounts in bulk. + */ + public bulkDeleteAccounts = (accountIds: number[]): Promise => { + return this.bulkDeleteAccountsService.bulkDeleteAccounts(accountIds); + }; } diff --git a/packages/server/src/modules/Accounts/BulkDeleteAccounts.service.ts b/packages/server/src/modules/Accounts/BulkDeleteAccounts.service.ts new file mode 100644 index 000000000..cf65de70f --- /dev/null +++ b/packages/server/src/modules/Accounts/BulkDeleteAccounts.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { PromisePool } from '@supercharge/promise-pool'; +import { castArray, uniq } from 'lodash'; +import { DeleteAccount } from './DeleteAccount.service'; + +@Injectable() +export class BulkDeleteAccountsService { + constructor(private readonly deleteAccountService: DeleteAccount) { } + + /** + * Deletes multiple accounts. + * @param {number | Array} accountIds - The account id or ids. + * @param {Knex.Transaction} trx - The transaction. + */ + async bulkDeleteAccounts( + accountIds: number | Array, + trx?: Knex.Transaction, + ): Promise { + const accountsIds = uniq(castArray(accountIds)); + + const results = await PromisePool.withConcurrency(1) + .for(accountsIds) + .process(async (accountId: number) => { + await this.deleteAccountService.deleteAccount(accountId); + }); + + if (results.errors && results.errors.length > 0) { + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/Accounts/ValidateBulkDeleteAccounts.service.ts b/packages/server/src/modules/Accounts/ValidateBulkDeleteAccounts.service.ts new file mode 100644 index 000000000..de513bb48 --- /dev/null +++ b/packages/server/src/modules/Accounts/ValidateBulkDeleteAccounts.service.ts @@ -0,0 +1,63 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { Knex } from 'knex'; +import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants'; +import { DeleteAccount } from './DeleteAccount.service'; +import { ModelHasRelationsError } from '@/common/exceptions/ModelHasRelations.exception'; + +@Injectable() +export class ValidateBulkDeleteAccountsService { + constructor( + private readonly deleteAccountService: DeleteAccount, + @Inject(TENANCY_DB_CONNECTION) + private readonly tenantKnex: () => Knex, + ) {} + + /** + * Validates which accounts from the provided IDs can be deleted. + * Uses the actual deleteAccount service to validate, ensuring the same validation logic. + * Uses a transaction that is always rolled back to ensure no database changes. + * @param {number[]} accountIds - Array of account IDs to validate + * @returns {Promise<{deletableCount: number, nonDeletableCount: number, deletableIds: number[], nonDeletableIds: number[]}>} + */ + public async validateBulkDeleteAccounts(accountIds: number[]): Promise<{ + deletableCount: number; + nonDeletableCount: number; + deletableIds: number[]; + nonDeletableIds: number[]; + }> { + const trx = await this.tenantKnex().transaction({ + isolationLevel: 'read uncommitted', + }); + + try { + const deletableIds: number[] = []; + const nonDeletableIds: number[] = []; + + for (const accountId of accountIds) { + try { + await this.deleteAccountService.deleteAccount(accountId); + deletableIds.push(accountId); + } catch (error) { + if (error instanceof ModelHasRelationsError) { + nonDeletableIds.push(accountId); + } else { + nonDeletableIds.push(accountId); + } + } + } + + await trx.rollback(); + + return { + deletableCount: deletableIds.length, + nonDeletableCount: nonDeletableIds.length, + deletableIds, + nonDeletableIds, + }; + } catch (error) { + await trx.rollback(); + throw error; + } + } +} + diff --git a/packages/server/src/modules/Bills/BulkDeleteBills.service.ts b/packages/server/src/modules/Bills/BulkDeleteBills.service.ts new file mode 100644 index 000000000..dfc71dc2a --- /dev/null +++ b/packages/server/src/modules/Bills/BulkDeleteBills.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { PromisePool } from '@supercharge/promise-pool'; +import { castArray, uniq } from 'lodash'; +import { DeleteBill } from './commands/DeleteBill.service'; + +@Injectable() +export class BulkDeleteBillsService { + constructor(private readonly deleteBillService: DeleteBill) { } + + async bulkDeleteBills( + billIds: number | Array, + trx?: Knex.Transaction, + ): Promise { + const billsIds = uniq(castArray(billIds)); + + const results = await PromisePool.withConcurrency(1) + .for(billsIds) + .process(async (billId: number) => { + await this.deleteBillService.deleteBill(billId); + }); + + if (results.errors && results.errors.length > 0) { + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/Bills/ValidateBulkDeleteBills.service.ts b/packages/server/src/modules/Bills/ValidateBulkDeleteBills.service.ts new file mode 100644 index 000000000..d16145901 --- /dev/null +++ b/packages/server/src/modules/Bills/ValidateBulkDeleteBills.service.ts @@ -0,0 +1,51 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { Knex } from 'knex'; +import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants'; +import { DeleteBill } from './commands/DeleteBill.service'; + +@Injectable() +export class ValidateBulkDeleteBillsService { + constructor( + private readonly deleteBillService: DeleteBill, + @Inject(TENANCY_DB_CONNECTION) + private readonly tenantKnex: () => Knex, + ) { } + + public async validateBulkDeleteBills(billIds: number[]): Promise<{ + deletableCount: number; + nonDeletableCount: number; + deletableIds: number[]; + nonDeletableIds: number[]; + }> { + const trx = await this.tenantKnex().transaction({ + isolationLevel: 'read uncommitted', + }); + + try { + const deletableIds: number[] = []; + const nonDeletableIds: number[] = []; + + for (const billId of billIds) { + try { + await this.deleteBillService.deleteBill(billId); + deletableIds.push(billId); + } catch (error) { + nonDeletableIds.push(billId); + } + } + + await trx.rollback(); + + return { + deletableCount: deletableIds.length, + nonDeletableCount: nonDeletableIds.length, + deletableIds, + nonDeletableIds, + }; + } catch (error) { + await trx.rollback(); + throw error; + } + } +} + diff --git a/packages/server/src/modules/CreditNotes/BulkDeleteCreditNotes.service.ts b/packages/server/src/modules/CreditNotes/BulkDeleteCreditNotes.service.ts new file mode 100644 index 000000000..ec226d27d --- /dev/null +++ b/packages/server/src/modules/CreditNotes/BulkDeleteCreditNotes.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { PromisePool } from '@supercharge/promise-pool'; +import { castArray, uniq } from 'lodash'; +import { DeleteCreditNoteService } from './commands/DeleteCreditNote.service'; + +@Injectable() +export class BulkDeleteCreditNotesService { + constructor( + private readonly deleteCreditNoteService: DeleteCreditNoteService, + ) { } + + async bulkDeleteCreditNotes( + creditNoteIds: number | Array, + trx?: Knex.Transaction, + ): Promise { + const notesIds = uniq(castArray(creditNoteIds)); + + const results = await PromisePool.withConcurrency(1) + .for(notesIds) + .process(async (creditNoteId: number) => { + await this.deleteCreditNoteService.deleteCreditNote(creditNoteId); + }); + + if (results.errors && results.errors.length > 0) { + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/CreditNotes/ValidateBulkDeleteCreditNotes.service.ts b/packages/server/src/modules/CreditNotes/ValidateBulkDeleteCreditNotes.service.ts new file mode 100644 index 000000000..46fab3735 --- /dev/null +++ b/packages/server/src/modules/CreditNotes/ValidateBulkDeleteCreditNotes.service.ts @@ -0,0 +1,51 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { Knex } from 'knex'; +import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants'; +import { DeleteCreditNoteService } from './commands/DeleteCreditNote.service'; + +@Injectable() +export class ValidateBulkDeleteCreditNotesService { + constructor( + private readonly deleteCreditNoteService: DeleteCreditNoteService, + @Inject(TENANCY_DB_CONNECTION) + private readonly tenantKnex: () => Knex, + ) {} + + public async validateBulkDeleteCreditNotes(creditNoteIds: number[]): Promise<{ + deletableCount: number; + nonDeletableCount: number; + deletableIds: number[]; + nonDeletableIds: number[]; + }> { + const trx = await this.tenantKnex().transaction({ + isolationLevel: 'read uncommitted', + }); + + try { + const deletableIds: number[] = []; + const nonDeletableIds: number[] = []; + + for (const creditNoteId of creditNoteIds) { + try { + await this.deleteCreditNoteService.deleteCreditNote(creditNoteId); + deletableIds.push(creditNoteId); + } catch (error) { + nonDeletableIds.push(creditNoteId); + } + } + + await trx.rollback(); + + return { + deletableCount: deletableIds.length, + nonDeletableCount: nonDeletableIds.length, + deletableIds, + nonDeletableIds, + }; + } catch (error) { + await trx.rollback(); + throw error; + } + } +} + diff --git a/packages/server/src/modules/Expenses/BulkDeleteExpenses.service.ts b/packages/server/src/modules/Expenses/BulkDeleteExpenses.service.ts new file mode 100644 index 000000000..59dca523b --- /dev/null +++ b/packages/server/src/modules/Expenses/BulkDeleteExpenses.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { PromisePool } from '@supercharge/promise-pool'; +import { castArray, uniq } from 'lodash'; +import { DeleteExpense } from './commands/DeleteExpense.service'; + +@Injectable() +export class BulkDeleteExpensesService { + constructor(private readonly deleteExpenseService: DeleteExpense) {} + + async bulkDeleteExpenses( + expenseIds: number | Array, + trx?: Knex.Transaction, + ): Promise { + const expensesIds = uniq(castArray(expenseIds)); + + const results = await PromisePool.withConcurrency(1) + .for(expensesIds) + .process(async (expenseId: number) => { + await this.deleteExpenseService.deleteExpense(expenseId); + }); + + if (results.errors && results.errors.length > 0) { + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/Expenses/ValidateBulkDeleteExpenses.service.ts b/packages/server/src/modules/Expenses/ValidateBulkDeleteExpenses.service.ts new file mode 100644 index 000000000..49684968d --- /dev/null +++ b/packages/server/src/modules/Expenses/ValidateBulkDeleteExpenses.service.ts @@ -0,0 +1,51 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { Knex } from 'knex'; +import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants'; +import { DeleteExpense } from './commands/DeleteExpense.service'; + +@Injectable() +export class ValidateBulkDeleteExpensesService { + constructor( + private readonly deleteExpenseService: DeleteExpense, + @Inject(TENANCY_DB_CONNECTION) + private readonly tenantKnex: () => Knex, + ) { } + + public async validateBulkDeleteExpenses(expenseIds: number[]): Promise<{ + deletableCount: number; + nonDeletableCount: number; + deletableIds: number[]; + nonDeletableIds: number[]; + }> { + const trx = await this.tenantKnex().transaction({ + isolationLevel: 'read uncommitted', + }); + + try { + const deletableIds: number[] = []; + const nonDeletableIds: number[] = []; + + for (const expenseId of expenseIds) { + try { + await this.deleteExpenseService.deleteExpense(expenseId); + deletableIds.push(expenseId); + } catch (error) { + nonDeletableIds.push(expenseId); + } + } + + await trx.rollback(); + + return { + deletableCount: deletableIds.length, + nonDeletableCount: nonDeletableIds.length, + deletableIds, + nonDeletableIds, + }; + } catch (error) { + await trx.rollback(); + throw error; + } + } +} + diff --git a/packages/server/src/modules/ItemCategories/BulkDeleteItemCategories.service.ts b/packages/server/src/modules/ItemCategories/BulkDeleteItemCategories.service.ts new file mode 100644 index 000000000..30e608d52 --- /dev/null +++ b/packages/server/src/modules/ItemCategories/BulkDeleteItemCategories.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { PromisePool } from '@supercharge/promise-pool'; +import { castArray, uniq } from 'lodash'; +import { DeleteItemCategoryService } from './commands/DeleteItemCategory.service'; + +@Injectable() +export class BulkDeleteItemCategoriesService { + constructor( + private readonly deleteItemCategoryService: DeleteItemCategoryService, + ) { } + + async bulkDeleteItemCategories( + itemCategoryIds: number | Array, + trx?: Knex.Transaction, + ): Promise { + const categoriesIds = uniq(castArray(itemCategoryIds)); + + const results = await PromisePool.withConcurrency(1) + .for(categoriesIds) + .process(async (itemCategoryId: number) => { + await this.deleteItemCategoryService.deleteItemCategory(itemCategoryId); + }); + + if (results.errors && results.errors.length > 0) { + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/ItemCategories/ValidateBulkDeleteItemCategories.service.ts b/packages/server/src/modules/ItemCategories/ValidateBulkDeleteItemCategories.service.ts new file mode 100644 index 000000000..a9de7ab7d --- /dev/null +++ b/packages/server/src/modules/ItemCategories/ValidateBulkDeleteItemCategories.service.ts @@ -0,0 +1,51 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { Knex } from 'knex'; +import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants'; +import { DeleteItemCategoryService } from './commands/DeleteItemCategory.service'; + +@Injectable() +export class ValidateBulkDeleteItemCategoriesService { + constructor( + private readonly deleteItemCategoryService: DeleteItemCategoryService, + @Inject(TENANCY_DB_CONNECTION) + private readonly tenantKnex: () => Knex, + ) {} + + public async validateBulkDeleteItemCategories(itemCategoryIds: number[]): Promise<{ + deletableCount: number; + nonDeletableCount: number; + deletableIds: number[]; + nonDeletableIds: number[]; + }> { + const trx = await this.tenantKnex().transaction({ + isolationLevel: 'read uncommitted', + }); + + try { + const deletableIds: number[] = []; + const nonDeletableIds: number[] = []; + + for (const itemCategoryId of itemCategoryIds) { + try { + await this.deleteItemCategoryService.deleteItemCategory(itemCategoryId); + deletableIds.push(itemCategoryId); + } catch (error) { + nonDeletableIds.push(itemCategoryId); + } + } + + await trx.rollback(); + + return { + deletableCount: deletableIds.length, + nonDeletableCount: nonDeletableIds.length, + deletableIds, + nonDeletableIds, + }; + } catch (error) { + await trx.rollback(); + throw error; + } + } +} + diff --git a/packages/server/src/modules/Items/BulkDeleteItems.service.ts b/packages/server/src/modules/Items/BulkDeleteItems.service.ts new file mode 100644 index 000000000..e5ca88a3c --- /dev/null +++ b/packages/server/src/modules/Items/BulkDeleteItems.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { PromisePool } from '@supercharge/promise-pool'; +import { castArray, uniq } from 'lodash'; +import { DeleteItemService } from './DeleteItem.service'; + +@Injectable() +export class BulkDeleteItemsService { + constructor(private readonly deleteItemService: DeleteItemService) { } + + /** + * Deletes multiple items. + * @param {number | Array} itemIds - The item id or ids. + * @param {Knex.Transaction} trx - The transaction. + */ + async bulkDeleteItems( + itemIds: number | Array, + trx?: Knex.Transaction, + ): Promise { + const itemsIds = uniq(castArray(itemIds)); + + // Use PromisePool to delete items sequentially (concurrency: 1) + // to avoid potential database locks and maintain transaction integrity + const results = await PromisePool.withConcurrency(1) + .for(itemsIds) + .process(async (itemId: number) => { + await this.deleteItemService.deleteItem(itemId, trx); + }); + + // Check if there were any errors + if (results.errors && results.errors.length > 0) { + // If needed, you can throw an error here or handle errors individually + // For now, we'll let individual errors bubble up + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/Items/Item.controller.ts b/packages/server/src/modules/Items/Item.controller.ts index 0afa9b467..eed9ef503 100644 --- a/packages/server/src/modules/Items/Item.controller.ts +++ b/packages/server/src/modules/Items/Item.controller.ts @@ -29,6 +29,10 @@ import { ItemEstimatesResponseDto } from './dtos/ItemEstimatesResponse.dto'; import { ItemBillsResponseDto } from './dtos/ItemBillsResponse.dto'; import { ItemReceiptsResponseDto } from './dtos/ItemReceiptsResponse.dto'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; +import { + BulkDeleteItemsDto, + ValidateBulkDeleteItemsResponseDto, +} from './dtos/BulkDeleteItems.dto'; @Controller('/items') @ApiTags('Items') @@ -39,6 +43,7 @@ import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; @ApiExtraModels(ItemBillsResponseDto) @ApiExtraModels(ItemEstimatesResponseDto) @ApiExtraModels(ItemReceiptsResponseDto) +@ApiExtraModels(ValidateBulkDeleteItemsResponseDto) @ApiCommonHeaders() export class ItemsController extends TenantController { constructor(private readonly itemsApplication: ItemsApplicationService) { @@ -340,4 +345,35 @@ export class ItemsController extends TenantController { const itemId = parseInt(id, 10); return this.itemsApplication.getItemReceiptsTransactions(itemId); } + + @Post('validate-bulk-delete') + @ApiOperation({ + summary: + 'Validates which items can be deleted and returns counts of deletable and non-deletable items.', + }) + @ApiResponse({ + status: 200, + description: + 'Validation completed. Returns counts and IDs of deletable and non-deletable items.', + schema: { + $ref: getSchemaPath(ValidateBulkDeleteItemsResponseDto), + }, + }) + async validateBulkDeleteItems( + @Body() bulkDeleteDto: BulkDeleteItemsDto, + ): Promise { + return this.itemsApplication.validateBulkDeleteItems(bulkDeleteDto.ids); + } + + @Post('bulk-delete') + @ApiOperation({ summary: 'Deletes multiple items in bulk.' }) + @ApiResponse({ + status: 200, + description: 'The items have been successfully deleted.', + }) + async bulkDeleteItems( + @Body() bulkDeleteDto: BulkDeleteItemsDto, + ): Promise { + return this.itemsApplication.bulkDeleteItems(bulkDeleteDto.ids); + } } diff --git a/packages/server/src/modules/Items/Items.module.ts b/packages/server/src/modules/Items/Items.module.ts index 8cc26d759..b18e8dad8 100644 --- a/packages/server/src/modules/Items/Items.module.ts +++ b/packages/server/src/modules/Items/Items.module.ts @@ -18,6 +18,8 @@ import { DynamicListModule } from '../DynamicListing/DynamicList.module'; import { InventoryAdjustmentsModule } from '../InventoryAdjutments/InventoryAdjustments.module'; import { ItemsExportable } from './ItemsExportable.service'; import { ItemsImportable } from './ItemsImportable.service'; +import { BulkDeleteItemsService } from './BulkDeleteItems.service'; +import { ValidateBulkDeleteItemsService } from './ValidateBulkDeleteItems.service'; @Module({ imports: [ @@ -41,8 +43,10 @@ import { ItemsImportable } from './ItemsImportable.service'; TransformerInjectable, ItemsEntriesService, ItemsExportable, - ItemsImportable + ItemsImportable, + BulkDeleteItemsService, + ValidateBulkDeleteItemsService, ], exports: [ItemsEntriesService, ItemsExportable, ItemsImportable], }) -export class ItemsModule {} +export class ItemsModule { } diff --git a/packages/server/src/modules/Items/ItemsApplication.service.ts b/packages/server/src/modules/Items/ItemsApplication.service.ts index a93e29587..f8b347a5d 100644 --- a/packages/server/src/modules/Items/ItemsApplication.service.ts +++ b/packages/server/src/modules/Items/ItemsApplication.service.ts @@ -13,6 +13,8 @@ import { GetItemsService } from './GetItems.service'; import { IItemsFilter } from './types/Items.types'; import { EditItemDto, CreateItemDto } from './dtos/Item.dto'; import { GetItemsQueryDto } from './dtos/GetItemsQuery.dto'; +import { BulkDeleteItemsService } from './BulkDeleteItems.service'; +import { ValidateBulkDeleteItemsService } from './ValidateBulkDeleteItems.service'; @Injectable() export class ItemsApplicationService { @@ -25,7 +27,9 @@ export class ItemsApplicationService { private readonly getItemService: GetItemService, private readonly getItemsService: GetItemsService, private readonly itemTransactionsService: ItemTransactionsService, - ) {} + private readonly bulkDeleteItemsService: BulkDeleteItemsService, + private readonly validateBulkDeleteItemsService: ValidateBulkDeleteItemsService, + ) { } /** * Creates a new item. @@ -134,4 +138,27 @@ export class ItemsApplicationService { async getItemReceiptsTransactions(itemId: number): Promise { return this.itemTransactionsService.getItemReceiptTransactions(itemId); } + + /** + * Validates which items can be deleted in bulk. + * @param {number[]} itemIds - Array of item IDs to validate + * @returns {Promise<{deletableCount: number, nonDeletableCount: number, deletableIds: number[], nonDeletableIds: number[]}>} + */ + async validateBulkDeleteItems(itemIds: number[]): Promise<{ + deletableCount: number; + nonDeletableCount: number; + deletableIds: number[]; + nonDeletableIds: number[]; + }> { + return this.validateBulkDeleteItemsService.validateBulkDeleteItems(itemIds); + } + + /** + * Deletes multiple items in bulk. + * @param {number[]} itemIds - Array of item IDs to delete + * @returns {Promise} + */ + async bulkDeleteItems(itemIds: number[]): Promise { + return this.bulkDeleteItemsService.bulkDeleteItems(itemIds); + } } diff --git a/packages/server/src/modules/Items/ValidateBulkDeleteItems.service.ts b/packages/server/src/modules/Items/ValidateBulkDeleteItems.service.ts new file mode 100644 index 000000000..45f7e6c8d --- /dev/null +++ b/packages/server/src/modules/Items/ValidateBulkDeleteItems.service.ts @@ -0,0 +1,74 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { Knex } from 'knex'; +import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants'; +import { DeleteItemService } from './DeleteItem.service'; +import { ModelHasRelationsError } from '@/common/exceptions/ModelHasRelations.exception'; + +@Injectable() +export class ValidateBulkDeleteItemsService { + constructor( + private readonly deleteItemService: DeleteItemService, + @Inject(TENANCY_DB_CONNECTION) + private readonly tenantKnex: () => Knex, + ) { } + + /** + * Validates which items from the provided IDs can be deleted. + * Uses the actual deleteItem service to validate, ensuring the same validation logic. + * Uses a transaction that is always rolled back to ensure no database changes. + * @param {number[]} itemIds - Array of item IDs to validate + * @returns {Promise<{deletableCount: number, nonDeletableCount: number, deletableIds: number[], nonDeletableIds: number[]}>} + */ + public async validateBulkDeleteItems(itemIds: number[]): Promise<{ + deletableCount: number; + nonDeletableCount: number; + deletableIds: number[]; + nonDeletableIds: number[]; + }> { + // Create a transaction that will be rolled back + const trx = await this.tenantKnex().transaction({ + isolationLevel: 'read uncommitted', + }); + + try { + const deletableIds: number[] = []; + const nonDeletableIds: number[] = []; + + // Check each item to see if it can be deleted by attempting deletion in transaction + for (const itemId of itemIds) { + try { + // Attempt to delete the item using the deleteItem service with the transaction + // This will use the exact same validation logic as the actual delete + await this.deleteItemService.deleteItem(itemId, trx); + + // If deletion succeeds, item is deletable + deletableIds.push(itemId); + } catch (error) { + // If error occurs, check the type of error + if (error instanceof ModelHasRelationsError) { + // Item has associated transactions/relations, cannot be deleted + nonDeletableIds.push(itemId); + } else { + // Other errors (e.g., item not found), also mark as non-deletable + nonDeletableIds.push(itemId); + } + } + } + + // Always rollback the transaction to ensure no changes are persisted + await trx.rollback(); + + return { + deletableCount: deletableIds.length, + nonDeletableCount: nonDeletableIds.length, + deletableIds, + nonDeletableIds, + }; + } catch (error) { + // Rollback in case of any error + await trx.rollback(); + throw error; + } + } +} + diff --git a/packages/server/src/modules/Items/dtos/BulkDeleteItems.dto.ts b/packages/server/src/modules/Items/dtos/BulkDeleteItems.dto.ts new file mode 100644 index 000000000..64217cadb --- /dev/null +++ b/packages/server/src/modules/Items/dtos/BulkDeleteItems.dto.ts @@ -0,0 +1,43 @@ +import { IsArray, IsInt, ArrayNotEmpty } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class BulkDeleteItemsDto { + @IsArray() + @ArrayNotEmpty() + @IsInt({ each: true }) + @ApiProperty({ + description: 'Array of item IDs to delete', + type: [Number], + example: [1, 2, 3], + }) + ids: number[]; +} + +export class ValidateBulkDeleteItemsResponseDto { + @ApiProperty({ + description: 'Number of items that can be deleted', + example: 2, + }) + deletableCount: number; + + @ApiProperty({ + description: 'Number of items that cannot be deleted', + example: 1, + }) + nonDeletableCount: number; + + @ApiProperty({ + description: 'IDs of items that can be deleted', + type: [Number], + example: [1, 2], + }) + deletableIds: number[]; + + @ApiProperty({ + description: 'IDs of items that cannot be deleted', + type: [Number], + example: [3], + }) + nonDeletableIds: number[]; +} + diff --git a/packages/server/src/modules/ManualJournals/BulkDeleteManualJournals.service.ts b/packages/server/src/modules/ManualJournals/BulkDeleteManualJournals.service.ts new file mode 100644 index 000000000..f4a49ea90 --- /dev/null +++ b/packages/server/src/modules/ManualJournals/BulkDeleteManualJournals.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { PromisePool } from '@supercharge/promise-pool'; +import { castArray, uniq } from 'lodash'; +import { DeleteManualJournalService } from './commands/DeleteManualJournal.service'; + +@Injectable() +export class BulkDeleteManualJournalsService { + constructor( + private readonly deleteManualJournalService: DeleteManualJournalService, + ) {} + + async bulkDeleteManualJournals( + manualJournalIds: number | Array, + trx?: Knex.Transaction, + ): Promise { + const journalsIds = uniq(castArray(manualJournalIds)); + + const results = await PromisePool.withConcurrency(1) + .for(journalsIds) + .process(async (manualJournalId: number) => { + await this.deleteManualJournalService.deleteManualJournal( + manualJournalId, + ); + }); + + if (results.errors && results.errors.length > 0) { + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/ManualJournals/ValidateBulkDeleteManualJournals.service.ts b/packages/server/src/modules/ManualJournals/ValidateBulkDeleteManualJournals.service.ts new file mode 100644 index 000000000..44eba602c --- /dev/null +++ b/packages/server/src/modules/ManualJournals/ValidateBulkDeleteManualJournals.service.ts @@ -0,0 +1,55 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { Knex } from 'knex'; +import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants'; +import { DeleteManualJournalService } from './commands/DeleteManualJournal.service'; + +@Injectable() +export class ValidateBulkDeleteManualJournalsService { + constructor( + private readonly deleteManualJournalService: DeleteManualJournalService, + @Inject(TENANCY_DB_CONNECTION) + private readonly tenantKnex: () => Knex, + ) {} + + public async validateBulkDeleteManualJournals( + manualJournalIds: number[], + ): Promise<{ + deletableCount: number; + nonDeletableCount: number; + deletableIds: number[]; + nonDeletableIds: number[]; + }> { + const trx = await this.tenantKnex().transaction({ + isolationLevel: 'read uncommitted', + }); + + try { + const deletableIds: number[] = []; + const nonDeletableIds: number[] = []; + + for (const manualJournalId of manualJournalIds) { + try { + await this.deleteManualJournalService.deleteManualJournal( + manualJournalId, + ); + deletableIds.push(manualJournalId); + } catch (error) { + nonDeletableIds.push(manualJournalId); + } + } + + await trx.rollback(); + + return { + deletableCount: deletableIds.length, + nonDeletableCount: nonDeletableIds.length, + deletableIds, + nonDeletableIds, + }; + } catch (error) { + await trx.rollback(); + throw error; + } + } +} + diff --git a/packages/server/src/modules/PaymentReceived/BulkDeletePaymentReceived.service.ts b/packages/server/src/modules/PaymentReceived/BulkDeletePaymentReceived.service.ts new file mode 100644 index 000000000..cb0f69348 --- /dev/null +++ b/packages/server/src/modules/PaymentReceived/BulkDeletePaymentReceived.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { PromisePool } from '@supercharge/promise-pool'; +import { castArray, uniq } from 'lodash'; +import { DeletePaymentReceivedService } from './commands/DeletePaymentReceived.service'; + +@Injectable() +export class BulkDeletePaymentReceivedService { + constructor( + private readonly deletePaymentReceivedService: DeletePaymentReceivedService, + ) {} + + async bulkDeletePaymentReceived( + paymentReceiveIds: number | Array, + trx?: Knex.Transaction, + ): Promise { + const paymentsIds = uniq(castArray(paymentReceiveIds)); + + const results = await PromisePool.withConcurrency(1) + .for(paymentsIds) + .process(async (paymentReceiveId: number) => { + await this.deletePaymentReceivedService.deletePaymentReceive( + paymentReceiveId, + ); + }); + + if (results.errors && results.errors.length > 0) { + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/PaymentReceived/ValidateBulkDeletePaymentReceived.service.ts b/packages/server/src/modules/PaymentReceived/ValidateBulkDeletePaymentReceived.service.ts new file mode 100644 index 000000000..159572f2f --- /dev/null +++ b/packages/server/src/modules/PaymentReceived/ValidateBulkDeletePaymentReceived.service.ts @@ -0,0 +1,55 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { Knex } from 'knex'; +import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants'; +import { DeletePaymentReceivedService } from './commands/DeletePaymentReceived.service'; + +@Injectable() +export class ValidateBulkDeletePaymentReceivedService { + constructor( + private readonly deletePaymentReceivedService: DeletePaymentReceivedService, + @Inject(TENANCY_DB_CONNECTION) + private readonly tenantKnex: () => Knex, + ) {} + + public async validateBulkDeletePaymentReceived( + paymentReceiveIds: number[], + ): Promise<{ + deletableCount: number; + nonDeletableCount: number; + deletableIds: number[]; + nonDeletableIds: number[]; + }> { + const trx = await this.tenantKnex().transaction({ + isolationLevel: 'read uncommitted', + }); + + try { + const deletableIds: number[] = []; + const nonDeletableIds: number[] = []; + + for (const paymentReceiveId of paymentReceiveIds) { + try { + await this.deletePaymentReceivedService.deletePaymentReceive( + paymentReceiveId, + ); + deletableIds.push(paymentReceiveId); + } catch (error) { + nonDeletableIds.push(paymentReceiveId); + } + } + + await trx.rollback(); + + return { + deletableCount: deletableIds.length, + nonDeletableCount: nonDeletableIds.length, + deletableIds, + nonDeletableIds, + }; + } catch (error) { + await trx.rollback(); + throw error; + } + } +} + diff --git a/packages/server/src/modules/SaleEstimates/BulkDeleteSaleEstimates.service.ts b/packages/server/src/modules/SaleEstimates/BulkDeleteSaleEstimates.service.ts new file mode 100644 index 000000000..a122c69ff --- /dev/null +++ b/packages/server/src/modules/SaleEstimates/BulkDeleteSaleEstimates.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { PromisePool } from '@supercharge/promise-pool'; +import { castArray, uniq } from 'lodash'; +import { DeleteSaleEstimate } from './commands/DeleteSaleEstimate.service'; + +@Injectable() +export class BulkDeleteSaleEstimatesService { + constructor(private readonly deleteSaleEstimateService: DeleteSaleEstimate) { } + + async bulkDeleteSaleEstimates( + saleEstimateIds: number | Array, + trx?: Knex.Transaction, + ): Promise { + const estimatesIds = uniq(castArray(saleEstimateIds)); + + const results = await PromisePool.withConcurrency(1) + .for(estimatesIds) + .process(async (saleEstimateId: number) => { + await this.deleteSaleEstimateService.deleteEstimate(saleEstimateId); + }); + + if (results.errors && results.errors.length > 0) { + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/SaleEstimates/ValidateBulkDeleteSaleEstimates.service.ts b/packages/server/src/modules/SaleEstimates/ValidateBulkDeleteSaleEstimates.service.ts new file mode 100644 index 000000000..efde94c4a --- /dev/null +++ b/packages/server/src/modules/SaleEstimates/ValidateBulkDeleteSaleEstimates.service.ts @@ -0,0 +1,51 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { Knex } from 'knex'; +import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants'; +import { DeleteSaleEstimate } from './commands/DeleteSaleEstimate.service'; + +@Injectable() +export class ValidateBulkDeleteSaleEstimatesService { + constructor( + private readonly deleteSaleEstimateService: DeleteSaleEstimate, + @Inject(TENANCY_DB_CONNECTION) + private readonly tenantKnex: () => Knex, + ) { } + + public async validateBulkDeleteSaleEstimates(saleEstimateIds: number[]): Promise<{ + deletableCount: number; + nonDeletableCount: number; + deletableIds: number[]; + nonDeletableIds: number[]; + }> { + const trx = await this.tenantKnex().transaction({ + isolationLevel: 'read uncommitted', + }); + + try { + const deletableIds: number[] = []; + const nonDeletableIds: number[] = []; + + for (const saleEstimateId of saleEstimateIds) { + try { + await this.deleteSaleEstimateService.deleteEstimate(saleEstimateId); + deletableIds.push(saleEstimateId); + } catch (error) { + nonDeletableIds.push(saleEstimateId); + } + } + + await trx.rollback(); + + return { + deletableCount: deletableIds.length, + nonDeletableCount: nonDeletableIds.length, + deletableIds, + nonDeletableIds, + }; + } catch (error) { + await trx.rollback(); + throw error; + } + } +} + diff --git a/packages/server/src/modules/SaleInvoices/BulkDeleteSaleInvoices.service.ts b/packages/server/src/modules/SaleInvoices/BulkDeleteSaleInvoices.service.ts new file mode 100644 index 000000000..cb29c222a --- /dev/null +++ b/packages/server/src/modules/SaleInvoices/BulkDeleteSaleInvoices.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { PromisePool } from '@supercharge/promise-pool'; +import { castArray, uniq } from 'lodash'; +import { DeleteSaleInvoice } from './commands/DeleteSaleInvoice.service'; + +@Injectable() +export class BulkDeleteSaleInvoicesService { + constructor(private readonly deleteSaleInvoiceService: DeleteSaleInvoice) { } + + async bulkDeleteSaleInvoices( + saleInvoiceIds: number | Array, + trx?: Knex.Transaction, + ): Promise { + const invoicesIds = uniq(castArray(saleInvoiceIds)); + + const results = await PromisePool.withConcurrency(1) + .for(invoicesIds) + .process(async (saleInvoiceId: number) => { + await this.deleteSaleInvoiceService.deleteSaleInvoice(saleInvoiceId); + }); + + if (results.errors && results.errors.length > 0) { + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/SaleInvoices/ValidateBulkDeleteSaleInvoices.service.ts b/packages/server/src/modules/SaleInvoices/ValidateBulkDeleteSaleInvoices.service.ts new file mode 100644 index 000000000..c69157c57 --- /dev/null +++ b/packages/server/src/modules/SaleInvoices/ValidateBulkDeleteSaleInvoices.service.ts @@ -0,0 +1,51 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { Knex } from 'knex'; +import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants'; +import { DeleteSaleInvoice } from './commands/DeleteSaleInvoice.service'; + +@Injectable() +export class ValidateBulkDeleteSaleInvoicesService { + constructor( + private readonly deleteSaleInvoiceService: DeleteSaleInvoice, + @Inject(TENANCY_DB_CONNECTION) + private readonly tenantKnex: () => Knex, + ) { } + + public async validateBulkDeleteSaleInvoices(saleInvoiceIds: number[]): Promise<{ + deletableCount: number; + nonDeletableCount: number; + deletableIds: number[]; + nonDeletableIds: number[]; + }> { + const trx = await this.tenantKnex().transaction({ + isolationLevel: 'read uncommitted', + }); + + try { + const deletableIds: number[] = []; + const nonDeletableIds: number[] = []; + + for (const saleInvoiceId of saleInvoiceIds) { + try { + await this.deleteSaleInvoiceService.deleteSaleInvoice(saleInvoiceId); + deletableIds.push(saleInvoiceId); + } catch (error) { + nonDeletableIds.push(saleInvoiceId); + } + } + + await trx.rollback(); + + return { + deletableCount: deletableIds.length, + nonDeletableCount: nonDeletableIds.length, + deletableIds, + nonDeletableIds, + }; + } catch (error) { + await trx.rollback(); + throw error; + } + } +} + diff --git a/packages/server/src/modules/VendorCredit/BulkDeleteVendorCredits.service.ts b/packages/server/src/modules/VendorCredit/BulkDeleteVendorCredits.service.ts new file mode 100644 index 000000000..987905af3 --- /dev/null +++ b/packages/server/src/modules/VendorCredit/BulkDeleteVendorCredits.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { PromisePool } from '@supercharge/promise-pool'; +import { castArray, uniq } from 'lodash'; +import { DeleteVendorCreditService } from './commands/DeleteVendorCredit.service'; + +@Injectable() +export class BulkDeleteVendorCreditsService { + constructor( + private readonly deleteVendorCreditService: DeleteVendorCreditService, + ) {} + + async bulkDeleteVendorCredits( + vendorCreditIds: number | Array, + trx?: Knex.Transaction, + ): Promise { + const creditsIds = uniq(castArray(vendorCreditIds)); + + const results = await PromisePool.withConcurrency(1) + .for(creditsIds) + .process(async (vendorCreditId: number) => { + await this.deleteVendorCreditService.deleteVendorCredit(vendorCreditId); + }); + + if (results.errors && results.errors.length > 0) { + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/VendorCredit/ValidateBulkDeleteVendorCredits.service.ts b/packages/server/src/modules/VendorCredit/ValidateBulkDeleteVendorCredits.service.ts new file mode 100644 index 000000000..4bdf864ac --- /dev/null +++ b/packages/server/src/modules/VendorCredit/ValidateBulkDeleteVendorCredits.service.ts @@ -0,0 +1,55 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { Knex } from 'knex'; +import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants'; +import { DeleteVendorCreditService } from './commands/DeleteVendorCredit.service'; + +@Injectable() +export class ValidateBulkDeleteVendorCreditsService { + constructor( + private readonly deleteVendorCreditService: DeleteVendorCreditService, + @Inject(TENANCY_DB_CONNECTION) + private readonly tenantKnex: () => Knex, + ) { } + + public async validateBulkDeleteVendorCredits( + vendorCreditIds: number[], + ): Promise<{ + deletableCount: number; + nonDeletableCount: number; + deletableIds: number[]; + nonDeletableIds: number[]; + }> { + const trx = await this.tenantKnex().transaction({ + isolationLevel: 'read uncommitted', + }); + + try { + const deletableIds: number[] = []; + const nonDeletableIds: number[] = []; + + for (const vendorCreditId of vendorCreditIds) { + try { + await this.deleteVendorCreditService.deleteVendorCredit( + vendorCreditId, + ); + deletableIds.push(vendorCreditId); + } catch (error) { + nonDeletableIds.push(vendorCreditId); + } + } + + await trx.rollback(); + + return { + deletableCount: deletableIds.length, + nonDeletableCount: nonDeletableIds.length, + deletableIds, + nonDeletableIds, + }; + } catch (error) { + await trx.rollback(); + throw error; + } + } +} + diff --git a/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalsAlerts.tsx b/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalsAlerts.tsx index d45132c5a..fe4a242e2 100644 --- a/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalsAlerts.tsx +++ b/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalsAlerts.tsx @@ -8,6 +8,10 @@ const JournalPublishAlert = React.lazy( () => import('@/containers/Alerts/ManualJournals/JournalPublishAlert'), ); +const JournalBulkDeleteAlert = React.lazy( + () => import('@/containers/Alerts/ManualJournals/JournalBulkDeleteAlert'), +); + /** * Manual journals alerts. */ @@ -15,4 +19,5 @@ const JournalPublishAlert = React.lazy( export default [ { name: 'journal-delete', component: JournalDeleteAlert }, { name: 'journal-publish', component: JournalPublishAlert }, + { name: 'journals-bulk-delete', component: JournalBulkDeleteAlert }, ]; diff --git a/packages/webapp/src/containers/Accounting/JournalsLanding/components.tsx b/packages/webapp/src/containers/Accounting/JournalsLanding/components.tsx index de5a9bbff..74e3dc902 100644 --- a/packages/webapp/src/containers/Accounting/JournalsLanding/components.tsx +++ b/packages/webapp/src/containers/Accounting/JournalsLanding/components.tsx @@ -102,13 +102,13 @@ export const StatusAccessor = (row) => { return ( - + - + diff --git a/packages/webapp/src/containers/Accounts/AccountsActionsBar.tsx b/packages/webapp/src/containers/Accounts/AccountsActionsBar.tsx index b495842b2..edaac4899 100644 --- a/packages/webapp/src/containers/Accounts/AccountsActionsBar.tsx +++ b/packages/webapp/src/containers/Accounts/AccountsActionsBar.tsx @@ -182,6 +182,7 @@ function AccountsActionsBar({ intent={Intent.DANGER} onClick={handleBulkDelete} /> + + + + + + + ); +} + +export default compose( + withDialogRedux(), + withDialogActions, + withAccountsTableActions, +)(AccountBulkDeleteDialog); + diff --git a/packages/webapp/src/containers/Dialogs/Bills/BillBulkDeleteDialog.tsx b/packages/webapp/src/containers/Dialogs/Bills/BillBulkDeleteDialog.tsx new file mode 100644 index 000000000..a484cb8a9 --- /dev/null +++ b/packages/webapp/src/containers/Dialogs/Bills/BillBulkDeleteDialog.tsx @@ -0,0 +1,105 @@ +// @ts-nocheck +import React from 'react'; +import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; +import { FormattedMessage as T, AppToaster } from '@/components'; +import intl from 'react-intl-universal'; +import { queryCache } from 'react-query'; + +import BulkDeleteDialogContent from '@/containers/Dialogs/components/BulkDeleteDialogContent'; +import { useBulkDeleteBills } from '@/hooks/query/bills'; +import withDialogRedux from '@/components/DialogReduxConnect'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import withBillsActions from '@/containers/Purchases/Bills/BillsLanding/withBillsActions'; +import { compose } from '@/utils'; + +function BillBulkDeleteDialog({ + dialogName, + isOpen, + payload: { + ids = [], + deletableCount = 0, + undeletableCount = 0, + totalSelected = ids.length, + } = {}, + + // #withBillsActions + setBillsSelectedRows, + + // #withDialogActions + closeDialog, +}) { + const { mutateAsync: bulkDeleteBills, isLoading } = useBulkDeleteBills(); + + const handleCancel = () => { + closeDialog(dialogName); + }; + + const handleConfirmBulkDelete = () => { + bulkDeleteBills({ + ids, + skipUndeletable: true, + }) + .then(() => { + AppToaster.show({ + message: intl.get('the_bills_has_been_deleted_successfully'), + intent: Intent.SUCCESS, + }); + queryCache.invalidateQueries('bills-table'); + closeDialog(dialogName); + setBillsSelectedRows([]); + }) + .catch(() => { + AppToaster.show({ + message: intl.get('something_went_wrong'), + intent: Intent.DANGER, + }); + }); + }; + + return ( + + } + isOpen={isOpen} + onClose={handleCancel} + canEscapeKeyClose={!isLoading} + canOutsideClickClose={!isLoading} + > + + +
+
+ + + +
+
+
+ ); +} + +export default compose( + withDialogRedux(), + withDialogActions, + withBillsActions, +)(BillBulkDeleteDialog); + diff --git a/packages/webapp/src/containers/Dialogs/CreditNotes/CreditNoteBulkDeleteDialog.tsx b/packages/webapp/src/containers/Dialogs/CreditNotes/CreditNoteBulkDeleteDialog.tsx new file mode 100644 index 000000000..731cbcd8e --- /dev/null +++ b/packages/webapp/src/containers/Dialogs/CreditNotes/CreditNoteBulkDeleteDialog.tsx @@ -0,0 +1,106 @@ +// @ts-nocheck +import React from 'react'; +import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; +import { FormattedMessage as T, AppToaster } from '@/components'; +import intl from 'react-intl-universal'; +import { queryCache } from 'react-query'; + +import BulkDeleteDialogContent from '@/containers/Dialogs/components/BulkDeleteDialogContent'; +import { useBulkDeleteCreditNotes } from '@/hooks/query/creditNote'; +import withDialogRedux from '@/components/DialogReduxConnect'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import withCreditNotesActions from '@/containers/Sales/CreditNotes/CreditNotesLanding/withCreditNotesActions'; +import { compose } from '@/utils'; + +function CreditNoteBulkDeleteDialog({ + dialogName, + isOpen, + payload: { + ids = [], + deletableCount = 0, + undeletableCount = 0, + totalSelected = ids.length, + } = {}, + + // #withCreditNotesActions + setCreditNotesSelectedRows, + + // #withDialogActions + closeDialog, +}) { + const { mutateAsync: bulkDeleteCreditNotes, isLoading } = + useBulkDeleteCreditNotes(); + + const handleCancel = () => { + closeDialog(dialogName); + }; + + const handleConfirmBulkDelete = () => { + bulkDeleteCreditNotes({ + ids, + skipUndeletable: true, + }) + .then(() => { + AppToaster.show({ + message: intl.get('the_credit_notes_has_been_deleted_successfully'), + intent: Intent.SUCCESS, + }); + queryCache.invalidateQueries('credit-notes-table'); + closeDialog(dialogName); + setCreditNotesSelectedRows([]); + }) + .catch(() => { + AppToaster.show({ + message: intl.get('something_went_wrong'), + intent: Intent.DANGER, + }); + }); + }; + + return ( + + } + isOpen={isOpen} + onClose={handleCancel} + canEscapeKeyClose={!isLoading} + canOutsideClickClose={!isLoading} + > + + +
+
+ + + +
+
+
+ ); +} + +export default compose( + withDialogRedux(), + withDialogActions, + withCreditNotesActions, +)(CreditNoteBulkDeleteDialog); + diff --git a/packages/webapp/src/containers/Dialogs/Estimates/EstimateBulkDeleteDialog.tsx b/packages/webapp/src/containers/Dialogs/Estimates/EstimateBulkDeleteDialog.tsx new file mode 100644 index 000000000..95aa299f4 --- /dev/null +++ b/packages/webapp/src/containers/Dialogs/Estimates/EstimateBulkDeleteDialog.tsx @@ -0,0 +1,106 @@ +// @ts-nocheck +import React from 'react'; +import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; +import { FormattedMessage as T, AppToaster } from '@/components'; +import intl from 'react-intl-universal'; +import { queryCache } from 'react-query'; + +import BulkDeleteDialogContent from '@/containers/Dialogs/components/BulkDeleteDialogContent'; +import { useBulkDeleteEstimates } from '@/hooks/query/estimates'; +import withDialogRedux from '@/components/DialogReduxConnect'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import withEstimatesActions from '@/containers/Sales/Estimates/EstimatesLanding/withEstimatesActions'; +import { compose } from '@/utils'; + +function EstimateBulkDeleteDialog({ + dialogName, + isOpen, + payload: { + ids = [], + deletableCount = 0, + undeletableCount = 0, + totalSelected = ids.length, + } = {}, + + // #withEstimatesActions + setEstimatesSelectedRows, + + // #withDialogActions + closeDialog, +}) { + const { mutateAsync: bulkDeleteEstimates, isLoading } = + useBulkDeleteEstimates(); + + const handleCancel = () => { + closeDialog(dialogName); + }; + + const handleConfirmBulkDelete = () => { + bulkDeleteEstimates({ + ids, + skipUndeletable: true, + }) + .then(() => { + AppToaster.show({ + message: intl.get('the_estimates_has_been_deleted_successfully'), + intent: Intent.SUCCESS, + }); + queryCache.invalidateQueries('estimates-table'); + closeDialog(dialogName); + setEstimatesSelectedRows([]); + }) + .catch(() => { + AppToaster.show({ + message: intl.get('something_went_wrong'), + intent: Intent.DANGER, + }); + }); + }; + + return ( + + } + isOpen={isOpen} + onClose={handleCancel} + canEscapeKeyClose={!isLoading} + canOutsideClickClose={!isLoading} + > + + +
+
+ + + +
+
+
+ ); +} + +export default compose( + withDialogRedux(), + withDialogActions, + withEstimatesActions, +)(EstimateBulkDeleteDialog); + diff --git a/packages/webapp/src/containers/Dialogs/Expenses/ExpenseBulkDeleteDialog.tsx b/packages/webapp/src/containers/Dialogs/Expenses/ExpenseBulkDeleteDialog.tsx new file mode 100644 index 000000000..c8c3d22fe --- /dev/null +++ b/packages/webapp/src/containers/Dialogs/Expenses/ExpenseBulkDeleteDialog.tsx @@ -0,0 +1,106 @@ +// @ts-nocheck +import React from 'react'; +import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; +import { FormattedMessage as T, AppToaster } from '@/components'; +import intl from 'react-intl-universal'; +import { queryCache } from 'react-query'; + +import BulkDeleteDialogContent from '@/containers/Dialogs/components/BulkDeleteDialogContent'; +import { useBulkDeleteExpenses } from '@/hooks/query/expenses'; +import withDialogRedux from '@/components/DialogReduxConnect'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import withExpensesActions from '@/containers/Expenses/ExpensesLanding/withExpensesActions'; +import { compose } from '@/utils'; + +function ExpenseBulkDeleteDialog({ + dialogName, + isOpen, + payload: { + ids = [], + deletableCount = 0, + undeletableCount = 0, + totalSelected = ids.length, + } = {}, + + // #withExpensesActions + setExpensesSelectedRows, + + // #withDialogActions + closeDialog, +}) { + const { mutateAsync: bulkDeleteExpenses, isLoading } = + useBulkDeleteExpenses(); + + const handleCancel = () => { + closeDialog(dialogName); + }; + + const handleConfirmBulkDelete = () => { + bulkDeleteExpenses({ + ids, + skipUndeletable: true, + }) + .then(() => { + AppToaster.show({ + message: intl.get('the_expenses_has_been_deleted_successfully'), + intent: Intent.SUCCESS, + }); + queryCache.invalidateQueries('expenses-table'); + closeDialog(dialogName); + setExpensesSelectedRows([]); + }) + .catch(() => { + AppToaster.show({ + message: intl.get('something_went_wrong'), + intent: Intent.DANGER, + }); + }); + }; + + return ( + + } + isOpen={isOpen} + onClose={handleCancel} + canEscapeKeyClose={!isLoading} + canOutsideClickClose={!isLoading} + > + + +
+
+ + + +
+
+
+ ); +} + +export default compose( + withDialogRedux(), + withDialogActions, + withExpensesActions, +)(ExpenseBulkDeleteDialog); + diff --git a/packages/webapp/src/containers/Dialogs/Invoices/InvoiceBulkDeleteDialog.tsx b/packages/webapp/src/containers/Dialogs/Invoices/InvoiceBulkDeleteDialog.tsx new file mode 100644 index 000000000..9b13c394b --- /dev/null +++ b/packages/webapp/src/containers/Dialogs/Invoices/InvoiceBulkDeleteDialog.tsx @@ -0,0 +1,110 @@ +// @ts-nocheck +import React from 'react'; +import { FormattedMessage as T } from '@/components'; +import intl from 'react-intl-universal'; +import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; +import { queryCache } from 'react-query'; + +import withDialogRedux from '@/components/DialogReduxConnect'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import withInvoiceActions from '@/containers/Sales/Invoices/InvoicesLanding/withInvoiceActions'; +import { useBulkDeleteInvoices } from '@/hooks/query/invoices'; +import { AppToaster } from '@/components'; +import BulkDeleteDialogContent from '@/containers/Dialogs/components/BulkDeleteDialogContent'; + +import { compose } from '@/utils'; + +/** + * Invoice bulk delete dialog. + */ +function InvoiceBulkDeleteDialog({ + dialogName, + isOpen, + payload: { + ids = [], + deletableCount = 0, + undeletableCount = 0, + totalSelected = ids.length, + } = {}, + + // #withInvoiceActions + resetInvoicesSelectedRows, + + // #withDialogActions + closeDialog, +}) { + const { mutateAsync: bulkDeleteInvoices, isLoading } = + useBulkDeleteInvoices(); + + const handleCancel = () => { + closeDialog(dialogName); + }; + + const handleConfirmBulkDelete = () => { + bulkDeleteInvoices({ + ids, + skipUndeletable: true, + }) + .then(() => { + AppToaster.show({ + message: intl.get('the_invoices_has_been_deleted_successfully'), + intent: Intent.SUCCESS, + }); + queryCache.invalidateQueries('invoices-table'); + closeDialog(dialogName); + resetInvoicesSelectedRows(); + }) + .catch((errors) => { + AppToaster.show({ + message: intl.get('something_went_wrong'), + intent: Intent.DANGER, + }); + }); + }; + + return ( + + } + isOpen={isOpen} + onClose={handleCancel} + canEscapeKeyClose={!isLoading} + canOutsideClickClose={!isLoading} + > + + +
+
+ + + +
+
+
+ ); +} + +export default compose( + withDialogRedux(), + withDialogActions, + withInvoiceActions, +)(InvoiceBulkDeleteDialog); diff --git a/packages/webapp/src/containers/Dialogs/ManualJournals/ManualJournalBulkDeleteDialog.tsx b/packages/webapp/src/containers/Dialogs/ManualJournals/ManualJournalBulkDeleteDialog.tsx new file mode 100644 index 000000000..643333069 --- /dev/null +++ b/packages/webapp/src/containers/Dialogs/ManualJournals/ManualJournalBulkDeleteDialog.tsx @@ -0,0 +1,108 @@ +// @ts-nocheck +import React from 'react'; +import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; +import { FormattedMessage as T, AppToaster } from '@/components'; +import intl from 'react-intl-universal'; +import { queryCache } from 'react-query'; + +import BulkDeleteDialogContent from '@/containers/Dialogs/components/BulkDeleteDialogContent'; +import { useBulkDeleteManualJournals } from '@/hooks/query/manualJournals'; +import withDialogRedux from '@/components/DialogReduxConnect'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import withManualJournalsActions from '@/containers/Accounting/JournalsLanding/withManualJournalsActions'; +import { compose } from '@/utils'; + +function ManualJournalBulkDeleteDialog({ + dialogName, + isOpen, + payload: { + ids = [], + deletableCount = 0, + undeletableCount = 0, + totalSelected = ids.length, + } = {}, + + // #withManualJournalsActions + setManualJournalsSelectedRows, + + // #withDialogActions + closeDialog, +}) { + const { mutateAsync: bulkDeleteManualJournals, isLoading } = + useBulkDeleteManualJournals(); + + const handleCancel = () => { + closeDialog(dialogName); + }; + + const handleConfirmBulkDelete = () => { + bulkDeleteManualJournals({ + ids, + skipUndeletable: true, + }) + .then(() => { + AppToaster.show({ + message: intl.get('the_journals_has_been_deleted_successfully'), + intent: Intent.SUCCESS, + }); + queryCache.invalidateQueries('manual-journals-table'); + closeDialog(dialogName); + setManualJournalsSelectedRows([]); + }) + .catch(() => { + AppToaster.show({ + message: intl.get('something_went_wrong'), + intent: Intent.DANGER, + }); + }); + }; + + return ( + + } + isOpen={isOpen} + onClose={handleCancel} + canEscapeKeyClose={!isLoading} + canOutsideClickClose={!isLoading} + > + + +
+
+ + + +
+
+
+ ); +} + +export default compose( + withDialogRedux(), + withDialogActions, + withManualJournalsActions, +)(ManualJournalBulkDeleteDialog); + diff --git a/packages/webapp/src/containers/Dialogs/PaymentsReceived/PaymentReceivedBulkDeleteDialog.tsx b/packages/webapp/src/containers/Dialogs/PaymentsReceived/PaymentReceivedBulkDeleteDialog.tsx new file mode 100644 index 000000000..7ef6743e1 --- /dev/null +++ b/packages/webapp/src/containers/Dialogs/PaymentsReceived/PaymentReceivedBulkDeleteDialog.tsx @@ -0,0 +1,110 @@ +// @ts-nocheck +import React from 'react'; +import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; +import { FormattedMessage as T, AppToaster } from '@/components'; +import intl from 'react-intl-universal'; +import { queryCache } from 'react-query'; + +import BulkDeleteDialogContent from '@/containers/Dialogs/components/BulkDeleteDialogContent'; +import { useBulkDeletePaymentReceives } from '@/hooks/query/paymentReceives'; +import withDialogRedux from '@/components/DialogReduxConnect'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import withPaymentsReceivedActions from '@/containers/Sales/PaymentsReceived/PaymentsLanding/withPaymentsReceivedActions'; +import { compose } from '@/utils'; + +function PaymentReceivedBulkDeleteDialog({ + dialogName, + isOpen, + payload: { + ids = [], + deletableCount = 0, + undeletableCount = 0, + totalSelected = ids.length, + } = {}, + + // #withPaymentsReceivedActions + setPaymentReceivesSelectedRows, + + // #withDialogActions + closeDialog, +}) { + const { mutateAsync: bulkDeletePaymentReceives, isLoading } = + useBulkDeletePaymentReceives(); + + const handleCancel = () => { + closeDialog(dialogName); + }; + + const handleConfirmBulkDelete = () => { + bulkDeletePaymentReceives({ + ids, + skipUndeletable: true, + }) + .then(() => { + AppToaster.show({ + message: intl.get( + 'the_payments_received_has_been_deleted_successfully', + ), + intent: Intent.SUCCESS, + }); + queryCache.invalidateQueries('payments-received-table'); + closeDialog(dialogName); + setPaymentReceivesSelectedRows([]); + }) + .catch(() => { + AppToaster.show({ + message: intl.get('something_went_wrong'), + intent: Intent.DANGER, + }); + }); + }; + + return ( + + } + isOpen={isOpen} + onClose={handleCancel} + canEscapeKeyClose={!isLoading} + canOutsideClickClose={!isLoading} + > + + +
+
+ + + +
+
+
+ ); +} + +export default compose( + withDialogRedux(), + withDialogActions, + withPaymentsReceivedActions, +)(PaymentReceivedBulkDeleteDialog); + diff --git a/packages/webapp/src/containers/Dialogs/Receipts/ReceiptBulkDeleteDialog.tsx b/packages/webapp/src/containers/Dialogs/Receipts/ReceiptBulkDeleteDialog.tsx new file mode 100644 index 000000000..e5db5ab96 --- /dev/null +++ b/packages/webapp/src/containers/Dialogs/Receipts/ReceiptBulkDeleteDialog.tsx @@ -0,0 +1,106 @@ +// @ts-nocheck +import React from 'react'; +import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; +import { FormattedMessage as T, AppToaster } from '@/components'; +import intl from 'react-intl-universal'; +import { queryCache } from 'react-query'; + +import BulkDeleteDialogContent from '@/containers/Dialogs/components/BulkDeleteDialogContent'; +import { useBulkDeleteReceipts } from '@/hooks/query/receipts'; +import withDialogRedux from '@/components/DialogReduxConnect'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import withReceiptsActions from '@/containers/Sales/Receipts/ReceiptsLanding/withReceiptsActions'; +import { compose } from '@/utils'; + +function ReceiptBulkDeleteDialog({ + dialogName, + isOpen, + payload: { + ids = [], + deletableCount = 0, + undeletableCount = 0, + totalSelected = ids.length, + } = {}, + + // #withReceiptsActions + setReceiptsSelectedRows, + + // #withDialogActions + closeDialog, +}) { + const { mutateAsync: bulkDeleteReceipts, isLoading } = + useBulkDeleteReceipts(); + + const handleCancel = () => { + closeDialog(dialogName); + }; + + const handleConfirmBulkDelete = () => { + bulkDeleteReceipts({ + ids, + skipUndeletable: true, + }) + .then(() => { + AppToaster.show({ + message: intl.get('the_receipts_has_been_deleted_successfully'), + intent: Intent.SUCCESS, + }); + queryCache.invalidateQueries('sale-receipts-table'); + closeDialog(dialogName); + setReceiptsSelectedRows([]); + }) + .catch(() => { + AppToaster.show({ + message: intl.get('something_went_wrong'), + intent: Intent.DANGER, + }); + }); + }; + + return ( + + } + isOpen={isOpen} + onClose={handleCancel} + canEscapeKeyClose={!isLoading} + canOutsideClickClose={!isLoading} + > + + +
+
+ + + +
+
+
+ ); +} + +export default compose( + withDialogRedux(), + withDialogActions, + withReceiptsActions, +)(ReceiptBulkDeleteDialog); + diff --git a/packages/webapp/src/containers/Dialogs/VendorCredits/VendorCreditBulkDeleteDialog.tsx b/packages/webapp/src/containers/Dialogs/VendorCredits/VendorCreditBulkDeleteDialog.tsx new file mode 100644 index 000000000..27decd9ee --- /dev/null +++ b/packages/webapp/src/containers/Dialogs/VendorCredits/VendorCreditBulkDeleteDialog.tsx @@ -0,0 +1,108 @@ +// @ts-nocheck +import React from 'react'; +import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; +import { FormattedMessage as T, AppToaster } from '@/components'; +import intl from 'react-intl-universal'; +import { queryCache } from 'react-query'; + +import BulkDeleteDialogContent from '@/containers/Dialogs/components/BulkDeleteDialogContent'; +import { useBulkDeleteVendorCredits } from '@/hooks/query/vendorCredit'; +import withDialogRedux from '@/components/DialogReduxConnect'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import withVendorsCreditNotesActions from '@/containers/Purchases/CreditNotes/CreditNotesLanding/withVendorsCreditNotesActions'; +import { compose } from '@/utils'; + +function VendorCreditBulkDeleteDialog({ + dialogName, + isOpen, + payload: { + ids = [], + deletableCount = 0, + undeletableCount = 0, + totalSelected = ids.length, + } = {}, + + // #withVendorsCreditNotesActions + setVendorsCreditNoteSelectedRows, + + // #withDialogActions + closeDialog, +}) { + const { mutateAsync: bulkDeleteVendorCredits, isLoading } = + useBulkDeleteVendorCredits(); + + const handleCancel = () => { + closeDialog(dialogName); + }; + + const handleConfirmBulkDelete = () => { + bulkDeleteVendorCredits({ + ids, + skipUndeletable: true, + }) + .then(() => { + AppToaster.show({ + message: intl.get( + 'the_vendor_credits_has_been_deleted_successfully', + ), + intent: Intent.SUCCESS, + }); + queryCache.invalidateQueries('vendor-credits-table'); + closeDialog(dialogName); + setVendorsCreditNoteSelectedRows([]); + }) + .catch(() => { + AppToaster.show({ + message: intl.get('something_went_wrong'), + intent: Intent.DANGER, + }); + }); + }; + + return ( + + } + isOpen={isOpen} + onClose={handleCancel} + canEscapeKeyClose={!isLoading} + canOutsideClickClose={!isLoading} + > + + +
+
+ + + +
+
+
+ ); +} + +export default compose( + withDialogRedux(), + withDialogActions, + withVendorsCreditNotesActions, +)(VendorCreditBulkDeleteDialog); + diff --git a/packages/webapp/src/containers/Dialogs/components/BulkDeleteDialogContent.tsx b/packages/webapp/src/containers/Dialogs/components/BulkDeleteDialogContent.tsx new file mode 100644 index 000000000..6f3068abd --- /dev/null +++ b/packages/webapp/src/containers/Dialogs/components/BulkDeleteDialogContent.tsx @@ -0,0 +1,89 @@ +// @ts-nocheck +import React from 'react'; +import { Classes, Intent, Tag } from '@blueprintjs/core'; +import { FormattedMessage as T } from '@/components'; +import { x } from '@xstyled/emotion'; + +interface BulkDeleteDialogContentProps { + totalSelected: number; + deletableCount: number; + undeletableCount: number; + resourceSingularLabel: string; + resourcePluralLabel: string; +} + +function BulkDeleteDialogContent({ + totalSelected, + deletableCount, + undeletableCount, + resourceSingularLabel, + resourcePluralLabel, +}: BulkDeleteDialogContentProps) { + return ( +
+ + + + + + + {deletableCount} + + + {' '} + + + + + + + + + {undeletableCount} + + + {' '} + + + + + + + + + + + + + + {':'} + + + + + +
+ ); +} + +export default BulkDeleteDialogContent; + diff --git a/packages/webapp/src/containers/Expenses/ExpensesAlerts.tsx b/packages/webapp/src/containers/Expenses/ExpensesAlerts.tsx index 9283f2725..5b772b42c 100644 --- a/packages/webapp/src/containers/Expenses/ExpensesAlerts.tsx +++ b/packages/webapp/src/containers/Expenses/ExpensesAlerts.tsx @@ -8,15 +8,10 @@ const ExpensePublishAlert = React.lazy( () => import('@/containers/Alerts/Expenses/ExpensePublishAlert'), ); -const ExpenseBulkDeleteAlert = React.lazy( - () => import('@/containers/Alerts/Expenses/ExpenseBulkDeleteAlert'), -); - /** * Accounts alert. */ export default [ { name: 'expense-delete', component: ExpenseDeleteAlert }, { name: 'expense-publish', component: ExpensePublishAlert }, - { name: 'expenses-bulk-delete', component: ExpenseBulkDeleteAlert }, ]; diff --git a/packages/webapp/src/containers/Expenses/ExpensesLanding/ExpenseActionsBar.tsx b/packages/webapp/src/containers/Expenses/ExpensesLanding/ExpenseActionsBar.tsx index faa460978..3aaa86217 100644 --- a/packages/webapp/src/containers/Expenses/ExpensesLanding/ExpenseActionsBar.tsx +++ b/packages/webapp/src/containers/Expenses/ExpensesLanding/ExpenseActionsBar.tsx @@ -34,10 +34,10 @@ import withExpensesActions from './withExpensesActions'; import withSettingsActions from '@/containers/Settings/withSettingsActions'; import withDialogActions from '@/containers/Dialog/withDialogActions'; import withSettings from '@/containers/Settings/withSettings'; -import withAlertActions from '@/containers/Alert/withAlertActions'; import { compose } from '@/utils'; import { isEmpty } from 'lodash'; +import { useBulkDeleteExpensesDialog } from './hooks/use-bulk-delete-expenses-dialog'; /** * Expenses actions bar. @@ -58,9 +58,6 @@ function ExpensesActionsBar({ // #withDialogActions openDialog, - - // #withAlertActions - openAlert, }) { // History context. const history = useHistory(); @@ -78,11 +75,14 @@ function ExpensesActionsBar({ const onClickNewExpense = () => { history.push('/expenses/new'); }; + const { + openBulkDeleteDialog, + isValidatingBulkDeleteExpenses, + } = useBulkDeleteExpensesDialog(); + // Handle delete button click. const handleBulkDelete = () => { - openAlert('expenses-bulk-delete', { - expensesIds: expensesSelectedRows, - }); + openBulkDeleteDialog(expensesSelectedRows); }; // Handles the tab chaning. @@ -122,6 +122,7 @@ function ExpensesActionsBar({ text={} intent={Intent.DANGER} onClick={handleBulkDelete} + disabled={isValidatingBulkDeleteExpenses} /> @@ -209,7 +210,6 @@ function ExpensesActionsBar({ export default compose( withDialogActions, - withAlertActions, withExpensesActions, withSettingsActions, withExpenses(({ expensesTableState, expensesSelectedRows }) => ({ diff --git a/packages/webapp/src/containers/Expenses/ExpensesLanding/hooks/use-bulk-delete-expenses-dialog.ts b/packages/webapp/src/containers/Expenses/ExpensesLanding/hooks/use-bulk-delete-expenses-dialog.ts new file mode 100644 index 000000000..dcb7c18b2 --- /dev/null +++ b/packages/webapp/src/containers/Expenses/ExpensesLanding/hooks/use-bulk-delete-expenses-dialog.ts @@ -0,0 +1,20 @@ +// @ts-nocheck +import { DialogsName } from '@/constants/dialogs'; +import { useValidateBulkDeleteExpenses } from '@/hooks/query/expenses'; +import { useBulkDeleteDialog } from '@/hooks/dialogs/useBulkDeleteDialog'; + +export const useBulkDeleteExpensesDialog = () => { + const validateBulkDeleteMutation = useValidateBulkDeleteExpenses(); + const { + openBulkDeleteDialog, + closeBulkDeleteDialog, + isValidatingBulkDelete, + } = useBulkDeleteDialog(DialogsName.ExpenseBulkDelete, validateBulkDeleteMutation); + + return { + openBulkDeleteDialog, + closeBulkDeleteDialog, + isValidatingBulkDeleteExpenses: isValidatingBulkDelete, + }; +}; + diff --git a/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsActionsBar.tsx b/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsActionsBar.tsx index 94bdd7bea..3e7e39193 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsActionsBar.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsActionsBar.tsx @@ -29,11 +29,13 @@ import withBillsActions from './withBillsActions'; import withSettings from '@/containers/Settings/withSettings'; import withSettingsActions from '@/containers/Settings/withSettingsActions'; import withDialogActions from '@/containers/Dialog/withDialogActions'; -import withAlertActions from '@/containers/Alert/withAlertActions'; import { useBillsListContext } from './BillsListProvider'; -import { useRefreshBills } from '@/hooks/query/bills'; +import { + useRefreshBills, +} from '@/hooks/query/bills'; import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf'; +import { useBulkDeleteBillsDialog } from './hooks/use-bulk-delete-bills-dialog'; import { compose } from '@/utils'; import { DialogsName } from '@/constants/dialogs'; @@ -58,9 +60,6 @@ function BillActionsBar({ // #withDialogActions openDialog, - - // #withAlertActions - openAlert, }) { const history = useHistory(); @@ -103,9 +102,14 @@ function BillActionsBar({ const handlePrintBtnClick = () => { downloadExportPdf({ resource: 'Bill' }); }; + const { + openBulkDeleteDialog, + isValidatingBulkDeleteBills, + } = useBulkDeleteBillsDialog(); + // Handle bulk delete. const handleBulkDelete = () => { - openAlert('bills-bulk-delete', { billsIds: billsSelectedRows }); + openBulkDeleteDialog(billsSelectedRows); }; if (!isEmpty(billsSelectedRows)) { @@ -118,6 +122,7 @@ function BillActionsBar({ text={} intent={Intent.DANGER} onClick={handleBulkDelete} + disabled={isValidatingBulkDeleteBills} /> @@ -214,5 +219,4 @@ export default compose( billsTableSize: billsettings?.tableSize, })), withDialogActions, - withAlertActions, )(BillActionsBar); diff --git a/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsAlerts.tsx b/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsAlerts.tsx index 791cb5cb4..266075f52 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsAlerts.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsAlerts.tsx @@ -12,10 +12,6 @@ const BillLocatedLandedCostDeleteAlert = React.lazy( () => import('@/containers/Alerts/Bills/BillLocatedLandedCostDeleteAlert'), ); -const BillBulkDeleteAlert = React.lazy( - () => import('@/containers/Alerts/Bills/BillBulkDeleteAlert'), -); - export default [ { name: 'bill-delete', component: BillDeleteAlert }, { name: 'bill-open', component: BillOpenAlert }, @@ -23,5 +19,4 @@ export default [ name: 'bill-located-cost-delete', component: BillLocatedLandedCostDeleteAlert, }, - { name: 'bills-bulk-delete', component: BillBulkDeleteAlert }, ]; diff --git a/packages/webapp/src/containers/Purchases/Bills/BillsLanding/hooks/use-bulk-delete-bills-dialog.ts b/packages/webapp/src/containers/Purchases/Bills/BillsLanding/hooks/use-bulk-delete-bills-dialog.ts new file mode 100644 index 000000000..80008c167 --- /dev/null +++ b/packages/webapp/src/containers/Purchases/Bills/BillsLanding/hooks/use-bulk-delete-bills-dialog.ts @@ -0,0 +1,20 @@ +// @ts-nocheck +import { DialogsName } from '@/constants/dialogs'; +import { useValidateBulkDeleteBills } from '@/hooks/query/bills'; +import { useBulkDeleteDialog } from '@/hooks/dialogs/useBulkDeleteDialog'; + +export const useBulkDeleteBillsDialog = () => { + const validateBulkDeleteMutation = useValidateBulkDeleteBills(); + const { + openBulkDeleteDialog, + closeBulkDeleteDialog, + isValidatingBulkDelete, + } = useBulkDeleteDialog(DialogsName.BillBulkDelete, validateBulkDeleteMutation); + + return { + openBulkDeleteDialog, + closeBulkDeleteDialog, + isValidatingBulkDeleteBills: isValidatingBulkDelete, + }; +}; + diff --git a/packages/webapp/src/containers/Purchases/CreditNotes/CreditNotesLanding/VendorsCreditNoteActionsBar.tsx b/packages/webapp/src/containers/Purchases/CreditNotes/CreditNotesLanding/VendorsCreditNoteActionsBar.tsx index 75bbcacd6..9978092cd 100644 --- a/packages/webapp/src/containers/Purchases/CreditNotes/CreditNotesLanding/VendorsCreditNoteActionsBar.tsx +++ b/packages/webapp/src/containers/Purchases/CreditNotes/CreditNotesLanding/VendorsCreditNoteActionsBar.tsx @@ -1,6 +1,7 @@ // @ts-nocheck import React from 'react'; import { useHistory } from 'react-router-dom'; +import { isEmpty } from 'lodash'; import { Button, Classes, @@ -32,7 +33,6 @@ import { VendorCreditAction, AbilitySubject } from '@/constants/abilityOption'; import withVendorsCreditNotesActions from './withVendorsCreditNotesActions'; import withSettings from '@/containers/Settings/withSettings'; import withSettingsActions from '@/containers/Settings/withSettingsActions'; -import withVendorsCreditNotes from './withVendorsCreditNotes'; import withDialogActions from '@/containers/Dialog/withDialogActions'; import withVendorActions from './withVendorActions'; import withDrawerActions from '@/containers/Drawer/withDrawerActions'; @@ -40,6 +40,8 @@ import withDrawerActions from '@/containers/Drawer/withDrawerActions'; import { compose } from '@/utils'; import { DialogsName } from '@/constants/dialogs'; import { DRAWERS } from '@/constants/drawers'; +import withVendorsCreditNotes from './withVendorsCreditNotes'; +import { useBulkDeleteVendorCreditsDialog } from './hooks/use-bulk-delete-vendor-credits-dialog'; /** * Vendors Credit note table actions bar. @@ -49,6 +51,7 @@ function VendorsCreditNoteActionsBar({ // #withVendorsCreditNotes vendorCreditFilterRoles, + vendorsCreditNoteSelectedRows, // #withVendorsCreditNotesActions setVendorsCreditNoteTableState, @@ -107,6 +110,32 @@ function VendorsCreditNoteActionsBar({ openDrawer(DRAWERS.CREDIT_NOTE_DETAILS); }; + const { + openBulkDeleteDialog, + isValidatingBulkDeleteVendorCredits, + } = useBulkDeleteVendorCreditsDialog(); + + if (!isEmpty(vendorsCreditNoteSelectedRows)) { + const handleBulkDelete = () => { + openBulkDeleteDialog(vendorsCreditNoteSelectedRows); + }; + + return ( + + + + + + + + + ); +} + +export default compose( + withDialogRedux(), + withDialogActions, + withCustomersActions, +)(CustomerBulkDeleteDialog); + diff --git a/packages/webapp/src/containers/Dialogs/Estimates/EstimateBulkDeleteDialog.tsx b/packages/webapp/src/containers/Dialogs/Estimates/EstimateBulkDeleteDialog.tsx index 767cfa8bd..1a85eccf7 100644 --- a/packages/webapp/src/containers/Dialogs/Estimates/EstimateBulkDeleteDialog.tsx +++ b/packages/webapp/src/containers/Dialogs/Estimates/EstimateBulkDeleteDialog.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; import { FormattedMessage as T, AppToaster } from '@/components'; import intl from 'react-intl-universal'; -import { queryCache } from 'react-query'; import BulkDeleteDialogContent from '@/containers/Dialogs/components/BulkDeleteDialogContent'; import { useBulkDeleteEstimates } from '@/hooks/query/estimates'; @@ -45,7 +44,6 @@ function EstimateBulkDeleteDialog({ message: intl.get('the_estimates_has_been_deleted_successfully'), intent: Intent.SUCCESS, }); - queryCache.invalidateQueries('estimates-table'); setEstimatesSelectedRows([]); closeDialog(dialogName); }) diff --git a/packages/webapp/src/containers/Dialogs/Expenses/ExpenseBulkDeleteDialog.tsx b/packages/webapp/src/containers/Dialogs/Expenses/ExpenseBulkDeleteDialog.tsx index a22c832da..f5673cb61 100644 --- a/packages/webapp/src/containers/Dialogs/Expenses/ExpenseBulkDeleteDialog.tsx +++ b/packages/webapp/src/containers/Dialogs/Expenses/ExpenseBulkDeleteDialog.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; import { FormattedMessage as T, AppToaster } from '@/components'; import intl from 'react-intl-universal'; -import { queryCache } from 'react-query'; import BulkDeleteDialogContent from '@/containers/Dialogs/components/BulkDeleteDialogContent'; import { useBulkDeleteExpenses } from '@/hooks/query/expenses'; @@ -45,7 +44,6 @@ function ExpenseBulkDeleteDialog({ message: intl.get('the_expenses_has_been_deleted_successfully'), intent: Intent.SUCCESS, }); - queryCache.invalidateQueries('expenses-table'); setExpensesSelectedRows([]); closeDialog(dialogName); }) diff --git a/packages/webapp/src/containers/Dialogs/Invoices/InvoiceBulkDeleteDialog.tsx b/packages/webapp/src/containers/Dialogs/Invoices/InvoiceBulkDeleteDialog.tsx index cdc44db34..8633c5367 100644 --- a/packages/webapp/src/containers/Dialogs/Invoices/InvoiceBulkDeleteDialog.tsx +++ b/packages/webapp/src/containers/Dialogs/Invoices/InvoiceBulkDeleteDialog.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { FormattedMessage as T } from '@/components'; import intl from 'react-intl-universal'; import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; -import { queryCache } from 'react-query'; import withDialogRedux from '@/components/DialogReduxConnect'; import withDialogActions from '@/containers/Dialog/withDialogActions'; @@ -50,7 +49,6 @@ function InvoiceBulkDeleteDialog({ message: intl.get('the_invoices_has_been_deleted_successfully'), intent: Intent.SUCCESS, }); - queryCache.invalidateQueries('invoices-table'); resetInvoicesSelectedRows(); closeDialog(dialogName); }) diff --git a/packages/webapp/src/containers/Dialogs/Items/ItemBulkDeleteDialog.tsx b/packages/webapp/src/containers/Dialogs/Items/ItemBulkDeleteDialog.tsx new file mode 100644 index 000000000..a3cb8ca9d --- /dev/null +++ b/packages/webapp/src/containers/Dialogs/Items/ItemBulkDeleteDialog.tsx @@ -0,0 +1,103 @@ +// @ts-nocheck +import React from 'react'; +import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; +import { FormattedMessage as T, AppToaster } from '@/components'; +import intl from 'react-intl-universal'; + +import BulkDeleteDialogContent from '@/containers/Dialogs/components/BulkDeleteDialogContent'; +import { useBulkDeleteItems } from '@/hooks/query/items'; +import withDialogRedux from '@/components/DialogReduxConnect'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import withItemsActions from '@/containers/Items/withItemsActions'; +import { compose } from '@/utils'; + +function ItemBulkDeleteDialog({ + dialogName, + isOpen, + payload: { + ids = [], + deletableCount = 0, + undeletableCount = 0, + totalSelected = ids.length, + } = {}, + + // #withItemsActions + setItemsSelectedRows, + + // #withDialogActions + closeDialog, +}) { + const { mutateAsync: bulkDeleteItems, isLoading } = useBulkDeleteItems(); + + const handleCancel = () => { + closeDialog(dialogName); + }; + + const handleConfirmBulkDelete = () => { + bulkDeleteItems({ + ids, + skipUndeletable: true, + }) + .then(() => { + AppToaster.show({ + message: intl.get('the_items_has_been_deleted_successfully'), + intent: Intent.SUCCESS, + }); + setItemsSelectedRows([]); + closeDialog(dialogName); + }) + .catch(() => { + AppToaster.show({ + message: intl.get('something_went_wrong'), + intent: Intent.DANGER, + }); + }); + }; + + return ( + + } + isOpen={isOpen} + onClose={handleCancel} + canEscapeKeyClose={!isLoading} + canOutsideClickClose={!isLoading} + > + + +
+
+ + + +
+
+
+ ); +} + +export default compose( + withDialogRedux(), + withDialogActions, + withItemsActions, +)(ItemBulkDeleteDialog); + diff --git a/packages/webapp/src/containers/Dialogs/ManualJournals/ManualJournalBulkDeleteDialog.tsx b/packages/webapp/src/containers/Dialogs/ManualJournals/ManualJournalBulkDeleteDialog.tsx index a18e9c93f..1bab4a340 100644 --- a/packages/webapp/src/containers/Dialogs/ManualJournals/ManualJournalBulkDeleteDialog.tsx +++ b/packages/webapp/src/containers/Dialogs/ManualJournals/ManualJournalBulkDeleteDialog.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; import { FormattedMessage as T, AppToaster } from '@/components'; import intl from 'react-intl-universal'; -import { queryCache } from 'react-query'; import BulkDeleteDialogContent from '@/containers/Dialogs/components/BulkDeleteDialogContent'; import { useBulkDeleteManualJournals } from '@/hooks/query/manualJournals'; @@ -45,7 +44,6 @@ function ManualJournalBulkDeleteDialog({ message: intl.get('the_journals_has_been_deleted_successfully'), intent: Intent.SUCCESS, }); - queryCache.invalidateQueries('manual-journals-table'); setManualJournalsSelectedRows([]); closeDialog(dialogName); }) diff --git a/packages/webapp/src/containers/Dialogs/PaymentsReceived/PaymentReceivedBulkDeleteDialog.tsx b/packages/webapp/src/containers/Dialogs/PaymentsReceived/PaymentReceivedBulkDeleteDialog.tsx index 73aa0cee1..63bcd88a6 100644 --- a/packages/webapp/src/containers/Dialogs/PaymentsReceived/PaymentReceivedBulkDeleteDialog.tsx +++ b/packages/webapp/src/containers/Dialogs/PaymentsReceived/PaymentReceivedBulkDeleteDialog.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; import { FormattedMessage as T, AppToaster } from '@/components'; import intl from 'react-intl-universal'; -import { queryCache } from 'react-query'; import BulkDeleteDialogContent from '@/containers/Dialogs/components/BulkDeleteDialogContent'; import { useBulkDeletePaymentReceives } from '@/hooks/query/paymentReceives'; @@ -47,7 +46,6 @@ function PaymentReceivedBulkDeleteDialog({ ), intent: Intent.SUCCESS, }); - queryCache.invalidateQueries('payments-received-table'); setPaymentReceivesSelectedRows([]); closeDialog(dialogName); }) diff --git a/packages/webapp/src/containers/Dialogs/Receipts/ReceiptBulkDeleteDialog.tsx b/packages/webapp/src/containers/Dialogs/Receipts/ReceiptBulkDeleteDialog.tsx index b975e1647..d942b4044 100644 --- a/packages/webapp/src/containers/Dialogs/Receipts/ReceiptBulkDeleteDialog.tsx +++ b/packages/webapp/src/containers/Dialogs/Receipts/ReceiptBulkDeleteDialog.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; import { FormattedMessage as T, AppToaster } from '@/components'; import intl from 'react-intl-universal'; -import { queryCache } from 'react-query'; import BulkDeleteDialogContent from '@/containers/Dialogs/components/BulkDeleteDialogContent'; import { useBulkDeleteReceipts } from '@/hooks/query/receipts'; @@ -45,7 +44,6 @@ function ReceiptBulkDeleteDialog({ message: intl.get('the_receipts_has_been_deleted_successfully'), intent: Intent.SUCCESS, }); - queryCache.invalidateQueries('sale-receipts-table'); setReceiptsSelectedRows([]); closeDialog(dialogName); }) diff --git a/packages/webapp/src/containers/Dialogs/VendorCredits/VendorCreditBulkDeleteDialog.tsx b/packages/webapp/src/containers/Dialogs/VendorCredits/VendorCreditBulkDeleteDialog.tsx index a79b2ae70..253da9607 100644 --- a/packages/webapp/src/containers/Dialogs/VendorCredits/VendorCreditBulkDeleteDialog.tsx +++ b/packages/webapp/src/containers/Dialogs/VendorCredits/VendorCreditBulkDeleteDialog.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; import { FormattedMessage as T, AppToaster } from '@/components'; import intl from 'react-intl-universal'; -import { queryCache } from 'react-query'; import BulkDeleteDialogContent from '@/containers/Dialogs/components/BulkDeleteDialogContent'; import { useBulkDeleteVendorCredits } from '@/hooks/query/vendorCredit'; @@ -47,7 +46,6 @@ function VendorCreditBulkDeleteDialog({ ), intent: Intent.SUCCESS, }); - queryCache.invalidateQueries('vendor-credits-table'); setVendorsCreditNoteSelectedRows([]); closeDialog(dialogName); }) diff --git a/packages/webapp/src/containers/Dialogs/Vendors/VendorBulkDeleteDialog.tsx b/packages/webapp/src/containers/Dialogs/Vendors/VendorBulkDeleteDialog.tsx new file mode 100644 index 000000000..bcfc13d78 --- /dev/null +++ b/packages/webapp/src/containers/Dialogs/Vendors/VendorBulkDeleteDialog.tsx @@ -0,0 +1,103 @@ +// @ts-nocheck +import React from 'react'; +import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; +import { FormattedMessage as T, AppToaster } from '@/components'; +import intl from 'react-intl-universal'; + +import BulkDeleteDialogContent from '@/containers/Dialogs/components/BulkDeleteDialogContent'; +import { useBulkDeleteVendors } from '@/hooks/query/vendors'; +import withDialogRedux from '@/components/DialogReduxConnect'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import withVendorsActions from '@/containers/Vendors/VendorsLanding/withVendorsActions'; +import { compose } from '@/utils'; + +function VendorBulkDeleteDialog({ + dialogName, + isOpen, + payload: { + ids = [], + deletableCount = 0, + undeletableCount = 0, + totalSelected = ids.length, + } = {}, + + // #withVendorsActions + setVendorsSelectedRows, + + // #withDialogActions + closeDialog, +}) { + const { mutateAsync: bulkDeleteVendors, isLoading } = useBulkDeleteVendors(); + + const handleCancel = () => { + closeDialog(dialogName); + }; + + const handleConfirmBulkDelete = () => { + bulkDeleteVendors({ + ids, + skipUndeletable: true, + }) + .then(() => { + AppToaster.show({ + message: intl.get('the_vendors_has_been_deleted_successfully'), + intent: Intent.SUCCESS, + }); + setVendorsSelectedRows([]); + closeDialog(dialogName); + }) + .catch(() => { + AppToaster.show({ + message: intl.get('something_went_wrong'), + intent: Intent.DANGER, + }); + }); + }; + + return ( + + } + isOpen={isOpen} + onClose={handleCancel} + canEscapeKeyClose={!isLoading} + canOutsideClickClose={!isLoading} + > + + +
+
+ + + +
+
+
+ ); +} + +export default compose( + withDialogRedux(), + withDialogActions, + withVendorsActions, +)(VendorBulkDeleteDialog); + diff --git a/packages/webapp/src/containers/Items/ItemsActionsBar.tsx b/packages/webapp/src/containers/Items/ItemsActionsBar.tsx index 3c9f69f39..3c97aa3bb 100644 --- a/packages/webapp/src/containers/Items/ItemsActionsBar.tsx +++ b/packages/webapp/src/containers/Items/ItemsActionsBar.tsx @@ -31,7 +31,6 @@ import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export- import withItems from './withItems'; import withItemsActions from './withItemsActions'; -import withAlertActions from '@/containers/Alert/withAlertActions'; import withSettings from '@/containers/Settings/withSettings'; import withSettingsActions from '@/containers/Settings/withSettingsActions'; import withDialogActions from '../Dialog/withDialogActions'; @@ -39,6 +38,7 @@ import withDialogActions from '../Dialog/withDialogActions'; import { DialogsName } from '@/constants/dialogs'; import { compose } from '@/utils'; import { isEmpty } from 'lodash'; +import { useBulkDeleteItemsDialog } from './hooks/use-bulk-delete-items-dialog'; /** * Items actions bar. @@ -52,9 +52,6 @@ function ItemsActionsBar({ setItemsTableState, itemsInactiveMode, - // #withAlertActions - openAlert, - // #withSettings itemsTableSize, @@ -64,6 +61,9 @@ function ItemsActionsBar({ // #withDialogActions openDialog, }) { + const { openBulkDeleteDialog, isValidatingBulkDeleteItems } = + useBulkDeleteItemsDialog(); + // Items list context. const { itemsViews, fields } = useItemsListContext(); @@ -88,7 +88,7 @@ function ItemsActionsBar({ // Handle cancel/confirm items bulk. const handleBulkDelete = () => { - openAlert('items-bulk-delete', { itemsIds: itemsSelectedRows }); + openBulkDeleteDialog(itemsSelectedRows); }; // Handle inactive switch changing. @@ -129,6 +129,7 @@ function ItemsActionsBar({ text={} intent={Intent.DANGER} onClick={handleBulkDelete} + disabled={isValidatingBulkDeleteItems} />
@@ -224,6 +225,5 @@ export default compose( itemsTableSize: itemsSettings.tableSize, })), withItemsActions, - withAlertActions, withDialogActions, )(ItemsActionsBar); diff --git a/packages/webapp/src/containers/Items/ItemsAlerts.tsx b/packages/webapp/src/containers/Items/ItemsAlerts.tsx index 045e09cc4..9b1978730 100644 --- a/packages/webapp/src/containers/Items/ItemsAlerts.tsx +++ b/packages/webapp/src/containers/Items/ItemsAlerts.tsx @@ -13,10 +13,6 @@ const ItemActivateAlert = React.lazy( () => import('@/containers/Alerts/Items/ItemActivateAlert'), ); -const ItemBulkDeleteAlert = React.lazy( - () => import('@/containers/Alerts/Items/ItemBulkDeleteAlert'), -); - const cancelUnlockingPartialAlert = React.lazy( () => import( @@ -40,8 +36,4 @@ export default [ name: 'item-activate', component: ItemActivateAlert, }, - { - name: 'items-bulk-delete', - component: ItemBulkDeleteAlert, - }, ]; diff --git a/packages/webapp/src/containers/Items/hooks/use-bulk-delete-items-dialog.ts b/packages/webapp/src/containers/Items/hooks/use-bulk-delete-items-dialog.ts new file mode 100644 index 000000000..4c24a3e8b --- /dev/null +++ b/packages/webapp/src/containers/Items/hooks/use-bulk-delete-items-dialog.ts @@ -0,0 +1,14 @@ +// @ts-nocheck +import { DialogsName } from '@/constants/dialogs'; +import { useValidateBulkDeleteItems } from '@/hooks/query/items'; +import { useBulkDeleteDialog } from '@/hooks/dialogs/useBulkDeleteDialog'; + +export const useBulkDeleteItemsDialog = () => { + const validateBulkDeleteMutation = useValidateBulkDeleteItems(); + + return useBulkDeleteDialog( + DialogsName.ItemBulkDelete, + validateBulkDeleteMutation, + ); +}; + diff --git a/packages/webapp/src/containers/Vendors/VendorsLanding/VendorActionsBar.tsx b/packages/webapp/src/containers/Vendors/VendorsLanding/VendorActionsBar.tsx index 8ca8a3592..825842270 100644 --- a/packages/webapp/src/containers/Vendors/VendorsLanding/VendorActionsBar.tsx +++ b/packages/webapp/src/containers/Vendors/VendorsLanding/VendorActionsBar.tsx @@ -11,7 +11,6 @@ import { } from '@blueprintjs/core'; import { - If, Can, Icon, FormattedMessage as T, @@ -28,6 +27,8 @@ import { useRefreshVendors } from '@/hooks/query/vendors'; import { useVendorsListContext } from './VendorsListProvider'; import { useHistory } from 'react-router-dom'; import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf'; +import { useBulkDeleteVendorsDialog } from './hooks/use-bulk-delete-vendors-dialog'; +import { isEmpty } from 'lodash'; import withVendors from './withVendors'; import withVendorsActions from './withVendorsActions'; @@ -43,6 +44,7 @@ import { DialogsName } from '@/constants/dialogs'; */ function VendorActionsBar({ // #withVendors + vendorsSelectedRows = [], vendorsFilterConditions, // #withVendorActions @@ -59,6 +61,9 @@ function VendorActionsBar({ openDialog, }) { const history = useHistory(); + const { openBulkDeleteDialog, isValidatingBulkDeleteVendors } = + useBulkDeleteVendorsDialog(); + // Vendors list context. const { vendorsViews, fields } = useVendorsListContext(); @@ -102,6 +107,27 @@ function VendorActionsBar({ downloadExportPdf({ resource: 'Vendor' }); }; + const handleBulkDelete = () => { + openBulkDeleteDialog(vendorsSelectedRows); + }; + + if (!isEmpty(vendorsSelectedRows)) { + return ( + + +