Compare commits

...

3 Commits

Author SHA1 Message Date
Sosthene
d85d90bd16 Add all certificates download button 2025-08-11 14:39:45 +02:00
Sosthene
a6e08eaea0 Notary added files watermarked 2025-08-11 13:36:14 +02:00
Sosthene
649d5b1e18 French text for watermark + timestamp on images too 2025-08-11 11:43:27 +02:00
7 changed files with 468 additions and 116 deletions

View File

@ -21,6 +21,7 @@ import DocumentService from "src/common/Api/LeCoffreApi/sdk/DocumentService";
import FileService from "src/common/Api/LeCoffreApi/sdk/FileService"; import FileService from "src/common/Api/LeCoffreApi/sdk/FileService";
import CustomerService from "src/common/Api/LeCoffreApi/sdk/CustomerService"; import CustomerService from "src/common/Api/LeCoffreApi/sdk/CustomerService";
import FolderService from "src/common/Api/LeCoffreApi/sdk/FolderService"; import FolderService from "src/common/Api/LeCoffreApi/sdk/FolderService";
import WatermarkService from "@Front/Services/WatermarkService";
type IProps = { type IProps = {
onChange?: (files: File[]) => void; onChange?: (files: File[]) => void;
@ -252,6 +253,8 @@ export default class DepositOtherDocument extends React.Component<IProps, IState
for (let i = 0; i < filesArray.length; i++) { for (let i = 0; i < filesArray.length; i++) {
const file = filesArray[i]!.file; const file = filesArray[i]!.file;
await new Promise<void>((resolve: () => void) => { await new Promise<void>((resolve: () => void) => {
// Add watermark to the file before processing
WatermarkService.getInstance().addWatermark(file).then(async (watermarkedFile) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (event) => { reader.onload = (event) => {
if (event.target?.result) { if (event.target?.result) {
@ -264,7 +267,7 @@ export default class DepositOtherDocument extends React.Component<IProps, IState
const uint8Array: Uint8Array = new Uint8Array(arrayBuffer); const uint8Array: Uint8Array = new Uint8Array(arrayBuffer);
const fileBlob: any = { const fileBlob: any = {
type: file.type, type: watermarkedFile.type,
data: uint8Array data: uint8Array
}; };
@ -295,7 +298,8 @@ export default class DepositOtherDocument extends React.Component<IProps, IState
}); });
} }
}; };
reader.readAsArrayBuffer(file); reader.readAsArrayBuffer(watermarkedFile);
});
}); });
} }

View File

@ -5,7 +5,7 @@ import { IItem } from "@Front/Components/DesignSystem/Menu/MenuItem";
import Tag, { ETagColor } from "@Front/Components/DesignSystem/Tag"; import Tag, { ETagColor } from "@Front/Components/DesignSystem/Tag";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import Module from "@Front/Config/Module"; 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 classNames from "classnames";
import { OfficeFolder } from "le-coffre-resources/dist/Notary"; import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import Link from "next/link"; import Link from "next/link";
@ -21,10 +21,11 @@ type IProps = {
onArchive: () => void; onArchive: () => void;
anchorStatus: AnchorStatus; anchorStatus: AnchorStatus;
isArchived: boolean; isArchived: boolean;
onDownloadAllCertificates: () => void;
}; };
export default function InformationSection(props: IProps) { 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 router = useRouter();
const menuItemsDekstop = useMemo(() => { const menuItemsDekstop = useMemo(() => {
@ -56,6 +57,13 @@ export default function InformationSection(props: IProps) {
.modules.pages.Folder.pages.VerifyDocuments.props.path.replace("[folderUid]", folder?.uid ?? ""); .modules.pages.Folder.pages.VerifyDocuments.props.path.replace("[folderUid]", folder?.uid ?? "");
router.push(verifyPath); router.push(verifyPath);
}, },
hasSeparator: true,
};
const downloadAllCertificatesElement = {
icon: <DocumentTextIcon />,
text: "Télécharger tous les certificats",
onClick: () => onDownloadAllCertificates(),
hasSeparator: false, hasSeparator: false,
}; };
@ -67,6 +75,7 @@ export default function InformationSection(props: IProps) {
// Add verify document option // Add verify document option
elements.push(verifyDocumentElement); elements.push(verifyDocumentElement);
elements.push(downloadAllCertificatesElement);
return elements; return elements;
}, [anchorStatus, folder?.uid, router]); }, [anchorStatus, folder?.uid, router]);
@ -103,6 +112,13 @@ export default function InformationSection(props: IProps) {
hasSeparator: true, hasSeparator: true,
}; };
const downloadAllCertificatesElement = {
icon: <DocumentTextIcon />,
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 the folder is not anchored, we can modify the collaborators and the informations
if (anchorStatus === AnchorStatus.NOT_ANCHORED) { if (anchorStatus === AnchorStatus.NOT_ANCHORED) {
elements.push(modifyCollaboratorsElement); elements.push(modifyCollaboratorsElement);
@ -111,6 +127,7 @@ export default function InformationSection(props: IProps) {
// Add verify document option // Add verify document option
elements.push(verifyDocumentElement); elements.push(verifyDocumentElement);
elements.push(downloadAllCertificatesElement);
elements.push({ elements.push({
icon: <PaperAirplaneIcon />, icon: <PaperAirplaneIcon />,
@ -131,7 +148,7 @@ export default function InformationSection(props: IProps) {
} }
return elements; return elements;
}, [anchorStatus, folder?.uid, isArchived, onArchive, router]); }, [anchorStatus, folder?.uid, isArchived, onArchive, onDownloadAllCertificates, router]);
return ( return (
<section className={classes["root"]}> <section className={classes["root"]}>
<div className={classes["info-box1"]}> <div className={classes["info-box1"]}>

View File

@ -21,6 +21,13 @@ import NoClientView from "./NoClientView";
import AnchoringProcessingInfo from "./elements/AnchoringProcessingInfo"; import AnchoringProcessingInfo from "./elements/AnchoringProcessingInfo";
import FolderService from "src/common/Api/LeCoffreApi/sdk/FolderService"; 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"; import EFolderStatus from "le-coffre-resources/dist/Customer/EFolderStatus";
export enum AnchorStatus { export enum AnchorStatus {
@ -145,6 +152,121 @@ export default function FolderInformation(props: IProps) {
archiveModal.open(); archiveModal.open();
}, [anchorStatus, archiveModal, requireAnchoringModal]); }, [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 ( return (
<DefaultNotaryDashboard title={"Dossier"} isArchived={isArchived} mobileBackText="Retour aux dossiers"> <DefaultNotaryDashboard title={"Dossier"} isArchived={isArchived} mobileBackText="Retour aux dossiers">
{!isLoading && ( {!isLoading && (
@ -155,6 +277,7 @@ export default function FolderInformation(props: IProps) {
onArchive={onArchive} onArchive={onArchive}
anchorStatus={anchorStatus} anchorStatus={anchorStatus}
isArchived={isArchived} isArchived={isArchived}
onDownloadAllCertificates={onDownloadAllCertificates}
/> />
{progress === 100 && /*anchorStatus === AnchorStatus.NOT_ANCHORED*/ folder.status !== EFolderStatus.ARCHIVED && ( {progress === 100 && /*anchorStatus === AnchorStatus.NOT_ANCHORED*/ folder.status !== EFolderStatus.ARCHIVED && (
<AnchoringAlertInfo onAnchor={anchoringModal.open} /> <AnchoringAlertInfo onAnchor={anchoringModal.open} />

View File

@ -23,6 +23,7 @@ import FolderService from "src/common/Api/LeCoffreApi/sdk/FolderService";
import DocumentService from "src/common/Api/LeCoffreApi/sdk/DocumentService"; import DocumentService from "src/common/Api/LeCoffreApi/sdk/DocumentService";
import FileService from "src/common/Api/LeCoffreApi/sdk/FileService"; import FileService from "src/common/Api/LeCoffreApi/sdk/FileService";
import CustomerService from "src/common/Api/LeCoffreApi/sdk/CustomerService"; import CustomerService from "src/common/Api/LeCoffreApi/sdk/CustomerService";
import WatermarkService from "@Front/Services/WatermarkService";
enum EClientSelection { enum EClientSelection {
ALL_CLIENTS = "all_clients", ALL_CLIENTS = "all_clients",
@ -77,6 +78,8 @@ export default function SendDocuments() {
for (const file of files) { for (const file of files) {
await new Promise<void>((resolve: () => void) => { await new Promise<void>((resolve: () => void) => {
// Add watermark to the file before processing
WatermarkService.getInstance().addWatermark(file).then(async (watermarkedFile) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (event) => { reader.onload = (event) => {
if (event.target?.result) { if (event.target?.result) {
@ -89,7 +92,7 @@ export default function SendDocuments() {
const uint8Array: Uint8Array = new Uint8Array(arrayBuffer); const uint8Array: Uint8Array = new Uint8Array(arrayBuffer);
const fileBlob: any = { const fileBlob: any = {
type: file.type, type: watermarkedFile.type,
data: uint8Array data: uint8Array
}; };
@ -123,7 +126,8 @@ export default function SendDocuments() {
}); });
} }
}; };
reader.readAsArrayBuffer(file); reader.readAsArrayBuffer(watermarkedFile);
});
}); });
} }
} }

View File

@ -11,6 +11,7 @@ import Loader from "@Front/Components/DesignSystem/Loader";
import LoaderService from "src/common/Api/LeCoffreApi/sdk/Loader/LoaderService"; import LoaderService from "src/common/Api/LeCoffreApi/sdk/Loader/LoaderService";
import OfficeRibService from "src/common/Api/LeCoffreApi/sdk/OfficeRibService"; import OfficeRibService from "src/common/Api/LeCoffreApi/sdk/OfficeRibService";
import WatermarkService from "@Front/Services/WatermarkService";
export default function Rib() { export default function Rib() {
const [documentList, setDocumentList] = useState<File[]>([]); const [documentList, setDocumentList] = useState<File[]>([]);
@ -60,6 +61,8 @@ export default function Rib() {
const file = documentList[0]!; const file = documentList[0]!;
LoaderService.getInstance().show(); LoaderService.getInstance().show();
// Add watermark to the file before processing
WatermarkService.getInstance().addWatermark(file).then(async (watermarkedFile) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (event) => { reader.onload = (event) => {
if (event.target?.result) { if (event.target?.result) {
@ -72,7 +75,7 @@ export default function Rib() {
const uint8Array: Uint8Array = new Uint8Array(arrayBuffer); const uint8Array: Uint8Array = new Uint8Array(arrayBuffer);
const fileBlob: any = { const fileBlob: any = {
type: file.type, type: watermarkedFile.type,
data: uint8Array data: uint8Array
}; };
@ -91,7 +94,8 @@ export default function Rib() {
}); });
} }
}; };
reader.readAsArrayBuffer(file); reader.readAsArrayBuffer(watermarkedFile);
});
} }
function openRibModal(): void { function openRibModal(): void {

View File

@ -335,4 +335,193 @@ export default class PdfService {
fileReader.readAsArrayBuffer(certificateFile); fileReader.readAsArrayBuffer(certificateFile);
}); });
} }
/**
* Generate a combined PDF with multiple certificates
* @param certificates - Array of certificate data
* @param folder - Folder information
* @returns Promise<Blob> - Combined PDF as blob
*/
public async generateCombinedCertificates(certificates: CertificateData[], folder: any): Promise<Blob> {
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;
}
}
} }

View File

@ -1,5 +1,6 @@
export default class WatermarkService { export default class WatermarkService {
private static instance: WatermarkService; private static instance: WatermarkService;
private watermarkText: string = 'Certifié par LeCoffre';
public static getInstance(): WatermarkService { public static getInstance(): WatermarkService {
if (!WatermarkService.instance) { if (!WatermarkService.instance) {
@ -142,18 +143,29 @@ export default class WatermarkService {
ctx.save(); ctx.save();
// Set watermark properties // Set watermark properties
ctx.fillStyle = 'rgba(128, 128, 128, 0.7)'; // Semi-transparent gray ctx.fillStyle = 'rgba(204, 51, 51, 0.7)'; // Semi-transparent pale red (matching PDF color)
ctx.font = '12px Arial'; ctx.font = '10px Arial';
ctx.textAlign = 'right'; ctx.textAlign = 'right';
ctx.textBaseline = 'bottom'; ctx.textBaseline = 'bottom';
// Position watermark in bottom-right corner // Position watermark in bottom-right corner with timestamp
const text = 'Processed by LeCoffre'; const dateTime = new Date().toLocaleString('fr-FR', {
const x = width - 10; // 10 pixels from right edge year: 'numeric',
const y = height - 10; // 10 pixels from bottom month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
// Add watermark text const lineHeight = 12; // Space between lines
ctx.fillText(text, x, y); const x = width - 20; // 20 pixels from right edge (matching PDF margins)
const y = 20; // 20 pixels from bottom (matching PDF margins)
// Add watermark text (second line - top)
ctx.fillText(this.watermarkText, x, y + lineHeight);
// Add date/time (first line - bottom)
ctx.fillText(dateTime, x, y);
// Restore state // Restore state
ctx.restore(); ctx.restore();
@ -168,7 +180,6 @@ export default class WatermarkService {
const { width, height } = page.getSize(); const { width, height } = page.getSize();
// Calculate watermark position (bottom-right corner) // Calculate watermark position (bottom-right corner)
const text = 'Processed by LeCoffre';
const dateTime = new Date().toLocaleString('fr-FR', { const dateTime = new Date().toLocaleString('fr-FR', {
year: 'numeric', year: 'numeric',
month: '2-digit', month: '2-digit',
@ -182,7 +193,7 @@ export default class WatermarkService {
// Calculate text widths (approximate - pdf-lib doesn't have a direct method for this) // Calculate text widths (approximate - pdf-lib doesn't have a direct method for this)
// Using a conservative estimate: ~6 points per character for 10pt font // Using a conservative estimate: ~6 points per character for 10pt font
const estimatedTextWidth1 = text.length * 6; const estimatedTextWidth1 = this.watermarkText.length * 6;
const estimatedTextWidth2 = dateTime.length * 6; const estimatedTextWidth2 = dateTime.length * 6;
const maxTextWidth = Math.max(estimatedTextWidth1, estimatedTextWidth2); const maxTextWidth = Math.max(estimatedTextWidth1, estimatedTextWidth2);
@ -191,7 +202,7 @@ export default class WatermarkService {
const y = 20; // 20 points from bottom const y = 20; // 20 points from bottom
// Add watermark text with transparency (first line) // Add watermark text with transparency (first line)
page.drawText(text, { page.drawText(this.watermarkText, {
x: x, x: x,
y: y + lineHeight, // Second line (top) y: y + lineHeight, // Second line (top)
size: fontSize, size: fontSize,