diff --git a/apps/api/src/app/review/review.controller.ts b/apps/api/src/app/review/review.controller.ts index 2bb52c315..ebe7ea1b0 100644 --- a/apps/api/src/app/review/review.controller.ts +++ b/apps/api/src/app/review/review.controller.ts @@ -1,5 +1,5 @@ import { ApiOperation, ApiTags, ApiSecurity, ApiQuery, ApiOkResponse } from '@nestjs/swagger'; -import { BadRequestException, Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; +import { BadRequestException, Body, Controller, Delete, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; import { APIMessages } from '@shared/constants'; import { RecordEntity, UploadEntity } from '@impler/dal'; @@ -13,6 +13,7 @@ import { DoReReview, UpdateRecord, StartProcess, + DeleteRecord, GetUploadData, UpdateImportCount, UpdateImportCountCommand, @@ -31,6 +32,7 @@ export class ReviewController { private doReview: DoReview, private getUpload: GetUpload, private doReReview: DoReReview, + private deleteRecord: DeleteRecord, private startProcess: StartProcess, private updateRecord: UpdateRecord, private updateImportCount: UpdateImportCount, @@ -128,9 +130,22 @@ export class ReviewController { @Put(':uploadId/record') @UseGuards(JwtAuthGuard) @ApiOperation({ - summary: 'Update review data for ongoing import', + summary: 'Update review record for ongoing import', }) async updateReviewData(@Param('uploadId', ValidateMongoId) _uploadId: string, @Body() body: RecordEntity) { await this.updateRecord.execute(_uploadId, body); } + + @Delete(':uploadId/record/:index') + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: 'Delete review record for ongoing import', + }) + async deleteReviewRecord( + @Param('index') index: number, + @Query('isValid') isValid: boolean, + @Param('uploadId', ValidateMongoId) _uploadId: string + ) { + await this.deleteRecord.execute(_uploadId, index, isValid); + } } diff --git a/apps/api/src/app/review/usecases/delete-record/delete-record.usecase.ts b/apps/api/src/app/review/usecases/delete-record/delete-record.usecase.ts new file mode 100644 index 000000000..f986c4f0f --- /dev/null +++ b/apps/api/src/app/review/usecases/delete-record/delete-record.usecase.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@nestjs/common'; +import { DalService, UploadRepository } from '@impler/dal'; + +@Injectable() +export class DeleteRecord { + constructor(private dalService: DalService, private uploadRepository: UploadRepository) {} + + async execute(_uploadId: string, index: number, isValid?: boolean) { + await this.dalService.deleteRecord(_uploadId, index); + if (typeof isValid !== 'undefined') { + await this.uploadRepository.update( + { + _id: _uploadId, + }, + { + $inc: { + totalRecords: -1, + validRecords: isValid ? -1 : 0, + invalidRecords: isValid ? 0 : -1, + }, + } + ); + } + } +} diff --git a/apps/api/src/app/review/usecases/index.ts b/apps/api/src/app/review/usecases/index.ts index 4378c961b..a24b91808 100644 --- a/apps/api/src/app/review/usecases/index.ts +++ b/apps/api/src/app/review/usecases/index.ts @@ -1,5 +1,6 @@ import { UpdateRecord } from './update-cell'; import { DoReview } from './do-review/do-review.usecase'; +import { DeleteRecord } from './delete-record/delete-record.usecase'; import { DoReReview } from './do-review/re-review-data.usecase'; import { StartProcess } from './start-process/start-process.usecase'; import { ConfirmReview } from './confirm-review/confirm-review.usecase'; @@ -13,6 +14,7 @@ export const USE_CASES = [ DoReview, GetUpload, DoReReview, + DeleteRecord, UpdateRecord, StartProcess, ConfirmReview, @@ -21,5 +23,15 @@ export const USE_CASES = [ // ]; -export { DoReview, DoReReview, GetUpload, UpdateRecord, StartProcess, ConfirmReview, GetUploadData, UpdateImportCount }; +export { + DoReview, + GetUpload, + DoReReview, + DeleteRecord, + UpdateRecord, + StartProcess, + ConfirmReview, + GetUploadData, + UpdateImportCount, +}; export { UpdateImportCountCommand }; diff --git a/apps/widget/src/components/Common/Table/HandsonTable.styles.min.css b/apps/widget/src/components/Common/Table/HandsonTable.styles.min.css index 86f1bbad2..2c20459df 100644 --- a/apps/widget/src/components/Common/Table/HandsonTable.styles.min.css +++ b/apps/widget/src/components/Common/Table/HandsonTable.styles.min.css @@ -54,4 +54,20 @@ background-color: #f0f0f0 !important; color: #222 !important; padding: 5px !important; +} +.del-cell { + cursor: pointer; + text-align: center; + padding: 5px !important; + +} +.del-btn { + color: #fff; + border: none; + outline: none; + border-radius: 5px; + background-color: #f00; +} +.del-btn:hover { + background-color: #d00; } \ No newline at end of file diff --git a/apps/widget/src/components/Common/Table/Table.tsx b/apps/widget/src/components/Common/Table/Table.tsx index bdc634ef4..1fc7f1d45 100644 --- a/apps/widget/src/components/Common/Table/Table.tsx +++ b/apps/widget/src/components/Common/Table/Table.tsx @@ -48,6 +48,7 @@ interface TableProps { afterRender?: () => void; data: Record[]; columnDefs: HotItemSchema[]; + onRecordDelete?: (index: number, isValid: boolean) => void; onValueChange?: (row: number, prop: string, oldVal: any, newVal: any) => void; } @@ -100,9 +101,30 @@ Handsontable.renderers.registerRenderer( } ); +// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars +Handsontable.renderers.registerRenderer('del', function renderer(instance, TD, row, col, prop, value, cellProperties) { + TD.classList.add('del-cell'); + const soureData = instance.getSourceDataAtRow(row) as IRecord; + + TD.dataset.index = String(soureData.index); + TD.dataset.isValid = String(soureData.isValid); + TD.innerHTML = ``; + + return TD; +}); + export const Table = forwardRef( ( - { afterRender, height = 'auto', width = 'auto', headings, columnDefs, data, onValueChange }: TableProps, + { + afterRender, + height = 'auto', + width = 'auto', + headings, + columnDefs, + data, + onValueChange, + onRecordDelete, + }: TableProps, gridRef ) => { return ( @@ -124,6 +146,13 @@ export const Table = forwardRef( autoInsertRow: false, direction: 'vertical', }} + afterOnCellMouseDown={function (e, coords, TD) { + if (TD.classList.contains('del-cell')) { + const dataIndex = TD.dataset.index; + const isValid = TD.dataset.isValid === 'true'; + if (onRecordDelete && Number(dataIndex)) onRecordDelete(Number(dataIndex), isValid); + } + }} stretchH="all" columns={columnDefs} colHeaders={headings} diff --git a/apps/widget/src/components/widget/Phases/Phase3/Phase3.tsx b/apps/widget/src/components/widget/Phases/Phase3/Phase3.tsx index 0a8f3d991..e8df36f16 100644 --- a/apps/widget/src/components/widget/Phases/Phase3/Phase3.tsx +++ b/apps/widget/src/components/widget/Phases/Phase3/Phase3.tsx @@ -30,6 +30,7 @@ export function Phase3(props: IPhase3Props) { columnDefs, totalPages, reviewData, + deleteRecord, onTypeChange, reReviewData, updateRecord, @@ -76,9 +77,10 @@ export function Phase3(props: IPhase3Props) { validDataLength={numberFormatter(totalRecords - invalidRecords)} /> deleteRecord([index, isValid])} width={tableWrapperDimensions.width} height={tableWrapperDimensions.height} - ref={tableRef} onValueChange={(row, prop, oldVal, newVal) => { const name = String(prop).replace('record.', ''); let formattedNewVal = newVal; diff --git a/apps/widget/src/hooks/Phase3/usePhase3.tsx b/apps/widget/src/hooks/Phase3/usePhase3.tsx index 763f246a9..6c73d8f4c 100644 --- a/apps/widget/src/hooks/Phase3/usePhase3.tsx +++ b/apps/widget/src/hooks/Phase3/usePhase3.tsx @@ -89,6 +89,16 @@ export function usePhase3({ onNext }: IUsePhase3Props) { } newColumnDefs.push(columnItem); }); + newColumnDefs.push({ + type: 'text', + data: 'record._id', + readOnly: true, + editor: false, + renderer: 'del', + className: 'del-cell', + disableVisualSelection: true, + }); + newHeadings.push('X'); setHeadings(newHeadings); setColumnDefs(newColumnDefs); }, @@ -173,6 +183,25 @@ export function usePhase3({ onNext }: IUsePhase3Props) { const { mutate: updateRecord } = useMutation([`update`], (record) => api.updateRecord(uploadInfo._id, record) ); + const { mutate: deleteRecord } = useMutation( + [`delete`], + ([index, isValid]) => api.deleteRecord(uploadInfo._id, index, isValid), + { + onSuccess(data, vars) { + const newReviewData = reviewData.filter((record) => record.index !== vars[0]); + const newUploadInfo = { ...uploadInfo }; + newUploadInfo.totalRecords = newUploadInfo.totalRecords - 1; + if (!vars[1]) { + newUploadInfo.invalidRecords = newUploadInfo.invalidRecords - 1; + } + setUploadInfo(newUploadInfo); + setReviewData(newReviewData); + if (newReviewData.length === 0) { + refetchReviewData([defaultPage, type]); + } + }, + } + ); const onTypeChange = (newType: ReviewDataTypesEnum) => { setType(newType); @@ -192,6 +221,7 @@ export function usePhase3({ onNext }: IUsePhase3Props) { updateRecord, onPageChange, onTypeChange, + deleteRecord, setReviewData, isDoReviewLoading, isReviewDataLoading, diff --git a/apps/widget/src/types/utility.types.ts b/apps/widget/src/types/utility.types.ts index 09dd6339a..6c9755e76 100644 --- a/apps/widget/src/types/utility.types.ts +++ b/apps/widget/src/types/utility.types.ts @@ -18,6 +18,7 @@ export type HotItemSchema = { disableVisualSelection?: boolean; renderer?: | 'custom' + | 'del' | (( instance: Core, TD: HTMLTableCellElement, diff --git a/libs/dal/src/dal.service.ts b/libs/dal/src/dal.service.ts index 46f1004cd..a071ea12b 100644 --- a/libs/dal/src/dal.service.ts +++ b/libs/dal/src/dal.service.ts @@ -74,6 +74,14 @@ export class DalService { record ); } + async deleteRecord(_uploadId: string, index: number) { + const model = this.getRecordCollection(_uploadId); + if (!model) return; + + await model.deleteOne({ + index, + }); + } getRecordBulkOp(_uploadId: string) { const model = this.getRecordCollection(_uploadId); if (!model) return; diff --git a/packages/client/src/api/api.service.ts b/packages/client/src/api/api.service.ts index 633755b7a..e5560a4a6 100644 --- a/packages/client/src/api/api.service.ts +++ b/packages/client/src/api/api.service.ts @@ -178,4 +178,10 @@ export class ApiService { async updateRecord(uploadId: string, record: IRecord) { return this.httpClient.put(`/review/${uploadId}/record`, record); } + + async deleteRecord(uploadId: string, index: number, isValid: boolean) { + return this.httpClient.delete( + `/review/${uploadId}/record/${index}?isValid=${isValid}` + ); + } }