Compare commits

...

8 Commits

Author SHA1 Message Date
Sosthene
31ca61963a Get merkle proof when generating certificate 2025-07-01 21:21:58 +02:00
Sosthene
deca1c8eaf Add merkleProof to CertificateData 2025-07-01 21:21:58 +02:00
Sosthene
add4eb8862 Add generateMerkleProof 2025-07-01 21:21:58 +02:00
Sosthene
3925f3e9e5 Add hashDocument to MessageBus 2025-07-01 21:21:58 +02:00
Sosthene
20669e090e Use FileBlob and FileData everywhere 2025-07-01 21:21:58 +02:00
Sosthene
ca1b2d5c7d Add FileBlob and FileData 2025-07-01 21:21:58 +02:00
Sosthene
4532d59863 Add basic certificate 2025-07-01 21:21:57 +02:00
ca5a59c51a Fix somes issues 2025-07-01 17:35:23 +02:00
20 changed files with 740 additions and 105 deletions

View File

@ -2,6 +2,7 @@ import { v4 as uuidv4 } from 'uuid';
import MessageBus from 'src/sdk/MessageBus';
import User from 'src/sdk/User';
import { FileData } from '../../../../front/Api/Entities/types';
export default class FileService {
@ -9,7 +10,7 @@ export default class FileService {
private constructor() { }
public static createFile(fileData: any, validatorId: string): Promise<any> {
public static createFile(fileData: FileData, validatorId: string): Promise<any> {
const ownerId = User.getInstance().getPairingId()!;
const processData: any = {
@ -81,7 +82,7 @@ export default class FileService {
return this.messageBus.getFileByUid(uid);
}
public static updateFile(process: any, newData: any): Promise<void> {
public static updateFile(process: any, newData: Partial<FileData> & { isDeleted?: string }): Promise<void> {
return new Promise<void>((resolve: () => void, reject: (error: string) => void) => {
this.messageBus.updateProcess(process.processId, { updated_at: new Date().toISOString(), ...newData }, [], null).then((processUpdated: any) => {
const newStateId: string = processUpdated.diffs[0]?.state_id;

View File

@ -0,0 +1,2 @@
export * from './types';
export * from './rule';

View File

@ -0,0 +1,21 @@
/**
* FileBlob interface representing a file's binary data and metadata
* Used for file transmission and storage in the application
*/
export interface FileBlob {
/** MIME type of the file (e.g., "application/pdf", "image/jpeg") */
type: string;
/** Binary data of the file as Uint8Array */
data: Uint8Array;
}
/**
* FileData interface representing a complete file object with blob and metadata
* Used when creating or updating files in the system
*/
export interface FileData {
/** The file blob containing type and binary data */
file_blob: FileBlob;
/** The name of the file */
file_name: string;
}

View File

@ -213,9 +213,9 @@ export default function DragAndDrop(props: IProps) {
</div>
{documentFiles.length > 0 && (
<div className={classes["documents"]}>
{documentFiles.map((documentFile) => (
{documentFiles.map((documentFile, index) => (
<DocumentFileElement
key={documentFile.id}
key={documentFile.uid || `${documentFile.id}-${index}`}
isLoading={documentFile.isLoading}
file={documentFile.file}
onRemove={() => handleRemove(documentFile)}

View File

@ -9,6 +9,7 @@ import Confirm from "@Front/Components/DesignSystem/OldModal/Confirm";
import FileService from "src/common/Api/LeCoffreApi/sdk/FileService";
import DocumentService from "src/common/Api/LeCoffreApi/sdk/DocumentService";
import { FileBlob, FileData } from "@Front/Api/Entities/types";
type IProps = {
document: any;
@ -39,12 +40,12 @@ export default function DepositDocumentComponent(props: IProps) {
const arrayBuffer = event.target.result as ArrayBuffer;
const uint8Array = new Uint8Array(arrayBuffer);
const fileBlob: any = {
const fileBlob: FileBlob = {
type: file.type,
data: uint8Array
};
const fileData: any = {
const fileData: FileData = {
file_blob: fileBlob,
file_name: file.name
};
@ -82,9 +83,9 @@ export default function DepositDocumentComponent(props: IProps) {
(fileUid: string) => {
return new Promise<void>(
(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;

View File

@ -7,6 +7,7 @@ import { DocumentNotary } from "le-coffre-resources/dist/Notary";
import Image from "next/image";
import { NextRouter, useRouter } from "next/router";
import React from "react";
import { FileBlob } from "@Front/Api/Entities/types";
import BasePage from "../../Base";
import classes from "./classes.module.scss";
@ -25,7 +26,7 @@ type IState = {
isValidateModalVisible: boolean;
refuseText: string;
selectedFileIndex: number;
selectedFile: any;
selectedFile: { uid: string; file_name: string; file_blob: FileBlob } | null;
documentNotary: DocumentNotary | null;
fileBlob: Blob | null;
isLoading: boolean;
@ -132,7 +133,7 @@ class ViewDocumentsNotaryClass extends BasePage<IPropsClass, IState> {
{
documentNotary,
selectedFileIndex: 0,
selectedFile: documentNotary.files![0]!,
selectedFile: documentNotary.files![0] as any,
isLoading: false,
},
() => {

View File

@ -156,6 +156,7 @@ export default function AddClientToFolder(props: IProps) {
);
const loadCustomers = useCallback(async () => {
LoaderService.getInstance().show();
CustomerService.getCustomers().then(async (processes: any[]) => {
const availableCustomers: any[] = processes.map((process: any) => process.processData);
@ -176,6 +177,8 @@ export default function AddClientToFolder(props: IProps) {
setExistingCustomers(existingCustomers);
setIsLoaded(true);
setSelectedOption(selectedOption);
LoaderService.getInstance().hide();
});
}, [folderUid, getFolderPreSelectedCustomers]);

View File

@ -17,6 +17,8 @@ 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();
@ -58,26 +60,29 @@ export default function AskDocuments() {
) => {
try {
// TODO: review
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';
await DocumentService.createDocument(documentData, validatorId);
}
router.push(
Module.getInstance()
.get()
@ -127,11 +132,13 @@ export default function AskDocuments() {
const loadData = useCallback(async () => {
try {
LoaderService.getInstance().show();
FolderService.getFolderByUid(folderUid as string).then(async (process: any) => {
if (process) {
const folder: any = process.processData;
setFolder(folder);
setDocumentTypes(await getAvailableDocuments(folder));
LoaderService.getInstance().hide();
}
});
} catch (e) {

View File

@ -17,6 +17,7 @@ import Note from "le-coffre-resources/dist/Customer/Note";
import FolderService from "src/common/Api/LeCoffreApi/sdk/FolderService";
import CustomerService from "src/common/Api/LeCoffreApi/sdk/CustomerService";
import NoteService from "src/common/Api/LeCoffreApi/sdk/NoteService";
import LoaderService from "src/common/Api/LeCoffreApi/sdk/Loader/LoaderService";
type IProps = {};
@ -83,6 +84,8 @@ class CreateCustomerNoteClass extends BasePage<IPropsClass, IState> {
const customer = await Customers.getInstance().getByUid(this.props.customerUid);
*/
LoaderService.getInstance().show();
const folder: any = await FolderService.getFolderByUid(this.props.folderUid).then((process: any) => {
if (process) {
const folder: any = process.processData;
@ -97,6 +100,8 @@ class CreateCustomerNoteClass extends BasePage<IPropsClass, IState> {
}
});
LoaderService.getInstance().hide();
//get the note of the folder that has customer_uid = this.props.customer.uid
// const folderNote = folder.notes?.find((note) => note.customer?.uid === this.props.customerUid);
// this.setState({ note, folder: note.office_folder || null, customer: note.customer || null });
@ -121,6 +126,7 @@ class CreateCustomerNoteClass extends BasePage<IPropsClass, IState> {
};
const validatorId: string = '884cb36a346a79af8697559f16940141f068bdf1656f88fa0df0e9ecd7311fb8:0';
LoaderService.getInstance().show();
NoteService.createNote(noteData, validatorId).then(() => {
this.props.router.push(this.backwardPath);
});

View File

@ -4,6 +4,7 @@ import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography";
import React, { useCallback } from "react";
import DocumentService from "src/common/Api/LeCoffreApi/sdk/DocumentService";
import LoaderService from "src/common/Api/LeCoffreApi/sdk/Loader/LoaderService";
type IProps = {
documentUid: string;
@ -16,7 +17,8 @@ export default function DeleteAskedDocumentModal(props: IProps) {
const { isOpen, onClose, documentUid, onDeleteSuccess } = props;
const onDelete = useCallback(
() =>
() => {
LoaderService.getInstance().show();
new Promise<void>(
(resolve: () => void) => {
DocumentService.getDocumentByUid(documentUid).then((process: any) => {
@ -27,8 +29,10 @@ export default function DeleteAskedDocumentModal(props: IProps) {
})
.then(() => onDeleteSuccess(documentUid))
.then(() => ToasterService.getInstance().success({ title: "Succès !", description: "Le document a été supprimé avec succès." }))
.then(() => LoaderService.getInstance().hide())
.then(onClose)
.catch((error) => console.warn(error)),
.catch((error) => console.warn(error));
},
[documentUid, onClose, onDeleteSuccess],
);

View File

@ -1,9 +1,11 @@
import DocumentsNotary from "@Front/Api/LeCoffreApi/Notary/DocumentsNotary/DocumentsNotary";
import Modal from "@Front/Components/DesignSystem/Modal";
import { ToasterService } from "@Front/Components/DesignSystem/Toaster";
import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography";
import React, { useCallback } from "react";
import DocumentService from "src/common/Api/LeCoffreApi/sdk/DocumentService";
import LoaderService from "src/common/Api/LeCoffreApi/sdk/Loader/LoaderService";
type IProps = {
documentUid: string;
isOpen: boolean;
@ -15,13 +17,18 @@ export default function DeleteSentDocumentModal(props: IProps) {
const { isOpen, onClose, documentUid, onDeleteSuccess } = props;
const onDelete = useCallback(
() =>
DocumentsNotary.getInstance()
.delete(documentUid)
() => {
LoaderService.getInstance().show();
DocumentService.getDocumentByUid(documentUid).then((process: any) => {
if (process) {
DocumentService.updateDocument(process, { isDeleted: 'true' })
.then(() => onDeleteSuccess(documentUid))
.then(() => ToasterService.getInstance().success({ title: "Succès !", description: "Le document a été supprimé avec succès." }))
.then(onClose)
.catch((error) => console.warn(error)),
.then(() => LoaderService.getInstance().hide())
.then(onClose);
}
});
},
[documentUid, onClose, onDeleteSuccess],
);

View File

@ -1,8 +1,12 @@
import Modal from "@Front/Components/DesignSystem/Modal";
import React from "react";
import { FileBlob } from "@Front/Api/Entities/types";
type IProps = {
file: any;
file: {
uid: string;
file_blob: FileBlob;
};
url: string;
isOpen: boolean;
onClose?: () => void;

View File

@ -1,4 +1,3 @@
import DocumentsNotary from "@Front/Api/LeCoffreApi/Notary/DocumentsNotary/DocumentsNotary";
import FilesNotary from "@Front/Api/LeCoffreApi/Notary/FilesNotary/Files";
import CircleProgress from "@Front/Components/DesignSystem/CircleProgress";
import IconButton from "@Front/Components/DesignSystem/IconButton";
@ -8,7 +7,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 +23,10 @@ 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 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";
import MessageBus from "src/sdk/MessageBus";
type IProps = {
customerUid: string;
@ -55,14 +58,19 @@ export default function DocumentTables(props: IProps) {
const fetchDocuments = useCallback(
() => {
setDocuments([]);
LoaderService.getInstance().show();
DocumentService.getDocuments().then(async (processes: any[]) => {
if (processes.length > 0) {
let documents: any[] = processes.map((process: any) => process.processData);
// FilterBy folder.uid & depositor.uid
documents = documents.filter((document: any) => document.folder.uid === folderUid && document.depositor.uid === customerUid);
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;
@ -80,24 +88,45 @@ export default function DocumentTables(props: IProps) {
} else {
setDocuments([]);
}
})
LoaderService.getInstance().hide();
});
},
[customerUid, folderUid],
);
const fetchDocumentsNotary = useCallback(
() =>
DocumentsNotary.getInstance()
.get({ where: { folder: { uid: folderUid }, customer: { uid: customerUid } }, include: { files: true } })
.then(setDocumentsNotary)
.catch(console.warn),
() => {
LoaderService.getInstance().show();
DocumentService.getDocuments().then(async (processes: any[]) => {
if (processes.length > 0) {
let documents: any[] = processes.map((process: any) => process.processData);
// FilterBy folder.uid & customer.uid
documents = documents.filter((document: any) => document.folder.uid === folderUid && document.customer && document.customer.uid === customerUid);
for (const document of documents) {
if (document.files && document.files.length > 0) {
const files: any[] = [];
for (const file of document.files) {
files.push((await FileService.getFileByUid(file.uid)).processData);
}
document.files = files;
}
}
setDocumentsNotary(documents);
} else {
setDocumentsNotary([]);
}
LoaderService.getInstance().hide();
});
},
[customerUid, folderUid],
);
useEffect(() => {
// TODO: review
fetchDocuments();
//fetchDocumentsNotary();
fetchDocumentsNotary();
}, [fetchDocuments, fetchDocumentsNotary]);
const openDeleteAskedDocumentModal = useCallback(
@ -154,6 +183,77 @@ 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",
merkleProof: "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;
MessageBus.getInstance().generateMerkleProof(res.lastUpdatedFileState, 'file_blob').then((proof) => {
console.log('[DocumentTables] onDownloadCertificate: proof', proof);
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,
merkleProof: proof
};
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 +333,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 +366,17 @@ export default function DocumentTables(props: IProps) {
<IconButton icon={<EyeIcon />} />
</Link>
<IconButton onClick={() => onDownload(document)} icon={<ArrowDownTrayIcon />} />
<IconButton onClick={() => onDownloadCertificate(document)} icon={<DocumentTextIcon />} />
</div>
),
},
};
})
.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(

View File

@ -135,7 +135,7 @@ export default function FolderInformation(props: IProps) {
if (processes.length > 0) {
const documents: any[] = processes.map((process: any) => process.processData);
for (const customer of folder.customers) {
customer.documents = documents.filter((document: any) => document.depositor.uid === customer.uid);
customer.documents = documents.filter((document: any) => document.depositor && document.depositor.uid === customer.uid);
}
}
resolve();

View File

@ -1,6 +1,5 @@
import backgroundImage from "@Assets/images/background_refonte.svg";
import DocumentsNotary from "@Front/Api/LeCoffreApi/Notary/DocumentsNotary/DocumentsNotary";
import Folders from "@Front/Api/LeCoffreApi/Notary/Folders/Folders";
import { EDocumentNotaryStatus } from "le-coffre-resources/dist/Notary/DocumentNotary";
import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import DragAndDrop from "@Front/Components/DesignSystem/DragAndDrop";
import Form from "@Front/Components/DesignSystem/Form";
@ -19,6 +18,11 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
import classes from "./classes.module.scss";
import LoaderService from "src/common/Api/LeCoffreApi/sdk/Loader/LoaderService";
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";
enum EClientSelection {
ALL_CLIENTS = "all_clients",
SELECTED_CLIENTS = "selected_clients",
@ -59,6 +63,64 @@ export default function SendDocuments() {
throw new Error("No clients selected");
}
LoaderService.getInstance().show();
for (const selectedClient of selectedClients) {
for (const file of files) {
await new Promise<void>((resolve: () => void) => {
const reader = new FileReader();
reader.onload = (event) => {
if (event.target?.result) {
const arrayBuffer = event.target.result as ArrayBuffer;
const uint8Array = new Uint8Array(arrayBuffer);
const fileBlob: any = {
type: file.type,
data: uint8Array
};
const fileData: any = {
file_blob: fileBlob,
file_name: file.name
};
const validatorId: string = '884cb36a346a79af8697559f16940141f068bdf1656f88fa0df0e9ecd7311fb8:0';
FileService.createFile(fileData, validatorId).then((processCreated: any) => {
const fileUid: string = processCreated.processData.uid;
const documentData: any = {
folder: {
uid: folderUid as string
},
customer: {
uid: selectedClient as string
},
files: [
{
uid: fileUid
}
],
document_status: EDocumentNotaryStatus.SENT
};
DocumentService.createDocument(documentData, validatorId).then(() => resolve());
});
}
};
reader.readAsArrayBuffer(file);
});
}
}
LoaderService.getInstance().hide();
router.push(
Module.getInstance()
.get()
.modules.pages.Folder.pages.FolderInformation.props.path.replace("[folderUid]", folderUid as string),
);
setIsSending(false);
ToasterService.getInstance().success({ title: "Succès !", description: "Votre document a été envoyée avec succès." });
/*
await Promise.all(
selectedClients.map(async (customer) => {
const promises = files.map(async (file) => {
@ -83,6 +145,7 @@ export default function SendDocuments() {
);
setIsSending(false);
ToasterService.getInstance().success({ title: "Succès !", description: "Votre document a été envoyée avec succès." });
*/
} catch (error) {
setIsSending(false);
console.warn("Error while sending files: ", error);
@ -92,18 +155,14 @@ export default function SendDocuments() {
);
const fetchFolder = useCallback(async () => {
Folders.getInstance()
.getByUid(folderUid as string, {
q: {
customers: {
include: {
contact: true,
},
},
},
})
.then((folder) => setFolder(folder))
.catch((e) => console.warn(e));
LoaderService.getInstance().show();
FolderService.getFolderByUid(folderUid as string).then((process: any) => {
if (process) {
const folder: any = process.processData;
setFolder(folder);
LoaderService.getInstance().hide();
}
});
}, [folderUid]);
const onClientSelectionChange = useCallback(

View File

@ -13,6 +13,7 @@ import classes from "./classes.module.scss";
import Note from "le-coffre-resources/dist/Customer/Note";
import NoteService from "src/common/Api/LeCoffreApi/sdk/NoteService";
import LoaderService from "src/common/Api/LeCoffreApi/sdk/Loader/LoaderService";
type IProps = {};
@ -67,6 +68,7 @@ class UpdateCustomerNoteClass extends BasePage<IPropsClass, IState> {
}
public override async componentDidMount() {
LoaderService.getInstance().show();
NoteService.getNoteByUid(this.props.noteUid).then((process: any) => {
if (process) {
const note: any = process.processData;
@ -80,15 +82,17 @@ class UpdateCustomerNoteClass extends BasePage<IPropsClass, IState> {
.get()
.modules.pages.Folder.pages.FolderInformation.props.path.replace("[folderUid]", note.folder?.uid!),
});
LoaderService.getInstance().hide();
}
});
}
private async onFormSubmit(e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) {
try {
LoaderService.getInstance().show();
NoteService.getNoteByUid(this.props.noteUid).then((process: any) => {
if (process) {
NoteService.updateNote(process, { content: values["content"] }).then(() => {
this.props.router.push(this.state.backwardPath);
});

View File

@ -49,7 +49,6 @@ export default function UpdateFolderMetadata() {
setValidationError(validationErrors as ValidationError[]);
return;
}
try {
LoaderService.getInstance().show();
FolderService.getFolderByUid(folderUid).then((process: any) => {

View File

@ -19,6 +19,7 @@ import MessageBox from "@Front/Components/Elements/MessageBox";
import DocumentService from "src/common/Api/LeCoffreApi/sdk/DocumentService";
import FileService from "src/common/Api/LeCoffreApi/sdk/FileService";
import { FileBlob } from "@Front/Api/Entities/types";
type IProps = {};
type IPropsClass = {
@ -32,7 +33,7 @@ type IState = {
isValidateModalVisible: boolean;
refuseText: string;
selectedFileIndex: number;
selectedFile: any; // File | null; TODO: review
selectedFile: { uid: string; file_name: string; file_blob: FileBlob } | null;
validatedPercentage: number;
document: Document | null;
fileBlob: Blob | null;
@ -237,6 +238,8 @@ class ViewDocumentsClass extends BasePage<IPropsClass, IState> {
private async getFilePreview(): Promise<void> {
try {
if (!this.state.selectedFile) return;
const fileBlob: Blob = new Blob([this.state.selectedFile.file_blob.data], { type: this.state.selectedFile.file_blob.type });
this.setState({
fileBlob,
@ -247,7 +250,7 @@ class ViewDocumentsClass extends BasePage<IPropsClass, IState> {
}
private downloadFile() {
if (!this.state.fileBlob) return;
if (!this.state.fileBlob || !this.state.selectedFile) return;
const url = URL.createObjectURL(this.state.fileBlob);
const a = document.createElement('a');

View File

@ -0,0 +1,312 @@
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;
merkleProof: 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<Blob> - The generated PDF as a blob
*/
public async generateCertificate(certificateData: CertificateData): Promise<Blob> {
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<void> {
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<Blob> - The generated PDF as a blob
*/
public async generateNotaryCertificate(certificateData: CertificateData): Promise<Blob> {
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<void> {
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;
}
}
}

View File

@ -6,6 +6,7 @@ import EventBus from './EventBus';
import User from './User';
import MapUtils from './MapUtils';
import { FileBlob } from '../front/Api/Entities/types';
export default class MessageBus {
private static instance: MessageBus;
@ -113,13 +114,15 @@ 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) {
// 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);
if (!lastCommitedState) {
continue;
}
const publicDataEncoded = state.public_data;
const publicDataEncoded = lastCommitedState.public_data;
if (!publicDataEncoded) {
continue;
}
@ -127,48 +130,56 @@ export default class MessageBus {
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) {
continue;
}
// Which is the last state that updated file_blob?
const lastUpdatedFileState = process.states.findLast((state: any) => state.pcd_commitment['file_blob']);
const lastStateId = lastState.state_id;
if (!lastStateId) {
if (!lastUpdatedFileState) {
continue;
}
try {
const processData = await this.getData(processId, lastStateId);
const processData = await this.getData(processId, lastUpdatedFileState.state_id);
const isEmpty = Object.keys(processData).length === 0;
if (isEmpty) {
continue;
}
const publicDataEncoded = lastUpdatedFileState.public_data;
if (!publicDataEncoded) {
continue;
}
const publicDataDecoded: { [key: string]: any } = {};
for (const key of Object.keys(publicDataEncoded)) {
publicDataDecoded[key] = await this.getPublicData(publicDataEncoded[key]);
}
if (!file) {
file = {
processId,
lastStateId,
lastUpdatedFileState,
processData,
publicDataDecoded,
};
} else {
for (const key of Object.keys(processData)) {
file.processData[key] = processData[key];
}
file.lastStateId = lastStateId;
file.lastUpdatedFileState = lastUpdatedFileState;
for (const key of Object.keys(publicDataDecoded)) {
file.publicDataDecoded[key] = publicDataDecoded[key];
}
}
} catch (error) {
console.error(error);
}
}
files.push(file);
break;
@ -557,6 +568,84 @@ export default class MessageBus {
});
}
/**
* Hash a document file using SHA-256
* @param fileBlob - The file blob to hash
* @returns Promise<string> - The SHA-256 hash of the file
*/
public hashDocument(fileBlob: FileBlob, commitedIn: string): Promise<string> {
return new Promise<string>((resolve: (hash: string) => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const messageId = `HASH_VALUE_${uuidv4()}`;
const unsubscribe = EventBus.getInstance().on('VALUE_HASHED', (responseId: string, hash: string) => {
if (responseId !== messageId) {
return;
}
unsubscribe();
resolve(hash);
});
const unsubscribeError = EventBus.getInstance().on('ERROR_VALUE_HASHED', (responseId: string, error: string) => {
if (responseId !== messageId) {
return;
}
unsubscribeError();
reject(error);
});
const user = User.getInstance();
const accessToken = user.getAccessToken()!;
const label = 'file_blob';
this.sendMessage({
type: 'HASH_VALUE',
accessToken,
commitedIn,
label,
fileBlob,
messageId
});
}).catch(reject);
});
}
public generateMerkleProof(processState: any, attributeName: string): Promise<any> {
return new Promise<any>((resolve: (proof: any) => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const messageId = `GET_MERKLE_PROOF_${uuidv4()}`;
const unsubscribe = EventBus.getInstance().on('MERKLE_PROOF_RETRIEVED', (responseId: string, proof: any) => {
if (responseId !== messageId) {
return;
}
unsubscribe();
resolve(proof);
});
const unsubscribeError = EventBus.getInstance().on('ERROR_MERKLE_PROOF_RETRIEVED', (responseId: string, error: string) => {
if (responseId !== messageId) {
return;
}
unsubscribeError();
reject(error);
});
const user = User.getInstance();
const accessToken = user.getAccessToken()!;
this.sendMessage({
type: 'GET_MERKLE_PROOF',
accessToken,
processState,
attributeName,
messageId
});
}).catch(reject);
});
}
private validateToken(): Promise<boolean> {
return new Promise<boolean>((resolve: (isValid: boolean) => void, reject: (error: string) => void) => {
const messageId = `VALIDATE_TOKEN_${uuidv4()}`;
@ -647,7 +736,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 +773,7 @@ export default class MessageBus {
}
const message = event.data;
console.log('[MessageBus] handleMessage:', message);
// console.log('[MessageBus] handleMessage:', message);
switch (message.type) {
case 'LISTENING':
@ -746,6 +835,14 @@ export default class MessageBus {
this.doHandleMessage(message.messageId, 'STATE_VALIDATED', message, (message: any) => message.validatedProcess);
break;
case 'VALUE_HASHED': // HASH_VALUE
this.doHandleMessage(message.messageId, 'VALUE_HASHED', message, (message: any) => message.hash);
break;
case 'MERKLE_PROOF_RETRIEVED': // GENERATE_MERKLE_PROOF
this.doHandleMessage(message.messageId, 'MERKLE_PROOF_RETRIEVED', message, (message: any) => message.proof);
break;
case 'ERROR':
console.error('Error:', message);
this.errors[message.messageId] = message.error;