From 42bc8eb3b1506a33de1c494623eeb83d9f801054 Mon Sep 17 00:00:00 2001 From: blokh Date: Mon, 23 Sep 2024 17:26:22 +0300 Subject: [PATCH 01/13] updated ocr button logic of the backend --- .../workflows-service/prisma/data-migrations | 2 +- .../unified-api-client/unified-api-client.ts | 64 ++++++++++- .../storage/storage.controller.internal.ts | 77 ++----------- .../src/storage/storage.service.ts | 107 +++++++++++++++++- .../workflow/workflow.controller.internal.ts | 20 ++++ .../src/workflow/workflow.service.ts | 92 ++++++++++++++- 6 files changed, 288 insertions(+), 74 deletions(-) diff --git a/services/workflows-service/prisma/data-migrations b/services/workflows-service/prisma/data-migrations index 86f4d309b2..f24018634e 160000 --- a/services/workflows-service/prisma/data-migrations +++ b/services/workflows-service/prisma/data-migrations @@ -1 +1 @@ -Subproject commit 86f4d309b228ddb718af09907b6e8e8c8cd1874c +Subproject commit f24018634e549b969157c09f97f32b6049e422dc diff --git a/services/workflows-service/src/common/utils/unified-api-client/unified-api-client.ts b/services/workflows-service/src/common/utils/unified-api-client/unified-api-client.ts index 5338887f02..1c5c3338a2 100644 --- a/services/workflows-service/src/common/utils/unified-api-client/unified-api-client.ts +++ b/services/workflows-service/src/common/utils/unified-api-client/unified-api-client.ts @@ -2,6 +2,7 @@ import axios, { AxiosInstance } from 'axios'; import { env } from '@/env'; import { Logger } from '@nestjs/common'; import { BusinessReportType } from '@prisma/client'; +import { TSchema } from '@sinclair/typebox'; export type TReportRequest = Array<{ websiteUrl: string; @@ -14,8 +15,39 @@ export type TReportRequest = Array<{ businessReportId?: string; withQualityControl?: boolean; }>; +export type TOcrImages = Array< + | { + remote: { + imageUri: string; + mimeType: string; + }; + } + | { + base64: string; + } +>; -export class UnifiedApiClient { +interface TOcrImage { + runOcr({ + images, + schema, + }: { + images: Array< + | { + remote: { + imageUri: string; + mimeType: string; + }; + } + | { + base64: string; + } + >; + schema: TSchema; + }): Promise>; +} + +export class UnifiedApiClient implements TOcrImage { private readonly axiosInstance: AxiosInstance; private readonly logger = new Logger(UnifiedApiClient.name); @@ -74,4 +106,34 @@ export class UnifiedApiClient { throw error; } } + + async runOcr({ images, schema }: { images: TOcrImages; schema: TSchema }) { + return await this.axiosInstance.post('/v1/smart-ocr', { + images, + schema, + }); + } + + async runDocumentOcr({ + images, + supportedCountries, + overrideSchemas, + }: { + images: TOcrImages; + supportedCountries: string[]; + overrideSchemas: { + overrideSchemas: Array<{ + countryCode: string; + documentType: string; + documentCategory: string; + schema: TSchema; + }>; + }; + }) { + return await this.axiosInstance.post('/v1/document/smart-ocr', { + images, + supportedCountries, + overrideSchemas, + }); + } } diff --git a/services/workflows-service/src/storage/storage.controller.internal.ts b/services/workflows-service/src/storage/storage.controller.internal.ts index c7e5218ac9..a5591cfd1e 100644 --- a/services/workflows-service/src/storage/storage.controller.internal.ts +++ b/services/workflows-service/src/storage/storage.controller.internal.ts @@ -8,24 +8,12 @@ import type { Response } from 'express'; import { StorageService } from './storage.service'; import * as errors from '../errors'; import { fileFilter } from './file-filter'; -import { - createPresignedUrlWithClient, - downloadFileFromS3, - manageFileByProvider, -} from '@/storage/get-file-storage-manager'; -import { AwsS3FileConfig } from '@/providers/file/file-provider/aws-s3-file.config'; -import path from 'path'; -import os from 'os'; -import { File } from '@prisma/client'; -import { z } from 'zod'; -import { HttpFileService } from '@/providers/file/file-provider/http-file.service'; +import { manageFileByProvider } from '@/storage/get-file-storage-manager'; import { ProjectIds } from '@/common/decorators/project-ids.decorator'; import type { TProjectId, TProjectIds } from '@/types'; import { CurrentProject } from '@/common/decorators/current-project.decorator'; -import { isBase64 } from '@/common/utils/is-base64/is-base64'; import { AppLoggerService } from '@/common/app-logger/app-logger.service'; import { HttpService } from '@nestjs/axios'; -import mime from 'mime'; import { getFileMetadata } from '@/common/get-file-metadata/get-file-metadata'; // Temporarily identical to StorageControllerExternal @@ -114,67 +102,18 @@ export class StorageControllerInternal { @Res() res: Response, @Query('format') format: string, ) { - // currently ignoring user id due to no user info - const persistedFile = await this.service.getFileById({ id }, projectIds, {}); - - if (!persistedFile) { - throw new errors.NotFoundException('file not found'); - } - - const mimeType = - persistedFile.mimeType || - mime.getType(persistedFile.fileName || persistedFile.uri || '') || - undefined; - - if (persistedFile.fileNameInBucket && format === 'signed-url') { - const signedUrl = await createPresignedUrlWithClient({ - bucketName: AwsS3FileConfig.getBucketName(process.env) as string, - fileNameInBucket: persistedFile.fileNameInBucket, - mimeType, - }); + const { mimeType, signedUrl, filePath } = await this.service.fetchFileContent({ + id, + projectIds, + format, + }); + if (signedUrl) { return res.json({ signedUrl, mimeType }); } res.set('Content-Type', mimeType || 'application/octet-stream'); - if (persistedFile.fileNameInBucket) { - const localFilePath = await downloadFileFromS3( - AwsS3FileConfig.getBucketName(process.env) as string, - persistedFile.fileNameInBucket, - ); - - return res.sendFile(localFilePath, { root: '/' }); - } - - if (!isBase64(persistedFile.uri) && this._isUri(persistedFile)) { - const downloadFilePath = await this.__downloadFileFromRemote(persistedFile); - - return res.sendFile(this.__getAbsoluteFilePAth(downloadFilePath)); - } - - return res.sendFile(this.__getAbsoluteFilePAth(persistedFile.fileNameOnDisk)); - } - - private __getAbsoluteFilePAth(filePath: string) { - if (!path.isAbsolute(filePath)) return filePath; - - const rootDir = path.parse(os.homedir()).root; - - return path.join(rootDir, filePath); - } - - private async __downloadFileFromRemote(persistedFile: File) { - const localeFilePath = `${os.tmpdir()}/${persistedFile.id}`; - const downloadedFilePath = await new HttpFileService({ - client: this.httpService, - logger: this.logger, - }).download(persistedFile.uri, localeFilePath); - - return downloadedFilePath; - } - - _isUri(persistedFile: File) { - return z.string().url().safeParse(persistedFile.uri).success; + return res.sendFile(filePath!); } } diff --git a/services/workflows-service/src/storage/storage.service.ts b/services/workflows-service/src/storage/storage.service.ts index 0adf888b77..408e30d20d 100644 --- a/services/workflows-service/src/storage/storage.service.ts +++ b/services/workflows-service/src/storage/storage.service.ts @@ -1,12 +1,31 @@ import { Injectable } from '@nestjs/common'; import { FileRepository } from './storage.repository'; import { IFileIds } from './types'; -import { Prisma } from '@prisma/client'; +import { File, Prisma } from '@prisma/client'; import type { TProjectId, TProjectIds } from '@/types'; +import * as errors from '@/errors'; +import mime from 'mime'; +import { + createPresignedUrlWithClient, + downloadFileFromS3, +} from '@/storage/get-file-storage-manager'; +import { AwsS3FileConfig } from '@/providers/file/file-provider/aws-s3-file.config'; +import { isBase64 } from '@/common/utils/is-base64/is-base64'; +import path from 'path'; +import os from 'os'; +import { HttpFileService } from '@/providers/file/file-provider/http-file.service'; +import { z } from 'zod'; +import { HttpService } from '@nestjs/axios'; +import { AppLoggerService } from '@/common/app-logger/app-logger.service'; +import { readFileSync } from 'fs'; @Injectable() export class StorageService { - constructor(protected readonly fileRepository: FileRepository) {} + constructor( + protected readonly fileRepository: FileRepository, + protected readonly httpService: HttpService, + protected readonly logger: AppLoggerService, + ) {} async createFileLink({ uri, @@ -44,4 +63,88 @@ export class StorageService { async getFileById({ id }: IFileIds, projectIds: TProjectIds, args?: Prisma.FileFindFirstArgs) { return await this.fileRepository.findById({ id }, args || {}, projectIds); } + + async fetchFileContent({ + id, + projectIds, + format, + }: { + id: string; + projectIds: TProjectIds; + format?: string; + }) { + const persistedFile = await this.getFileById({ id }, projectIds, {}); + + if (!persistedFile) { + throw new errors.NotFoundException('file not found'); + } + + let mimeType = + persistedFile.mimeType || + mime.getType(persistedFile.fileName || persistedFile.uri || '') || + 'image/jpg'; + + if (persistedFile.fileNameInBucket && format === 'signed-url') { + const signedUrl = await createPresignedUrlWithClient({ + bucketName: AwsS3FileConfig.getBucketName(process.env) as string, + fileNameInBucket: persistedFile.fileNameInBucket, + mimeType, + }); + + return { signedUrl, mimeType }; + } + + mimeType ||= 'application/octet-stream'; + + if (persistedFile.fileNameInBucket) { + const localFilePath = await downloadFileFromS3( + AwsS3FileConfig.getBucketName(process.env) as string, + persistedFile.fileNameInBucket, + ); + + return { filePath: path.resolve('/', localFilePath), mimeType }; + } + + if (!isBase64(persistedFile.uri) && this._isUri(persistedFile)) { + const downloadFilePath = await this.__downloadFileFromRemote(persistedFile); + + const filePath = this.__getAbsoluteFilePAth(downloadFilePath); + + return { filePath: filePath, mimeType }; + } + + const filePath = this.__getAbsoluteFilePAth(persistedFile.fileNameOnDisk); + + return { filePath: filePath, mimeType }; + } + + private __getAbsoluteFilePAth(filePath: string) { + if (!path.isAbsolute(filePath)) return filePath; + + const rootDir = path.parse(os.homedir()).root; + + return path.join(rootDir, filePath); + } + + private async __downloadFileFromRemote(persistedFile: File) { + const localeFilePath = `${os.tmpdir()}/${persistedFile.id}`; + const downloadedFilePath = await new HttpFileService({ + client: this.httpService, + logger: this.logger, + }).download(persistedFile.uri, localeFilePath); + + return downloadedFilePath; + } + + _isUri(persistedFile: File) { + return z.string().url().safeParse(persistedFile.uri).success; + } + + fileToBase64(filepath: string): string { + const fileBuffer = readFileSync(filepath); + + const base64String = fileBuffer.toString('base64'); + + return base64String; + } } diff --git a/services/workflows-service/src/workflow/workflow.controller.internal.ts b/services/workflows-service/src/workflow/workflow.controller.internal.ts index ccddb26dc4..05208eb907 100644 --- a/services/workflows-service/src/workflow/workflow.controller.internal.ts +++ b/services/workflows-service/src/workflow/workflow.controller.internal.ts @@ -338,6 +338,26 @@ export class WorkflowControllerInternal { } } + @common.Patch(':id/documents/:documentId/run-ocr') + @swagger.ApiOkResponse({ type: WorkflowDefinitionModel }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + @swagger.ApiForbiddenResponse({ type: errors.ForbiddenException }) + @UseGuards(WorkflowAssigneeGuard) + async runDocumentOcr( + @common.Param() params: DocumentUpdateParamsInput, + @CurrentProject() currentProjectId: TProjectId, + ) { + return await this.service.runOCROnDocument( + { + workflowId: params?.id, + documentId: params?.documentId, + validateDocumentSchema: false, + }, + data.document, + currentProjectId, + ); + } + // @nestAccessControl.UseRoles({ // resource: 'Workflow', // action: 'delete', diff --git a/services/workflows-service/src/workflow/workflow.service.ts b/services/workflows-service/src/workflow/workflow.service.ts index bb10e8c1a9..32e31d331b 100644 --- a/services/workflows-service/src/workflow/workflow.service.ts +++ b/services/workflows-service/src/workflow/workflow.service.ts @@ -48,6 +48,7 @@ import { import { AnyRecord, DefaultContextSchema, + DocumentsSchema, getDocumentId, isErrorWithMessage, isObject, @@ -79,7 +80,6 @@ import { BusinessReportStatus, BusinessReportType, EndUser, - Prisma, PrismaClient, UiDefinitionContext, User, @@ -107,6 +107,9 @@ import { addPropertiesSchemaToDocument } from './utils/add-properties-schema-to- import { entitiesUpdate } from './utils/entities-update'; import { WorkflowEventEmitterService } from './workflow-event-emitter.service'; import { WorkflowRuntimeDataRepository } from './workflow-runtime-data.repository'; +import { Prisma } from '@prisma/client/extension'; +import { StorageService } from '@/storage/storage.service'; +import { UnifiedApiClient } from '@/common/utils/unified-api-client/unified-api-client'; type TEntityId = string; @@ -142,6 +145,7 @@ export class WorkflowService { private readonly salesforceService: SalesforceService, private readonly workflowTokenService: WorkflowTokenService, private readonly uiDefinitionService: UiDefinitionService, + private readonly storageService: StorageService, private readonly prismaService: PrismaService, private readonly riskRuleService: RiskRuleService, private readonly ruleEngineService: RuleEngineService, @@ -2498,4 +2502,90 @@ export class WorkflowService { data: args, }); } + + async findDocumentById({ + workflowId, + projectId, + documentId, + transaction, + }: { + workflowId: string; + projectId: string; + documentId: string; + transaction: PrismaTransaction; + }) { + const runtimeData = await this.workflowRuntimeDataRepository.findByIdAndLock( + workflowId, + {}, + [projectId], + transaction, + ); + const workflowDef = await this.workflowDefinitionRepository.findById( + runtimeData.workflowDefinitionId, + {}, + [projectId], + transaction, + ); + const document = runtimeData?.context?.documents?.find( + (document: DefaultContextSchema['documents'][number]) => document.id === documentId, + ); + + const documentSchema = addPropertiesSchemaToDocument(document, workflowDef.documentsSchema); + const propertiesSchema = documentSchema?.propertiesSchema ?? {}; + + return { + documentSchema, + ...propertiesSchema, + }; + } + + async runOCROnDocument({ + workflowId, + projectId, + documentId, + transaction, + }: { + workflowId: string; + projectId: string; + documentId: string; + transaction: PrismaTransaction; + }) { + const document = (await this.findDocumentById({ + workflowId, + projectId, + documentId, + transaction, + })) as unknown as Static[number]; + + if (!('pages' in document)) { + throw new BadRequestException('Cannot run document OCR on document without pages'); + } + + const documentPagesContent = document.pages.map(async page => { + const { signedUrl, mimeType, filePath } = await this.storageService.fetchFileContent({ + id: page.ballerineFileId!, + format: 'signed-url', + projectIds: [projectId], + }); + + if (signedUrl) { + return { + remote: { + imageUri: signedUrl, + mimeType, + }, + }; + } + + const base64String = this.storageService.fileToBase64(filePath!); + + return { base64: `data:${mimeType};base64,${base64String}` }; + }); + + const images = await Promise.all(documentPagesContent); + + await new UnifiedApiClient().runDocumentOcr({ + images, + }); + } } From 29fa3e46f9adf91722bceb21c531f2919acf40a2 Mon Sep 17 00:00:00 2001 From: blokh Date: Mon, 23 Sep 2024 18:16:39 +0300 Subject: [PATCH 02/13] generated fetcher for backoffice functionality --- .../src/domains/workflows/fetchers.ts | 18 ++++++++++++++++++ .../workflow/workflow.controller.internal.ts | 14 +++++--------- .../src/workflow/workflow.service.ts | 8 +++++--- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/apps/backoffice-v2/src/domains/workflows/fetchers.ts b/apps/backoffice-v2/src/domains/workflows/fetchers.ts index 47cd387770..51c45b5fb6 100644 --- a/apps/backoffice-v2/src/domains/workflows/fetchers.ts +++ b/apps/backoffice-v2/src/domains/workflows/fetchers.ts @@ -298,3 +298,21 @@ export const createWorkflowRequest = async ({ return handleZodError(error, workflow); }; + +export const executeWorkflowDocumentOCR = async ({ + workflowDefinitionId, + documentId, +}: { + workflowDefinitionId: string; + documentId: string; +}) => { + const [workflow, error] = await apiClient({ + method: Method.PATCH, + url: `${getOriginUrl( + env.VITE_API_URL, + )}/api/v1/internal/workflows/${workflowDefinitionId}/documents/${documentId}/run-ocr`, + schema: z.any(), + }); + + return handleZodError(error, workflow); +}; diff --git a/services/workflows-service/src/workflow/workflow.controller.internal.ts b/services/workflows-service/src/workflow/workflow.controller.internal.ts index 05208eb907..26eca27df2 100644 --- a/services/workflows-service/src/workflow/workflow.controller.internal.ts +++ b/services/workflows-service/src/workflow/workflow.controller.internal.ts @@ -347,15 +347,11 @@ export class WorkflowControllerInternal { @common.Param() params: DocumentUpdateParamsInput, @CurrentProject() currentProjectId: TProjectId, ) { - return await this.service.runOCROnDocument( - { - workflowId: params?.id, - documentId: params?.documentId, - validateDocumentSchema: false, - }, - data.document, - currentProjectId, - ); + return await this.service.runOCROnDocument({ + workflowId: params?.id, + documentId: params?.documentId, + projectId: currentProjectId, + }); } // @nestAccessControl.UseRoles({ diff --git a/services/workflows-service/src/workflow/workflow.service.ts b/services/workflows-service/src/workflow/workflow.service.ts index 32e31d331b..2ca63c811f 100644 --- a/services/workflows-service/src/workflow/workflow.service.ts +++ b/services/workflows-service/src/workflow/workflow.service.ts @@ -109,7 +109,7 @@ import { WorkflowEventEmitterService } from './workflow-event-emitter.service'; import { WorkflowRuntimeDataRepository } from './workflow-runtime-data.repository'; import { Prisma } from '@prisma/client/extension'; import { StorageService } from '@/storage/storage.service'; -import { UnifiedApiClient } from '@/common/utils/unified-api-client/unified-api-client'; +import { TOcrImages, UnifiedApiClient } from '@/common/utils/unified-api-client/unified-api-client'; type TEntityId = string; @@ -2548,8 +2548,10 @@ export class WorkflowService { workflowId: string; projectId: string; documentId: string; - transaction: PrismaTransaction; + transaction?: PrismaTransaction; }) { + transaction ||= await this.prismaService.transaction(); + const document = (await this.findDocumentById({ workflowId, projectId, @@ -2582,7 +2584,7 @@ export class WorkflowService { return { base64: `data:${mimeType};base64,${base64String}` }; }); - const images = await Promise.all(documentPagesContent); + const images = (await Promise.all(documentPagesContent)) satisfies TOcrImages; await new UnifiedApiClient().runDocumentOcr({ images, From 456522c46b4447f92f15406be7ea7fb6d80718c9 Mon Sep 17 00:00:00 2001 From: blokh Date: Tue, 24 Sep 2024 12:22:21 +0300 Subject: [PATCH 03/13] feat: generated ocr request in workflow service backoffice --- .../workflow/workflow.controller.internal.ts | 2 +- .../src/workflow/workflow.service.ts | 91 +++++++++++-------- 2 files changed, 54 insertions(+), 39 deletions(-) diff --git a/services/workflows-service/src/workflow/workflow.controller.internal.ts b/services/workflows-service/src/workflow/workflow.controller.internal.ts index 26eca27df2..48ede281ab 100644 --- a/services/workflows-service/src/workflow/workflow.controller.internal.ts +++ b/services/workflows-service/src/workflow/workflow.controller.internal.ts @@ -348,7 +348,7 @@ export class WorkflowControllerInternal { @CurrentProject() currentProjectId: TProjectId, ) { return await this.service.runOCROnDocument({ - workflowId: params?.id, + workflowRuntimeId: params?.id, documentId: params?.documentId, projectId: currentProjectId, }); diff --git a/services/workflows-service/src/workflow/workflow.service.ts b/services/workflows-service/src/workflow/workflow.service.ts index 2ca63c811f..20af03f4a1 100644 --- a/services/workflows-service/src/workflow/workflow.service.ts +++ b/services/workflows-service/src/workflow/workflow.service.ts @@ -80,6 +80,7 @@ import { BusinessReportStatus, BusinessReportType, EndUser, + Prisma, PrismaClient, UiDefinitionContext, User, @@ -87,7 +88,7 @@ import { WorkflowRuntimeData, WorkflowRuntimeDataStatus, } from '@prisma/client'; -import { Static } from '@sinclair/typebox'; +import { Static, TSchema } from '@sinclair/typebox'; import { plainToClass } from 'class-transformer'; import dayjs from 'dayjs'; import { isEqual, merge } from 'lodash'; @@ -107,7 +108,6 @@ import { addPropertiesSchemaToDocument } from './utils/add-properties-schema-to- import { entitiesUpdate } from './utils/entities-update'; import { WorkflowEventEmitterService } from './workflow-event-emitter.service'; import { WorkflowRuntimeDataRepository } from './workflow-runtime-data.repository'; -import { Prisma } from '@prisma/client/extension'; import { StorageService } from '@/storage/storage.service'; import { TOcrImages, UnifiedApiClient } from '@/common/utils/unified-api-client/unified-api-client'; @@ -2512,7 +2512,7 @@ export class WorkflowService { workflowId: string; projectId: string; documentId: string; - transaction: PrismaTransaction; + transaction: PrismaTransaction | PrismaClient; }) { const runtimeData = await this.workflowRuntimeDataRepository.findByIdAndLock( workflowId, @@ -2540,54 +2540,69 @@ export class WorkflowService { } async runOCROnDocument({ - workflowId, + workflowRuntimeId, projectId, documentId, - transaction, }: { - workflowId: string; + workflowRuntimeId: string; projectId: string; documentId: string; - transaction?: PrismaTransaction; }) { - transaction ||= await this.prismaService.transaction(); + await this.prismaService.$transaction( + async transaction => { + const workflowDef = await this.workflowDefinitionRepository.findById( + workflowRuntimeId, + {}, + [projectId], + transaction, + ); - const document = (await this.findDocumentById({ - workflowId, - projectId, - documentId, - transaction, - })) as unknown as Static[number]; + const document = (await this.findDocumentById({ + workflowId: workflowRuntimeId, + projectId, + documentId, + transaction, + })) as unknown as Static[number]; - if (!('pages' in document)) { - throw new BadRequestException('Cannot run document OCR on document without pages'); - } + if (!('pages' in document)) { + throw new BadRequestException('Cannot run document OCR on document without pages'); + } - const documentPagesContent = document.pages.map(async page => { - const { signedUrl, mimeType, filePath } = await this.storageService.fetchFileContent({ - id: page.ballerineFileId!, - format: 'signed-url', - projectIds: [projectId], - }); + const documentWithSchema = addPropertiesSchemaToDocument( + document, + workflowDef.documentsSchema, + ); + const documentPagesContent = documentWithSchema.pages.map(async page => { + const { signedUrl, mimeType, filePath } = await this.storageService.fetchFileContent({ + id: page.ballerineFileId!, + format: 'signed-url', + projectIds: [projectId], + }); - if (signedUrl) { - return { - remote: { - imageUri: signedUrl, - mimeType, - }, - }; - } + if (signedUrl) { + return { + remote: { + imageUri: signedUrl, + mimeType, + }, + }; + } - const base64String = this.storageService.fileToBase64(filePath!); + const base64String = this.storageService.fileToBase64(filePath!); - return { base64: `data:${mimeType};base64,${base64String}` }; - }); + return { base64: `data:${mimeType};base64,${base64String}` }; + }); - const images = (await Promise.all(documentPagesContent)) satisfies TOcrImages; + const images = (await Promise.all(documentPagesContent)) satisfies TOcrImages; - await new UnifiedApiClient().runDocumentOcr({ - images, - }); + return await new UnifiedApiClient().runOcr({ + images, + schema: documentWithSchema.propertiesSchema as unknown as TSchema, + }); + }, + { + timeout: 180_000, + }, + ); } } From 6408f951e46339fac69d2d14715c0eebbf08af01 Mon Sep 17 00:00:00 2001 From: blokh Date: Tue, 24 Sep 2024 17:37:10 +0300 Subject: [PATCH 04/13] updated logic of pressing on the OCR button --- apps/backoffice-v2/package.json | 2 +- .../public/locales/en/toast.json | 4 ++ .../molecules/ImageOCR/ImageOCR.tsx | 28 ++++++++++ .../useDocumentOcr/useDocumentOcr.ts | 38 ++++++++++++++ .../src/domains/workflows/fetchers.ts | 2 +- .../useCallToActionLegacyLogic.tsx | 1 - .../components/MultiDocuments/interfaces.ts | 1 + .../hooks/useDocumentOcr/useDocumentOcr.ts | 29 +++++++++++ .../useDocumentBlocks/useDocumentBlocks.tsx | 8 +++ .../utils/check-can-ocr/check-can-ocr.ts | 29 +++++++++++ .../Case/Case.Documents.Toolbar.tsx | 10 ++++ .../Entity/components/Case/Case.Documents.tsx | 9 ++-- ...useDocuments.tsx => useDocumentsLogic.tsx} | 52 ++----------------- .../Entity/components/Case/interfaces.ts | 1 + 14 files changed, 159 insertions(+), 55 deletions(-) create mode 100644 apps/backoffice-v2/src/common/components/molecules/ImageOCR/ImageOCR.tsx create mode 100644 apps/backoffice-v2/src/domains/entities/hooks/mutations/useDocumentOcr/useDocumentOcr.ts create mode 100644 apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/hooks/useDocumentOcr/useDocumentOcr.ts create mode 100644 apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/utils/check-can-ocr/check-can-ocr.ts rename apps/backoffice-v2/src/pages/Entity/components/Case/hooks/useDocuments/{useDocuments.tsx => useDocumentsLogic.tsx} (71%) diff --git a/apps/backoffice-v2/package.json b/apps/backoffice-v2/package.json index fd21671650..1bd3eecad6 100644 --- a/apps/backoffice-v2/package.json +++ b/apps/backoffice-v2/package.json @@ -97,7 +97,7 @@ "i18next-http-backend": "^2.1.1", "leaflet": "^1.9.4", "libphonenumber-js": "^1.10.49", - "lucide-react": "^0.239.0", + "lucide-react": "^0.445.0", "match-sorter": "^6.3.1", "msw": "^1.0.0", "posthog-js": "^1.154.2", diff --git a/apps/backoffice-v2/public/locales/en/toast.json b/apps/backoffice-v2/public/locales/en/toast.json index 5809b1b785..bce138fba5 100644 --- a/apps/backoffice-v2/public/locales/en/toast.json +++ b/apps/backoffice-v2/public/locales/en/toast.json @@ -81,6 +81,10 @@ "pdf_certificate": { "error": "Failed to open PDF certificate." }, + "document_ocr": { + "success": "OCR performed successfully.", + "error": "Failed to perform OCR on the document." + }, "business_report_creation": { "success": "Merchant check created successfully.", "error": "Error occurred while creating a merchant check.", diff --git a/apps/backoffice-v2/src/common/components/molecules/ImageOCR/ImageOCR.tsx b/apps/backoffice-v2/src/common/components/molecules/ImageOCR/ImageOCR.tsx new file mode 100644 index 0000000000..79366920d3 --- /dev/null +++ b/apps/backoffice-v2/src/common/components/molecules/ImageOCR/ImageOCR.tsx @@ -0,0 +1,28 @@ +import { ctw } from '@/common/utils/ctw/ctw'; +import { ComponentProps, FunctionComponent } from 'react'; +import { ScanTextIcon } from 'lucide-react'; + +export interface IImageOCR extends ComponentProps<'div'> { + onOcrPressed: () => void; + isOcrDisabled: boolean; +} + +export const ImageOCR: FunctionComponent = ({ + isOcrDisabled, + onOcrPressed, + className, + ...props +}) => ( + <> + + +); diff --git a/apps/backoffice-v2/src/domains/entities/hooks/mutations/useDocumentOcr/useDocumentOcr.ts b/apps/backoffice-v2/src/domains/entities/hooks/mutations/useDocumentOcr/useDocumentOcr.ts new file mode 100644 index 0000000000..9998e4c70d --- /dev/null +++ b/apps/backoffice-v2/src/domains/entities/hooks/mutations/useDocumentOcr/useDocumentOcr.ts @@ -0,0 +1,38 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { fetchWorkflowDocumentOCRResult } from '@/domains/workflows/fetchers'; +import { toast } from 'sonner'; +import { t } from 'i18next'; +import { workflowsQueryKeys } from '@/domains/workflows/query-keys'; +import { useFilterId } from '@/common/hooks/useFilterId/useFilterId'; + +export const useDocumentOrc = ({ + workflowId, + onSuccess, +}: { + workflowId: string; + onSuccess: (ocrProperties: Record, document: { documentId: string }) => void; +}) => { + const filterId = useFilterId(); + const workflowById = workflowsQueryKeys.byId({ workflowId, filterId }); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ documentId }: { documentId: string }) => + fetchWorkflowDocumentOCRResult({ + workflowDefinitionId: workflowId, + documentId, + }), + onSuccess: (data, variables) => { + void queryClient.invalidateQueries(workflowsQueryKeys._def); + + toast.error(t('toast:document_ocr.success')); + + onSuccess(data, variables); + }, + onError: (_error, _variables) => { + console.error(_error); + void queryClient.invalidateQueries(workflowsQueryKeys._def); + toast.error(t('toast:document_ocr.error')); + }, + }); +}; diff --git a/apps/backoffice-v2/src/domains/workflows/fetchers.ts b/apps/backoffice-v2/src/domains/workflows/fetchers.ts index 51c45b5fb6..8ea9371df9 100644 --- a/apps/backoffice-v2/src/domains/workflows/fetchers.ts +++ b/apps/backoffice-v2/src/domains/workflows/fetchers.ts @@ -299,7 +299,7 @@ export const createWorkflowRequest = async ({ return handleZodError(error, workflow); }; -export const executeWorkflowDocumentOCR = async ({ +export const fetchWorkflowDocumentOCRResult = async ({ workflowDefinitionId, documentId, }: { diff --git a/apps/backoffice-v2/src/lib/blocks/components/CallToActionLegacy/hooks/useCallToActionLegacyLogic/useCallToActionLegacyLogic.tsx b/apps/backoffice-v2/src/lib/blocks/components/CallToActionLegacy/hooks/useCallToActionLegacyLogic/useCallToActionLegacyLogic.tsx index 1677b129f8..dd7b4718b5 100644 --- a/apps/backoffice-v2/src/lib/blocks/components/CallToActionLegacy/hooks/useCallToActionLegacyLogic/useCallToActionLegacyLogic.tsx +++ b/apps/backoffice-v2/src/lib/blocks/components/CallToActionLegacy/hooks/useCallToActionLegacyLogic/useCallToActionLegacyLogic.tsx @@ -1,4 +1,3 @@ -import { CommonWorkflowEvent } from '@ballerine/common'; import { ComponentProps, FunctionComponent, useCallback, useEffect, useState } from 'react'; import { toast } from 'sonner'; import { useApproveTaskByIdMutation } from '../../../../../../domains/entities/hooks/mutations/useApproveTaskByIdMutation/useApproveTaskByIdMutation'; diff --git a/apps/backoffice-v2/src/lib/blocks/components/MultiDocuments/interfaces.ts b/apps/backoffice-v2/src/lib/blocks/components/MultiDocuments/interfaces.ts index fe92eb3c6e..f1f2ff46e7 100644 --- a/apps/backoffice-v2/src/lib/blocks/components/MultiDocuments/interfaces.ts +++ b/apps/backoffice-v2/src/lib/blocks/components/MultiDocuments/interfaces.ts @@ -1,6 +1,7 @@ export interface IMultiDocumentsProps { value: { isLoading: boolean; + onOCRClicked: () => void; data: Array<{ imageUrl: string; title: string; diff --git a/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/hooks/useDocumentOcr/useDocumentOcr.ts b/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/hooks/useDocumentOcr/useDocumentOcr.ts new file mode 100644 index 0000000000..ae05ed6beb --- /dev/null +++ b/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/hooks/useDocumentOcr/useDocumentOcr.ts @@ -0,0 +1,29 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { fetchWorkflowDocumentOCRResult } from '@/domains/workflows/fetchers'; +import { toast } from 'sonner'; +import { t } from 'i18next'; + +export const useDocumentOrc = ({ + workflowId, + onSuccess, +}: { + workflowId: string; + onSuccess: (ocrProperties: Record, document: { documentId: string }) => void; +}) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ documentId }: { documentId: string }) => + fetchWorkflowDocumentOCRResult({ + workflowDefinitionId: workflowId, + documentId, + }), + onSuccess: (data, variables, context) => { + onSuccess(data, variables); + }, + onError: (_error, _variables, context) => { + console.error(_error); + toast.error(t('toast:document_ocr.error')); + }, + }); +}; diff --git a/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/useDocumentBlocks.tsx b/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/useDocumentBlocks.tsx index 3b47725a7f..4316008734 100644 --- a/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/useDocumentBlocks.tsx +++ b/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/useDocumentBlocks.tsx @@ -29,6 +29,7 @@ import { X } from 'lucide-react'; import * as React from 'react'; import { FunctionComponent, useCallback, useMemo } from 'react'; import { toTitleCase } from 'string-ts'; +import { useDocumentOrc } from '@/domains/entities/hooks/mutations/useDocumentOcr/useDocumentOcr'; export const useDocumentBlocks = ({ workflow, @@ -79,6 +80,12 @@ export const useDocumentBlocks = ({ const { mutate: mutateApproveTaskById, isLoading: isLoadingApproveTaskById } = useApproveTaskByIdMutation(workflow?.id); + const { mutate: mutateOCRDocument, isLoading: isLoadingOCRDocument } = useDocumentOrc({ + workflowId: workflow?.id, + onSuccess: (ocrProperties, document) => { + console.log(ocrProperties, document); + }, + }); const { isLoading: isLoadingRejectTaskById } = useRejectTaskByIdMutation(workflow?.id); const { comment, onClearComment, onCommentChange } = useCommentInputLogic(); @@ -450,6 +457,7 @@ export const useDocumentBlocks = ({ type: 'multiDocuments', value: { isLoading: storageFilesQueryResult?.some(({ isLoading }) => isLoading), + onOCRClicked: () => mutateOCRDocument({ documentId: id }), data: documents?.[docIndex]?.pages?.map( ({ type, fileName, metadata, ballerineFileId }, pageIndex) => ({ diff --git a/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/utils/check-can-ocr/check-can-ocr.ts b/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/utils/check-can-ocr/check-can-ocr.ts new file mode 100644 index 0000000000..a2a97d6794 --- /dev/null +++ b/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/utils/check-can-ocr/check-can-ocr.ts @@ -0,0 +1,29 @@ +import { useCaseState } from '@/pages/Entity/components/Case/hooks/useCaseState/useCaseState'; +import { TWorkflowById } from '@/domains/workflows/fetchers'; +import { CommonWorkflowEvent, CommonWorkflowStates, DefaultContextSchema } from '@ballerine/common'; +import { checkCanMakeDecision } from '@/lib/blocks/hooks/useDocumentBlocks/utils/check-can-make-decision/check-can-make-decision'; + +export const checkCanOcr = ({ + caseState, + noAction, + workflow, + decision, + isLoadingApprove, +}: { + caseState: ReturnType; + noAction: boolean; + workflow: TWorkflowById; + decision: DefaultContextSchema['documents'][number]['decision']; + isLoadingApprove: boolean; +}) => { + const isStateManualReview = workflow.state === CommonWorkflowStates.MANUAL_REVIEW; + + const hasApproveEvent = workflow?.nextEvents?.includes(CommonWorkflowEvent.APPROVE); + const canMakeDecision = checkCanMakeDecision({ + caseState, + noAction, + decision, + }); + + return !isLoadingApprove && canMakeDecision && (isStateManualReview || hasApproveEvent); +}; diff --git a/apps/backoffice-v2/src/pages/Entity/components/Case/Case.Documents.Toolbar.tsx b/apps/backoffice-v2/src/pages/Entity/components/Case/Case.Documents.Toolbar.tsx index d2dc82764c..cfe5201a69 100644 --- a/apps/backoffice-v2/src/pages/Entity/components/Case/Case.Documents.Toolbar.tsx +++ b/apps/backoffice-v2/src/pages/Entity/components/Case/Case.Documents.Toolbar.tsx @@ -5,6 +5,7 @@ import { ImageViewer } from '@/common/components/organisms/ImageViewer/ImageView import { ctw } from '@/common/utils/ctw/ctw'; import { isPdf } from '@/common/utils/is-pdf/is-pdf'; import { useDocumentsToolbarLogic } from '@/pages/Entity/components/Case/hooks/useDocumentsToolbarLogic/useDocumentsToolbarLogic'; +import { ImageOCR } from '@/common/components/molecules/ImageOCR/ImageOCR'; export const DocumentsToolbar: FunctionComponent<{ image: { id: string; imageUrl: string; fileType: string; fileName: string }; @@ -13,6 +14,8 @@ export const DocumentsToolbar: FunctionComponent<{ onRotateDocument: () => void; onOpenDocumentInNewTab: (id: string) => void; shouldDownload: boolean; + onOcrPressed?: () => void; + shouldOCR: boolean; fileToDownloadBase64: string; }> = ({ image, @@ -20,7 +23,9 @@ export const DocumentsToolbar: FunctionComponent<{ hideOpenExternalButton, onRotateDocument, onOpenDocumentInNewTab, + onOcrPressed, shouldDownload, + shouldOCR, fileToDownloadBase64, }) => { const { onOpenInNewTabClick } = useDocumentsToolbarLogic({ @@ -31,6 +36,11 @@ export const DocumentsToolbar: FunctionComponent<{ return (
+ {shouldOCR && ( +
+ onOcrPressed} /> +
+ )} {!hideOpenExternalButton && !isLoading && image?.id && ( - + ); diff --git a/apps/backoffice-v2/src/domains/customer/fetchers.ts b/apps/backoffice-v2/src/domains/customer/fetchers.ts index ae20cbd752..94587404af 100644 --- a/apps/backoffice-v2/src/domains/customer/fetchers.ts +++ b/apps/backoffice-v2/src/domains/customer/fetchers.ts @@ -27,6 +27,7 @@ const CustomerSchema = z.object({ createBusinessReportBatch: z .object({ enabled: z.boolean().default(false), options: createBusinessReportOptions }) .optional(), + isDocumentOcrEnabled: z.boolean().default(false).optional(), }) .nullable(), config: z diff --git a/apps/backoffice-v2/src/lib/blocks/components/Details/Details.tsx b/apps/backoffice-v2/src/lib/blocks/components/Details/Details.tsx index 77b7a34163..6f85036c0f 100644 --- a/apps/backoffice-v2/src/lib/blocks/components/Details/Details.tsx +++ b/apps/backoffice-v2/src/lib/blocks/components/Details/Details.tsx @@ -13,6 +13,7 @@ export const Details: FunctionComponent> = ({ workflowId, documents = [], onSubmit, + isSaveDisabled, props, }) => { if (!value.data?.length) { @@ -38,6 +39,7 @@ export const Details: FunctionComponent> = ({ documents={documents} title={value?.title} data={sortedData} + isSaveDisabled={isSaveDisabled} contextUpdateMethod={contextUpdateMethod} onSubmit={onSubmit} /> diff --git a/apps/backoffice-v2/src/lib/blocks/components/EditableDetails/EditableDetails.tsx b/apps/backoffice-v2/src/lib/blocks/components/EditableDetails/EditableDetails.tsx index b967d6b1ea..85199220a6 100644 --- a/apps/backoffice-v2/src/lib/blocks/components/EditableDetails/EditableDetails.tsx +++ b/apps/backoffice-v2/src/lib/blocks/components/EditableDetails/EditableDetails.tsx @@ -103,6 +103,7 @@ export const EditableDetails: FunctionComponent = ({ documents, title, workflowId, + isSaveDisabled, contextUpdateMethod = 'base', onSubmit: onSubmitCallback, }) => { @@ -427,7 +428,11 @@ export const EditableDetails: FunctionComponent = ({
{data?.some(({ isEditable }) => isEditable) && ( - )} diff --git a/apps/backoffice-v2/src/lib/blocks/components/EditableDetails/interfaces.ts b/apps/backoffice-v2/src/lib/blocks/components/EditableDetails/interfaces.ts index c1c2cc0d23..4da7f1351a 100644 --- a/apps/backoffice-v2/src/lib/blocks/components/EditableDetails/interfaces.ts +++ b/apps/backoffice-v2/src/lib/blocks/components/EditableDetails/interfaces.ts @@ -25,6 +25,7 @@ export interface IEditableDetails { documents: IEditableDetailsDocument[]; title: string; workflowId: string; + isSaveDisabled?: boolean; contextUpdateMethod?: 'base' | 'director'; onSubmit?: (document: AnyObject) => void; } diff --git a/apps/backoffice-v2/src/lib/blocks/components/MultiDocuments/MultiDocuments.tsx b/apps/backoffice-v2/src/lib/blocks/components/MultiDocuments/MultiDocuments.tsx index 3a3d0816fb..bc47eedc61 100644 --- a/apps/backoffice-v2/src/lib/blocks/components/MultiDocuments/MultiDocuments.tsx +++ b/apps/backoffice-v2/src/lib/blocks/components/MultiDocuments/MultiDocuments.tsx @@ -11,6 +11,7 @@ export const MultiDocuments: FunctionComponent = ({ value documents={documents} isLoading={value?.isLoading} onOcrPressed={value?.onOcrPressed} + isLoadingOCR={value?.isLoadingOCR} />
); diff --git a/apps/backoffice-v2/src/lib/blocks/components/MultiDocuments/interfaces.ts b/apps/backoffice-v2/src/lib/blocks/components/MultiDocuments/interfaces.ts index e900d33199..975a33d73c 100644 --- a/apps/backoffice-v2/src/lib/blocks/components/MultiDocuments/interfaces.ts +++ b/apps/backoffice-v2/src/lib/blocks/components/MultiDocuments/interfaces.ts @@ -2,6 +2,7 @@ export interface IMultiDocumentsProps { value: { isLoading: boolean; onOcrPressed: () => void; + isLoadingOCR: boolean; data: Array<{ imageUrl: string; title: string; diff --git a/apps/backoffice-v2/src/lib/blocks/create-blocks-typed/types.ts b/apps/backoffice-v2/src/lib/blocks/create-blocks-typed/types.ts index 28075eb23b..a94904702d 100644 --- a/apps/backoffice-v2/src/lib/blocks/create-blocks-typed/types.ts +++ b/apps/backoffice-v2/src/lib/blocks/create-blocks-typed/types.ts @@ -115,6 +115,7 @@ export type TDetailsCell = { hideSeparator?: boolean; documents?: IEditableDetailsDocument[]; contextUpdateMethod?: 'base' | 'director'; + isSaveDisabled?: boolean; value: { id: string; title: string; diff --git a/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/useDocumentBlocks.tsx b/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/useDocumentBlocks.tsx index d35aca332e..6f8ae5aea9 100644 --- a/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/useDocumentBlocks.tsx +++ b/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/useDocumentBlocks.tsx @@ -453,6 +453,7 @@ export const useDocumentBlocks = ({ ), }, workflowId: workflow?.id, + isSaveDisabled: isLoadingOCRDocument, documents: workflow?.context?.documents, }) .addCell(decisionCell) @@ -465,9 +466,11 @@ export const useDocumentBlocks = ({ .addBlock() .addCell({ type: 'multiDocuments', + value: { isLoading: storageFilesQueryResult?.some(({ isLoading }) => isLoading), onOcrPressed: () => mutateOCRDocument({ documentId: id }), + isLoadingOCR: isLoadingOCRDocument, data: documents?.[docIndex]?.pages?.map( ({ type, fileName, metadata, ballerineFileId }, pageIndex) => ({ diff --git a/apps/backoffice-v2/src/pages/Entity/components/Case/Case.Documents.Toolbar.tsx b/apps/backoffice-v2/src/pages/Entity/components/Case/Case.Documents.Toolbar.tsx index 5235b7d623..dc00f2e091 100644 --- a/apps/backoffice-v2/src/pages/Entity/components/Case/Case.Documents.Toolbar.tsx +++ b/apps/backoffice-v2/src/pages/Entity/components/Case/Case.Documents.Toolbar.tsx @@ -16,6 +16,7 @@ export const DocumentsToolbar: FunctionComponent<{ shouldDownload: boolean; onOcrPressed?: () => void; shouldOCR: boolean; + isLoadingOCR: boolean; fileToDownloadBase64: string; }> = ({ image, @@ -25,6 +26,7 @@ export const DocumentsToolbar: FunctionComponent<{ onOpenDocumentInNewTab, onOcrPressed, shouldDownload, + isLoadingOCR, shouldOCR, fileToDownloadBase64, }) => { @@ -38,7 +40,11 @@ export const DocumentsToolbar: FunctionComponent<{
{shouldOCR && image && (
- +
)} {!hideOpenExternalButton && !isLoading && image?.id && ( diff --git a/apps/backoffice-v2/src/pages/Entity/components/Case/Case.Documents.tsx b/apps/backoffice-v2/src/pages/Entity/components/Case/Case.Documents.tsx index 226bdbbe8b..7d0f0d5a28 100644 --- a/apps/backoffice-v2/src/pages/Entity/components/Case/Case.Documents.tsx +++ b/apps/backoffice-v2/src/pages/Entity/components/Case/Case.Documents.tsx @@ -26,6 +26,7 @@ export const Documents: FunctionComponent = ({ documents, onOcrPressed, isLoading, + isLoadingOCR, hideOpenExternalButton, }) => { const { @@ -36,7 +37,6 @@ export const Documents: FunctionComponent = ({ selectedImageRef, initialImage, skeletons, - isLoadingOCR, selectedImage, onSelectImage, documentRotation, @@ -92,7 +92,7 @@ export const Documents: FunctionComponent = ({ shouldOCR={shouldOCR} onOcrPressed={onOcrPressed} // isCropping={isCropping} - // isLoadingOCR={isLoadingOCR} + isLoadingOCR={isLoadingOCR} // onCancelCrop={onCancelCrop} fileToDownloadBase64={fileToDownloadBase64} /> diff --git a/apps/backoffice-v2/src/pages/Entity/components/Case/hooks/useDocuments/useDocumentsLogic.tsx b/apps/backoffice-v2/src/pages/Entity/components/Case/hooks/useDocuments/useDocumentsLogic.tsx index 29010ce4f5..371fae67af 100644 --- a/apps/backoffice-v2/src/pages/Entity/components/Case/hooks/useDocuments/useDocumentsLogic.tsx +++ b/apps/backoffice-v2/src/pages/Entity/components/Case/hooks/useDocuments/useDocumentsLogic.tsx @@ -4,7 +4,6 @@ import { IDocumentsProps } from '../../interfaces'; import { TransformWrapper } from 'react-zoom-pan-pinch'; import { useCrop } from '@/common/hooks/useCrop/useCrop'; import { DOWNLOAD_ONLY_MIME_TYPES } from '@/common/constants'; -import { useToggle } from '@/common/hooks/useToggle/useToggle'; import { useFilterId } from '@/common/hooks/useFilterId/useFilterId'; import { useTesseract } from '@/common/hooks/useTesseract/useTesseract'; import { createArrayOfNumbers } from '@/common/utils/create-array-of-numbers/create-array-of-numbers'; @@ -15,7 +14,6 @@ export const useDocumentsLogic = (documents: IDocumentsProps['documents']) => { const initialImage = documents?.[0]; const { data: customer } = useCustomerQuery(); const { crop, isCropping, onCrop, onCancelCrop } = useCrop(); - const [isLoadingOCR, , toggleOnIsLoadingOCR, toggleOffIsLoadingOCR] = useToggle(false); const selectedImageRef = useRef(); const recognize = useTesseract(); const filterId = useFilterId(); @@ -70,11 +68,10 @@ export const useDocumentsLogic = (documents: IDocumentsProps['documents']) => { onCrop, onCancelCrop, isCropping, - shouldOCR: true, + shouldOCR: customer?.features?.isDocumentOcrEnabled || true, // TODO remove default true after review selectedImageRef, initialImage, skeletons, - isLoadingOCR, selectedImage, onSelectImage, documentRotation, diff --git a/apps/backoffice-v2/src/pages/Entity/components/Case/interfaces.ts b/apps/backoffice-v2/src/pages/Entity/components/Case/interfaces.ts index a723337dcb..f90698b8cc 100644 --- a/apps/backoffice-v2/src/pages/Entity/components/Case/interfaces.ts +++ b/apps/backoffice-v2/src/pages/Entity/components/Case/interfaces.ts @@ -50,6 +50,7 @@ export interface IDocumentsProps { }>; onOcrPressed: (documentId: string) => void; isLoading?: boolean; + isLoadingOCR?: boolean; hideOpenExternalButton?: boolean; } From 9df1bfc256717248e34c3327972348177794b885 Mon Sep 17 00:00:00 2001 From: blokh Date: Thu, 26 Sep 2024 13:34:16 +0300 Subject: [PATCH 08/13] removed unnecessary blocks logic --- .../useDocumentOcr/useDocumentOcr.ts | 2 +- .../useDocumentBlocks/useDocumentBlocks.tsx | 4 +-- .../utils/check-can-ocr/check-can-ocr.ts | 29 ------------------- .../workflows-service/prisma/data-migrations | 2 +- 4 files changed, 4 insertions(+), 33 deletions(-) delete mode 100644 apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/utils/check-can-ocr/check-can-ocr.ts diff --git a/apps/backoffice-v2/src/domains/entities/hooks/mutations/useDocumentOcr/useDocumentOcr.ts b/apps/backoffice-v2/src/domains/entities/hooks/mutations/useDocumentOcr/useDocumentOcr.ts index d634f95908..d2e7614e41 100644 --- a/apps/backoffice-v2/src/domains/entities/hooks/mutations/useDocumentOcr/useDocumentOcr.ts +++ b/apps/backoffice-v2/src/domains/entities/hooks/mutations/useDocumentOcr/useDocumentOcr.ts @@ -5,7 +5,7 @@ import { t } from 'i18next'; import { workflowsQueryKeys } from '@/domains/workflows/query-keys'; import { useFilterId } from '@/common/hooks/useFilterId/useFilterId'; -export const useDocumentOrc = ({ workflowId }: { workflowId: string }) => { +export const useDocumentOcr = ({ workflowId }: { workflowId: string }) => { const filterId = useFilterId(); const workflowById = workflowsQueryKeys.byId({ workflowId, filterId }); const queryClient = useQueryClient(); diff --git a/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/useDocumentBlocks.tsx b/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/useDocumentBlocks.tsx index d35aca332e..916c861df4 100644 --- a/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/useDocumentBlocks.tsx +++ b/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/useDocumentBlocks.tsx @@ -29,7 +29,7 @@ import { X } from 'lucide-react'; import * as React from 'react'; import { FunctionComponent, useCallback, useMemo } from 'react'; import { toTitleCase } from 'string-ts'; -import { useDocumentOrc } from '@/domains/entities/hooks/mutations/useDocumentOcr/useDocumentOcr'; +import { useDocumentOcr } from '@/domains/entities/hooks/mutations/useDocumentOcr/useDocumentOcr'; export const useDocumentBlocks = ({ workflow, @@ -84,7 +84,7 @@ export const useDocumentBlocks = ({ mutate: mutateOCRDocument, isLoading: isLoadingOCRDocument, data: ocrResult, - } = useDocumentOrc({ + } = useDocumentOcr({ workflowId: workflow?.id, }); diff --git a/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/utils/check-can-ocr/check-can-ocr.ts b/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/utils/check-can-ocr/check-can-ocr.ts deleted file mode 100644 index a2a97d6794..0000000000 --- a/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/utils/check-can-ocr/check-can-ocr.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useCaseState } from '@/pages/Entity/components/Case/hooks/useCaseState/useCaseState'; -import { TWorkflowById } from '@/domains/workflows/fetchers'; -import { CommonWorkflowEvent, CommonWorkflowStates, DefaultContextSchema } from '@ballerine/common'; -import { checkCanMakeDecision } from '@/lib/blocks/hooks/useDocumentBlocks/utils/check-can-make-decision/check-can-make-decision'; - -export const checkCanOcr = ({ - caseState, - noAction, - workflow, - decision, - isLoadingApprove, -}: { - caseState: ReturnType; - noAction: boolean; - workflow: TWorkflowById; - decision: DefaultContextSchema['documents'][number]['decision']; - isLoadingApprove: boolean; -}) => { - const isStateManualReview = workflow.state === CommonWorkflowStates.MANUAL_REVIEW; - - const hasApproveEvent = workflow?.nextEvents?.includes(CommonWorkflowEvent.APPROVE); - const canMakeDecision = checkCanMakeDecision({ - caseState, - noAction, - decision, - }); - - return !isLoadingApprove && canMakeDecision && (isStateManualReview || hasApproveEvent); -}; diff --git a/services/workflows-service/prisma/data-migrations b/services/workflows-service/prisma/data-migrations index f24018634e..e810f58cf3 160000 --- a/services/workflows-service/prisma/data-migrations +++ b/services/workflows-service/prisma/data-migrations @@ -1 +1 @@ -Subproject commit f24018634e549b969157c09f97f32b6049e422dc +Subproject commit e810f58cf3f020290ff9639251d1d9e13d227b48 From a5a8a1a8c8ccd588ebbf776b22257a952b779caa Mon Sep 17 00:00:00 2001 From: blokh Date: Sun, 29 Sep 2024 12:55:40 +0300 Subject: [PATCH 09/13] feat: fixed comments over OCR logic --- .../molecules/ImageOCR/ImageOCR.tsx | 16 ++++++---- .../MultiDocuments/MultiDocuments.tsx | 1 + .../components/MultiDocuments/interfaces.ts | 1 + .../useDocumentBlocks/useDocumentBlocks.tsx | 2 +- .../Case/Case.Documents.Toolbar.tsx | 18 +++++------ .../Entity/components/Case/Case.Documents.tsx | 7 ++-- .../hooks/useDocuments/useDocumentsLogic.tsx | 2 +- .../Entity/components/Case/interfaces.ts | 3 +- .../workflows-service/prisma/data-migrations | 2 +- .../src/workflow/workflow.service.ts | 32 ++++--------------- 10 files changed, 34 insertions(+), 50 deletions(-) diff --git a/apps/backoffice-v2/src/common/components/molecules/ImageOCR/ImageOCR.tsx b/apps/backoffice-v2/src/common/components/molecules/ImageOCR/ImageOCR.tsx index f63e2f72da..4d794514cf 100644 --- a/apps/backoffice-v2/src/common/components/molecules/ImageOCR/ImageOCR.tsx +++ b/apps/backoffice-v2/src/common/components/molecules/ImageOCR/ImageOCR.tsx @@ -2,32 +2,34 @@ import { ctw } from '@/common/utils/ctw/ctw'; import { ComponentProps, FunctionComponent } from 'react'; import { Loader2, ScanTextIcon } from 'lucide-react'; -export interface IImageOCR extends ComponentProps<'div'> { +export interface IImageOCR extends ComponentProps<'button'> { onOcrPressed?: () => void; isOcrDisabled: boolean; isLoadingOCR?: boolean; - documentId: string; } export const ImageOCR: FunctionComponent = ({ isOcrDisabled, onOcrPressed, - documentId, className, isLoadingOCR, ...props }) => ( ); diff --git a/apps/backoffice-v2/src/lib/blocks/components/MultiDocuments/MultiDocuments.tsx b/apps/backoffice-v2/src/lib/blocks/components/MultiDocuments/MultiDocuments.tsx index bc47eedc61..c053fff65a 100644 --- a/apps/backoffice-v2/src/lib/blocks/components/MultiDocuments/MultiDocuments.tsx +++ b/apps/backoffice-v2/src/lib/blocks/components/MultiDocuments/MultiDocuments.tsx @@ -9,6 +9,7 @@ export const MultiDocuments: FunctionComponent = ({ value
void; isLoadingOCR: boolean; + isDocumentEditable: boolean; data: Array<{ imageUrl: string; title: string; diff --git a/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/useDocumentBlocks.tsx b/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/useDocumentBlocks.tsx index 86130fee4f..98d18f23d6 100644 --- a/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/useDocumentBlocks.tsx +++ b/apps/backoffice-v2/src/lib/blocks/hooks/useDocumentBlocks/useDocumentBlocks.tsx @@ -466,10 +466,10 @@ export const useDocumentBlocks = ({ .addBlock() .addCell({ type: 'multiDocuments', - value: { isLoading: storageFilesQueryResult?.some(({ isLoading }) => isLoading), onOcrPressed: () => mutateOCRDocument({ documentId: id }), + isDocumentEditable: caseState.writeEnabled, isLoadingOCR: isLoadingOCRDocument, data: documents?.[docIndex]?.pages?.map( diff --git a/apps/backoffice-v2/src/pages/Entity/components/Case/Case.Documents.Toolbar.tsx b/apps/backoffice-v2/src/pages/Entity/components/Case/Case.Documents.Toolbar.tsx index dc00f2e091..7bb8a47f16 100644 --- a/apps/backoffice-v2/src/pages/Entity/components/Case/Case.Documents.Toolbar.tsx +++ b/apps/backoffice-v2/src/pages/Entity/components/Case/Case.Documents.Toolbar.tsx @@ -15,7 +15,7 @@ export const DocumentsToolbar: FunctionComponent<{ onOpenDocumentInNewTab: (id: string) => void; shouldDownload: boolean; onOcrPressed?: () => void; - shouldOCR: boolean; + isOCREnabled: boolean; isLoadingOCR: boolean; fileToDownloadBase64: string; }> = ({ @@ -27,7 +27,7 @@ export const DocumentsToolbar: FunctionComponent<{ onOcrPressed, shouldDownload, isLoadingOCR, - shouldOCR, + isOCREnabled, fileToDownloadBase64, }) => { const { onOpenInNewTabClick } = useDocumentsToolbarLogic({ @@ -38,14 +38,12 @@ export const DocumentsToolbar: FunctionComponent<{ return (
- {shouldOCR && image && ( -
- -
+ {image && ( + )} {!hideOpenExternalButton && !isLoading && image?.id && (