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 { 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<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 { 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<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')
@ApiOperation({ summary: 'Delete a credit note' })
@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 { 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,

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 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);

View File

@@ -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 (
<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 (
<DashboardActionsBar>
<NavbarGroup>
@@ -195,12 +222,14 @@ function CreditNotesActionsBar({
export default compose(
withCreditNotesActions,
withSettingsActions,
withCreditNotes(({ creditNoteTableState }) => ({
withCreditNotes(({ creditNoteTableState, creditNotesSelectedRows }) => ({
creditNoteFilterRoles: creditNoteTableState.filterRoles,
creditNotesSelectedRows,
})),
withSettings(({ creditNoteSettings }) => ({
creditNoteTableSize: creditNoteSettings?.tableSize,
})),
withDialogActions,
withDrawerActions
withDrawerActions,
withAlertActions,
)(CreditNotesActionsBar);

View File

@@ -7,6 +7,9 @@ const ReceiptDeleteAlert = React.lazy(
const ReceiptCloseAlert = React.lazy(
() => import('@/containers/Alerts/Receipts/ReceiptCloseAlert'),
);
const ReceiptBulkDeleteAlert = React.lazy(
() => import('@/containers/Alerts/Receipts/ReceiptBulkDeleteAlert'),
);
/**
* Receipts alerts.
@@ -14,4 +17,5 @@ const ReceiptCloseAlert = React.lazy(
export default [
{ name: 'receipt-delete', component: ReceiptDeleteAlert },
{ 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 withSettingsActions from '@/containers/Settings/withSettingsActions';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import withAlertActions from '@/containers/Alert/withAlertActions';
import { useReceiptsListContext } from './ReceiptsListProvider';
import { useRefreshReceipts } from '@/hooks/query/receipts';
@@ -70,6 +71,9 @@ function ReceiptActionsBar({
// #withSettingsActions
addSetting,
// #withAlertActions
openAlert,
}) {
const history = useHistory();
@@ -250,4 +254,5 @@ export default compose(
})),
withDialogActions,
withDrawerActions,
withAlertActions,
)(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.
*/

View File

@@ -544,9 +544,11 @@
"send_to_email": "Send to email",
"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_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_edited_successfully": "The receipt #{number} has been edited 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": "Bills",
"accept": "Accept",

View File

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