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 9897aed5..9cad8c2c 100644
--- a/src/front/Components/Layouts/Folder/AskDocuments/index.tsx
+++ b/src/front/Components/Layouts/Folder/AskDocuments/index.tsx
@@ -17,6 +17,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 { DocumentData } from "../FolderInformation/ClientView/DocumentTables/types";
export default function AskDocuments() {
const router = useRouter();
@@ -61,17 +62,21 @@ export default function AskDocuments() {
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 f38d5e35..aa04bddd 100644
--- a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/index.tsx
+++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/index.tsx
@@ -8,7 +8,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 { Document } from "le-coffre-resources/dist/Customer";
import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document";
@@ -24,6 +24,8 @@ import DeleteSentDocumentModal from "./DeleteSentDocumentModal";
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 FolderService from "src/common/Api/LeCoffreApi/sdk/FolderService";
+import PdfService, { CertificateData, Metadata } from "@Front/Services/PdfService";
type IProps = {
customerUid: string;
@@ -64,6 +66,12 @@ export default function DocumentTables(props: IProps) {
// FilterBy folder.uid & depositor.uid
documents = documents.filter((document: any) => document.folder.uid === folderUid && 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;
@@ -154,6 +162,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
@@ -233,8 +306,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 {
@@ -266,13 +339,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':