From 6e176a32e5f4b603c6e0a461591f9396126b4449 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 11 Aug 2025 14:39:45 +0200 Subject: [PATCH] Add all certificates download button --- .../InformationSection/index.tsx | 23 ++- .../Folder/FolderInformation/index.tsx | 123 ++++++++++++ src/front/Services/PdfService/index.ts | 189 ++++++++++++++++++ 3 files changed, 332 insertions(+), 3 deletions(-) diff --git a/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/index.tsx index ba604883..3c98f0e7 100644 --- a/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/index.tsx +++ b/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/index.tsx @@ -5,7 +5,7 @@ import { IItem } from "@Front/Components/DesignSystem/Menu/MenuItem"; import Tag, { ETagColor } from "@Front/Components/DesignSystem/Tag"; import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; import Module from "@Front/Config/Module"; -import { ArchiveBoxIcon, EllipsisHorizontalIcon, PaperAirplaneIcon, PencilSquareIcon, ShieldCheckIcon, UsersIcon } from "@heroicons/react/24/outline"; +import { ArchiveBoxIcon, DocumentTextIcon, EllipsisHorizontalIcon, PaperAirplaneIcon, PencilSquareIcon, ShieldCheckIcon, UsersIcon } from "@heroicons/react/24/outline"; import classNames from "classnames"; import { OfficeFolder } from "le-coffre-resources/dist/Notary"; import Link from "next/link"; @@ -21,10 +21,11 @@ type IProps = { onArchive: () => void; anchorStatus: AnchorStatus; isArchived: boolean; + onDownloadAllCertificates: () => void; }; export default function InformationSection(props: IProps) { - const { folder, progress, onArchive, anchorStatus, isArchived } = props; + const { folder, progress, onArchive, anchorStatus, isArchived, onDownloadAllCertificates } = props; const router = useRouter(); const menuItemsDekstop = useMemo(() => { @@ -56,6 +57,13 @@ export default function InformationSection(props: IProps) { .modules.pages.Folder.pages.VerifyDocuments.props.path.replace("[folderUid]", folder?.uid ?? ""); router.push(verifyPath); }, + hasSeparator: true, + }; + + const downloadAllCertificatesElement = { + icon: , + text: "Télécharger tous les certificats", + onClick: () => onDownloadAllCertificates(), hasSeparator: false, }; @@ -67,6 +75,7 @@ export default function InformationSection(props: IProps) { // Add verify document option elements.push(verifyDocumentElement); + elements.push(downloadAllCertificatesElement); return elements; }, [anchorStatus, folder?.uid, router]); @@ -103,6 +112,13 @@ export default function InformationSection(props: IProps) { hasSeparator: true, }; + const downloadAllCertificatesElement = { + icon: , + text: "Télécharger tous les certificats", + onClick: () => onDownloadAllCertificates(), + hasSeparator: false, + }; + // If the folder is not anchored, we can modify the collaborators and the informations if (anchorStatus === AnchorStatus.NOT_ANCHORED) { elements.push(modifyCollaboratorsElement); @@ -111,6 +127,7 @@ export default function InformationSection(props: IProps) { // Add verify document option elements.push(verifyDocumentElement); + elements.push(downloadAllCertificatesElement); elements.push({ icon: , @@ -131,7 +148,7 @@ export default function InformationSection(props: IProps) { } return elements; - }, [anchorStatus, folder?.uid, isArchived, onArchive, router]); + }, [anchorStatus, folder?.uid, isArchived, onArchive, onDownloadAllCertificates, router]); return (
diff --git a/src/front/Components/Layouts/Folder/FolderInformation/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/index.tsx index 40092017..b18eab68 100644 --- a/src/front/Components/Layouts/Folder/FolderInformation/index.tsx +++ b/src/front/Components/Layouts/Folder/FolderInformation/index.tsx @@ -21,6 +21,13 @@ import NoClientView from "./NoClientView"; import AnchoringProcessingInfo from "./elements/AnchoringProcessingInfo"; import FolderService from "src/common/Api/LeCoffreApi/sdk/FolderService"; +import DocumentService from "src/common/Api/LeCoffreApi/sdk/DocumentService"; +import FileService from "src/common/Api/LeCoffreApi/sdk/FileService"; +import LoaderService from "src/common/Api/LeCoffreApi/sdk/Loader/LoaderService"; +import { ToasterService } from "@Front/Components/DesignSystem/Toaster"; +import PdfService from "@Front/Services/PdfService"; +import MessageBus from "src/sdk/MessageBus"; +import { saveAs } from "file-saver"; import EFolderStatus from "le-coffre-resources/dist/Customer/EFolderStatus"; export enum AnchorStatus { @@ -145,6 +152,121 @@ export default function FolderInformation(props: IProps) { archiveModal.open(); }, [anchorStatus, archiveModal, requireAnchoringModal]); + const onDownloadAllCertificates = useCallback(async () => { + if (!folder?.uid) return; + + try { + LoaderService.getInstance().show(); + + // Get all documents for this folder + const allDocuments = await DocumentService.getDocuments(); + const folderDocuments = allDocuments + .map((process: any) => process.processData) + .filter((doc: any) => + doc.folder?.uid === folder.uid && + doc.document_status === EDocumentStatus.VALIDATED + ); + + if (folderDocuments.length === 0) { + ToasterService.getInstance().warning({ + title: "Aucun certificat", + description: "Aucun document validé trouvé pour ce dossier." + }); + LoaderService.getInstance().hide(); + return; + } + + // Generate certificates for all validated documents + const certificates: any[] = []; + + for (const doc of folderDocuments) { + try { + // Get customer info + const customer = doc.depositor || doc.customer; + + const certificateData: any = { + customer: { + firstName: customer?.first_name || customer?.firstName || "N/A", + lastName: customer?.last_name || customer?.lastName || "N/A", + postalAddress: customer?.postal_address || customer?.address || customer?.postalAddress || "N/A", + email: customer?.email || "N/A" + }, + notary: { + name: "N/A" + }, + folderUid: folder.uid, + documentHash: "N/A", + metadata: { + fileName: "N/A", + isDeleted: false, + updatedAt: new Date(), + commitmentId: "N/A", + createdAt: new Date(), + documentUid: "N/A", + documentType: "N/A", + merkleProof: "N/A" + } + }; + + // Process files for this document + if (doc.files && doc.files.length > 0) { + for (const file of doc.files) { + const fileProcess = await FileService.getFileByUid(file.uid); + + if (fileProcess.lastUpdatedFileState?.pcd_commitment?.file_blob) { + const hash = fileProcess.lastUpdatedFileState.pcd_commitment.file_blob; + certificateData.documentHash = hash; + + const proof = await MessageBus.getInstance().generateMerkleProof( + fileProcess.lastUpdatedFileState, + 'file_blob' + ); + + const metadata: any = { + fileName: fileProcess.processData.file_name, + isDeleted: false, + updatedAt: new Date(fileProcess.processData.updated_at), + commitmentId: fileProcess.lastUpdatedFileState.commited_in, + createdAt: new Date(fileProcess.processData.created_at), + documentUid: doc.uid, + documentType: doc.document_type?.name || "N/A", + merkleProof: proof + }; + certificateData.metadata = metadata; + break; // Use first file for now + } + } + } + + certificates.push(certificateData); + } catch (error) { + console.error(`Error processing document ${doc.uid}:`, error); + } + } + + // Generate combined PDF + const combinedPdf = await PdfService.getInstance().generateCombinedCertificates(certificates, folder); + + // Download the combined PDF + const filename = `certificats_${folder.folder_number || folder.uid}_${new Date().toISOString().split('T')[0]}.pdf`; + saveAs(combinedPdf, filename); + + ToasterService.getInstance().success({ + title: "Succès !", + description: `${certificates.length} certificat(s) téléchargé(s) avec succès.` + }); + + } catch (error) { + console.error('Error downloading all certificates:', error); + ToasterService.getInstance().error({ + title: "Erreur", + description: "Une erreur est survenue lors du téléchargement des certificats." + }); + } finally { + LoaderService.getInstance().hide(); + } + }, [folder?.uid]); + return ( {!isLoading && ( @@ -155,6 +277,7 @@ export default function FolderInformation(props: IProps) { onArchive={onArchive} anchorStatus={anchorStatus} isArchived={isArchived} + onDownloadAllCertificates={onDownloadAllCertificates} /> {progress === 100 && /*anchorStatus === AnchorStatus.NOT_ANCHORED*/ folder.status !== EFolderStatus.ARCHIVED && ( diff --git a/src/front/Services/PdfService/index.ts b/src/front/Services/PdfService/index.ts index fcfa0eb3..927e455f 100644 --- a/src/front/Services/PdfService/index.ts +++ b/src/front/Services/PdfService/index.ts @@ -335,4 +335,193 @@ export default class PdfService { fileReader.readAsArrayBuffer(certificateFile); }); } + + /** + * Generate a combined PDF with multiple certificates + * @param certificates - Array of certificate data + * @param folder - Folder information + * @returns Promise - Combined PDF as blob + */ + public async generateCombinedCertificates(certificates: CertificateData[], folder: any): Promise { + try { + // Import pdf-lib dynamically to avoid SSR issues + const { PDFDocument, rgb, StandardFonts } = await import('pdf-lib'); + + // Create a new PDF document + const pdfDoc = await PDFDocument.create(); + const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica); + const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold); + + // Add a cover page + const coverPage = pdfDoc.addPage([595.28, 841.89]); // A4 size + const { width, height } = coverPage.getSize(); + + // Cover page title + coverPage.drawText('Certificats de Validation', { + x: 50, + y: height - 100, + size: 24, + font: helveticaBold, + color: rgb(0, 0, 0), + }); + + // Folder information + coverPage.drawText(`Dossier: ${folder.folder_number || folder.uid}`, { + x: 50, + y: height - 150, + size: 16, + font: helveticaFont, + color: rgb(0, 0, 0), + }); + + coverPage.drawText(`Nom: ${folder.name || 'N/A'}`, { + x: 50, + y: height - 180, + size: 16, + font: helveticaFont, + color: rgb(0, 0, 0), + }); + + coverPage.drawText(`Date de génération: ${new Date().toLocaleDateString('fr-FR')}`, { + x: 50, + y: height - 210, + size: 16, + font: helveticaFont, + color: rgb(0, 0, 0), + }); + + coverPage.drawText(`Nombre de certificats: ${certificates.length}`, { + x: 50, + y: height - 240, + size: 16, + font: helveticaFont, + color: rgb(0, 0, 0), + }); + + // Add each certificate as a separate page + for (let i = 0; i < certificates.length; i++) { + const certificate = certificates[i]; + if (!certificate) continue; + + const page = pdfDoc.addPage([595.28, 841.89]); // A4 size + const { width: pageWidth, height: pageHeight } = page.getSize(); + + // Certificate title + page.drawText(`Certificat ${i + 1}`, { + x: 50, + y: pageHeight - 50, + size: 20, + font: helveticaBold, + color: rgb(0, 0, 0), + }); + + // Customer information + page.drawText('Informations Client:', { + x: 50, + y: pageHeight - 100, + size: 16, + font: helveticaBold, + color: rgb(0, 0, 0), + }); + + page.drawText(`Nom: ${certificate.customer.firstName} ${certificate.customer.lastName}`, { + x: 50, + y: pageHeight - 130, + size: 12, + font: helveticaFont, + color: rgb(0, 0, 0), + }); + + page.drawText(`Adresse: ${certificate.customer.postalAddress}`, { + x: 50, + y: pageHeight - 150, + size: 12, + font: helveticaFont, + color: rgb(0, 0, 0), + }); + + page.drawText(`Email: ${certificate.customer.email}`, { + x: 50, + y: pageHeight - 170, + size: 12, + font: helveticaFont, + color: rgb(0, 0, 0), + }); + + // Document information + page.drawText('Informations Document:', { + x: 50, + y: pageHeight - 200, + size: 16, + font: helveticaBold, + color: rgb(0, 0, 0), + }); + + page.drawText(`Type: ${certificate.metadata.documentType}`, { + x: 50, + y: pageHeight - 230, + size: 12, + font: helveticaFont, + color: rgb(0, 0, 0), + }); + + page.drawText(`Fichier: ${certificate.metadata.fileName}`, { + x: 50, + y: pageHeight - 250, + size: 12, + font: helveticaFont, + color: rgb(0, 0, 0), + }); + + page.drawText(`Hash: ${certificate.documentHash}`, { + x: 50, + y: pageHeight - 270, + size: 12, + font: helveticaFont, + color: rgb(0, 0, 0), + }); + + page.drawText(`Commitment ID: ${certificate.metadata.commitmentId}`, { + x: 50, + y: pageHeight - 290, + size: 12, + font: helveticaFont, + color: rgb(0, 0, 0), + }); + + page.drawText(`Date de création: ${certificate.metadata.createdAt.toLocaleDateString('fr-FR')}`, { + x: 50, + y: pageHeight - 310, + size: 12, + font: helveticaFont, + color: rgb(0, 0, 0), + }); + + page.drawText(`Date de mise à jour: ${certificate.metadata.updatedAt.toLocaleDateString('fr-FR')}`, { + x: 50, + y: pageHeight - 330, + size: 12, + font: helveticaFont, + color: rgb(0, 0, 0), + }); + + // Add page number + page.drawText(`Page ${i + 2}`, { + x: pageWidth - 80, + y: 30, + size: 10, + font: helveticaFont, + color: rgb(0.5, 0.5, 0.5), + }); + } + + // Save the PDF + const pdfBytes = await pdfDoc.save(); + return new Blob([pdfBytes], { type: 'application/pdf' }); + + } catch (error) { + console.error('Error generating combined certificates:', error); + throw error; + } + } }