From b00fb2a55ae7746e358e5cf62e29da3b80ce3a16 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 30 Jun 2025 15:04:40 +0200 Subject: [PATCH] Add basic certificate --- .../DesignSystem/DragAndDrop/index.tsx | 4 +- .../DepositDocumentComponent/index.tsx | 6 +- .../Layouts/Folder/AskDocuments/index.tsx | 13 +- .../ClientView/DocumentTables/index.tsx | 87 ++++- src/front/Services/PdfService/index.ts | 311 ++++++++++++++++++ src/sdk/MessageBus.ts | 92 +++--- 6 files changed, 458 insertions(+), 55 deletions(-) create mode 100644 src/front/Services/PdfService/index.ts diff --git a/src/front/Components/DesignSystem/DragAndDrop/index.tsx b/src/front/Components/DesignSystem/DragAndDrop/index.tsx index 29896fc4..bc2a8021 100644 --- a/src/front/Components/DesignSystem/DragAndDrop/index.tsx +++ b/src/front/Components/DesignSystem/DragAndDrop/index.tsx @@ -213,9 +213,9 @@ export default function DragAndDrop(props: IProps) { {documentFiles.length > 0 && (
- {documentFiles.map((documentFile) => ( + {documentFiles.map((documentFile, index) => ( handleRemove(documentFile)} diff --git a/src/front/Components/Layouts/ClientDashboard/DepositDocumentComponent/index.tsx b/src/front/Components/Layouts/ClientDashboard/DepositDocumentComponent/index.tsx index a967c7c3..aa0f9063 100644 --- a/src/front/Components/Layouts/ClientDashboard/DepositDocumentComponent/index.tsx +++ b/src/front/Components/Layouts/ClientDashboard/DepositDocumentComponent/index.tsx @@ -82,9 +82,9 @@ export default function DepositDocumentComponent(props: IProps) { (fileUid: string) => { return new Promise( (resolve: () => void) => { - FileService.getFileByUid(fileUid).then((process: any) => { - if (process) { - FileService.updateFile(process, { isDeleted: 'true' }).then(() => { + FileService.getFileByUid(fileUid).then((res: any) => { + if (res) { + FileService.updateFile(res.processId, { isDeleted: 'true' }).then(() => { DocumentService.getDocumentByUid(document.uid!).then((process: any) => { if (process) { const document: any = process.processData; diff --git a/src/front/Components/Layouts/Folder/AskDocuments/index.tsx b/src/front/Components/Layouts/Folder/AskDocuments/index.tsx index 949c1ca4..0828bfa1 100644 --- a/src/front/Components/Layouts/Folder/AskDocuments/index.tsx +++ b/src/front/Components/Layouts/Folder/AskDocuments/index.tsx @@ -18,6 +18,7 @@ import backgroundImage from "@Assets/images/background_refonte.svg"; import FolderService from "src/common/Api/LeCoffreApi/sdk/FolderService"; import DocumentService from "src/common/Api/LeCoffreApi/sdk/DocumentService"; import LoaderService from "src/common/Api/LeCoffreApi/sdk/Loader/LoaderService"; +import { DocumentData } from "../FolderInformation/ClientView/DocumentTables/types"; export default function AskDocuments() { const router = useRouter(); @@ -62,17 +63,21 @@ export default function AskDocuments() { LoaderService.getInstance().show(); const documentAsked: [] = values["document_types"] as []; for (let i = 0; i < documentAsked.length; i++) { + const documentTypeUid = documentAsked[i]; + if (!documentTypeUid) continue; + const documentData: any = { folder: { - uid: folderUid, + uid: folderUid as string, }, depositor: { - uid: customerUid, + uid: customerUid as string, }, document_type: { - uid: documentAsked[i] + uid: documentTypeUid }, - document_status: EDocumentStatus.ASKED + document_status: EDocumentStatus.ASKED, + file_uid: null, }; const validatorId: string = '884cb36a346a79af8697559f16940141f068bdf1656f88fa0df0e9ecd7311fb8:0'; diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/index.tsx index 5f85bb8c..33218968 100644 --- a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/index.tsx +++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/index.tsx @@ -6,7 +6,7 @@ import Tag, { ETagColor, ETagVariant } from "@Front/Components/DesignSystem/Tag" import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; import Module from "@Front/Config/Module"; import useOpenable from "@Front/Hooks/useOpenable"; -import { ArrowDownTrayIcon, EyeIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { ArrowDownTrayIcon, EyeIcon, TrashIcon, DocumentTextIcon } from "@heroicons/react/24/outline"; import { useMediaQuery } from "@mui/material"; import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document"; import DocumentNotary, { EDocumentNotaryStatus } from "le-coffre-resources/dist/Notary/DocumentNotary"; @@ -22,6 +22,8 @@ import DocumentService from "src/common/Api/LeCoffreApi/sdk/DocumentService"; import DocumentTypeService from "src/common/Api/LeCoffreApi/sdk/DocumentTypeService"; import FileService from "src/common/Api/LeCoffreApi/sdk/FileService"; import LoaderService from "src/common/Api/LeCoffreApi/sdk/Loader/LoaderService"; +import FolderService from "src/common/Api/LeCoffreApi/sdk/FolderService"; +import PdfService, { CertificateData, Metadata } from "@Front/Services/PdfService"; type IProps = { customerUid: string; @@ -61,6 +63,12 @@ export default function DocumentTables(props: IProps) { // FilterBy folder.uid & depositor.uid documents = documents.filter((document: any) => document.folder.uid === folderUid && document.depositor && document.depositor.uid === customerUid); + console.log('[DocumentTables] fetchDocuments: all documents for this folder/customer:', documents.map(doc => ({ + uid: doc.uid, + status: doc.document_status, + type: doc.document_type?.name + }))); + for (const document of documents) { document.document_type = (await DocumentTypeService.getDocumentTypeByUid(document.document_type.uid)).processData; @@ -174,6 +182,71 @@ export default function DocumentTables(props: IProps) { }).catch((e) => console.warn(e)); }, []); + const onDownloadCertificate = useCallback(async (doc: any) => { + try { + console.log('[DocumentTables] onDownloadCertificate: doc', doc); + const certificateData: CertificateData = { + customer: { + firstName: doc.depositor.first_name || doc.depositor.firstName || "N/A", + lastName: doc.depositor.last_name || doc.depositor.lastName || "N/A", + postalAddress: doc.depositor.postal_address || doc.depositor.address || doc.depositor.postalAddress || "N/A", + email: doc.depositor.email || "N/A" + }, + notary: { + name: "N/A" + }, + folderUid: folderUid, + documentHash: "N/A", + metadata: { + fileName: "N/A", + isDeleted: false, + updatedAt: new Date(), + commitmentId: "N/A", + createdAt: new Date(), + documentUid: "N/A", + documentType: "N/A" + } + }; + + // Fetch folder data to get office/notary information + // const folderProcess = await FolderService.getFolderByUid(folderUid, false, true); + // const folderData = folderProcess?.processData; + + // console.log('[DocumentTables] onDownloadCertificate: folderData', folderData); + + const documentProcesses = await DocumentService.getDocuments(); + documentProcesses.filter((process: any) => process.processData.folder.uid === folderUid); + + console.log('[DocumentTables] onDownloadCertificate: documentProcesses', documentProcesses); + + for (const document of documentProcesses) { + for (const file of document.processData.files) { + await FileService.getFileByUid(file.uid).then((res: any) => { + console.log('[DocumentTables] onDownloadCertificate: fileProcess', res); + const hash = res.lastUpdatedFileState.pcd_commitment.file_blob; + certificateData.documentHash = hash; + const metadata: Metadata = { + fileName: res.processData.file_name, + isDeleted: false, + updatedAt: new Date(res.processData.updated_at), + commitmentId: res.lastUpdatedFileState.commited_in, + createdAt: new Date(res.processData.created_at), + documentUid: doc.document_type.uid, + documentType: doc.document_type.name + }; + certificateData.metadata = metadata; + } + )} + } + + console.log('[DocumentTables] onDownloadCertificate: certificateData', certificateData); + + await PdfService.getInstance().downloadCertificate(certificateData); + } catch (error) { + console.error('Error downloading certificate:', error); + } + }, [folderUid, customerUid]); + const askedDocuments: IRowProps[] = useMemo( () => documents @@ -261,8 +334,8 @@ export default function DocumentTables(props: IProps) { ); const validatedDocuments: IRowProps[] = useMemo( - () => - documents + () => { + const validated = documents .map((document) => { if (document.document_status !== EDocumentStatus.VALIDATED) return null; return { @@ -298,13 +371,17 @@ export default function DocumentTables(props: IProps) { } /> onDownload(document)} icon={} /> + onDownloadCertificate(document)} icon={} />
), }, }; }) - .filter((document) => document !== null) as IRowProps[], - [documents, folderUid, onDownload], + .filter((document) => document !== null) as IRowProps[]; + + return validated; + }, + [documents, folderUid, onDownload, onDownloadCertificate], ); const refusedDocuments: IRowProps[] = useMemo( diff --git a/src/front/Services/PdfService/index.ts b/src/front/Services/PdfService/index.ts new file mode 100644 index 00000000..66f2ba2c --- /dev/null +++ b/src/front/Services/PdfService/index.ts @@ -0,0 +1,311 @@ +import { saveAs } from 'file-saver'; +import jsPDF from 'jspdf'; + +export interface CustomerInfo { + firstName: string; + lastName: string; + postalAddress: string; + email: string; +} + +export interface NotaryInfo { + name: string; + // Add more notary fields as needed +} + +export interface Metadata { + fileName: string, + createdAt: Date, + isDeleted: boolean, + updatedAt: Date, + commitmentId: string, + documentUid: string; + documentType: string; +} + +export interface CertificateData { + customer: CustomerInfo; + notary: NotaryInfo; + folderUid: string; + documentHash: string; + metadata: Metadata; +} + +export default class PdfService { + private static instance: PdfService; + + public static getInstance(): PdfService { + if (!PdfService.instance) { + PdfService.instance = new PdfService(); + } + return PdfService.instance; + } + + /** + * Generate a certificate PDF for a document + * @param certificateData - Data needed for the certificate + * @returns Promise - The generated PDF as a blob + */ + public async generateCertificate(certificateData: CertificateData): Promise { + try { + const doc = new jsPDF(); + + // Notary Information Section (Top Left) + doc.setFontSize(12); + doc.setFont('helvetica', 'bold'); + doc.text('Notaire Validateur:', 20, 30); + doc.setFont('helvetica', 'normal'); + doc.text(certificateData.notary.name, 20, 37); + + // Customer Information Section (Top Right) + doc.setFont('helvetica', 'bold'); + doc.text('Fournisseur de Document:', 120, 30); + doc.setFont('helvetica', 'normal'); + doc.text(`${certificateData.customer.firstName} ${certificateData.customer.lastName}`, 120, 37); + doc.text(certificateData.customer.email, 120, 44); + doc.text(certificateData.customer.postalAddress, 120, 51); + + // Centered Title + doc.setFontSize(20); + doc.setFont('helvetica', 'bold'); + doc.text('Certificat de Validation de Document', 105, 80, { align: 'center' }); + + // Add a line separator + doc.setLineWidth(0.5); + doc.line(20, 90, 190, 90); + + // Reset font for content + doc.setFontSize(12); + doc.setFont('helvetica', 'normal'); + + let yPosition = 110; + + // Document Information Section + doc.setFont('helvetica', 'bold'); + doc.text('Informations du Document', 20, yPosition); + yPosition += 10; + + doc.setFont('helvetica', 'normal'); + doc.text(`UID du Document: ${certificateData.metadata.documentUid}`, 20, yPosition); + yPosition += 7; + doc.text(`Type de Document: ${certificateData.metadata.documentType}`, 20, yPosition); + yPosition += 7; + doc.text(`UID du Dossier: ${certificateData.folderUid}`, 20, yPosition); + yPosition += 7; + doc.text(`Créé le: ${certificateData.metadata.createdAt.toLocaleDateString('fr-FR')}`, 20, yPosition); + yPosition += 7; + doc.text(`Nom du fichier: ${certificateData.metadata.fileName}`, 20, yPosition); + yPosition += 7; + doc.text(`ID de transaction: ${certificateData.metadata.commitmentId}`, 20, yPosition); + yPosition += 7; + doc.text(`Dernière modification: ${certificateData.metadata.updatedAt.toLocaleDateString('fr-FR')}`, 20, yPosition); + yPosition += 7; + doc.text(`Statut: ${certificateData.metadata.isDeleted ? 'Supprimé' : 'Actif'}`, 20, yPosition); + yPosition += 15; + + // Document Hash Section + doc.setFont('helvetica', 'bold'); + doc.text('Intégrité du Document', 20, yPosition); + yPosition += 10; + + doc.setFont('helvetica', 'normal'); + doc.text(`Hash SHA256 du Document:`, 20, yPosition); + yPosition += 7; + + // Split hash into multiple lines if needed + const hash = certificateData.documentHash; + const maxWidth = 170; // Available width for text + const hashLines = this.splitTextToFit(doc, hash, maxWidth); + + for (const line of hashLines) { + doc.text(line, 25, yPosition); + yPosition += 7; + } + yPosition += 8; // Extra space after hash + + // Footer + const pageCount = doc.getNumberOfPages(); + for (let i = 1; i <= pageCount; i++) { + doc.setPage(i); + doc.setFontSize(10); + doc.setFont('helvetica', 'italic'); + doc.text(`Page ${i} sur ${pageCount}`, 105, 280, { align: 'center' }); + doc.text(`Généré le ${new Date().toLocaleString('fr-FR')}`, 105, 285, { align: 'center' }); + } + + return doc.output('blob'); + } catch (error) { + console.error('Error generating certificate:', error); + throw new Error('Failed to generate certificate'); + } + } + + /** + * Split text to fit within a specified width + * @param doc - jsPDF document instance + * @param text - Text to split + * @param maxWidth - Maximum width in points + * @returns Array of text lines + */ + private splitTextToFit(doc: jsPDF, text: string, maxWidth: number): string[] { + const lines: string[] = []; + let currentLine = ''; + + // Split by words first + const words = text.split(''); + + for (const char of words) { + const testLine = currentLine + char; + const testWidth = doc.getTextWidth(testLine); + + if (testWidth <= maxWidth) { + currentLine = testLine; + } else { + if (currentLine) { + lines.push(currentLine); + currentLine = char; + } else { + // If even a single character is too wide, force a break + lines.push(char); + } + } + } + + if (currentLine) { + lines.push(currentLine); + } + + return lines; + } + + /** + * Download a certificate PDF + * @param certificateData - Data needed for the certificate + * @param filename - Optional filename for the download + */ + public async downloadCertificate(certificateData: CertificateData, filename?: string): Promise { + try { + const pdfBlob = await this.generateCertificate(certificateData); + const defaultFilename = `certificate_${certificateData.metadata.documentUid}_${new Date().toISOString().split('T')[0]}.pdf`; + saveAs(pdfBlob, filename || defaultFilename); + } catch (error) { + console.error('Error downloading certificate:', error); + throw error; + } + } + + /** + * Generate certificate for notary documents + * @param certificateData - Data needed for the certificate + * @returns Promise - The generated PDF as a blob + */ + public async generateNotaryCertificate(certificateData: CertificateData): Promise { + try { + const doc = new jsPDF(); + + // Notary Information Section (Top Left) + doc.setFontSize(12); + doc.setFont('helvetica', 'bold'); + doc.text('Notaire Validateur:', 20, 30); + doc.setFont('helvetica', 'normal'); + doc.text(certificateData.notary.name, 20, 37); + + // Customer Information Section (Top Right) + doc.setFont('helvetica', 'bold'); + doc.text('Fournisseur de Document:', 120, 30); + doc.setFont('helvetica', 'normal'); + doc.text(`${certificateData.customer.firstName} ${certificateData.customer.lastName}`, 120, 37); + doc.text(certificateData.customer.email, 120, 44); + doc.text(certificateData.customer.postalAddress, 120, 51); + + // Centered Title + doc.setFontSize(20); + doc.setFont('helvetica', 'bold'); + doc.text('Certificat Notarial de Validation de Document', 105, 80, { align: 'center' }); + + // Add a line separator + doc.setLineWidth(0.5); + doc.line(20, 90, 190, 90); + + // Reset font for content + doc.setFontSize(12); + doc.setFont('helvetica', 'normal'); + + let yPosition = 110; + + // Document Information Section + doc.setFont('helvetica', 'bold'); + doc.text('Informations du Document', 20, yPosition); + yPosition += 10; + + doc.setFont('helvetica', 'normal'); + doc.text(`Identifiant du Document: ${certificateData.metadata.documentUid}`, 20, yPosition); + yPosition += 7; + doc.text(`Type de Document: ${certificateData.metadata.documentType}`, 20, yPosition); + yPosition += 7; + doc.text(`Numéro de dossier: ${certificateData.folderUid}`, 20, yPosition); + yPosition += 7; + doc.text(`Créé le: ${certificateData.metadata.createdAt.toLocaleDateString('fr-FR')}`, 20, yPosition); + yPosition += 7; + doc.text(`Nom du fichier: ${certificateData.metadata.fileName}`, 20, yPosition); + yPosition += 7; + doc.text(`ID de transaction: ${certificateData.metadata.commitmentId}`, 20, yPosition); + yPosition += 7; + doc.text(`Dernière modification: ${certificateData.metadata.updatedAt.toLocaleDateString('fr-FR')}`, 20, yPosition); + yPosition += 7; + doc.text(`Statut: ${certificateData.metadata.isDeleted ? 'Supprimé' : 'Actif'}`, 20, yPosition); + yPosition += 15; + + // Document Hash Section + doc.setFont('helvetica', 'bold'); + doc.text('Intégrité du Document', 20, yPosition); + yPosition += 10; + + doc.setFont('helvetica', 'normal'); + doc.text(`Hash SHA256 du Document:`, 20, yPosition); + yPosition += 7; + + // Split hash into multiple lines if needed + const hash = certificateData.documentHash; + const maxWidth = 170; // Available width for text + const hashLines = this.splitTextToFit(doc, hash, maxWidth); + + for (const line of hashLines) { + doc.text(line, 25, yPosition); + yPosition += 7; + } + yPosition += 8; // Extra space after hash + + // Footer + const pageCount = doc.getNumberOfPages(); + for (let i = 1; i <= pageCount; i++) { + doc.setPage(i); + doc.setFontSize(10); + doc.setFont('helvetica', 'italic'); + doc.text(`Page ${i} sur ${pageCount}`, 105, 280, { align: 'center' }); + doc.text(`Certificat Notarial - Généré le ${new Date().toLocaleString('fr-FR')}`, 105, 285, { align: 'center' }); + } + + return doc.output('blob'); + } catch (error) { + console.error('Error generating notary certificate:', error); + throw new Error('Failed to generate notary certificate'); + } + } + + /** + * Download a notary certificate PDF + * @param certificateData - Data needed for the certificate + * @param filename - Optional filename for the download + */ + public async downloadNotaryCertificate(certificateData: CertificateData, filename?: string): Promise { + try { + const pdfBlob = await this.generateNotaryCertificate(certificateData); + const defaultFilename = `notary_certificate_${certificateData.metadata.documentUid}_${new Date().toISOString().split('T')[0]}.pdf`; + saveAs(pdfBlob, filename || defaultFilename); + } catch (error) { + console.error('Error downloading notary certificate:', error); + throw error; + } + } +} \ No newline at end of file diff --git a/src/sdk/MessageBus.ts b/src/sdk/MessageBus.ts index f97e2755..e578060d 100644 --- a/src/sdk/MessageBus.ts +++ b/src/sdk/MessageBus.ts @@ -113,61 +113,71 @@ export default class MessageBus { const publicDataDecoded: { [key: string]: any } = {}; - for (let stateId = 0; stateId < process.states.length - 1; stateId++) { - const state = process.states[stateId]; - if (!state) { - continue; - } + // We only care about the public data as they are in the last commited state + const processTip = process.states[process.states.length - 1].commited_in; + const lastCommitedState = process.states.findLast((state: any) => state.commited_in !== processTip); - const publicDataEncoded = state.public_data; - if (!publicDataEncoded) { - continue; - } + if (!lastCommitedState) { + continue; + } - for (const key of Object.keys(publicDataEncoded)) { - publicDataDecoded[key] = await this.getPublicData(publicDataEncoded[key]); - } + const publicDataEncoded = lastCommitedState.public_data; + if (!publicDataEncoded) { + continue; + } + + for (const key of Object.keys(publicDataEncoded)) { + publicDataDecoded[key] = await this.getPublicData(publicDataEncoded[key]); } if (!(publicDataDecoded['uid'] && publicDataDecoded['uid'] === uid && publicDataDecoded['utype'] && publicDataDecoded['utype'] === 'file')) { continue; } + // We take the file in it's latest commited state let file: any; - for (let stateId = 0; stateId < process.states.length - 1; stateId++) { - const lastState = process.states[stateId]; - if (!lastState) { + // Which is the last state that updated file_blob? + const lastUpdatedFileState = process.states.findLast((state: any) => state.pcd_commitment['file_blob']); + + if (!lastUpdatedFileState) { + continue; + } + + try { + const processData = await this.getData(processId, lastUpdatedFileState.state_id); + const isEmpty = Object.keys(processData).length === 0; + if (isEmpty) { continue; } - const lastStateId = lastState.state_id; - if (!lastStateId) { + const publicDataEncoded = lastUpdatedFileState.public_data; + if (!publicDataEncoded) { continue; } - - try { - const processData = await this.getData(processId, lastStateId); - const isEmpty = Object.keys(processData).length === 0; - if (isEmpty) { - continue; - } - - if (!file) { - file = { - processId, - lastStateId, - processData, - }; - } else { - for (const key of Object.keys(processData)) { - file.processData[key] = processData[key]; - } - file.lastStateId = lastStateId; - } - } catch (error) { - console.error(error); + const publicDataDecoded: { [key: string]: any } = {}; + for (const key of Object.keys(publicDataEncoded)) { + publicDataDecoded[key] = await this.getPublicData(publicDataEncoded[key]); } + + if (!file) { + file = { + processId, + lastUpdatedFileState, + processData, + publicDataDecoded, + }; + } else { + for (const key of Object.keys(processData)) { + file.processData[key] = processData[key]; + } + file.lastUpdatedFileState = lastUpdatedFileState; + for (const key of Object.keys(publicDataDecoded)) { + file.publicDataDecoded[key] = publicDataDecoded[key]; + } + } + } catch (error) { + console.error(error); } files.push(file); @@ -647,7 +657,7 @@ export default class MessageBus { console.error('[MessageBus] sendMessage: iframe not found'); return; } - console.log('[MessageBus] sendMessage:', message); + // console.log('[MessageBus] sendMessage:', message); iframe.contentWindow?.postMessage(message, targetOrigin); } @@ -684,7 +694,7 @@ export default class MessageBus { } const message = event.data; - console.log('[MessageBus] handleMessage:', message); + // console.log('[MessageBus] handleMessage:', message); switch (message.type) { case 'LISTENING':