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

Pdf export feature #2331

Merged
merged 47 commits into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
6666ace
feat: added title page markup
chesterkmr Apr 19, 2024
f92550e
feat: added registry information page report
chesterkmr Apr 19, 2024
cfb9feb
feat: added company ownership page
chesterkmr Apr 19, 2024
e1d0ad1
feat: added company sanctions page
chesterkmr Apr 19, 2024
d33c33d
feat: added identity verifications page
chesterkmr Apr 20, 2024
e6d42b7
feat: added individual sanctions page
chesterkmr Apr 24, 2024
d54070c
Merge branch 'dev' into bal-1641
chesterkmr Apr 24, 2024
dbddedb
feat: added validation schemas
chesterkmr Apr 24, 2024
5e1cf17
feat: added download certificate logic & ui
chesterkmr Apr 24, 2024
420b1c1
Merge branch 'dev' into bal-1641
chesterkmr Apr 28, 2024
f956d85
feat: added mapping for title & registry page
chesterkmr Apr 28, 2024
acc752f
Merge branch 'dev' into bal-1641
chesterkmr Apr 28, 2024
de8484e
feat: added partial rendering of pdfs
chesterkmr Apr 28, 2024
f57e482
feat: implemented mapping for identity verifications page
chesterkmr Apr 30, 2024
47eb7e1
feat: implemented individual sanctions page
chesterkmr Apr 30, 2024
da3d438
feat: implemented empty state pages
chesterkmr May 1, 2024
8f21348
fix: lock fix
chesterkmr May 1, 2024
47c90cf
Merge branch 'dev' into bal-1641
chesterkmr May 15, 2024
10aa596
Merge branch 'dev' into bal-1641
chesterkmr May 15, 2024
2fe223b
fix: removed reduntant canvas removal from body
chesterkmr May 21, 2024
d8a0a9d
Merge branch 'dev' into bal-1641
chesterkmr May 22, 2024
d904642
fix: schemas
chesterkmr May 22, 2024
a7cdae1
fix: refactor
chesterkmr May 22, 2024
624e673
fix: registerFont moved to main file
chesterkmr May 22, 2024
83703f2
fix: removed reduntant method call
chesterkmr May 22, 2024
b44b1d9
fix: fixed type
chesterkmr May 22, 2024
7b70e50
fix: schema
chesterkmr May 22, 2024
97325c3
feat: pdf now opened in new tab instead of download
chesterkmr May 22, 2024
f77727b
fix: refactor
chesterkmr May 22, 2024
4399427
fix: refactor & cleaned assets
chesterkmr May 22, 2024
1b3e676
fix: fixed date formats
chesterkmr May 22, 2024
649e021
feat: refactored mapping & removed lodash
chesterkmr May 22, 2024
907985e
Merge branch 'dev' into bal-1641
chesterkmr May 28, 2024
d47969a
fix: fixed schema
chesterkmr May 28, 2024
b52a058
fix: renamed method
chesterkmr May 28, 2024
32954a4
fix: schema
chesterkmr May 28, 2024
5960c4c
feat: updated toast json
chesterkmr May 28, 2024
6bd1975
fix: cleaned leftovers
chesterkmr May 28, 2024
a55c2fb
fix: refactor
chesterkmr May 28, 2024
c053a45
migration
chesterkmr May 28, 2024
742e281
refactor(*): resolved pr comments
Omri-Levy Jun 11, 2024
c4f941c
fix(backoffice-v2): removed pdfUrl
Omri-Levy Jun 11, 2024
533b2b2
Merge branch 'dev' into bal-1641
chesterkmr Jun 18, 2024
4d5af47
Merge branch 'dev' into bal-1641
alonp99 Jun 18, 2024
39d210e
fix: fixed ballerine logo url
chesterkmr Jun 19, 2024
7a1563d
fix(seed.ts): fixed bad symbol in cdn link
Omri-Levy Jun 23, 2024
85c6f4c
Merge branch 'dev' into bal-1641
Omri-Levy Jun 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/backoffice-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
},
"dependencies": {
"@ballerine/blocks": "0.2.3",
"@ballerine/react-pdf-toolkit": "^1.2.1",
"@ballerine/common": "0.9.8",
"@ballerine/ui": "^0.5.3",
"@ballerine/workflow-browser-sdk": "0.6.14",
Expand All @@ -74,6 +75,7 @@
"@radix-ui/react-slot": "^1.0.1",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@react-pdf/renderer": "^3.1.14",
"@radix-ui/react-tooltip": "^1.0.7",
"@rjsf/utils": "^5.9.0",
"@tanstack/react-query": "^4.19.1",
Expand Down
3 changes: 3 additions & 0 deletions apps/backoffice-v2/public/locales/en/toast.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@
"success": "The alerts decision have been reverted successfully.",
"error": "Error occurred while reverting the alerts decision."
},
"pdf_certificate": {
"error": "Failed to open PDF certificate."
},
"business_report_creation": {
"success": "Merchant check created successfully.",
"error": "Error occurred while creating a merchant check.",
Expand Down
26 changes: 26 additions & 0 deletions apps/backoffice-v2/src/common/utils/svg-to-png/svg-to-png.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const svgToPng = (imageUrl: string): Promise<string> => {
return new Promise((resolve, reject) => {
const img = new Image();

img.crossOrigin = 'Anonymous';

img.onload = () => {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');

canvas.width = img.width;
canvas.height = img.height;
context?.drawImage(img, 0, 0);

const dataUrl = canvas.toDataURL('image/png');

resolve(dataUrl);
};

img.onerror = error => {
reject(error);
};

img.src = imageUrl;
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { valueOrFallback } from '@/common/utils/value-or-fallback/value-or-fallback';

export const valueOrNone = valueOrFallback('None', { checkFalsy: true });
52 changes: 27 additions & 25 deletions apps/backoffice-v2/src/domains/customer/fetchers.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,37 @@
import { apiClient } from '../../common/api-client/api-client';
import { z } from 'zod';
import { handleZodError } from '../../common/utils/handle-zod-error/handle-zod-error';
import { apiClient } from '../../common/api-client/api-client';
import { Method } from '../../common/enums';
import { handleZodError } from '../../common/utils/handle-zod-error/handle-zod-error';

const CustomerSchema = z.object({
id: z.string(),
name: z.string(),
displayName: z.string(),
logoImageUri: z.union([z.string(), z.null()]).optional(),
// Remove default once data migration is done
faviconImageUri: z.string().default(''),
customerStatus: z.string().optional(),
country: z.union([z.string(), z.null()]).optional(),
language: z.union([z.string(), z.null()]).optional(),
config: z
.object({
isMerchantMonitoringEnabled: z.boolean().default(false),
isExample: z.boolean().default(false),
})
.nullable()
.default({
isMerchantMonitoringEnabled: false,
isExample: false,
}),
});

export type TCustomer = z.infer<typeof CustomerSchema>;

export const fetchCustomer = async () => {
const [filter, error] = await apiClient({
endpoint: `customers`,
method: Method.GET,
schema: z
.object({
id: z.string(),
name: z.string(),
displayName: z.string(),
logoImageUri: z.union([z.string(), z.null()]).optional(),
// Remove default once data migration is done
faviconImageUri: z.string().default(''),
customerStatus: z.string().optional(),
country: z.union([z.string(), z.null()]).optional(),
language: z.union([z.string(), z.null()]).optional(),
config: z
.object({
isMerchantMonitoringEnabled: z.boolean().default(false),
isExample: z.boolean().default(false),
})
.nullable()
.default({
isMerchantMonitoringEnabled: false,
isExample: false,
}),
})
.optional(),
schema: CustomerSchema,
});

return handleZodError(error, filter);
Expand Down
4 changes: 2 additions & 2 deletions apps/backoffice-v2/src/domains/workflows/fetchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ export const fetchWorkflows = async (params: {
return handleZodError(error, workflows);
};

export type TWorkflowById = z.output<typeof WorkflowByIdSchema>;

export const BaseWorkflowByIdSchema = z.object({
id: z.string(),
status: z.string(),
Expand Down Expand Up @@ -121,6 +119,8 @@ export const WorkflowByIdSchema = BaseWorkflowByIdSchema.extend({
.optional(),
});

export type TWorkflowById = z.output<typeof WorkflowByIdSchema>;

export const fetchWorkflowById = async ({
workflowId,
filterId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useCallback, useMemo } from 'react';
import { createBlocksTyped } from '@/lib/blocks/create-blocks-typed/create-blocks-typed';
import { WarningFilledSvg } from '@/common/components/atoms/icons';
import { createBlocksTyped } from '@/lib/blocks/create-blocks-typed/create-blocks-typed';
import { useCallback, useMemo } from 'react';

type Ubo = {
export type Ubo = {
name?: string;
type?: string;
level?: number;
Expand Down
4 changes: 4 additions & 0 deletions apps/backoffice-v2/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import '@ballerine/ui/dist/style.css';
import '@fontsource/inter';

import { Toaster } from '@/common/components/organisms/Toaster/Toaster';
import { registerFont } from '@ballerine/react-pdf-toolkit';
import { Font } from '@react-pdf/renderer';
import { Router } from './Router/Router';
import { env } from './common/env/env';
import './i18n';
Expand All @@ -16,6 +18,8 @@ import advancedFormat from 'dayjs/plugin/advancedFormat';

dayjs.extend(advancedFormat);

registerFont(Font);

export const TOAST_DURATION_IN_MS = 1000 * 3;

const rootElement = document.getElementById('root');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Badge } from '@ballerine/ui';
import { FunctionComponent } from 'react';

import { ActionsVariant } from '@/pages/Entity/components/Case/actions-variants/ActionsVariant/ActionsVariant';
import { CaseOptions } from '@/pages/Entity/components/Case/components/CaseOptions/CaseOptions';
import { AssignDropdown } from '../../../../common/components/atoms/AssignDropdown/AssignDropdown';
import { ctw } from '../../../../common/utils/ctw/ctw';
import { tagToBadgeData } from './consts';
Expand Down Expand Up @@ -40,7 +41,7 @@ export const Actions: FunctionComponent<IActionsProps> = ({

return (
<div className={`col-span-2 space-y-2 bg-base-100 px-4 pt-4`}>
<div className={`mb-8 flex flex-row space-x-3.5`}>
<div className={`mb-8 flex flex-row justify-between space-x-3.5`}>
<AssignDropdown
assignedUser={assignedUser}
assignees={assignees}
Expand All @@ -50,6 +51,7 @@ export const Actions: FunctionComponent<IActionsProps> = ({
authenticatedUserId={authenticatedUser?.id}
isDisabled={isWorkflowCompleted}
/>
<CaseOptions />
</div>
<div className={`flex h-20 justify-between gap-4`}>
<div className={`flex flex-col space-y-3`}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Button } from '@/common/components/atoms/Button/Button';
import { DropdownMenu } from '@/common/components/molecules/DropdownMenu/DropdownMenu';
import { DropdownMenuContent } from '@/common/components/molecules/DropdownMenu/DropdownMenu.Content';
import { DropdownMenuItem } from '@/common/components/molecules/DropdownMenu/DropdownMenu.Item';
import { DropdownMenuTrigger } from '@/common/components/molecules/DropdownMenu/DropdownMenu.Trigger';
import { useCaseOptionsLogic } from '@/pages/Entity/components/Case/components/CaseOptions/hooks/useCaseOptionsLogic/useCaseOptionsLogic';

export const CaseOptions = () => {
const { isGeneratingPDF, generateAndOpenPDFInNewTab } = useCaseOptionsLogic();

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">Options</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem className="px-8 py-1" asChild>
<Button
onClick={() => generateAndOpenPDFInNewTab()}
disabled={isGeneratingPDF}
variant={'ghost'}
>
Open PDF Certificate
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { TWorkflowById } from '@/domains/workflows/fetchers';
import { TCustomer } from '@/domains/customer/fetchers';
import { useMutation } from '@tanstack/react-query';
import { TitlePagePDF } from '@/pages/Entity/components/Case/components/CaseOptions/hooks/useCaseOptionsLogic/renderers/title-page.pdf';
import { RegistryPagePDF } from '@/pages/Entity/components/Case/components/CaseOptions/hooks/useCaseOptionsLogic/renderers/registry-page.pdf';
import { CompanyOwnershipPagePDF } from '@/pages/Entity/components/Case/components/CaseOptions/hooks/useCaseOptionsLogic/renderers/company-ownership-page.pdf';
import { CompanySanctionsPagePDF } from '@/pages/Entity/components/Case/components/CaseOptions/hooks/useCaseOptionsLogic/renderers/company-sanctions-page.pdf';
import { IdentityVerificationsPagePDF } from '@/pages/Entity/components/Case/components/CaseOptions/hooks/useCaseOptionsLogic/renderers/identity-verifications-page.pdf';
import { IndividualSantcionsPagePDF } from '@/pages/Entity/components/Case/components/CaseOptions/hooks/useCaseOptionsLogic/renderers/individual-sanctions-page.pdf';
import { Document, pdf } from '@react-pdf/renderer';
import { toast } from 'sonner';
import { t } from 'i18next';

const openBlobInNewTab = (blob: Blob) => {
const url = URL.createObjectURL(blob);

window.open(url, '_blank');

setTimeout(() => {
URL.revokeObjectURL(url);
}, 10_000);
};

export const useGeneratePDFMutation = ({
workflow,
customer,
}: {
workflow: TWorkflowById;
customer: TCustomer;
}) => {
return useMutation({
mutationFn: async () => {
const pdfs = [
TitlePagePDF,
RegistryPagePDF,
CompanyOwnershipPagePDF,
CompanySanctionsPagePDF,
IdentityVerificationsPagePDF,
IndividualSantcionsPagePDF,
];
const renderers = pdfs.map(PDF => new PDF(workflow, customer));
const pages = await Promise.all(renderers.map(renderer => renderer.render()));

const pdfBlob = await pdf(<Document>{pages}</Document>).toBlob();

openBlobInNewTab(pdfBlob);
},
onError: error => {
console.error(`Failed to open PDF certificate: ${JSON.stringify(error)}`);
toast.error(t('toast:pdf_certificate.error'));
},
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { IPDFRenderer } from '@/pages/Entity/components/Case/components/CaseOptions/hooks/useCaseOptionsLogic/renderers/pdf-renderer.abstract';
import {
CompanyOwnershipPage,
CompanyOwnershipSchema,
EmptyCompanyOwnershipPage,
TCompanyOwnershipData,
} from '@/pages/Entity/pdfs/case-information/pages/CompanyOwnershipPage';

export class CompanyOwnershipPagePDF extends IPDFRenderer<TCompanyOwnershipData> {
static PDF_NAME = 'companyOwnershipPage';

async render(): Promise<JSX.Element> {
const pdfData = await this.getData();
this.isValid(pdfData);

if (this.isEmpty(pdfData)) return <EmptyCompanyOwnershipPage {...pdfData} />;

return <CompanyOwnershipPage {...pdfData} />;
}

async getData() {
const pdfData: TCompanyOwnershipData = {
companyName: this.workflow?.context?.entity?.data?.companyName || '',
creationDate: new Date(),
logoUrl: await this.getLogoUrl(),
items: (this.workflow?.context?.pluginsOutput?.ubo?.data?.uboGraph || []).map((ubo: any) => ({
companyName: ubo?.name,
companyType: ubo?.type,
ownershipPercentage: ubo?.shareHolders?.[0]?.sharePercentage,
level: ubo?.level,
})),
};

return pdfData;
}

isValid(data: TCompanyOwnershipData) {
CompanyOwnershipSchema.parse(data);
}

private isEmpty(data: TCompanyOwnershipData) {
return !data.items?.length;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { IPDFRenderer } from '@/pages/Entity/components/Case/components/CaseOptions/hooks/useCaseOptionsLogic/renderers/pdf-renderer.abstract';
import {
CompanySanctionsPage,
CompanySanctionsSchema,
EmptyCompanySanctionsPage,
TCompanySanctionsData,
} from '@/pages/Entity/pdfs/case-information/pages/CompanySanctionsPage';

export class CompanySanctionsPagePDF extends IPDFRenderer<TCompanySanctionsData> {
static PDF_NAME = 'companySanctionsPage';

async render(): Promise<JSX.Element> {
const pdfData = await this.getData();
this.isValid(pdfData);

if (this.isEmpty(pdfData)) return <EmptyCompanySanctionsPage {...pdfData} />;

return <CompanySanctionsPage {...pdfData} />;
}

async getData() {
const pdfData: TCompanySanctionsData = {
companyName: this.workflow?.context?.entity?.data?.companyName || '',
creationDate: new Date(),
logoUrl: await this.getLogoUrl(),
sanctions: (this.workflow?.context?.pluginsOutput?.companySanctions?.data || []).map(
(sanction: any) => ({
name: sanction.entity.name,
reviewDate: sanction.entity.lastReviewed,
labels: sanction.entity.categories,
sources: sanction.entity.sources.map((source: { url: string }) => source.url),
addresses: sanction.entity.places,
matchReasons: sanction.matchedFields,
}),
),
};

return pdfData;
}

isValid(data: TCompanySanctionsData) {
CompanySanctionsSchema.parse(data);
}

private isEmpty(data: TCompanySanctionsData) {
return !data.sanctions?.length;
}
}
Loading
Loading