diff --git a/packages/server/src/modules/CreditNotes/CreditNoteApplication.service.ts b/packages/server/src/modules/CreditNotes/CreditNoteApplication.service.ts index a024cde88..5eb4009b1 100644 --- a/packages/server/src/modules/CreditNotes/CreditNoteApplication.service.ts +++ b/packages/server/src/modules/CreditNotes/CreditNoteApplication.service.ts @@ -9,6 +9,8 @@ import { GetCreditNotesService } from './queries/GetCreditNotes.service'; import { CreateCreditNoteDto, EditCreditNoteDto } from './dtos/CreditNote.dto'; import { GetCreditNoteState } from './queries/GetCreditNoteState.service'; import { GetCreditNoteService } from './queries/GetCreditNote.service'; +import { BulkDeleteCreditNotesService } from './BulkDeleteCreditNotes.service'; +import { ValidateBulkDeleteCreditNotesService } from './ValidateBulkDeleteCreditNotes.service'; @Injectable() export class CreditNoteApplication { @@ -20,8 +22,10 @@ export class CreditNoteApplication { private readonly getCreditNotePdfService: GetCreditNotePdf, private readonly getCreditNotesService: GetCreditNotesService, private readonly getCreditNoteStateService: GetCreditNoteState, - private readonly getCreditNoteService: GetCreditNoteService - ) {} + private readonly getCreditNoteService: GetCreditNoteService, + private readonly bulkDeleteCreditNotesService: BulkDeleteCreditNotesService, + private readonly validateBulkDeleteCreditNotesService: ValidateBulkDeleteCreditNotesService, + ) { } /** * Creates a new credit note. @@ -97,4 +101,26 @@ export class CreditNoteApplication { getCreditNote(creditNoteId: number) { return this.getCreditNoteService.getCreditNote(creditNoteId); } + + /** + * Deletes multiple credit notes. + * @param {number[]} creditNoteIds + * @returns {Promise} + */ + bulkDeleteCreditNotes(creditNoteIds: number[]) { + return this.bulkDeleteCreditNotesService.bulkDeleteCreditNotes( + creditNoteIds, + ); + } + + /** + * Validates which credit notes can be deleted. + * @param {number[]} creditNoteIds + * @returns {Promise<{deletableCount: number, nonDeletableCount: number, deletableIds: number[], nonDeletableIds: number[]}>} + */ + validateBulkDeleteCreditNotes(creditNoteIds: number[]) { + return this.validateBulkDeleteCreditNotesService.validateBulkDeleteCreditNotes( + creditNoteIds, + ); + } } diff --git a/packages/server/src/modules/CreditNotes/CreditNotes.controller.ts b/packages/server/src/modules/CreditNotes/CreditNotes.controller.ts index 52dde0e72..3e4835334 100644 --- a/packages/server/src/modules/CreditNotes/CreditNotes.controller.ts +++ b/packages/server/src/modules/CreditNotes/CreditNotes.controller.ts @@ -22,11 +22,16 @@ import { CreateCreditNoteDto, EditCreditNoteDto } from './dtos/CreditNote.dto'; import { CreditNoteResponseDto } from './dtos/CreditNoteResponse.dto'; import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; +import { + BulkDeleteDto, + ValidateBulkDeleteResponseDto, +} from '@/common/dtos/BulkDelete.dto'; @Controller('credit-notes') @ApiTags('Credit Notes') @ApiExtraModels(CreditNoteResponseDto) @ApiExtraModels(PaginatedResponseDto) +@ApiExtraModels(ValidateBulkDeleteResponseDto) @ApiCommonHeaders() export class CreditNotesController { /** @@ -112,6 +117,39 @@ export class CreditNotesController { return this.creditNoteApplication.openCreditNote(creditNoteId); } + @Post('validate-bulk-delete') + @ApiOperation({ + summary: + 'Validates which credit notes can be deleted and returns the results.', + }) + @ApiResponse({ + status: 200, + description: + 'Validation completed with counts and IDs of deletable and non-deletable credit notes.', + schema: { + $ref: getSchemaPath(ValidateBulkDeleteResponseDto), + }, + }) + validateBulkDeleteCreditNotes( + @Body() bulkDeleteDto: BulkDeleteDto, + ): Promise { + return this.creditNoteApplication.validateBulkDeleteCreditNotes( + bulkDeleteDto.ids, + ); + } + + @Post('bulk-delete') + @ApiOperation({ summary: 'Deletes multiple credit notes.' }) + @ApiResponse({ + status: 200, + description: 'Credit notes deleted successfully', + }) + bulkDeleteCreditNotes(@Body() bulkDeleteDto: BulkDeleteDto): Promise { + return this.creditNoteApplication.bulkDeleteCreditNotes( + bulkDeleteDto.ids, + ); + } + @Delete(':id') @ApiOperation({ summary: 'Delete a credit note' }) @ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' }) diff --git a/packages/server/src/modules/CreditNotes/CreditNotes.module.ts b/packages/server/src/modules/CreditNotes/CreditNotes.module.ts index 3b4ed39ae..26a44e004 100644 --- a/packages/server/src/modules/CreditNotes/CreditNotes.module.ts +++ b/packages/server/src/modules/CreditNotes/CreditNotes.module.ts @@ -34,6 +34,8 @@ import { CreditNoteInventoryTransactions } from './commands/CreditNotesInventory import { InventoryCostModule } from '../InventoryCost/InventoryCost.module'; import { CreditNoteRefundsModule } from '../CreditNoteRefunds/CreditNoteRefunds.module'; import { CreditNotesApplyInvoiceModule } from '../CreditNotesApplyInvoice/CreditNotesApplyInvoice.module'; +import { BulkDeleteCreditNotesService } from './BulkDeleteCreditNotes.service'; +import { ValidateBulkDeleteCreditNotesService } from './ValidateBulkDeleteCreditNotes.service'; @Module({ imports: [ @@ -73,6 +75,8 @@ import { CreditNotesApplyInvoiceModule } from '../CreditNotesApplyInvoice/Credit RefundSyncCreditNoteBalanceSubscriber, DeleteCustomerLinkedCreditSubscriber, CreditNoteAutoSerialSubscriber, + BulkDeleteCreditNotesService, + ValidateBulkDeleteCreditNotesService, ], exports: [ CreateCreditNoteService, diff --git a/packages/webapp/src/containers/Alerts/Receipts/ReceiptBulkDeleteAlert.tsx b/packages/webapp/src/containers/Alerts/Receipts/ReceiptBulkDeleteAlert.tsx new file mode 100644 index 000000000..3a6d02c66 --- /dev/null +++ b/packages/webapp/src/containers/Alerts/Receipts/ReceiptBulkDeleteAlert.tsx @@ -0,0 +1,68 @@ +// @ts-nocheck +import React from 'react'; +import { FormattedMessage as T } from '@/components'; +import intl from 'react-intl-universal'; +import { Intent, Alert } from '@blueprintjs/core'; +import { queryCache } from 'react-query'; +import { AppToaster } from '@/components'; + +import { useBulkDeleteReceipts } from '@/hooks/query/receipts'; +import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect'; +import withAlertActions from '@/containers/Alert/withAlertActions'; + +import { compose } from '@/utils'; + +/** + * Receipt bulk delete alert. + */ +function ReceiptBulkDeleteAlert({ + name, + isOpen, + payload: { receiptsIds }, + closeAlert, +}) { + const { mutateAsync: bulkDeleteReceipts, isLoading } = useBulkDeleteReceipts(); + + const handleCancel = () => { + closeAlert(name); + }; + const handleConfirmBulkDelete = () => { + bulkDeleteReceipts(receiptsIds) + .then(() => { + AppToaster.show({ + message: intl.get('the_receipts_has_been_deleted_successfully'), + intent: Intent.SUCCESS, + }); + queryCache.invalidateQueries('sale-receipts-table'); + closeAlert(name); + }) + .catch((errors) => { + // Handle errors + }); + }; + + return ( + } + confirmButtonText={ + + } + icon="trash" + intent={Intent.DANGER} + isOpen={isOpen} + onCancel={handleCancel} + onConfirm={handleConfirmBulkDelete} + loading={isLoading} + > +

+ +

+
+ ); +} + +export default compose( + withAlertStoreConnect(), + withAlertActions, +)(ReceiptBulkDeleteAlert); + diff --git a/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsActionsBar.tsx b/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsActionsBar.tsx index 749ceb279..94bdd7bea 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsActionsBar.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsActionsBar.tsx @@ -29,6 +29,7 @@ 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'; @@ -57,6 +58,9 @@ function BillActionsBar({ // #withDialogActions openDialog, + + // #withAlertActions + openAlert, }) { const history = useHistory(); @@ -210,4 +214,5 @@ export default compose( billsTableSize: billsettings?.tableSize, })), withDialogActions, + withAlertActions, )(BillActionsBar); diff --git a/packages/webapp/src/containers/Sales/CreditNotes/CreditNotesLanding/CreditNotesActionsBar.tsx b/packages/webapp/src/containers/Sales/CreditNotes/CreditNotesLanding/CreditNotesActionsBar.tsx index 3def3a944..2c574e6b4 100644 --- a/packages/webapp/src/containers/Sales/CreditNotes/CreditNotesLanding/CreditNotesActionsBar.tsx +++ b/packages/webapp/src/containers/Sales/CreditNotes/CreditNotesLanding/CreditNotesActionsBar.tsx @@ -6,6 +6,7 @@ import { NavbarDivider, NavbarGroup, Alignment, + Intent, Menu, MenuItem, Popover, @@ -13,6 +14,7 @@ import { Position, } from '@blueprintjs/core'; import { useHistory } from 'react-router-dom'; +import { isEmpty } from 'lodash'; import { Icon, Can, @@ -34,6 +36,7 @@ import withSettings from '@/containers/Settings/withSettings'; import withSettingsActions from '@/containers/Settings/withSettingsActions'; import withDialogActions from '@/containers/Dialog/withDialogActions'; import withDrawerActions from '@/containers/Drawer/withDrawerActions'; +import withAlertActions from '@/containers/Alert/withAlertActions'; import { DialogsName } from '@/constants/dialogs'; import { compose } from '@/utils'; @@ -45,6 +48,7 @@ import { DRAWERS } from '@/constants/drawers'; function CreditNotesActionsBar({ // #withCreditNotes creditNoteFilterRoles, + creditNotesSelectedRows, // #withCreditNotesActions setCreditNotesTableState, @@ -59,7 +63,10 @@ function CreditNotesActionsBar({ openDialog, // #withDrawerActions - openDrawer + openDrawer, + + // #withAlertActions + openAlert, }) { const history = useHistory(); @@ -104,6 +111,26 @@ function CreditNotesActionsBar({ openDrawer(DRAWERS.BRANDING_TEMPLATES, { resource: 'CreditNote' }); } + // Show bulk delete button when rows are selected. + if (!isEmpty(creditNotesSelectedRows)) { + const handleBulkDelete = () => { + openAlert('credit-notes-bulk-delete', { creditNotesIds: creditNotesSelectedRows }); + }; + return ( + + +