diff --git a/src/front/Components/DesignSystem/DepositDocument/classes.module.scss b/src/front/Components/DesignSystem/DepositDocument/classes.module.scss deleted file mode 100644 index dbbcbd00..00000000 --- a/src/front/Components/DesignSystem/DepositDocument/classes.module.scss +++ /dev/null @@ -1,106 +0,0 @@ -.container { - .root { - padding: 24px; - background-color: var(--color-generic-white); - border: 1px dashed #e7e7e7; - - height: fit-content; - - &[data-drag-over="true"] { - border: 1px dashed var(--color-neutral-500); - } - - &.validated { - border: 1px dashed var(--color-success-600); - } - - .top-container { - display: flex; - align-items: center; - - .left { - margin-right: 28px; - } - - .separator { - background-color: #939393; - width: 1px; - align-self: stretch; - } - - .right { - margin-left: 18px; - - .validated { - color: var(--color-success-600); - } - - .refused-button { - font-size: 14px; - color: var(--color-error-800); - margin-left: 8px; - } - - .title { - display: flex; - align-items: center; - gap: 8px; - } - } - } - - .documents-container { - display: flex; - flex-direction: column; - gap: 16px; - margin-top: 16px; - - .file-container { - display: flex; - align-items: center; - justify-content: space-between; - - .left-part { - display: flex; - align-items: center; - gap: 8px; - .loader { - width: 32px; - height: 32px; - } - } - - .cross { - cursor: pointer; - } - } - } - - .bottom-container { - margin-top: 16px; - - .add-button { - .add-document { - display: flex; - align-items: center; - gap: 14px; - } - } - } - - .text { - margin-bottom: 12px; - } - } - - .modal-content { - display: flex; - flex-direction: column; - gap: 16px; - } - - .error-message { - color: var(--color-error-600); - margin-top: 8px; - } -} diff --git a/src/front/Components/DesignSystem/DepositDocument/index.tsx b/src/front/Components/DesignSystem/DepositDocument/index.tsx deleted file mode 100644 index 79958a8d..00000000 --- a/src/front/Components/DesignSystem/DepositDocument/index.tsx +++ /dev/null @@ -1,452 +0,0 @@ -import DepositDocumentIcon from "@Assets/Icons/deposit-document.svg"; -import PlusIcon from "@Assets/Icons/plus.svg"; -import CrossIcon from "@Assets/Icons/cross.svg"; -import DocumentCheckIcon from "@Assets/Icons/document-check.svg"; -import Image from "next/image"; -import React from "react"; - -import Button, { EButtonstyletype, EButtonVariant } from "../Button"; -import Tooltip from "../ToolTip"; -import Typography, { ETypo, ETypoColor } from "../Typography"; -import classes from "./classes.module.scss"; -import { Document, DocumentHistory, File as FileCustomer } from "le-coffre-resources/dist/Customer"; -import Files from "@Front/Api/LeCoffreApi/Customer/Files/Files"; -import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document"; -import classNames from "classnames"; -import Confirm from "../OldModal/Confirm"; -import Alert from "../OldModal/Alert"; -import GreenCheckIcon from "@Assets/Icons/green-check.svg"; -import Loader from "../Loader"; -import TextAreaField from "../Form/TextareaField"; -import { toast } from "react-toastify"; - -type IProps = { - defaultFiles?: FileCustomer[]; - onChange?: (files: File[]) => void; - document: Document; -}; - -type IFile = { - index: number; - file: File; - uid: string; - archived: Date | null; - fileName: string; -}; - -type IState = { - files: IFile[]; - isDragOver: boolean; - currentFiles?: FileCustomer[]; - refusedReason?: string; - isShowRefusedReasonModalVisible: boolean; - showFailedUploaded: string | null; - loading: boolean; -}; - -type fileAccepted = { - extension: string; - size: number; -}; - -const filesAccepted: { [key: string]: fileAccepted } = { - "application/pdf": { - extension: "pdf", - size: 41943040, - }, - "image/jpeg": { - extension: "jpeg", - size: 41943040, - }, - "image/png": { - extension: "png", - size: 41943040, - }, - "image/jpg": { - extension: "jpg", - size: 41943040, - }, -}; - -export default class DepositDocument extends React.Component { - private inputRef = React.createRef(); - private index = 0; - - public constructor(props: IProps) { - super(props); - - this.state = { - files: [], - isDragOver: false, - currentFiles: this.props.defaultFiles, - refusedReason: "", - isShowRefusedReasonModalVisible: false, - showFailedUploaded: null, - loading: false, - }; - - this.addDocument = this.addDocument.bind(this); - this.onFileChange = this.onFileChange.bind(this); - this.addFile = this.addFile.bind(this); - this.removeFile = this.removeFile.bind(this); - this.onDragOver = this.onDragOver.bind(this); - this.onDragDrop = this.onDragDrop.bind(this); - this.onDragLeave = this.onDragLeave.bind(this); - this.onCloseModalShowRefusedReason = this.onCloseModalShowRefusedReason.bind(this); - this.onOpenModalShowRefusedReason = this.onOpenModalShowRefusedReason.bind(this); - this.showRefusedReason = this.showRefusedReason.bind(this); - this.onCloseAlertUpload = this.onCloseAlertUpload.bind(this); - } - - public override render(): JSX.Element { - return ( -
-
- -
-
- Deposit document -
-
-
- -
- {this.props.document.document_type?.name} -
- {this.props.document.document_type?.public_description !== " " && - this.props.document.document_type?.public_description !== "" && - this.props.document.document_status !== EDocumentStatus.VALIDATED && ( - - )} - {this.props.document.document_status === EDocumentStatus.VALIDATED && ( - Document check - )} -
- {this.props.document.document_status !== EDocumentStatus.VALIDATED && ( - - Sélectionnez des documents .jpg, .pdf ou .png - - )} - {this.props.document.document_history?.map((history) => ( -
{this.renderDocumentHistory(history)}
- ))} -
-
-
- {this.state.files.map((file) => { - const fileObj = file.file; - if (file.archived) return; - return ( -
-
- Document check - - {this.shortName(file.fileName || fileObj.name)} - -
- Cross icon -
- ); - })} - {this.state.loading && ( -
-
-
- -
- - Chargement... - -
-
-
- )} -
- {this.props.document.document_status !== EDocumentStatus.VALIDATED && ( -
- -
- )} - -
- - Votre document a été refusé pour la raison suivante : - - -
-
-
- {this.props.document.document_status === EDocumentStatus.REFUSED && ( - - Ce document n'est pas conforme. Veuillez le déposer à nouveau. - - )} - {this.state.showFailedUploaded && ( - -
- - {this.state.showFailedUploaded} - -
-
- )} -
- ); - } - - public override componentDidMount(): void { - if (this.props.defaultFiles) { - this.setState({ - files: this.props.defaultFiles.map((file) => ({ - index: this.index++, - file: new File([""], file.file_path ?? "", {}), - uid: file.uid!, - fileName: file.file_name, - archived: file.archived_at ? new Date(file.archived_at) : null, - })), - }); - } - } - - private openSuccessToast() { - toast.success("Document envoyé avec succès"); - } - - private onCloseModalShowRefusedReason() { - this.setState({ - isShowRefusedReasonModalVisible: false, - }); - } - - private onOpenModalShowRefusedReason() { - this.setState({ - isShowRefusedReasonModalVisible: true, - }); - } - - private renderDocumentHistory(history: DocumentHistory): JSX.Element | null { - switch (history.document_status) { - case EDocumentStatus.ASKED: - return ( - - Demandé par votre notaire le {this.formatDate(history.created_at!)} - - ); - case EDocumentStatus.VALIDATED: - return ( - - Validé par votre notaire le {this.formatDate(history.created_at!)} - - ); - case EDocumentStatus.DEPOSITED: - return ( - - Déposé le {this.formatDate(history.created_at!)} - - ); - - case EDocumentStatus.REFUSED: - return ( - - Document non conforme - {history.refused_reason && history.refused_reason.length > 0 && ( - - )} - - ); - } - return null; - } - - private shortName(name: string): string { - const maxLength = 20; - if (name.length > maxLength) { - return name.substring(0, maxLength / 2) + "..." + name.substring(name.length - maxLength / 2, name.length); - } - return name; - } - - private onDragOver(event: React.DragEvent) { - if (!this.state.isDragOver) { - this.setState({ - isDragOver: true, - }); - } - event.preventDefault(); - } - - private showRefusedReason(refusedReason: string) { - this.setState({ - refusedReason, - }); - this.onOpenModalShowRefusedReason(); - } - - private onDragLeave(event: React.DragEvent) { - this.setState({ - isDragOver: false, - }); - event.preventDefault(); - } - - private async onDragDrop(event: React.DragEvent) { - event.preventDefault(); - this.setState({ - isDragOver: false, - }); - const file = event.dataTransfer.files[0]; - if (file) this.addFile(file); - } - - private async addFile(file: File) { - const fileAccepted = filesAccepted[file.type]; - if (!fileAccepted) { - alert("Le fichier déposé doit être au format .jpg .pdf .jpeg ou .png"); - return false; - } - if (file.size > fileAccepted.size) { - alert("Le fichier est trop volumineux et ne doit pas dépasser 32mo"); - return false; - } - this.setState({ - loading: true, - }); - - const formData = new FormData(); - formData.append("file", file, file.name); - const query = JSON.stringify({ document: { uid: this.props.document.uid } }); - formData.append("q", query); - - let newFile: FileCustomer; - try { - newFile = await Files.getInstance().post(formData); - } catch (e) { - this.setState({ showFailedUploaded: "Le fichier ne correspond pas aux critères demandés", loading: false }); - return false; - } - const files = this.state.currentFiles ? [...this.state.currentFiles, newFile] : [newFile]; - - const newFileList = [ - ...this.state.files, - { - index: this.index++, - file: file, - uid: newFile.uid!, - archived: null, - fileName: newFile?.file_name ?? "", - }, - ]; - this.openSuccessToast(); - this.setState( - { - currentFiles: files, - loading: false, - files: newFileList, - }, - () => { - if (this.props.onChange) this.props.onChange(newFileList.map((file) => file.file)); - }, - ); - - return true; - } - - private async removeFile(e: any) { - const image = e.target as HTMLElement; - const indexToRemove = image.getAttribute("data-file"); - if (!indexToRemove) return; - const file = this.state.files.find((file) => file.index === parseInt(indexToRemove)); - if (!file) return; - this.setState({ - files: this.state.files.filter((file) => file.index !== parseInt(indexToRemove)), - }); - - if (this.props.onChange) this.props.onChange(this.state.files.map((file) => file.file)); - await Files.getInstance().delete(file.uid); - } - - private async onFileChange() { - if (!this.inputRef.current) return; - const files = this.inputRef.current.files; - if (!files) { - this.setState({ loading: false }); - return; - } - const file = files[0]; - - try { - if (file) { - await this.setState({ loading: true }, () => { - this.addFile(file); - }); - } - } catch (e) { - this.setState({ loading: false }); - } - } - - private onCloseAlertUpload() { - this.setState({ showFailedUploaded: null }); - } - - private addDocument() { - if (!this.inputRef.current) return; - this.inputRef.current.value = ""; - this.inputRef.current.click(); - } - - private formatDate(date: Date) { - const dateToConvert = new Date(date); - return dateToConvert.toLocaleDateString("fr-FR"); - } -} diff --git a/src/front/Components/DesignSystem/DragAndDrop/DocumentElement/classes.module.scss b/src/front/Components/DesignSystem/DragAndDrop/DocumentFileElement/classes.module.scss similarity index 100% rename from src/front/Components/DesignSystem/DragAndDrop/DocumentElement/classes.module.scss rename to src/front/Components/DesignSystem/DragAndDrop/DocumentFileElement/classes.module.scss diff --git a/src/front/Components/DesignSystem/DragAndDrop/DocumentElement/index.tsx b/src/front/Components/DesignSystem/DragAndDrop/DocumentFileElement/index.tsx similarity index 93% rename from src/front/Components/DesignSystem/DragAndDrop/DocumentElement/index.tsx rename to src/front/Components/DesignSystem/DragAndDrop/DocumentFileElement/index.tsx index efb8aa20..f703b9f3 100644 --- a/src/front/Components/DesignSystem/DragAndDrop/DocumentElement/index.tsx +++ b/src/front/Components/DesignSystem/DragAndDrop/DocumentFileElement/index.tsx @@ -7,13 +7,13 @@ import Loader from "../../Loader"; import classes from "./classes.module.scss"; type IProps = { - isLoading: boolean; file: File | null; onRemove: () => void; + isLoading?: boolean; error?: string; }; -export default function DocumentElement(props: IProps) { +export default function DocumentFileElement(props: IProps) { const { isLoading, onRemove, file, error } = props; return ( diff --git a/src/front/Components/DesignSystem/DragAndDrop/index.tsx b/src/front/Components/DesignSystem/DragAndDrop/index.tsx index a803b05c..ef30c062 100644 --- a/src/front/Components/DesignSystem/DragAndDrop/index.tsx +++ b/src/front/Components/DesignSystem/DragAndDrop/index.tsx @@ -1,18 +1,33 @@ import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; import { DocumentPlusIcon } from "@heroicons/react/24/outline"; import classNames from "classnames"; -import React, { useCallback, useRef, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "../Button"; import Separator, { ESeperatorColor, ESeperatorDirection } from "../Separator"; import classes from "./classes.module.scss"; -import DocumentElement from "./DocumentElement"; +import DocumentFileElement from "./DocumentFileElement"; +/** + * @description Drag and drop component to upload files + * @param {string} title - Title of the component + * @param {string} description - Description of the component + * @param {IDocumentFileWithUid[]} defaultFiles - Default files to display + * @param {(fileUid: string) => Promise} onDelete - Function to delete a file (must be used with defaultFiles) + * @param {(file: File) => Promise} onAddFile - Function to add a file (must be used with defaultFiles) + */ type IProps = { - name?: string; title: string; - description: string; -}; + description?: string; + defaultFiles?: IDocumentFileWithUid[]; + onDelete?: (fileUid: string) => Promise; + onAddFile?: (file: File) => Promise; +} & ( + | { onDelete: (fileUid: string) => Promise; onAddFile?: never; defaultFiles: IDocumentFileWithUid[] } + | { onDelete?: never; onAddFile: (file: File) => Promise; defaultFiles: IDocumentFileWithUid[] } + | { onDelete?: (fileUid: string) => Promise; onAddFile?: (file: File) => Promise; defaultFiles: IDocumentFileWithUid[] } + | { onDelete?: never; onAddFile?: never; defaultFiles?: never } +); type IMimeTypes = { extension: string; @@ -38,53 +53,87 @@ const mimeTypesAccepted: { [key: string]: IMimeTypes } = { }, }; -type IDocument = { +type IDocumentFileBase = { id: string; file: File | null; - isLoading: boolean; + uid?: string; + isLoading?: boolean; error?: string; }; +export type IDocumentFileWithUid = IDocumentFileBase & { + uid: string; +}; + +type IDocumentFile = IDocumentFileBase | IDocumentFileWithUid; + export default function DragAndDrop(props: IProps) { - const { name, title, description } = props; + const { title, description, defaultFiles, onDelete, onAddFile } = props; const fileInputRef = useRef(null); - const [documents, setDocuments] = useState([]); + const [documentFiles, setDocumentFiles] = useState([]); - const handleFiles = useCallback((files: File[]) => { - files.forEach((file) => { - setDocuments((prevDocs) => [...prevDocs, { id: file.name, file: file, isLoading: true }]); - setTimeout(() => { - if (mimeTypesAccepted[file.type]) { - const newDoc: IDocument = { id: file.name, file, isLoading: false }; - setDocuments((prevDocs) => prevDocs.map((doc) => (doc.id === newDoc.id ? newDoc : doc))); - } else { - const errorDoc: IDocument = { id: file.name, file: null, isLoading: false, error: "Type de fichier non accepté" }; - setDocuments((prevDocs) => prevDocs.map((doc) => (doc.id === errorDoc.id ? errorDoc : doc))); + useEffect(() => { + if (defaultFiles) { + setDocumentFiles(defaultFiles); + } + }, [defaultFiles]); + + const handleAddFiles = useCallback( + (files: File[]) => { + files.forEach((file) => { + setDocumentFiles((prevDocs) => [...prevDocs, { id: file.name, file: file, isLoading: true }]); + try { + if (!mimeTypesAccepted[file.type]) { + throw new Error("Type de fichier non accepté"); + } + const newDoc: IDocumentFile = { id: file.name, file, isLoading: false }; + + if (onAddFile) { + // As onAddFile is used along defaultFiles prop we dont need to update the state here but the parent component should update the defaultFiles prop + return onAddFile(file); + } + + return setTimeout(async () => { + setDocumentFiles((prevDocs) => prevDocs.map((doc) => (doc.id === newDoc.id ? newDoc : doc))); + }, 1000); + } catch (error: any) { + const errorDoc: IDocumentFile = { id: file.name, file: null, isLoading: false, error: error.message }; + return setDocumentFiles((prevDocs) => prevDocs.map((doc) => (doc.id === errorDoc.id ? errorDoc : doc))); } - }, 1000); - }); - }, []); + }); + }, + [onAddFile], + ); const handleDrop = useCallback( (event: React.DragEvent) => { event.preventDefault(); const files = Array.from(event.dataTransfer.files); - handleFiles(files); + handleAddFiles(files); }, - [handleFiles], + [handleAddFiles], ); - const handleRemove = useCallback((id: string) => { - setDocuments((prevDocs) => prevDocs.filter((doc) => doc.id !== id)); - }, []); + const handleRemove = useCallback( + (documentFile: IDocumentFile) => { + const loadingDoc = { ...documentFile, isLoading: true }; + setDocumentFiles((prevDocs) => prevDocs.map((doc) => (doc.id === documentFile.id ? loadingDoc : doc))); + + if (documentFile.uid) { + return onDelete?.(documentFile.uid); + } + return setDocumentFiles((prevDocs) => prevDocs.filter((doc) => doc.id !== documentFile.id)); + }, + [onDelete], + ); const handleBrowse = useCallback( (event: React.ChangeEvent) => { const files = Array.from(event.target.files || []); - handleFiles(files); + handleAddFiles(files); }, - [handleFiles], + [handleAddFiles], ); const triggerFileInput = () => { @@ -95,7 +144,7 @@ export default function DragAndDrop(props: IProps) { return (
0 && classes["filled"])} + className={classNames(classes["root"], documentFiles.length > 0 && classes["filled"])} onDrop={handleDrop} onDragOver={(e) => e.preventDefault()}>
@@ -127,20 +176,22 @@ export default function DragAndDrop(props: IProps) {
- - {description} - + {description && ( + + {description} + + )}
- {documents.length > 0 && ( + {documentFiles.length > 0 && (
- {documents.map((doc) => ( - handleRemove(doc.id)} - error={doc.error} + {documentFiles.map((documentFile) => ( + handleRemove(documentFile)} + error={documentFile.error} /> ))}
@@ -152,7 +203,6 @@ export default function DragAndDrop(props: IProps) { return ( +
+ + {contact?.first_name} {contact?.last_name} + +
+
+ + Numéro de téléphone + + + {contact?.cell_phone_number ?? contact?.phone_number ?? "_"} + +
+
+ + E-mail + + + {contact?.email ?? "_"} + +
+
+ + Note client + + + {note?.content ?? "-"} + +
+
+ ); +} diff --git a/src/front/Components/LayoutTemplates/DefaultCustomerDashboard/index.tsx b/src/front/Components/LayoutTemplates/DefaultCustomerDashboard/index.tsx new file mode 100644 index 00000000..8dfc3a60 --- /dev/null +++ b/src/front/Components/LayoutTemplates/DefaultCustomerDashboard/index.tsx @@ -0,0 +1,68 @@ +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"; +import { OfficeFolder } from "le-coffre-resources/dist/Customer"; +import { useRouter } from "next/router"; +import React, { useEffect, useState } from "react"; + +import DefaultDashboardWithList, { IPropsDashboardWithList } from "../DefaultDashboardWithList"; + +type IProps = IPropsDashboardWithList & {}; + +export default function DefaultCustomerDashboard(props: IProps) { + const router = useRouter(); + const { folderUid } = router.query; + const [folders, setFolders] = useState([]); + + useEffect(() => { + const jwt = JwtService.getInstance().decodeCustomerJwt(); + if (!jwt) return; + + Folders.getInstance() + .get({ + q: { + where: { + customers: { + some: { + contact: { + email: jwt.email, + }, + }, + }, + }, + orderBy: [ + { + created_at: "desc", + }, + ], + include: { + customers: true, + }, + }, + }) + .then((folders) => setFolders(folders)); + }, []); + + const onSelectedBlock = (block: IBlock) => { + const folder = folders.find((folder) => folder.uid === block.id); + if (!folder) return; + router.push( + Module.getInstance() + .get() + .modules.pages.ClientDashboard.props.path.replace("[folderUid]", folder.uid ?? ""), + ); + }; + return ; + + function getBlocks(folders: OfficeFolder[]): IBlock[] { + return folders.map((folder) => { + return { + id: folder.uid!, + primaryText: folder.name!, + secondaryText: folder.folder_number!, + isActive: folderUid === folder.uid, + }; + }); + } +} diff --git a/src/front/Components/Layouts/ClientDashboard/DepositDocumentComponent/classes.module.scss b/src/front/Components/Layouts/ClientDashboard/DepositDocumentComponent/classes.module.scss new file mode 100644 index 00000000..cde520cf --- /dev/null +++ b/src/front/Components/Layouts/ClientDashboard/DepositDocumentComponent/classes.module.scss @@ -0,0 +1,15 @@ +@import "@Themes/constants.scss"; + +.root { + display: flex; + justify-content: space-between; + align-items: center; + align-self: stretch; + flex-wrap: wrap; + + .title { + display: flex; + flex-direction: column; + gap: var(--spacing-sm, 8px); + } +} diff --git a/src/front/Components/Layouts/ClientDashboard/DepositDocumentComponent/index.tsx b/src/front/Components/Layouts/ClientDashboard/DepositDocumentComponent/index.tsx new file mode 100644 index 00000000..943361af --- /dev/null +++ b/src/front/Components/Layouts/ClientDashboard/DepositDocumentComponent/index.tsx @@ -0,0 +1,64 @@ +import DragAndDrop, { IDocumentFileWithUid } from "@Front/Components/DesignSystem/DragAndDrop"; +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; +import { Document } from "le-coffre-resources/dist/Customer"; +import { useCallback, useMemo } from "react"; + +import classes from "./classes.module.scss"; +import Files from "@Front/Api/LeCoffreApi/Customer/Files/Files"; + +type IProps = { + document: Document; + onChange: () => void; +}; + +export default function DepositDocumentComponent(props: IProps) { + const { document, onChange } = props; + + const defaultFiles: IDocumentFileWithUid[] = useMemo(() => { + const filesNotArchived = document.files?.filter((file) => !file.archived_at) ?? []; + return filesNotArchived.map((file) => ({ + id: file.uid!, + file: new File([""], file.file_name!, { type: file.mimetype }), + uid: file.uid!, + })); + }, [document.files]); + + const addFile = useCallback( + (file: File) => { + const formData = new FormData(); + formData.append("file", file, file.name); + const query = JSON.stringify({ document: { uid: document.uid } }); + formData.append("q", query); + return Files.getInstance().post(formData).then(onChange); + }, + [document.uid, onChange], + ); + + const deleteFile = useCallback( + (filedUid: string) => { + return Files.getInstance().delete(filedUid).then(onChange); + }, + [onChange], + ); + + return ( +
+
+ + {document.document_type?.name ?? "_"} + + + Demandé le: {document.created_at ? new Date(document.created_at).toLocaleDateString() : "_"} + +
+ + +
+ ); +} diff --git a/src/front/Components/Layouts/ClientDashboard/classes.module.scss b/src/front/Components/Layouts/ClientDashboard/classes.module.scss index 5fc0ae9c..584a4e5c 100644 --- a/src/front/Components/Layouts/ClientDashboard/classes.module.scss +++ b/src/front/Components/Layouts/ClientDashboard/classes.module.scss @@ -1,120 +1,39 @@ @import "@Themes/constants.scss"; .root { - .header { + display: flex; + flex-direction: column; + gap: var(--spacing-xl, 32px); + + .title-container { + flex-direction: column; display: flex; - padding: 64px; - justify-content: space-between; + gap: var(--spacing-sm, 8px); - @media (max-width: $screen-m) { - flex-wrap: wrap; - - .text { - margin: 32px 0; - } - } - - @media (max-width: $screen-s) { - flex-wrap: wrap; - - .text { - margin: 32px 0; - } - - .button { - flex: 1; - } - } - - .folder-number { - margin-top: 16px; - } - - .office-name { - margin-top: 8px; - text-transform: uppercase; - } - - .subtitle { - margin: 64px 0 32px 0; - } - - .contact { + .office-container { display: flex; - gap: 20px; - } - - .contact-text { - text-align: right; - line-height: 15px; - } - - .contact-button { - margin-top: 4%; - } - - .separator { - width: 20px; - height: 50px; /* Adjust the height as needed */ - background-color: gray; - margin: 0 20px; /* Adjust the margin as needed */ - } - - .note-box { - border: 1px solid #e0e0e0; /* Light grey border */ - margin-top: 25px; - padding: 10px; - width: 100%; - height: 100px; /* Adjust height as needed */ - box-sizing: border-box; - position: relative; + align-items: center; + gap: var(--spacing-md, 16px); } } - .sub-container { - background-color: var(--color-neutral-50); - padding: 64px; + .content { + display: flex; + gap: var(--spacing-lg, 24px); + align-items: flex-start; - .content { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 32px; - margin-bottom: 64px; - - @media (max-width: $screen-s) { - grid-template-columns: 1fr; - } + .notary { + display: flex; + width: 300px; + flex-direction: column; + align-items: flex-start; + gap: var(--spacing-lg, 24px); } - .component-to-replace { - min-width: 124px; - height: 98px; - background-color: white; - } - - .text { - margin: 32px 0; - } - - .button { - width: fit-content; - } - - @media (max-width: $screen-s) { - .button { - width: 100%; - } - } - } - - .modal-content { - .text { - margin: 24px 0; - } - - .component-to-replace { - background-color: var(--color-neutral-50); - height: 98px; + .documents { + display: flex; + flex-direction: column; + gap: var(--spacing-xl, 32px); width: 100%; } } diff --git a/src/front/Components/Layouts/ClientDashboard/index.tsx b/src/front/Components/Layouts/ClientDashboard/index.tsx index d7478605..84e097c5 100644 --- a/src/front/Components/Layouts/ClientDashboard/index.tsx +++ b/src/front/Components/Layouts/ClientDashboard/index.tsx @@ -1,19 +1,25 @@ "use client"; import Documents, { IGetDocumentsparams } from "@Front/Api/LeCoffreApi/Customer/Documents/Documents"; -import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button"; -import DepositDocument from "@Front/Components/DesignSystem/DepositDocument"; + import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; -import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate"; -import Customer, { Document, DocumentType, Note, OfficeFolder } from "le-coffre-resources/dist/Customer"; -import React, { useCallback, useEffect, useState } from "react"; + +import Customer, { Document } from "le-coffre-resources/dist/Customer"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { type OfficeFolder as OfficeFolderNotary } from "le-coffre-resources/dist/Notary"; import classes from "./classes.module.scss"; import { useRouter } from "next/router"; import JwtService, { ICustomerJwtPayload } from "@Front/Services/JwtService/JwtService"; -import DepositOtherDocument from "@Front/Components/DesignSystem/DepositOtherDocument"; import Folders from "@Front/Api/LeCoffreApi/Customer/Folders/Folders"; import OfficeRib from "@Front/Api/LeCoffreApi/Customer/OfficeRib/OfficeRib"; +import Tag, { ETagColor } from "@Front/Components/DesignSystem/Tag"; +import DefaultCustomerDashboard from "@Front/Components/LayoutTemplates/DefaultCustomerDashboard"; +import ContactBox from "@Front/Components/Elements/ContactBox"; +import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button"; +import { ArrowDownTrayIcon } from "@heroicons/react/24/outline"; +import DepositDocumentComponent from "./DepositDocumentComponent"; + type IProps = {}; export default function ClientDashboard(props: IProps) { @@ -22,12 +28,11 @@ export default function ClientDashboard(props: IProps) { const [documents, setDocuments] = useState(null); const [customer, setCustomer] = useState(null); - const [contact, setContact] = useState(null); - const [folder, setFolder] = useState(null); - const [note, setNote] = useState(null); - const [isAddDocumentModalVisible, setIsAddDocumentModalVisible] = useState(false); + const [folder, setFolder] = useState(null); - const getDocuments = useCallback(async () => { + const [ribUrl, setRibUrl] = useState(null); + + const fetchFolderAndCustomer = useCallback(async () => { let jwt: ICustomerJwtPayload | undefined; if (typeof document !== "undefined") { jwt = JwtService.getInstance().decodeCustomerJwt(); @@ -50,66 +55,73 @@ export default function ClientDashboard(props: IProps) { }, }, }); - //Loop through the folder stakeholders, if there is at least one stakeholder that role is "Collaborateur" set contact to this stakeholders.contact, else, take the first stakeholders of the list - const contact = folder.stakeholders!.find((stakeholder) => stakeholder.office_role?.name === "Collaborateur")?.contact; - setContact(contact ?? folder.stakeholders![0]!.contact); - const actualCustomer = folder?.customers?.find((customer) => customer.contact?.email === jwt?.email); - if (!actualCustomer) throw new Error("Customer not found"); + const customer = folder?.customers?.find((customer) => customer.contact?.email === jwt?.email); + if (!customer) throw new Error("Customer not found"); - let note = folder.notes?.find((note) => note.customer?.uid === actualCustomer.uid); - // if (!note) throw new Error("Note not found"); - if (!note) { - note = { - content: "Aucune note", - created_at: new Date(), - updated_at: new Date(), - }; - } + setFolder(folder); + setCustomer(customer); - const query: IGetDocumentsparams = { - where: { depositor: { uid: actualCustomer.uid }, folder_uid: folderUid as string }, - include: { - files: true, - document_history: true, - document_type: true, - depositor: true, - folder: { - include: { - customers: { - include: { - contact: true, + return { folder, customer }; + }, [folderUid]); + + const fetchDocuments = useCallback( + (customerUid: string | undefined) => { + 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)); + }, + [folderUid], + ); + + useEffect(() => { + fetchFolderAndCustomer().then(({ customer }) => fetchDocuments(customer.uid)); + }, [fetchDocuments, fetchFolderAndCustomer]); + + const notaryContact = useMemo( + () => + folder?.stakeholders!.find((stakeholder) => stakeholder.office_role?.name === "Collaborateur")?.contact ?? + folder?.stakeholders![0]!.contact, + [folder], + ); + + const note = useMemo( + () => + folder?.notes?.find((note) => note.customer?.uid === customer?.uid) ?? { + content: "Aucune note", + created_at: new Date(), + updated_at: new Date(), }, - }; + [customer?.uid, folder?.notes], + ); - const documentList = await Documents.getInstance().get(query); - - //const folder = await Folders.getInstance().getByUid(folderUid as string, { q: { office: true, customers: true } }); - - setFolder(folder); - setDocuments(documentList); - setCustomer(actualCustomer); - setNote(note); - }, [folderUid]); - - const onCloseModalAddDocument = useCallback(() => { - setIsAddDocumentModalVisible(false); - getDocuments(); - }, [getDocuments]); - - const onOpenModalAddDocument = useCallback(() => { - setIsAddDocumentModalVisible(true); - }, []); - - const downloadFile = useCallback(async () => { + useEffect(() => { if (!folder?.office?.uid) return; - const blob = await OfficeRib.getInstance().getRibStream(folder.office.uid); - const ribUrl = URL.createObjectURL(blob); + OfficeRib.getInstance() + .getRibStream(folder.office.uid) + .then((blob) => setRibUrl(URL.createObjectURL(blob))); + }, [folder]); + const downloadRib = useCallback(async () => { if (!ribUrl) return; const a = document.createElement("a"); a.style.display = "none"; @@ -117,122 +129,64 @@ export default function ClientDashboard(props: IProps) { a.download = ""; document.body.appendChild(a); a.click(); - }, [folder]); - - useEffect(() => { - getDocuments(); - }, [folderUid, getDocuments]); - - const renderHeader = useCallback(() => { - return ( -
-
- {/* TODO Get name from userStore */} -
- - Bonjour {customer?.contact?.first_name.concat(" ", customer?.contact?.last_name)} - -
- - - Dossier {folder?.folder_number} - {folder?.name} - - - - {folder?.office?.name} - - - - Documents à envoyer - - - - Votre notaire est dans l'attente de documents pour valider votre dossier. Voici la liste des documents. -
Veuillez glisser / déposer chaque document dans la zone prévue à cet effet ou cliquez sur la zone puis - sélectionnez le document correspondant.
En déposant un document, celui-ci est automatiquement enregistré et - transmis à votre notaire. -
-
- - {note?.content} - -
-
- -
- -

- {contact?.first_name} {contact?.last_name} -

-

{contact?.phone_number ?? contact?.cell_phone_number}

-

{contact?.email}

-
-
- {folder?.office?.rib_name && ( - //Div to avoid the button to be on the same line as the text - - )} -
-
- ); - }, [ - contact?.cell_phone_number, - contact?.email, - contact?.first_name, - contact?.last_name, - contact?.phone_number, - customer?.contact?.first_name, - customer?.contact?.last_name, - downloadFile, - folder?.folder_number, - folder?.name, - folder?.office?.name, - folder?.office?.rib_name, - note?.content, - ]); - - const renderBox = useCallback(() => { - return ( - ({ - document_type: DocumentType.hydrate({ - name: "Autres documents", - }), - })} - /> - ); - }, [customer, folderUid, isAddDocumentModalVisible, onCloseModalAddDocument]); + }, [ribUrl]); return ( - +
- {renderHeader()} -
-
+
+ + Dossier {folder?.folder_number} - {folder?.name} + + + Bonjour {customer?.contact?.first_name.concat(" ", customer?.contact?.last_name)} + + +
+ + Office + + + + {folder?.office?.name} + +
+
+
+
+ + Votre Notaire + + {notaryContact && } + {ribUrl && ( + + )} + +
+
+ + Documents à envoyer + {documents?.map((document) => ( - + fetchDocuments(customer?.uid)} + /> ))}
- Documents supplémentaires (facultatif) - - Vous souhaitez envoyer d'autres documents à votre notaire ? - -
- {isAddDocumentModalVisible && renderBox()} - + ); } diff --git a/src/front/Components/Layouts/ClientDashboard/index2.tsx b/src/front/Components/Layouts/ClientDashboard/index2.tsx deleted file mode 100644 index 89cb3b6d..00000000 --- a/src/front/Components/Layouts/ClientDashboard/index2.tsx +++ /dev/null @@ -1,140 +0,0 @@ -//import Customers from "@Front/Api/LeCoffreApi/Customer/Customers/Customers"; -import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button"; -import DepositDocument from "@Front/Components/DesignSystem/DepositDocument"; -import TextField from "@Front/Components/DesignSystem/Form/TextField"; -import Confirm from "@Front/Components/DesignSystem/OldModal/Confirm"; -import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography"; -import Base from "@Front/Components/Layouts/Base"; -import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate"; -import Customer, { Document, DocumentType } from "le-coffre-resources/dist/Customer"; -import React from "react"; - -import classes from "./classes.module.scss"; - -type IProps = {}; -type IState = { - isAddDocumentModalVisible: boolean; - documents: Document[]; - mockedCustomer: Customer | null; -}; - -export default class ClientDashboard extends Base { - public constructor(props: IProps) { - super(props); - this.state = { - isAddDocumentModalVisible: false, - documents: [], - mockedCustomer: null, - }; - this.onCloseModalAddDocument = this.onCloseModalAddDocument.bind(this); - this.onOpenModalAddDocument = this.onOpenModalAddDocument.bind(this); - } - - public override render(): JSX.Element { - return ( - -
- {this.renderHeader()} -
-
- {this.state.documents?.map((document) => ( - - ))} -
- Documents supplémentaires (facultatif) - - Vous souhaitez envoyer d'autres documents à votre notaire ? - - -
- -
- - Vous souhaitez envoyer un autre document à votre notaire ? - - - - Glissez / Déposez votre document dans la zone prévue à cet effet ou cliquez sur la zone puis sélectionnez le - document correspondant. - - ({ - document_type: DocumentType.hydrate({ - name: "Autres documents", - }), - })} - /> -
-
-
-
- ); - } - - private renderHeader(): JSX.Element { - return ( -
-
- {/* TODO Get name from userStore */} - - Bonjour {this.state.mockedCustomer?.contact?.first_name.concat(" ", this.state.mockedCustomer?.contact?.last_name)} - - - - Documents à envoyer - - - - Votre notaire est dans l'attente de documents pour valider votre dossier. Voici la liste des documents.Veuillez - glisser / déposez chaque document dans la zone prévue à cet effet ou cliquez sur la zone puis sélectionnez le - document correspondant. Si un des documents demandés ne vous concernent pas, veuillez contacter votre notaire à - l'aide du bouton ci-dessus. - -
-
- ); - } - - // public override async componentDidMount() { - // // TODO Get documents of the current customer according to userStore - // // REMOVE this mock - - // const jwt = JwtService.getInstance().decodeJwt(); - // const mockedCustomers = await Customers.getInstance().get({ - // where: { contact: { email: jwt?.email } }, - // }); - // const mockedCustomer: Customer = mockedCustomers[0]!; - - // const query: IGetDocumentsparams = { - // where: { depositor: { uid: mockedCustomer.uid } }, - // include: { - // files: true, - // document_history: true, - // document_type: true, - // }, - // }; - // const documents: Document[] = await Documents.getInstance().get(query); - // this.setState({ documents, mockedCustomer }); - // } - - private onCloseModalAddDocument() { - this.setState({ isAddDocumentModalVisible: false }); - } - - private onOpenModalAddDocument() { - this.setState({ isAddDocumentModalVisible: true }); - } -} diff --git a/src/front/Components/Layouts/DesignSystem/index.tsx b/src/front/Components/Layouts/DesignSystem/index.tsx index 0cf4f2cd..ea214504 100644 --- a/src/front/Components/Layouts/DesignSystem/index.tsx +++ b/src/front/Components/Layouts/DesignSystem/index.tsx @@ -131,7 +131,7 @@ export default function DesignSystem() { Logged Out Drag and Drop - + Separators
diff --git a/src/front/Components/Layouts/Folder/index.tsx b/src/front/Components/Layouts/Folder/index.tsx index ead04297..099f05db 100644 --- a/src/front/Components/Layouts/Folder/index.tsx +++ b/src/front/Components/Layouts/Folder/index.tsx @@ -1,40 +1,24 @@ import LogoIcon from "@Assets/logo_small_blue.svg"; -import Users from "@Front/Api/LeCoffreApi/Notary/Users/Users"; +import Folders from "@Front/Api/LeCoffreApi/Notary/Folders/Folders"; import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button"; import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; import DefaultNotaryDashboard from "@Front/Components/LayoutTemplates/DefaultNotaryDashboard"; import Module from "@Front/Config/Module"; -import JwtService from "@Front/Services/JwtService/JwtService"; import { DocumentIcon } from "@heroicons/react/24/outline"; -import User from "le-coffre-resources/dist/Notary"; +import EFolderStatus from "le-coffre-resources/dist/Customer/EFolderStatus"; import Image from "next/image"; import Link from "next/link"; +import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import classes from "./classes.module.scss"; -import Folders from "@Front/Api/LeCoffreApi/Notary/Folders/Folders"; -import EFolderStatus from "le-coffre-resources/dist/Customer/EFolderStatus"; -import { useRouter } from "next/router"; +import useUser from "@Front/Hooks/useUser"; export default function Folder() { const [_isArchivedModalOpen, _setIsArchivedModalOpen] = useState(true); const router = useRouter(); - const [activeUser, setActiveUser] = useState(); - - useEffect(() => { - const decodedJwt = JwtService.getInstance().decodeJwt(); - if (!decodedJwt) return; - Users.getInstance() - .getByUid(decodedJwt.userId, { - q: { - contact: true, - }, - }) - .then((user) => { - setActiveUser(user); - }); - }, []); + const { user: activeUser } = useUser(); useEffect(() => { Folders.getInstance() diff --git a/src/front/Components/Layouts/SelectFolder/index.tsx b/src/front/Components/Layouts/SelectFolder/index.tsx index 74999979..29692b50 100644 --- a/src/front/Components/Layouts/SelectFolder/index.tsx +++ b/src/front/Components/Layouts/SelectFolder/index.tsx @@ -12,10 +12,11 @@ import { useRouter } from "next/router"; import { useCallback, useEffect, useState } from "react"; import classes from "./classes.module.scss"; +import Module from "@Front/Config/Module"; export default function SelectFolder() { - const [folders, setFolders] = useState([]); const router = useRouter(); + const [folders, setFolders] = useState([]); useEffect(() => { const jwt = JwtService.getInstance().decodeCustomerJwt(); @@ -48,7 +49,11 @@ export default function SelectFolder() { const handleSelectBlock = useCallback( (folder: IBlock) => { - router.push("/client-dashboard/" + folder.id); + router.push( + Module.getInstance() + .get() + .modules.pages.ClientDashboard.props.path.replace("[folderUid]", folder.id ?? ""), + ); }, [router], ); diff --git a/src/front/Config/Module/development.json b/src/front/Config/Module/development.json index 09587298..b3d4b6d1 100644 --- a/src/front/Config/Module/development.json +++ b/src/front/Config/Module/development.json @@ -31,6 +31,13 @@ "labelKey": "customer_login" } }, + "ClientDashboard": { + "enabled": true, + "props": { + "path": "/client-dashboard/[folderUid]", + "labelKey": "client-dashboard" + } + }, "Folder": { "enabled": true, "props": { diff --git a/src/front/Config/Module/preprod.json b/src/front/Config/Module/preprod.json index 09587298..b3d4b6d1 100644 --- a/src/front/Config/Module/preprod.json +++ b/src/front/Config/Module/preprod.json @@ -31,6 +31,13 @@ "labelKey": "customer_login" } }, + "ClientDashboard": { + "enabled": true, + "props": { + "path": "/client-dashboard/[folderUid]", + "labelKey": "client-dashboard" + } + }, "Folder": { "enabled": true, "props": { diff --git a/src/front/Config/Module/production.json b/src/front/Config/Module/production.json index 09587298..b3d4b6d1 100644 --- a/src/front/Config/Module/production.json +++ b/src/front/Config/Module/production.json @@ -31,6 +31,13 @@ "labelKey": "customer_login" } }, + "ClientDashboard": { + "enabled": true, + "props": { + "path": "/client-dashboard/[folderUid]", + "labelKey": "client-dashboard" + } + }, "Folder": { "enabled": true, "props": { diff --git a/src/front/Config/Module/staging.json b/src/front/Config/Module/staging.json index 09587298..b3d4b6d1 100644 --- a/src/front/Config/Module/staging.json +++ b/src/front/Config/Module/staging.json @@ -31,6 +31,13 @@ "labelKey": "customer_login" } }, + "ClientDashboard": { + "enabled": true, + "props": { + "path": "/client-dashboard/[folderUid]", + "labelKey": "client-dashboard" + } + }, "Folder": { "enabled": true, "props": { diff --git a/src/front/Hooks/useUser.ts b/src/front/Hooks/useUser.ts new file mode 100644 index 00000000..86a583a1 --- /dev/null +++ b/src/front/Hooks/useUser.ts @@ -0,0 +1,26 @@ +import Users from "@Front/Api/LeCoffreApi/Notary/Users/Users"; +import JwtService from "@Front/Services/JwtService/JwtService"; +import User from "le-coffre-resources/dist/Notary"; +import { useEffect, useState } from "react"; + +export default function useUser() { + const [user, setUser] = useState(); + + useEffect(() => { + const decodedJwt = JwtService.getInstance().decodeJwt(); + if (!decodedJwt) return; + Users.getInstance() + .getByUid(decodedJwt.userId, { + q: { + contact: true, + }, + }) + .then((user) => { + setUser(user); + }); + }, []); + + return { + user, + }; +}