diff --git a/src/App.tsx b/src/App.tsx index 7ea23d8..720b3f6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,6 +22,7 @@ import { RTCQC, Utilities } from "./views"; +import {KycSummarizeNative} from "./views/KycSummarizeNative"; function App() { const setCurrentUser = useSetAtom(currentUserAtom); @@ -42,6 +43,7 @@ function App() { {title: 'Customer Risk', href: '/customer-risk', element: }, {title: 'RTC - QC', href: '/rtc-qc', element: }, {title: 'KYC Summarization', href: '/kyc-summarization', element: }, + {title: 'KYC Summarize', href: '/kyc-summarize', excludeFromMenu: true, element: }, { title: 'Utilities', href: '/utilities', diff --git a/src/atoms/document-status.atom.ts b/src/atoms/document-status.atom.ts new file mode 100644 index 0000000..befcb94 --- /dev/null +++ b/src/atoms/document-status.atom.ts @@ -0,0 +1,4 @@ +import {atom} from "jotai"; +import {DocumentStatusModel} from "../models"; + +export const documentStatusAtom = atom([]); diff --git a/src/atoms/index.ts b/src/atoms/index.ts index 435acb1..2122884 100644 --- a/src/atoms/index.ts +++ b/src/atoms/index.ts @@ -6,3 +6,4 @@ export * from './data-extraction.atom'; export * from './dashboard.atom'; export * from './current-user.atom'; export * from './navigation.atom'; +export * from './kyc-case-summary.atom'; diff --git a/src/atoms/kyc-case-summary.atom.ts b/src/atoms/kyc-case-summary.atom.ts new file mode 100644 index 0000000..0da99de --- /dev/null +++ b/src/atoms/kyc-case-summary.atom.ts @@ -0,0 +1,16 @@ +import {atom} from "jotai"; +import {loadable} from "jotai/utils"; + +import {kycCaseSummaryApi} from "../services/kyc-case-summary"; +import {KycCaseSummaryModel} from "../models"; + +const baseAtom = atom>(Promise.resolve(undefined)); + +export const kycCaseSummaryAtom = atom( + get => get(baseAtom), + async (_get, set, name: string) => { + set(baseAtom, kycCaseSummaryApi().summarize(name).then(summary => ({summary}))) + } +) + +export const kycCaseSummaryAtomLoadable = loadable(kycCaseSummaryAtom) diff --git a/src/components/DocumentStatus/DocumentStatus.tsx b/src/components/DocumentStatus/DocumentStatus.tsx new file mode 100644 index 0000000..c239399 --- /dev/null +++ b/src/components/DocumentStatus/DocumentStatus.tsx @@ -0,0 +1,65 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import React, {useEffect} from 'react'; +import {useAtom} from "jotai"; +import {timer} from "rxjs"; + +import {documentStatusAtom} from "../../atoms/document-status.atom"; +import {DocumentModel, DocumentStatusModel} from "../../models"; +import {fileUploadApi, FileUploadContext, ListFileRequest} from "../../services"; +import {first} from "../../utils"; + +export interface DocumentStatusProps { + context: FileUploadContext; + statuses?: string[]; + documents: DocumentModel[]; +} + +const buildRequest = ({context, statuses}: {context: FileUploadContext, statuses?: string[]}): ListFileRequest => { + const request: ListFileRequest = {context} + + if (statuses) { + request.statuses = statuses; + } + + return request; +} + +const getFilename = (id: string, docs: DocumentModel[]): string => { + return first(docs.filter(doc => doc.id === id).map(doc => doc.name)).orElse(undefined) +} + +export const DocumentStatus: React.FunctionComponent = (props: DocumentStatusProps) => { + const [documentStatus, setDocumentStatus] = useAtom(documentStatusAtom) + useEffect(() => { + const handle = timer(500, 30000).subscribe({ + next: async () => { + if (props.documents.length === 0 && documentStatus.length === 0) { + return + } + + const result: DocumentStatusModel[] = await fileUploadApi().listFiles(buildRequest(props)) + + setDocumentStatus(result + .map(doc => Object.assign(doc, {name: getFilename(doc.id, props.documents)})) + .filter(doc => !!doc.name) + ) + } + }) + + return () => handle.unsubscribe(); + }) + + if (documentStatus.length === 0) { + return (<>) + } + + return ( +
+
Document status
+
    + {documentStatus.map(val => (
  • {val.name || val.id} - {val.status}
  • ))} +
+
+ ) +} diff --git a/src/components/DocumentStatus/index.ts b/src/components/DocumentStatus/index.ts new file mode 100644 index 0000000..84b9a16 --- /dev/null +++ b/src/components/DocumentStatus/index.ts @@ -0,0 +1 @@ +export * from './DocumentStatus' diff --git a/src/components/Stack/Stack.tsx b/src/components/Stack/Stack.tsx index 0301699..3b13704 100644 --- a/src/components/Stack/Stack.tsx +++ b/src/components/Stack/Stack.tsx @@ -6,7 +6,7 @@ import {Stack as CarbonStack} from "@carbon/react/lib/components/Stack" export interface StackProps { gap: number; - children: unknown[]; + children: unknown | unknown[]; } export const Stack: React.FunctionComponent = (props: StackProps) => { diff --git a/src/components/index.ts b/src/components/index.ts index df1fbab..c1fc884 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -17,3 +17,4 @@ export * from './KycSummary' export * from './KycCaseOverview' export * from './KycCaseResult' export * from './ListItems' +export * from './DocumentStatus' diff --git a/src/models/kyc-case.model.ts b/src/models/kyc-case.model.ts index 1e67711..7bf7f00 100644 --- a/src/models/kyc-case.model.ts +++ b/src/models/kyc-case.model.ts @@ -29,6 +29,11 @@ export interface DocumentModel extends DocumentInputModel { content: Buffer; } +export interface DocumentStatusModel extends Partial { + id: string; + status?: string; +} + export interface NegativeScreeningModel { subject: string; diff --git a/src/services/file-upload/file-upload.api.ts b/src/services/file-upload/file-upload.api.ts index c17183a..2f8eb6b 100644 --- a/src/services/file-upload/file-upload.api.ts +++ b/src/services/file-upload/file-upload.api.ts @@ -1,7 +1,17 @@ -import {DocumentModel} from "../../models"; +import {DocumentModel, DocumentStatusModel} from "../../models"; export type FileUploadContext = 'data-extraction' | 'kyc-case'; +export const isFileUploadContext = (value: unknown): value is FileUploadContext => { + return value === 'data-extraction' || value === 'kyc-case'; +} + +export interface ListFileRequest { + context?: FileUploadContext, + statuses?: string[], +} + export abstract class FileUploadApi { - abstract uploadFile(caseId: string, name: string, file: File, context?: FileUploadContext): Promise; + abstract uploadFile(caseId: string, name: string, file: File, context?: FileUploadContext, standalone?: boolean): Promise; + abstract listFiles(request?: ListFileRequest): Promise; } diff --git a/src/services/file-upload/file-upload.mock.ts b/src/services/file-upload/file-upload.mock.ts index a97e19b..ca4fb36 100644 --- a/src/services/file-upload/file-upload.mock.ts +++ b/src/services/file-upload/file-upload.mock.ts @@ -1,6 +1,6 @@ import {FileUploadApi} from "./file-upload.api"; import {kycCaseManagementApi, KycCaseManagementApi} from "../kyc-case-management"; -import {DocumentModel} from "../../models"; +import {DocumentModel, DocumentStatusModel} from "../../models"; export class FileUploadMock implements FileUploadApi { service: KycCaseManagementApi; @@ -15,4 +15,8 @@ export class FileUploadMock implements FileUploadApi { return this.service.addDocumentToCase(caseId, name, {content}) } + async listFiles(): Promise { + return [] + } + } diff --git a/src/services/file-upload/file-upload.rest.ts b/src/services/file-upload/file-upload.rest.ts index c6d78ab..ddfa6b4 100644 --- a/src/services/file-upload/file-upload.rest.ts +++ b/src/services/file-upload/file-upload.rest.ts @@ -1,10 +1,15 @@ import axios from "axios"; -import {FileUploadApi, FileUploadContext} from "./file-upload.api"; -import {DocumentModel} from "../../models"; +import {FileUploadApi, FileUploadContext, ListFileRequest} from "./file-upload.api"; +import {DocumentModel, DocumentStatusModel} from "../../models"; + +interface ListFilesParams { + context?: FileUploadContext; + status?: string; +} export class FileUploadRest implements FileUploadApi { - uploadFile(caseId: string, name: string, file: File, context: FileUploadContext = 'kyc-case'): Promise { + uploadFile(caseId: string, name: string, file: File, context: FileUploadContext = 'kyc-case', standalone: boolean = false): Promise { const url = '/api/document/upload' const form = new FormData(); @@ -12,6 +17,7 @@ export class FileUploadRest implements FileUploadApi { form.append('parentId', caseId); form.append('context', context); form.append('file', file); + form.append('standalone', '' + standalone); return axios .post(url, form) @@ -20,4 +26,17 @@ export class FileUploadRest implements FileUploadApi { }); } + async listFiles({context, statuses}: ListFileRequest = {context: 'kyc-case'}): Promise { + const url: string = '/api/document' + + const params: ListFilesParams = {context} + if (statuses) { + params.status = statuses.join(',') + } + + return axios + .get(url, {params}) + .then(response => response.data) + } + } diff --git a/src/services/kyc-case-summary/index.ts b/src/services/kyc-case-summary/index.ts new file mode 100644 index 0000000..daf19d7 --- /dev/null +++ b/src/services/kyc-case-summary/index.ts @@ -0,0 +1,13 @@ +import {KycCaseSummaryApi} from "./kyc-case-summary.api"; +import {KycCaseSummaryGraphql} from "./kyc-case-summary.graphql"; + +export * from './kyc-case-summary.api'; + +let _instance: KycCaseSummaryApi; +export const kycCaseSummaryApi = (): KycCaseSummaryApi => { + if (_instance) { + return _instance; + } + + return _instance = new KycCaseSummaryGraphql(); +} diff --git a/src/services/kyc-case-summary/kyc-case-summary.api.ts b/src/services/kyc-case-summary/kyc-case-summary.api.ts new file mode 100644 index 0000000..7994f31 --- /dev/null +++ b/src/services/kyc-case-summary/kyc-case-summary.api.ts @@ -0,0 +1,4 @@ + +export abstract class KycCaseSummaryApi { + abstract summarize(name: string): Promise; +} diff --git a/src/services/kyc-case-summary/kyc-case-summary.graphql.ts b/src/services/kyc-case-summary/kyc-case-summary.graphql.ts new file mode 100644 index 0000000..4335b88 --- /dev/null +++ b/src/services/kyc-case-summary/kyc-case-summary.graphql.ts @@ -0,0 +1,33 @@ +import {ApolloClient, gql} from "@apollo/client"; + +import {KycCaseSummaryApi} from "./kyc-case-summary.api.ts"; +import {getApolloClient} from "../../backends"; + +const SUMMARIZE = gql` + query Summarize($name:String!) { + summarize(name:$name) { + result + } + } +` + +export class KycCaseSummaryGraphql implements KycCaseSummaryApi { + client: ApolloClient; + + constructor() { + this.client = getApolloClient(); + } + + summarize(name: string): Promise { + return this.client + .query<{summarize: {result: string}}>({ + query: SUMMARIZE, + variables: {name}, + }) + .then(result => result.data.summarize.result) + .catch(() => { + return '[Error]' + }) as Promise + } + +} \ No newline at end of file diff --git a/src/services/kyc-case-summary/kyc-case-summary.mock.ts b/src/services/kyc-case-summary/kyc-case-summary.mock.ts new file mode 100644 index 0000000..167e793 --- /dev/null +++ b/src/services/kyc-case-summary/kyc-case-summary.mock.ts @@ -0,0 +1,7 @@ +import {KycCaseSummaryApi} from "./kyc-case-summary.api.ts"; + +export class KycCaseSummaryMock implements KycCaseSummaryApi { + async summarize(name: string): Promise { + return name; + } +} diff --git a/src/services/schema.gql b/src/services/schema.gql index f3a8c48..4546a0e 100644 --- a/src/services/schema.gql +++ b/src/services/schema.gql @@ -67,6 +67,13 @@ input DocumentInput { path: String! } +type DocumentOutput { + id: ID! + name: String + path: String + status: String +} + """Object representing a key/value pair""" type FormOption { text: String! @@ -92,16 +99,28 @@ type KycCase { status: String! } +type KycCaseChangeEvent { + caseId: ID! + event: String! +} + """KYC case summary""" type KycCaseSummary { error: String summary: String! } +input ListDocumentInput { + context: String + statuses: [String!] +} + type Mutation { addDocumentToCase(caseId: ID!, documentName: String!, documentUrl: String!): KycCase! approveCase(case: ApproveCaseInput!): KycCase! createCase(customer: CustomerInput!): KycCase! + deleteCase(id: ID!): KycCase! + deleteDocument(id: ID!): Document! processCase(id: ID!): KycCase! removeDocumentFromCase(caseId: ID!, documentId: ID!): KycCase! reviewCase(case: ReviewCaseInput!): KycCase! @@ -151,7 +170,9 @@ type Query { helloWorld: Greeting! listCases: [KycCase!]! listCountries: [FormOption!]! + listDocuments: [Document!]! listEntityTypes: [FormOption!]! + listFiles(input: ListDocumentInput): [DocumentOutput!]! listIndustryTypes: [FormOption!]! listQuestions: [DataExtractionQuestion!]! screenNews(country: String, dateOfBirth: String, name: String!): NegativeScreening! @@ -168,7 +189,9 @@ input ReviewCaseInput { type Subscription { extractDataObservable(customer: String!, questions: [DataExtractionQuestionIdInput!]!): [DataExtractionResult!]! + subscribeToCaseChanges: KycCaseChangeEvent! subscribeToCases: [KycCase!]! + watchCase(id: ID!): KycCase! } type SummaryResult { diff --git a/src/views/KYC/KYCCaseDetail/KYCCaseDetail.tsx b/src/views/KYC/KYCCaseDetail/KYCCaseDetail.tsx index b1e29ef..2ee083d 100644 --- a/src/views/KYC/KYCCaseDetail/KYCCaseDetail.tsx +++ b/src/views/KYC/KYCCaseDetail/KYCCaseDetail.tsx @@ -72,7 +72,7 @@ export const KYCCaseDetail: React.FunctionComponent = (props } if (selectedCase.status === 'Closed') { - return () + return () } return ( diff --git a/src/views/KYC/KYCCaseDetail/KYCCasePending/KYCCasePending.tsx b/src/views/KYC/KYCCaseDetail/KYCCasePending/KYCCasePending.tsx index ff64b26..2b344be 100644 --- a/src/views/KYC/KYCCaseDetail/KYCCasePending/KYCCasePending.tsx +++ b/src/views/KYC/KYCCaseDetail/KYCCasePending/KYCCasePending.tsx @@ -14,6 +14,7 @@ import {kycCaseManagementApi} from "../../../../services"; export interface KYCCasePendingProps { currentCase: KycCaseModel; returnUrl: string; + title?: string; } export const KYCCasePending: React.FunctionComponent = (props: KYCCasePendingProps) => { @@ -33,7 +34,7 @@ export const KYCCasePending: React.FunctionComponent = (pro return ( -

Pending information

+

{props.title ? props.title : 'Pending information'}

diff --git a/src/views/KYC/KYCCaseDetail/util/event-handler.util.ts b/src/views/KYC/KYCCaseDetail/util/event-handler.util.ts index 8416432..3dbad9b 100644 --- a/src/views/KYC/KYCCaseDetail/util/event-handler.util.ts +++ b/src/views/KYC/KYCCaseDetail/util/event-handler.util.ts @@ -2,7 +2,7 @@ import {DocumentModel} from "../../../../models"; import {fileUploadApi, FileUploadApi, FileUploadContext} from "../../../../services"; import {fileListUtil} from "../../../../utils"; -export const handleFileUploaderChange = (id: string, handleNewDocuments: (newDocuments: DocumentModel[]) => void, setFileStatus: (status: unknown) => void, context: FileUploadContext = 'kyc-case') => { +export const handleFileUploaderChange = (id: string, handleNewDocuments: (newDocuments: DocumentModel[]) => void, setFileStatus: (status: unknown) => void, context: FileUploadContext = 'kyc-case', standalone?: boolean) => { const fileUploadService: FileUploadApi = fileUploadApi(); return (event: {target: {files: FileList, filenameStatus: string}}) => { @@ -14,13 +14,16 @@ export const handleFileUploaderChange = (id: string, handleNewDocuments: (newDoc setFileStatus('uploading') // TODO handle document remove - Promise.all(files.map(f => fileUploadService.uploadFile(id, f.name, f, context))) + Promise.all(files.map(f => fileUploadService.uploadFile(id, f.name, f, context, standalone))) .then((result: DocumentModel[]) => { setFileStatus('complete'); return result.filter(doc => !!doc); }) .then(handleNewDocuments) - .catch(() => setFileStatus('error')) + .catch(err => { + console.log('Error uploading file: ', {err}) + setFileStatus('error') + }) } } diff --git a/src/views/KycSummarizeNative/KycSummarizeNative.tsx b/src/views/KycSummarizeNative/KycSummarizeNative.tsx new file mode 100644 index 0000000..696600e --- /dev/null +++ b/src/views/KycSummarizeNative/KycSummarizeNative.tsx @@ -0,0 +1,99 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import React, {useState} from 'react'; +import {useNavigate} from "react-router-dom"; +import {Button, Column, FileUploader, Form, Grid, Loading, TextInput} from "@carbon/react"; +import {useAtomValue, useSetAtom} from "jotai"; + +import {kycCaseSummaryAtom, kycCaseSummaryAtomLoadable} from "../../atoms"; +import {DocumentStatus, KycSummary, Stack} from "../../components"; +import {handleFileUploaderChange} from "../KYC/KYCCaseDetail/util"; +import {DocumentModel, KycCaseSummaryModel} from "../../models"; +import {Loadable} from "jotai/vanilla/utils/loadable"; + +export interface KycSummarizeNativeProps { + returnUrl: string; +} + +const KycSummaryPane = (props: {loadable: Loadable>}) => { + const caseSummaryLoadable = props.loadable; + + if (caseSummaryLoadable.state === 'loading') { + return ( + ) + } else if (caseSummaryLoadable.state === 'hasError') { + return (
Error {caseSummaryLoadable.error as string}
) + } + + const summary = caseSummaryLoadable.data + + return ( + + ) +} + +export const KycSummarizeNative: React.FunctionComponent = (props: KycSummarizeNativeProps) => { + const setCaseSummary = useSetAtom(kycCaseSummaryAtom) + const caseSummaryLoadable = useAtomValue(kycCaseSummaryAtomLoadable) + const [name, setName] = useState('') + const [fileStatus, setFileStatus] = useState<'edit' | 'complete' | 'uploading'>('edit') + const [documents, setDocuments] = useState([]) + const navigate = useNavigate() + + const handleCancel = () => { + navigate(props.returnUrl); + } + + const handleSubmit = (event: {preventDefault: () => void}) => { + event.preventDefault(); + + setCaseSummary(name) + } + + const context = 'kyc-case'; + + return ( + <> +
+ +

KYC Summary

+ setName(event.target.value)} + required={true} + /> + + + + + + + + +
+
+
+ + + ) +} diff --git a/src/views/KycSummarizeNative/index.ts b/src/views/KycSummarizeNative/index.ts new file mode 100644 index 0000000..424b440 --- /dev/null +++ b/src/views/KycSummarizeNative/index.ts @@ -0,0 +1 @@ +export * from './KycSummarizeNative';