Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export Review data #50

Merged
merged 11 commits into from
Nov 10, 2022
1 change: 0 additions & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
},
"devDependencies": {
"@types/express": "^4.17.14",
"@types/json2csv": "^5.0.3",
"@types/multer": "^1.4.7",
"@types/node": "^18.7.18",
"nodemon": "^2.0.20",
Expand Down
21 changes: 11 additions & 10 deletions apps/api/src/app/review/review.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,22 @@ import { BadRequestException, Body, Controller, Get, Param, Post, Query, UseGuar
import { ApiOperation, ApiTags, ApiSecurity, ApiQuery, ApiOkResponse } from '@nestjs/swagger';
import { FileEntity, UploadEntity } from '@impler/dal';
import { ACCESS_KEY_NAME, UploadStatusEnum } from '@impler/shared';
import { APIMessages } from '../shared/constants';
import { APIKeyGuard } from '../shared/framework/auth.gaurd';
import { validateUploadStatus } from '../shared/helpers/upload.helpers';
import { APIMessages } from '@shared/constants';
import { APIKeyGuard } from '@shared/framework/auth.gaurd';
import { validateUploadStatus } from '@shared/helpers/upload.helpers';
import { DoReview } from './usecases/do-review/do-review.usecase';
import { GetUploadInvalidData } from './usecases/get-upload-invalid-data/get-upload-invalid-data.usecase';
import { SaveReviewData } from './usecases/save-review-data/save-review-data.usecase';
import { GetFileInvalidData } from './usecases/get-file-invalid-data/get-file-invalid-data.usecase';
import { ValidateMongoId } from '../shared/validations/valid-mongo-id.validation';
import { ValidateMongoId } from '@shared/validations/valid-mongo-id.validation';
import { ConfirmReviewRequestDto } from './dtos/confirm-review-request.dto';
import { GetUploadCommand } from '../shared/usecases/get-upload/get-upload.command';
import { GetUpload } from '../shared/usecases/get-upload/get-upload.usecase';
import { paginateRecords, validateNotFound } from '../shared/helpers/common.helper';
import { GetUploadCommand } from '@shared/usecases/get-upload/get-upload.command';
import { GetUpload } from '@shared/usecases/get-upload/get-upload.usecase';
import { paginateRecords, validateNotFound } from '@shared/helpers/common.helper';
import { StartProcess } from './usecases/start-process/start-process.usecase';
import { StartProcessCommand } from './usecases/start-process/start-process.command';
import { PaginationResponseDto } from '../shared/dtos/pagination-response.dto';
import { PaginationResponseDto } from '@shared/dtos/pagination-response.dto';
import { Defaults } from '@shared/constants';

@Controller('/review')
@ApiTags('Review')
Expand Down Expand Up @@ -54,8 +55,8 @@ export class ReviewController {
})
async getReview(
@Param('uploadId') _uploadId: string,
@Query('page') page = 1,
@Query('limit') limit = 100
@Query('page') page = Defaults.PAGE,
@Query('limit') limit = Defaults.PAGE_LIMIT
): Promise<PaginationResponseDto> {
const uploadData = await this.getUploadInvalidData.execute(_uploadId);
if (!uploadData) throw new BadRequestException(APIMessages.UPLOAD_NOT_FOUND);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import * as XLSX from 'xlsx';
import { Injectable } from '@nestjs/common';
import { FileRepository, UploadRepository } from '@impler/dal';
import { FileMimeTypesEnum } from '@impler/shared';
import { FileNameService } from '../../../shared/file/name.service';
import { StorageService } from '../../../shared/storage/storage.service';
import { FileNameService } from '@shared/file/name.service';
import { StorageService } from '@shared/storage/storage.service';
import { Defaults } from '@shared/constants';

@Injectable()
export class SaveReviewData {
Expand All @@ -16,11 +18,16 @@ export class SaveReviewData {
async execute(_uploadId: string, invalidData: any[], validData: any[]) {
const _invalidDataFileId = await this.storeInvalidFile(_uploadId, invalidData);
const _validDataFileId = await this.storeValidFile(_uploadId, validData);
await this.uploadRepository.update({ _id: _uploadId }, { _invalidDataFileId, _validDataFileId });
const _invalidCSVDataFileId = await this.storeInvalidCSVFile(_uploadId, invalidData);
const invalidCSVDataFileUrl = this.fileNameService.getInvalidCSVDataFileUrl(_uploadId);
await this.uploadRepository.update(
{ _id: _uploadId },
{ _invalidDataFileId, _validDataFileId, _invalidCSVDataFileId, invalidCSVDataFileUrl }
);
}

private async storeValidFile(_uploadId: string, validData: any[]): Promise<string> {
if (validData.length < 1) return null;
if (validData.length < Defaults.DATA_LENGTH) return null;

const strinValidData = JSON.stringify(validData);
const validFilePath = this.fileNameService.getValidDataFilePath(_uploadId);
Expand All @@ -32,7 +39,7 @@ export class SaveReviewData {
}

private async storeInvalidFile(_uploadId: string, invalidData: any[]): Promise<string> {
if (invalidData.length < 1) return null;
if (invalidData.length < Defaults.DATA_LENGTH) return null;

const stringInvalidData = JSON.stringify(invalidData);
const invalidFilePath = this.fileNameService.getInvalidDataFilePath(_uploadId);
Expand All @@ -43,8 +50,22 @@ export class SaveReviewData {
return entry._id;
}

private async storeFile(invalidFilePath: string, data: string) {
await this.storageService.uploadFile(invalidFilePath, data, FileMimeTypesEnum.JSON);
private async storeInvalidCSVFile(_uploadId: string, invalidData: any[]): Promise<string> {
if (invalidData.length < Defaults.DATA_LENGTH) return null;

const ws = XLSX.utils.json_to_sheet(invalidData);
const invalidCSVDataContent = XLSX.utils.sheet_to_csv(ws, { FS: ',' });

const invalidCSVFilePath = this.fileNameService.getInvalidCSVDataFilePath(_uploadId);
await this.storeFile(invalidCSVFilePath, invalidCSVDataContent, true);
const invalidCSVFileName = this.fileNameService.getInvalidCSVDataFileName();
const entry = await this.makeFileEntry(invalidCSVFileName, invalidCSVFilePath);

return entry._id;
}

private async storeFile(invalidFilePath: string, data: string, isPublic = false) {
await this.storageService.uploadFile(invalidFilePath, data, FileMimeTypesEnum.JSON, isPublic);
}

private async makeFileEntry(fileName: string, filePath: string) {
Expand Down
6 changes: 6 additions & 0 deletions apps/api/src/app/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@ export const APIMessages = {
COMPLETED: 'You may landed to wrong place, This uploaded file is already completed, no more steps left to perform.',
PROJECT_WITH_TEMPLATE_MISSING: 'Template not found with provided ProjectId and Template',
};

export const Defaults = {
DATA_LENGTH: 1,
PAGE: 1,
PAGE_LIMIT: 100,
};
11 changes: 11 additions & 0 deletions apps/api/src/app/shared/file/name.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ export class FileNameService {
getInvalidDataFilePath(uploadId: string): string {
return `${uploadId}/${this.getInvalidDataFileName()}`;
}
getInvalidCSVDataFileName(): string {
return 'invalid-data.csv';
}
getInvalidCSVDataFilePath(uploadId: string): string {
return `${uploadId}/${this.getInvalidCSVDataFileName()}`;
}
getInvalidCSVDataFileUrl(uploadId: string): string {
const path = this.getInvalidCSVDataFilePath(uploadId);

return [process.env.S3_LOCAL_STACK, process.env.S3_BUCKET_NAME, path].join('/');
}
getValidDataFileName(): string {
return `valid-data.json`;
}
Expand Down
25 changes: 21 additions & 4 deletions apps/api/src/app/template/template.controller.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';
import { ApiOperation, ApiTags, ApiOkResponse, ApiSecurity } from '@nestjs/swagger';
import { UploadEntity } from '@impler/dal';
import { ACCESS_KEY_NAME } from '@impler/shared';
import { DocumentNotFoundException } from '../shared/exceptions/document-not-found.exception';
import { APIKeyGuard } from '../shared/framework/auth.gaurd';
import { ValidateMongoId } from '../shared/validations/valid-mongo-id.validation';
import { DocumentNotFoundException } from '@shared/exceptions/document-not-found.exception';
import { APIKeyGuard } from '@shared/framework/auth.gaurd';
import { ValidateMongoId } from '@shared/validations/valid-mongo-id.validation';

import { CreateTemplateRequestDto } from './dtos/create-template-request.dto';
import { TemplateResponseDto } from './dtos/template-response.dto';
import { UpdateTemplateRequestDto } from './dtos/update-template-request.dto';
Expand All @@ -13,6 +15,8 @@ import { DeleteTemplate } from './usecases/delete-template/delete-template.useca
import { GetTemplates } from './usecases/get-templates/get-templates.usecase';
import { UpdateTemplateCommand } from './usecases/update-template/update-template.command';
import { UpdateTemplate } from './usecases/update-template/update-template.usecase';
import { GetUploads } from './usecases/get-uploads/get-uploads.usecase';
import { GetUploadsCommand } from './usecases/get-uploads/get-uploads.command';

@Controller('/template')
@ApiTags('Template')
Expand All @@ -23,7 +27,8 @@ export class TemplateController {
private getTemplatesUsecase: GetTemplates,
private createTemplateUsecase: CreateTemplate,
private updateTemplateUsecase: UpdateTemplate,
private deleteTemplateUsecase: DeleteTemplate
private deleteTemplateUsecase: DeleteTemplate,
private getUploads: GetUploads
) {}

@Get(':projectId')
Expand Down Expand Up @@ -101,4 +106,16 @@ export class TemplateController {

return document;
}

@Get(':templateId/uploads')
@ApiOperation({
summary: 'Get all uploads information for template',
})
async getAllUploads(@Param('templateId', ValidateMongoId) templateId: string): Promise<UploadEntity[]> {
return this.getUploads.execute(
GetUploadsCommand.create({
_templateId: templateId,
})
);
}
}
2 changes: 2 additions & 0 deletions apps/api/src/app/template/usecases/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { GetTemplates } from './get-templates/get-templates.usecase';
import { CreateTemplate } from './create-template/create-template.usecase';
import { UpdateTemplate } from './update-template/update-template.usecase';
import { DeleteTemplate } from './delete-template/delete-template.usecase';
import { GetUploads } from './get-uploads/get-uploads.usecase';

export const USE_CASES = [
GetTemplates,
CreateTemplate,
UpdateTemplate,
DeleteTemplate,
GetUploads,
//
];
17 changes: 5 additions & 12 deletions apps/api/src/app/upload/upload.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { Body, Controller, Get, Param, Post, UploadedFile, UseGuards, UseInterce
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiTags, ApiSecurity, ApiConsumes, ApiOperation, ApiParam } from '@nestjs/swagger';
import { ACCESS_KEY_NAME } from '@impler/shared';
import { UploadEntity } from '@impler/dal';

import { APIKeyGuard } from '../shared/framework/auth.gaurd';
import { UploadRequestDto } from './dtos/upload-request.dto';
Expand All @@ -14,16 +13,14 @@ import { MakeUploadEntryCommand } from './usecases/make-upload-entry/make-upload
import { ValidateMongoId } from '../shared/validations/valid-mongo-id.validation';
import { GetUpload } from '../shared/usecases/get-upload/get-upload.usecase';
import { GetUploadCommand } from '../shared/usecases/get-upload/get-upload.command';
import { GetUploads } from './usecases/get-uploads/get-uploads.usecase';
import { GetUploadsCommand } from './usecases/get-uploads/get-uploads.command';
import { ValidateTemplate } from '../shared/validations/valid-template.validation';

@Controller('/upload')
@ApiTags('Uploads')
@ApiSecurity(ACCESS_KEY_NAME)
@UseGuards(APIKeyGuard)
export class UploadController {
constructor(private makeUploadEntry: MakeUploadEntry, private getUpload: GetUpload, private getUploads: GetUploads) {}
constructor(private makeUploadEntry: MakeUploadEntry, private getUpload: GetUpload) {}

@Post(':template')
@ApiOperation({
Expand Down Expand Up @@ -52,16 +49,12 @@ export class UploadController {
);
}

@Get(':templateId')
@Get(':uploadId')
@ApiOperation({
summary: 'Get uploads information for template',
summary: 'Get Upload information',
})
async getUploadsInformation(@Param('templateId', ValidateMongoId) templateId: string): Promise<UploadEntity[]> {
return this.getUploads.execute(
GetUploadsCommand.create({
_templateId: templateId,
})
);
getUploadInformation(@Param('uploadId', ValidateMongoId) uploadId: string) {
return this.getUpload.execute(GetUploadCommand.create({ uploadId }));
}

@Get(':uploadId/headings')
Expand Down
2 changes: 0 additions & 2 deletions apps/api/src/app/upload/usecases/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { MakeUploadEntry } from './make-upload-entry/make-upload-entry.usecase';
import { GetUpload } from '../../shared/usecases/get-upload/get-upload.usecase';
import { GetUploads } from './get-uploads/get-uploads.usecase';

export const USE_CASES = [
MakeUploadEntry,
GetUpload,
GetUploads,
//
];
5 changes: 4 additions & 1 deletion apps/api/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
"esModuleInterop": false,
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./src",
"baseUrl": "src",
"paths": {
"@shared/*": ["app/shared/*"],
},
"types": ["node"]
},
"include": [".eslintrc.js", "src/**/*", "src/**/*.d.ts"],
Expand Down
6 changes: 5 additions & 1 deletion apps/api/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
"target": "es2017",
"allowJs": false,
"esModuleInterop": false,
"declarationMap": true
"declarationMap": true,
"baseUrl": "src",
"paths": {
"@shared/*": ["app/shared/*"],
}
}
}
2 changes: 2 additions & 0 deletions apps/widget/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@tanstack/react-query": "^4.14.5",
"axios": "^0.26.1",
"cross-env": "^7.0.3",
"file-saver": "^2.0.5",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.39.1",
Expand All @@ -56,6 +57,7 @@
"webpack-dev-server": "^4.11.1"
},
"devDependencies": {
"@types/file-saver": "^2.0.5",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"typescript": "^4.8.3"
Expand Down
2 changes: 1 addition & 1 deletion apps/widget/src/components/Common/Footer/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function Footer(props: IFooterProps) {
</Button>
</>
),
[PhasesEum.CONFIRMATION]: (
[PhasesEum.COMPLETE]: (
<>
<Button loading={secondaryButtonLoading} onClick={onPrevClick} variant="outline">
{TEXTS.PHASE4.CLOSE}
Expand Down
5 changes: 2 additions & 3 deletions apps/widget/src/components/Common/Heading/Heading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const Titles = {
[PhasesEum.UPLOAD]: TEXTS.TITLES.UPLOAD,
[PhasesEum.MAPPING]: TEXTS.TITLES.MAPPING,
[PhasesEum.REVIEW]: TEXTS.TITLES.REVIEW,
[PhasesEum.CONFIRMATION]: TEXTS.TITLES.COMPLETE,
[PhasesEum.COMPLETE]: TEXTS.TITLES.COMPLETE,
};

const Steps = [
Expand All @@ -30,13 +30,12 @@ const Steps = [
];

export function Heading(props: IHeadingProps) {
const stepperDiff = 1;
const { active } = props;

return (
<Group style={{ justifyContent: 'space-between' }} mb="lg">
<Title order={3}>{Titles[active]}</Title>
<Stepper active={active + stepperDiff} steps={Steps} />
<Stepper active={active} steps={Steps} />
</Group>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ export function ConfirmModal(props: IConfirmModalProps) {
{TEXTS.CONFIRM_MODAL.subTitle}
</Text>
<Group spacing="sm" style={{ flexDirection: 'row' }}>
<Button onClick={() => onConfirm(true)} variant="outline">
<Button onClick={() => onConfirm(false)} variant="outline">
{TEXTS.CONFIRM_MODAL.EXEMPT_CONTINUE}
</Button>
<Button onClick={() => onConfirm(false)}>{TEXTS.CONFIRM_MODAL.KEEP_CONTINUE}</Button>
<Button onClick={() => onConfirm(true)}>{TEXTS.CONFIRM_MODAL.KEEP_CONTINUE}</Button>
</Group>
</Group>
</MantineModal>
Expand Down
3 changes: 2 additions & 1 deletion apps/widget/src/components/widget/Phases/Phase3/Phase3.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function Phase3(props: IPhase3Props) {
const [showConfirmModal, setShowConfirmModal] = useState(false);
const {
onPageChange,
onExportData,
heaings,
isInitialDataLoaded,
reviewData,
Expand Down Expand Up @@ -58,7 +59,7 @@ export function Phase3(props: IPhase3Props) {
<Warning fill={colors.red} className={classes.warningIcon} />
<Text color={colors.red}>{TEXTS.PHASE3.INVALID_DATA_INFO}</Text>
</Group>
<Button size="sm" leftIcon={<Download />}>
<Button size="sm" leftIcon={<Download />} onClick={onExportData}>
{TEXTS.PHASE3.EXPORT_DATA}
</Button>
</Group>
Expand Down
2 changes: 1 addition & 1 deletion apps/widget/src/components/widget/Phases/Phase4/Phase4.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function Phase4(props: IPhase4Props) {
</Text>
</Group>

<Footer active={PhasesEum.CONFIRMATION} onNextClick={onUploadAgainClick} onPrevClick={onCloseClick} />
<Footer active={PhasesEum.COMPLETE} onNextClick={onUploadAgainClick} onPrevClick={onCloseClick} />
</>
);
}
Loading