Compare commits

..

14 Commits

Author SHA1 Message Date
Sosthene
6f6d3e8de5 Fix pdf generation and parsing 2025-07-03 18:09:02 +02:00
Sosthene
c87ad8fed5 Add merkle proof validation message bus 2025-07-03 18:09:02 +02:00
Sosthene
56fe4fbcd3 Add document verification page 2025-07-03 18:09:02 +02:00
Sosthene
d94fd9e017 Refactor certificate generation 2025-07-03 18:09:02 +02:00
Sosthene
4f76d43f38 Add watermark when loading documents 2025-07-03 18:09:02 +02:00
Sosthene
7bfe3bcad2 Get merkle proof when generating certificate 2025-07-03 18:09:02 +02:00
Sosthene
c178b60d51 Add merkleProof to CertificateData 2025-07-03 18:09:02 +02:00
Sosthene
a450d80600 Add generateMerkleProof 2025-07-03 18:09:01 +02:00
Sosthene
095c4efba2 Add hashDocument to MessageBus 2025-07-03 18:09:01 +02:00
Sosthene
723322cc0a Use FileBlob and FileData everywhere 2025-07-03 18:09:01 +02:00
Sosthene
d17f4aa8d9 Add FileBlob and FileData 2025-07-03 18:09:01 +02:00
Sosthene
7a137dbe2f Add basic certificate 2025-07-03 18:09:01 +02:00
96ed1e50fa Fix somes issues 2025-07-02 14:54:59 +02:00
e4c440d6df Fix somes issues 2025-07-02 13:41:39 +02:00
10 changed files with 299 additions and 118 deletions

View File

@ -14,10 +14,12 @@ import classNames from "classnames";
import Button, { EButtonstyletype, EButtonVariant } from "../Button";
import Confirm from "../OldModal/Confirm";
import Documents from "@Front/Api/LeCoffreApi/Customer/Documents/Documents";
import Files from "@Front/Api/LeCoffreApi/Customer/Files/Files";
import Alert from "../OldModal/Alert";
import DocumentService from "src/common/Api/LeCoffreApi/sdk/DocumentService";
import LoaderService from "src/common/Api/LeCoffreApi/sdk/Loader/LoaderService";
import FileService from "src/common/Api/LeCoffreApi/sdk/FileService";
type IProps = {
onChange?: (files: File[]) => void;
open: boolean;
@ -196,7 +198,7 @@ export default class DepositOtherDocument extends React.Component<IProps, IState
);
}
public override componentDidMount(): void {}
public override componentDidMount(): void { }
private onCloseAlertUpload() {
this.setState({ showFailedUploaded: null });
@ -206,18 +208,30 @@ export default class DepositOtherDocument extends React.Component<IProps, IState
this.setState({
isLoading: true,
});
const filesArray = this.state.currentFiles;
const filesArray = this.state.currentFiles;
if (!filesArray) return;
let documentCreated: Document = {} as Document;
LoaderService.getInstance().show();
let documentCreated: any;
try {
documentCreated = await Documents.getInstance().post({
documentCreated = await new Promise<any>((resolve: (document: any) => void) => {
const documentTypeData: any = {
folder: {
uid: this.props.folder_uid,
},
depositor: {
uid: this.props.customer_uid,
},
}
};
const validatorId: string = '884cb36a346a79af8697559f16940141f068bdf1656f88fa0df0e9ecd7311fb8:0';
DocumentService.createDocument(documentTypeData, validatorId).then((processCreated: any) => {
if (processCreated) {
const document: any = processCreated.processData;
resolve(document);
}
});
});
} catch (e) {
this.setState({ showFailedDocument: "Le dossier est vérifié aucune modification n'est acceptée", isLoading: false });
@ -225,16 +239,46 @@ export default class DepositOtherDocument extends React.Component<IProps, IState
}
for (let i = 0; i < filesArray.length; i++) {
const formData = new FormData();
formData.append("file", filesArray[i]!.file, filesArray[i]!.fileName);
const query = JSON.stringify({ document: { uid: documentCreated.uid } });
formData.append("q", query);
try {
await Files.getInstance().post(formData);
} catch (e) {
this.setState({ showFailedUploaded: "Le fichier ne correspond pas aux critères demandés", isLoading: false });
return;
const file = filesArray[i]!.file;
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;
DocumentService.getDocumentByUid(documentCreated.uid).then((process: any) => {
if (process) {
const document: any = process.processData;
let files: any[] = document.files;
if (!files) {
files = [];
}
files.push({ uid: fileUid });
DocumentService.updateDocument(process, { files: files, document_status: EDocumentStatus.DEPOSITED }).then(() => resolve());
}
});
});
}
};
reader.readAsArrayBuffer(file);
});
}
this.setState({

View File

@ -1,4 +1,3 @@
import Folders from "@Front/Api/LeCoffreApi/Customer/Folders/Folders";
import { IBlock } from "@Front/Components/DesignSystem/SearchBlockList/BlockList/Block";
import Module from "@Front/Config/Module";
import JwtService from "@Front/Services/JwtService/JwtService";
@ -14,7 +13,7 @@ type IProps = IPropsDashboardWithList & {};
export default function DefaultCustomerDashboard(props: IProps) {
const router = useRouter();
const { folderUid } = router.query;
const { folderUid, profileUid } = router.query;
const [folders, setFolders] = useState<OfficeFolder[]>([]);
useEffect(() => {
@ -61,7 +60,9 @@ export default function DefaultCustomerDashboard(props: IProps) {
router.push(
Module.getInstance()
.get()
.modules.pages.ClientDashboard.props.path.replace("[folderUid]", folder.uid ?? ""),
.modules.pages.ClientDashboard.props.path
.replace("[folderUid]", folder.uid ?? "")
.replace("[profileUid]", profileUid as string ?? ""),
);
};
return <DefaultDashboardWithList {...props} onSelectedBlock={onSelectedBlock} blocks={getBlocks(folders)} headerConnected={false} />;

View File

@ -163,7 +163,8 @@ export default function DepositDocumentComponent(props: IProps) {
);
const onOpenModal = useCallback(async () => {
const refused_reason = document.document_history?.find((history: any) => history.document_status === "REFUSED")?.refused_reason;
if (document.document_status !== "REFUSED") return;
const refused_reason = document.refused_reason;
if (!refused_reason) return;
setRefusedReason(refused_reason);
setIsModalOpen(true);

View File

@ -1,5 +1,3 @@
import DocumentsNotary from "@Front/Api/LeCoffreApi/Customer/DocumentsNotary/DocumentsNotary";
import FilesNotary from "@Front/Api/LeCoffreApi/Customer/FilesNotary/Files";
import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import IconButton from "@Front/Components/DesignSystem/IconButton";
import Table from "@Front/Components/DesignSystem/Table";
@ -16,10 +14,14 @@ import { useRouter } from "next/router";
import React, { useCallback, useEffect, useState } from "react";
import classes from "./classes.module.scss";
import Link from "next/link";
import Folders from "@Front/Api/LeCoffreApi/Customer/Folders/Folders";
import Customer from "le-coffre-resources/dist/Customer";
import { DocumentNotary } from "le-coffre-resources/dist/Notary";
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";
const header: readonly IHead[] = [
{
key: "name",
@ -47,6 +49,26 @@ export default function ReceivedDocuments() {
jwt = JwtService.getInstance().decodeCustomerJwt();
}
// TODO: review
LoaderService.getInstance().show();
const folder: any = await new Promise<any>((resolve: (folder: any) => void) => {
FolderService.getFolderByUid(folderUid as string).then((process: any) => {
if (process) {
const folder: any = process.processData;
resolve(folder);
}
});
});
//const customer = folder?.customers?.find((customer) => customer.contact?.email === jwt?.email);
const customer = folder?.customers?.[0];
if (!customer) throw new Error("Customer not found");
setCustomer(customer);
return { folder, customer };
/*
const folder = await Folders.getInstance().getByUid(folderUid as string, {
q: {
office: true,
@ -76,32 +98,58 @@ export default function ReceivedDocuments() {
setCustomer(customer);
return { folder, customer };
*/
}, [folderUid]);
useEffect(() => {
fetchFolderAndCustomer();
}, [folderUid]); // Ne dépend que de folderUid
// Effet séparé pour charger les documents lorsque customer change
useEffect(() => {
const customerUid = customer?.uid;
if (!folderUid || !customerUid) return;
DocumentsNotary.getInstance()
.get({ where: { folder: { uid: folderUid }, customer: { uid: customerUid } }, include: { files: true } })
.then((documentsNotary) => setDocumentsNotary(documentsNotary));
}, [folderUid, customer, fetchFolderAndCustomer]);
const onDownload = useCallback((doc: DocumentNotary) => {
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);
LoaderService.getInstance().hide();
}
});
}, [folderUid, customer]);
const onDownload = useCallback((doc: any) => {
const file = doc.files?.[0];
if (!file || !file?.uid || !doc.uid) return;
if (!file) return;
return FilesNotary.getInstance()
.download(file.uid, doc.uid)
.then((blob) => {
return new Promise<void>((resolve: () => void) => {
const blob = new Blob([file.file_blob.data], { type: file.file_blob.type });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
const a = document.createElement('a');
a.href = url;
a.download = file.file_name ?? "file";
a.download = file.file_name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
})
.catch((e) => console.warn(e));
resolve();
}).catch((e) => console.warn(e));
}, []);
const onDownloadAll = useCallback(async () => {
@ -110,16 +158,14 @@ export default function ReceivedDocuments() {
const zip = new JSZip();
const folder = zip.folder("documents") || zip;
const downloadPromises = documentsNotary.map(async (doc) => {
documentsNotary.map((doc: any) => {
const file = doc.files?.[0];
if (file && file.uid && doc.uid) {
const blob = await FilesNotary.getInstance().download(file.uid, doc.uid);
if (file) {
const blob = new Blob([file.file_blob.data], { type: file.file_blob.type });
folder.file(file.file_name ?? "file", blob);
}
});
await Promise.all(downloadPromises);
zip.generateAsync({ type: "blob" })
.then((blob: any) => {
saveAs(blob, "documents.zip");

View File

@ -11,9 +11,12 @@ import { FileBlob } from "@Front/Api/Entities/types";
import BasePage from "../../Base";
import classes from "./classes.module.scss";
import DocumentsNotary from "@Front/Api/LeCoffreApi/Customer/DocumentsNotary/DocumentsNotary";
import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate";
import FilesNotary from "@Front/Api/LeCoffreApi/Customer/FilesNotary/Files";
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";
type IProps = {};
type IPropsClass = {
@ -124,11 +127,26 @@ class ViewDocumentsNotaryClass extends BasePage<IPropsClass, IState> {
override async componentDidMount() {
try {
const documentNotary = await DocumentsNotary.getInstance().getByUid(this.props.documentUid, {
files: true,
folder: true,
depositor: true,
LoaderService.getInstance().show();
const documentNotary: any = await new Promise<any>((resolve: (document: any) => void) => {
DocumentService.getDocumentByUid(this.props.documentUid).then(async (process: any) => {
if (process) {
const document: any = process.processData;
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;
}
resolve(document);
}
});
});
LoaderService.getInstance().hide();
this.setState(
{
documentNotary,
@ -150,8 +168,7 @@ class ViewDocumentsNotaryClass extends BasePage<IPropsClass, IState> {
private async getFilePreview(): Promise<void> {
try {
const fileBlob: Blob = await FilesNotary.getInstance().download(this.state.selectedFile?.uid as string, this.props.documentUid);
const fileBlob: Blob = new Blob([this.state.selectedFile.file_blob.data], { type: this.state.selectedFile.file_blob.type });
this.setState({
fileBlob,
});

View File

@ -1,5 +1,4 @@
"use client";
import Documents, { IGetDocumentsparams } from "@Front/Api/LeCoffreApi/Customer/Documents/Documents";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
@ -10,7 +9,6 @@ import { DocumentNotary, OfficeFolder as OfficeFolderNotary } from "le-coffre-re
import classes from "./classes.module.scss";
import { useRouter } from "next/router";
import JwtService, { ICustomerJwtPayload } from "@Front/Services/JwtService/JwtService";
import Folders from "@Front/Api/LeCoffreApi/Customer/Folders/Folders";
import Tag, { ETagColor } from "@Front/Components/DesignSystem/Tag";
import DefaultCustomerDashboard from "@Front/Components/LayoutTemplates/DefaultCustomerDashboard";
@ -21,16 +19,19 @@ import Module from "@Front/Config/Module";
import Separator, { ESeperatorColor, ESeperatorDirection } from "@Front/Components/DesignSystem/Separator";
import NotificationBox from "@Front/Components/DesignSystem/NotificationBox";
import ContactBox from "./ContactBox";
import DocumentsNotary from "@Front/Api/LeCoffreApi/Customer/DocumentsNotary/DocumentsNotary";
import { EDocumentNotaryStatus } from "le-coffre-resources/dist/Notary/DocumentNotary";
import DepositOtherDocument from "@Front/Components/DesignSystem/DepositOtherDocument";
import Modal from "@Front/Components/DesignSystem/Modal";
import TextField from "@Front/Components/DesignSystem/Form/TextField";
import AuthModal from "src/sdk/AuthModal";
import FolderService from "src/common/Api/LeCoffreApi/sdk/FolderService";
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";
type IProps = {};
@ -46,6 +47,30 @@ export default function ClientDashboard(props: IProps) {
const [isAddDocumentModalVisible, setIsAddDocumentModalVisible] = useState<boolean>(false);
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
const [isSmsModalOpen, setIsSmsModalOpen] = useState(false);
const [smsCode, setSmsCode] = useState("");
const [smsError, setSmsError] = useState("");
const verifySmsCode = useCallback(() => {
if (smsCode === "1234") {
setIsSmsModalOpen(false);
} else {
setSmsError("Code incorrect. Le code valide est 1234.");
}
}, [smsCode]);
useEffect(() => {
const handleKeyPress = (e: KeyboardEvent) => {
if (e.key === "Enter" && isSmsModalOpen) {
verifySmsCode();
}
};
window.addEventListener("keypress", handleKeyPress);
return () => {
window.removeEventListener("keypress", handleKeyPress);
};
}, [isSmsModalOpen, smsCode, verifySmsCode]);
const fetchFolderAndCustomer = useCallback(async () => {
let jwt: ICustomerJwtPayload | undefined;
@ -54,6 +79,7 @@ export default function ClientDashboard(props: IProps) {
}
// TODO: review
LoaderService.getInstance().show();
const { folder, customer } = await new Promise<any>((resolve) => {
FolderService.getFolderByUid(folderUid as string).then((process: any) => {
if (process) {
@ -72,6 +98,7 @@ export default function ClientDashboard(props: IProps) {
setFolder(folder);
setIsAuthModalOpen(true);
LoaderService.getInstance().hide();
return { folder, customer };
@ -111,40 +138,19 @@ export default function ClientDashboard(props: IProps) {
const fetchDocuments = useCallback(
async (customerUid: string | undefined) => {
/* TODO: review
const query: IGetDocumentsparams = {
where: { depositor: { uid: customerUid }, folder_uid: folderUid as string },
include: {
files: true,
document_history: true,
document_type: true,
depositor: true,
folder: {
include: {
customers: {
include: {
contact: true,
},
},
},
},
},
};
return Documents.getInstance()
.get(query)
.then((documents) => setDocuments(documents));
*/
return DocumentService.getDocuments().then(async (processes: any[]) => {
LoaderService.getInstance().show();
return new Promise<void>((resolve: () => void) => {
DocumentService.getDocuments().then(async (processes: any[]) => {
if (processes.length > 0) {
let documents: any[] = processes.map((process: any) => process.processData);
// FilterBy folder_uid
documents = documents.filter((document: any) => document.folder.uid === folderUid && document.depositor);
// FilterBy folder.uid & depositor.uid
documents = documents.filter((document: any) => document.folder.uid === folderUid && document.depositor && document.depositor.uid === customerUid);
for (const document of documents) {
if (document.document_type) {
document.document_type = (await DocumentTypeService.getDocumentTypeByUid(document.document_type.uid)).processData;
}
if (document.files && document.files.length > 0) {
const files: any[] = [];
@ -157,6 +163,9 @@ export default function ClientDashboard(props: IProps) {
setDocuments(documents);
}
LoaderService.getInstance().hide();
resolve();
});
});
},
[folderUid],
@ -170,12 +179,28 @@ export default function ClientDashboard(props: IProps) {
const customerUid = customer?.uid;
if (!folderUid || !customerUid) return;
/* TODO: review
DocumentsNotary.getInstance()
.get({ where: { folder: { uid: folderUid }, customer: { uid: customerUid } }, include: { files: true } })
.then((documentsNotary) => setDocumentsNotary(documentsNotary));
*/
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);
LoaderService.getInstance().hide();
}
});
}, [folderUid, customer?.uid]);
const documentsNotaryNotRead = useMemo(
@ -312,8 +337,52 @@ export default function ClientDashboard(props: IProps) {
isOpen={isAuthModalOpen}
onClose={() => {
setIsAuthModalOpen(false);
setIsSmsModalOpen(true);
}}
/>}
{isSmsModalOpen && (
<Modal
isOpen={isSmsModalOpen}
onClose={() => setIsSmsModalOpen(false)}
title="Vérification SMS"
>
<div className={classes["sms-modal-content"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_PRIMARY}>
Veuillez saisir le code à 4 chiffres que vous avez reçu par SMS
</Typography>
<TextField
name="smsCode"
placeholder="Code SMS à 4 chiffres"
value={smsCode}
onChange={(e) => {
const value = e.target.value;
// Only allow digits
if (value === "" || /^\d+$/.test(value)) {
setSmsCode(value);
setSmsError("");
}
}}
/>
{smsError && (
<Typography typo={ETypo.TEXT_SM_REGULAR} color={ETypoColor.TEXT_ACCENT}>
{smsError}
</Typography>
)}
<div style={{ marginTop: "20px" }}>
<Button
variant={EButtonVariant.PRIMARY}
onClick={verifySmsCode}
>
Vérifier
</Button>
</div>
</div>
</Modal>
)}
</div>
</DefaultCustomerDashboard>
);

View File

@ -43,8 +43,8 @@ export default function ClientBox(props: IProps) {
if (processes.length > 0) {
let documents: any[] = processes.map((process: any) => process.processData);
// FilterBy depositor_uid & folder_uid
documents = documents.filter((document: any) => document.depositor.uid === customerUid && document.folder.uid === folderUid);
// FilterBy folder.uid & depositor.uid
documents = documents.filter((document: any) => document.folder.uid === folderUid && document.depositor && document.depositor.uid === customerUid);
if (documents && documents.length > 0) {
closeDeleteModal();

View File

@ -71,7 +71,9 @@ export default function DocumentTables(props: IProps) {
})));
for (const document of documents) {
if (document.document_type) {
document.document_type = (await DocumentTypeService.getDocumentTypeByUid(document.document_type.uid)).processData;
}
if (document.files && document.files.length > 0) {
const files: any[] = [];
@ -404,9 +406,13 @@ export default function DocumentTables(props: IProps) {
),
},
date: {
sx: { width: 107 },
sx: { width: 120 },
content: document.updated_at ? new Date(document.updated_at).toLocaleDateString() : "_",
},
file: {
sx: { width: 120 },
content: document.files?.[0]?.file_name ?? "_",
},
actions: { sx: { width: 76 }, content: "" },
};
})

View File

@ -6,7 +6,7 @@ import Confirm from "@Front/Components/DesignSystem/OldModal/Confirm";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import DefaultNotaryDashboard from "@Front/Components/LayoutTemplates/DefaultNotaryDashboard";
import Module from "@Front/Config/Module";
import { Document, File } from "le-coffre-resources/dist/Notary";
import { Document } from "le-coffre-resources/dist/Notary";
import { EDocumentStatus } from "le-coffre-resources/dist/Notary/Document";
import Image from "next/image";
import { NextRouter, useRouter } from "next/router";

View File

@ -156,10 +156,7 @@ export default class MessageBus {
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 = {