This commit is contained in:
Ahmed Bouhuolia
2025-11-17 22:26:33 +02:00
parent 2c64e1b8ab
commit 17bcc14231
11 changed files with 210 additions and 5 deletions

View File

@@ -9,6 +9,8 @@ import { GetCreditNotesService } from './queries/GetCreditNotes.service';
import { CreateCreditNoteDto, EditCreditNoteDto } from './dtos/CreditNote.dto'; import { CreateCreditNoteDto, EditCreditNoteDto } from './dtos/CreditNote.dto';
import { GetCreditNoteState } from './queries/GetCreditNoteState.service'; import { GetCreditNoteState } from './queries/GetCreditNoteState.service';
import { GetCreditNoteService } from './queries/GetCreditNote.service'; import { GetCreditNoteService } from './queries/GetCreditNote.service';
import { BulkDeleteCreditNotesService } from './BulkDeleteCreditNotes.service';
import { ValidateBulkDeleteCreditNotesService } from './ValidateBulkDeleteCreditNotes.service';
@Injectable() @Injectable()
export class CreditNoteApplication { export class CreditNoteApplication {
@@ -20,7 +22,9 @@ export class CreditNoteApplication {
private readonly getCreditNotePdfService: GetCreditNotePdf, private readonly getCreditNotePdfService: GetCreditNotePdf,
private readonly getCreditNotesService: GetCreditNotesService, private readonly getCreditNotesService: GetCreditNotesService,
private readonly getCreditNoteStateService: GetCreditNoteState, private readonly getCreditNoteStateService: GetCreditNoteState,
private readonly getCreditNoteService: GetCreditNoteService private readonly getCreditNoteService: GetCreditNoteService,
private readonly bulkDeleteCreditNotesService: BulkDeleteCreditNotesService,
private readonly validateBulkDeleteCreditNotesService: ValidateBulkDeleteCreditNotesService,
) { } ) { }
/** /**
@@ -97,4 +101,26 @@ export class CreditNoteApplication {
getCreditNote(creditNoteId: number) { getCreditNote(creditNoteId: number) {
return this.getCreditNoteService.getCreditNote(creditNoteId); return this.getCreditNoteService.getCreditNote(creditNoteId);
} }
/**
* Deletes multiple credit notes.
* @param {number[]} creditNoteIds
* @returns {Promise<void>}
*/
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,
);
}
} }

View File

@@ -22,11 +22,16 @@ import { CreateCreditNoteDto, EditCreditNoteDto } from './dtos/CreditNote.dto';
import { CreditNoteResponseDto } from './dtos/CreditNoteResponse.dto'; import { CreditNoteResponseDto } from './dtos/CreditNoteResponse.dto';
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto'; import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import {
BulkDeleteDto,
ValidateBulkDeleteResponseDto,
} from '@/common/dtos/BulkDelete.dto';
@Controller('credit-notes') @Controller('credit-notes')
@ApiTags('Credit Notes') @ApiTags('Credit Notes')
@ApiExtraModels(CreditNoteResponseDto) @ApiExtraModels(CreditNoteResponseDto)
@ApiExtraModels(PaginatedResponseDto) @ApiExtraModels(PaginatedResponseDto)
@ApiExtraModels(ValidateBulkDeleteResponseDto)
@ApiCommonHeaders() @ApiCommonHeaders()
export class CreditNotesController { export class CreditNotesController {
/** /**
@@ -112,6 +117,39 @@ export class CreditNotesController {
return this.creditNoteApplication.openCreditNote(creditNoteId); 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<ValidateBulkDeleteResponseDto> {
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<void> {
return this.creditNoteApplication.bulkDeleteCreditNotes(
bulkDeleteDto.ids,
);
}
@Delete(':id') @Delete(':id')
@ApiOperation({ summary: 'Delete a credit note' }) @ApiOperation({ summary: 'Delete a credit note' })
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' }) @ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })

View File

@@ -34,6 +34,8 @@ import { CreditNoteInventoryTransactions } from './commands/CreditNotesInventory
import { InventoryCostModule } from '../InventoryCost/InventoryCost.module'; import { InventoryCostModule } from '../InventoryCost/InventoryCost.module';
import { CreditNoteRefundsModule } from '../CreditNoteRefunds/CreditNoteRefunds.module'; import { CreditNoteRefundsModule } from '../CreditNoteRefunds/CreditNoteRefunds.module';
import { CreditNotesApplyInvoiceModule } from '../CreditNotesApplyInvoice/CreditNotesApplyInvoice.module'; import { CreditNotesApplyInvoiceModule } from '../CreditNotesApplyInvoice/CreditNotesApplyInvoice.module';
import { BulkDeleteCreditNotesService } from './BulkDeleteCreditNotes.service';
import { ValidateBulkDeleteCreditNotesService } from './ValidateBulkDeleteCreditNotes.service';
@Module({ @Module({
imports: [ imports: [
@@ -73,6 +75,8 @@ import { CreditNotesApplyInvoiceModule } from '../CreditNotesApplyInvoice/Credit
RefundSyncCreditNoteBalanceSubscriber, RefundSyncCreditNoteBalanceSubscriber,
DeleteCustomerLinkedCreditSubscriber, DeleteCustomerLinkedCreditSubscriber,
CreditNoteAutoSerialSubscriber, CreditNoteAutoSerialSubscriber,
BulkDeleteCreditNotesService,
ValidateBulkDeleteCreditNotesService,
], ],
exports: [ exports: [
CreateCreditNoteService, CreateCreditNoteService,

View File

@@ -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 (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={
<T id={'delete_count'} values={{ count: receiptsIds?.length || 0 }} />
}
icon="trash"
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={handleCancel}
onConfirm={handleConfirmBulkDelete}
loading={isLoading}
>
<p>
<T id={'once_delete_these_receipts_you_will_not_able_restore_them'} />
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(ReceiptBulkDeleteAlert);

View File

@@ -29,6 +29,7 @@ import withBillsActions from './withBillsActions';
import withSettings from '@/containers/Settings/withSettings'; import withSettings from '@/containers/Settings/withSettings';
import withSettingsActions from '@/containers/Settings/withSettingsActions'; import withSettingsActions from '@/containers/Settings/withSettingsActions';
import withDialogActions from '@/containers/Dialog/withDialogActions'; import withDialogActions from '@/containers/Dialog/withDialogActions';
import withAlertActions from '@/containers/Alert/withAlertActions';
import { useBillsListContext } from './BillsListProvider'; import { useBillsListContext } from './BillsListProvider';
import { useRefreshBills } from '@/hooks/query/bills'; import { useRefreshBills } from '@/hooks/query/bills';
@@ -57,6 +58,9 @@ function BillActionsBar({
// #withDialogActions // #withDialogActions
openDialog, openDialog,
// #withAlertActions
openAlert,
}) { }) {
const history = useHistory(); const history = useHistory();
@@ -210,4 +214,5 @@ export default compose(
billsTableSize: billsettings?.tableSize, billsTableSize: billsettings?.tableSize,
})), })),
withDialogActions, withDialogActions,
withAlertActions,
)(BillActionsBar); )(BillActionsBar);

View File

@@ -6,6 +6,7 @@ import {
NavbarDivider, NavbarDivider,
NavbarGroup, NavbarGroup,
Alignment, Alignment,
Intent,
Menu, Menu,
MenuItem, MenuItem,
Popover, Popover,
@@ -13,6 +14,7 @@ import {
Position, Position,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { isEmpty } from 'lodash';
import { import {
Icon, Icon,
Can, Can,
@@ -34,6 +36,7 @@ import withSettings from '@/containers/Settings/withSettings';
import withSettingsActions from '@/containers/Settings/withSettingsActions'; import withSettingsActions from '@/containers/Settings/withSettingsActions';
import withDialogActions from '@/containers/Dialog/withDialogActions'; import withDialogActions from '@/containers/Dialog/withDialogActions';
import withDrawerActions from '@/containers/Drawer/withDrawerActions'; import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import withAlertActions from '@/containers/Alert/withAlertActions';
import { DialogsName } from '@/constants/dialogs'; import { DialogsName } from '@/constants/dialogs';
import { compose } from '@/utils'; import { compose } from '@/utils';
@@ -45,6 +48,7 @@ import { DRAWERS } from '@/constants/drawers';
function CreditNotesActionsBar({ function CreditNotesActionsBar({
// #withCreditNotes // #withCreditNotes
creditNoteFilterRoles, creditNoteFilterRoles,
creditNotesSelectedRows,
// #withCreditNotesActions // #withCreditNotesActions
setCreditNotesTableState, setCreditNotesTableState,
@@ -59,7 +63,10 @@ function CreditNotesActionsBar({
openDialog, openDialog,
// #withDrawerActions // #withDrawerActions
openDrawer openDrawer,
// #withAlertActions
openAlert,
}) { }) {
const history = useHistory(); const history = useHistory();
@@ -104,6 +111,26 @@ function CreditNotesActionsBar({
openDrawer(DRAWERS.BRANDING_TEMPLATES, { resource: 'CreditNote' }); 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 (
<DashboardActionsBar>
<NavbarGroup>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="trash-16" iconSize={16} />}
text={<T id={'delete'} />}
intent={Intent.DANGER}
onClick={handleBulkDelete}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
<NavbarGroup> <NavbarGroup>
@@ -195,12 +222,14 @@ function CreditNotesActionsBar({
export default compose( export default compose(
withCreditNotesActions, withCreditNotesActions,
withSettingsActions, withSettingsActions,
withCreditNotes(({ creditNoteTableState }) => ({ withCreditNotes(({ creditNoteTableState, creditNotesSelectedRows }) => ({
creditNoteFilterRoles: creditNoteTableState.filterRoles, creditNoteFilterRoles: creditNoteTableState.filterRoles,
creditNotesSelectedRows,
})), })),
withSettings(({ creditNoteSettings }) => ({ withSettings(({ creditNoteSettings }) => ({
creditNoteTableSize: creditNoteSettings?.tableSize, creditNoteTableSize: creditNoteSettings?.tableSize,
})), })),
withDialogActions, withDialogActions,
withDrawerActions withDrawerActions,
withAlertActions,
)(CreditNotesActionsBar); )(CreditNotesActionsBar);

View File

@@ -7,6 +7,9 @@ const ReceiptDeleteAlert = React.lazy(
const ReceiptCloseAlert = React.lazy( const ReceiptCloseAlert = React.lazy(
() => import('@/containers/Alerts/Receipts/ReceiptCloseAlert'), () => import('@/containers/Alerts/Receipts/ReceiptCloseAlert'),
); );
const ReceiptBulkDeleteAlert = React.lazy(
() => import('@/containers/Alerts/Receipts/ReceiptBulkDeleteAlert'),
);
/** /**
* Receipts alerts. * Receipts alerts.
@@ -14,4 +17,5 @@ const ReceiptCloseAlert = React.lazy(
export default [ export default [
{ name: 'receipt-delete', component: ReceiptDeleteAlert }, { name: 'receipt-delete', component: ReceiptDeleteAlert },
{ name: 'receipt-close', component: ReceiptCloseAlert }, { name: 'receipt-close', component: ReceiptCloseAlert },
{ name: 'receipts-bulk-delete', component: ReceiptBulkDeleteAlert },
]; ];

View File

@@ -35,6 +35,7 @@ import withReceiptsActions from './withReceiptsActions';
import withSettings from '@/containers/Settings/withSettings'; import withSettings from '@/containers/Settings/withSettings';
import withSettingsActions from '@/containers/Settings/withSettingsActions'; import withSettingsActions from '@/containers/Settings/withSettingsActions';
import withDialogActions from '@/containers/Dialog/withDialogActions'; import withDialogActions from '@/containers/Dialog/withDialogActions';
import withAlertActions from '@/containers/Alert/withAlertActions';
import { useReceiptsListContext } from './ReceiptsListProvider'; import { useReceiptsListContext } from './ReceiptsListProvider';
import { useRefreshReceipts } from '@/hooks/query/receipts'; import { useRefreshReceipts } from '@/hooks/query/receipts';
@@ -70,6 +71,9 @@ function ReceiptActionsBar({
// #withSettingsActions // #withSettingsActions
addSetting, addSetting,
// #withAlertActions
openAlert,
}) { }) {
const history = useHistory(); const history = useHistory();
@@ -250,4 +254,5 @@ export default compose(
})), })),
withDialogActions, withDialogActions,
withDrawerActions, withDrawerActions,
withAlertActions,
)(ReceiptActionsBar); )(ReceiptActionsBar);

View File

@@ -104,6 +104,25 @@ export function useDeleteReceipt(props) {
}); });
} }
/**
* Deletes multiple receipts in bulk.
*/
export function useBulkDeleteReceipts(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
(ids: number[]) => apiRequest.post('sale-receipts/bulk-delete', { ids }),
{
onSuccess: () => {
// Common invalidate queries.
commonInvalidateQueries(queryClient);
},
...props,
},
);
}
/** /**
* Deletes the given sale invoice. * Deletes the given sale invoice.
*/ */

View File

@@ -544,9 +544,11 @@
"send_to_email": "Send to email", "send_to_email": "Send to email",
"select_deposit_account": "Select Deposit Account...", "select_deposit_account": "Select Deposit Account...",
"once_delete_this_receipt_you_will_able_to_restore_it": "Once you delete this receipt, you won't be able to restore it later. Are you sure you want to delete it?", "once_delete_this_receipt_you_will_able_to_restore_it": "Once you delete this receipt, you won't be able to restore it later. Are you sure you want to delete it?",
"once_delete_these_receipts_you_will_not_able_restore_them": "Once you delete these receipts, you won't be able to retrieve them later. Are you sure you want to delete them?",
"the_receipt_has_been_created_successfully": "The receipt #{number} has been created successfully.", "the_receipt_has_been_created_successfully": "The receipt #{number} has been created successfully.",
"the_receipt_has_been_edited_successfully": "The receipt #{number} has been edited successfully.", "the_receipt_has_been_edited_successfully": "The receipt #{number} has been edited successfully.",
"the_receipt_has_been_deleted_successfully": "The receipt has been deleted successfully.", "the_receipt_has_been_deleted_successfully": "The receipt has been deleted successfully.",
"the_receipts_has_been_deleted_successfully": "The receipts have been deleted successfully.",
"bills_list": "Bills List", "bills_list": "Bills List",
"bills": "Bills", "bills": "Bills",
"accept": "Accept", "accept": "Accept",

View File

@@ -14,6 +14,7 @@ export const defaultTableQuery = {
const initialState = { const initialState = {
tableState: defaultTableQuery, tableState: defaultTableQuery,
selectedRows: [],
}; };
const STORAGE_KEY = 'bigcapital:receipts'; const STORAGE_KEY = 'bigcapital:receipts';
@@ -27,6 +28,10 @@ const CONFIG = {
const reducerInstance = createReducer(initialState, { const reducerInstance = createReducer(initialState, {
...createTableStateReducers('RECEIPTS', defaultTableQuery), ...createTableStateReducers('RECEIPTS', defaultTableQuery),
[t.RECEIPTS_SELECTED_ROWS_SET]: (state, action) => {
state.selectedRows = action.payload;
},
[t.RESET]: () => { [t.RESET]: () => {
purgeStoredState(CONFIG); purgeStoredState(CONFIG);
}, },