Feature/client dashboard (#183)

This commit is contained in:
Maxime Sallerin 2024-08-19 16:33:40 +02:00 committed by GitHub
parent 065e8ad559
commit b6304a2814
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 527 additions and 1040 deletions

View File

@ -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;
}
}

View File

@ -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<IProps, IState> {
private inputRef = React.createRef<HTMLInputElement>();
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 (
<div className={classes["container"]}>
<div
className={classNames(
classes["root"],
this.props.document.document_status === EDocumentStatus.VALIDATED && classes["validated"],
)}
onDragOver={this.onDragOver}
onDrop={this.onDragDrop}
onDragLeave={this.onDragLeave}
data-drag-over={this.state.isDragOver.toString()}>
<input
type="file"
ref={this.inputRef}
hidden
onChange={this.onFileChange}
accept={Object.keys(filesAccepted).join(",")}
/>
<div className={classes["top-container"]}>
<div className={classes["left"]}>
<Image src={DepositDocumentIcon} alt="Deposit document" />
</div>
<div className={classes["separator"]} />
<div className={classes["right"]}>
<Typography typo={ETypo.TEXT_MD_SEMIBOLD} color={ETypoColor.COLOR_GENERIC_BLACK} className={classes["title"]}>
<div
className={
this.props.document.document_status === EDocumentStatus.VALIDATED ? classes["validated"] : ""
}>
{this.props.document.document_type?.name}
</div>
{this.props.document.document_type?.public_description !== " " &&
this.props.document.document_type?.public_description !== "" &&
this.props.document.document_status !== EDocumentStatus.VALIDATED && (
<Tooltip text={this.props.document.document_type?.public_description || ""} />
)}
{this.props.document.document_status === EDocumentStatus.VALIDATED && (
<Image src={GreenCheckIcon} alt="Document check" />
)}
</Typography>
{this.props.document.document_status !== EDocumentStatus.VALIDATED && (
<Typography color={ETypoColor.COLOR_NEUTRAL_500} typo={ETypo.TEXT_SM_REGULAR}>
Sélectionnez des documents .jpg, .pdf ou .png
</Typography>
)}
{this.props.document.document_history?.map((history) => (
<div key={history.uid}>{this.renderDocumentHistory(history)}</div>
))}
</div>
</div>
<div className={classes["documents-container"]}>
{this.state.files.map((file) => {
const fileObj = file.file;
if (file.archived) return;
return (
<div className={classes["file-container"]} key={fileObj.name + file.index}>
<div className={classes["left-part"]}>
<Image src={DocumentCheckIcon} alt="Document check" />
<Typography
typo={ETypo.TEXT_MD_REGULAR}
color={ETypoColor.COLOR_NEUTRAL_500}
title={file.fileName ?? fileObj.name}>
{this.shortName(file.fileName || fileObj.name)}
</Typography>
</div>
<Image
src={CrossIcon}
alt="Cross icon"
className={classes["cross"]}
onClick={this.removeFile}
data-file={file.index}
/>
</div>
);
})}
{this.state.loading && (
<div className={classes["file-container"]}>
<div className={classes["left-part"]}>
<div className={classes["loader"]}>
<Loader />
</div>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_NEUTRAL_500}>
Chargement...
</Typography>
</div>
<div />
</div>
)}
</div>
{this.props.document.document_status !== EDocumentStatus.VALIDATED && (
<div className={classes["bottom-container"]}>
<Button
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.TEXT}
className={classes["add-button"]}
onClick={this.addDocument}>
<Typography
typo={ETypo.TEXT_MD_SEMIBOLD}
color={ETypoColor.COLOR_SECONDARY_500}
className={classes["add-document"]}>
Ajouter un document <Image src={PlusIcon} alt="Plus icon" />
</Typography>
</Button>
</div>
)}
<Confirm
isOpen={this.state.isShowRefusedReasonModalVisible}
onClose={this.onCloseModalShowRefusedReason}
showCancelButton={false}
onAccept={this.onCloseModalShowRefusedReason}
closeBtn
header={"Motif du refus"}
confirmText={"J'ai compris"}>
<div className={classes["modal-content"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR} className={classes["text"]}>
Votre document a é refusé pour la raison suivante :
</Typography>
<TextAreaField placeholder="Description" defaultValue={this.state.refusedReason} readonly />
</div>
</Confirm>
</div>
{this.props.document.document_status === EDocumentStatus.REFUSED && (
<Typography typo={ETypo.TEXT_SM_REGULAR} className={classes["error-message"]}>
Ce document n'est pas conforme. Veuillez le déposer à nouveau.
</Typography>
)}
{this.state.showFailedUploaded && (
<Alert onClose={this.onCloseAlertUpload} header={"Fichier non autorisé"} isOpen={!!this.state.showFailedUploaded}>
<div className={classes["modal-content"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR} className={classes["text"]}>
{this.state.showFailedUploaded}
</Typography>
</div>
</Alert>
)}
</div>
);
}
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 (
<Typography color={ETypoColor.COLOR_NEUTRAL_500} typo={ETypo.TEXT_SM_REGULAR}>
Demandé par votre notaire le {this.formatDate(history.created_at!)}
</Typography>
);
case EDocumentStatus.VALIDATED:
return (
<Typography color={ETypoColor.COLOR_NEUTRAL_500} typo={ETypo.TEXT_SM_REGULAR}>
Validé par votre notaire le {this.formatDate(history.created_at!)}
</Typography>
);
case EDocumentStatus.DEPOSITED:
return (
<Typography color={ETypoColor.COLOR_NEUTRAL_500} typo={ETypo.TEXT_SM_REGULAR}>
Déposé le {this.formatDate(history.created_at!)}
</Typography>
);
case EDocumentStatus.REFUSED:
return (
<Typography typo={ETypo.TEXT_SM_REGULAR} color={ETypoColor.COLOR_ERROR_800}>
Document non conforme
{history.refused_reason && history.refused_reason.length > 0 && (
<Button
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.TEXT}
className={classes["refused-button"]}
onClick={() => this.showRefusedReason(history.refused_reason ?? "")}>
Voir le motif de refus
</Button>
)}
</Typography>
);
}
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<HTMLDivElement>) {
if (!this.state.isDragOver) {
this.setState({
isDragOver: true,
});
}
event.preventDefault();
}
private showRefusedReason(refusedReason: string) {
this.setState({
refusedReason,
});
this.onOpenModalShowRefusedReason();
}
private onDragLeave(event: React.DragEvent<HTMLDivElement>) {
this.setState({
isDragOver: false,
});
event.preventDefault();
}
private async onDragDrop(event: React.DragEvent<HTMLDivElement>) {
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");
}
}

View File

@ -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 (

View File

@ -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<any>} onDelete - Function to delete a file (must be used with defaultFiles)
* @param {(file: File) => Promise<any>} 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<any>;
onAddFile?: (file: File) => Promise<any>;
} & (
| { onDelete: (fileUid: string) => Promise<any>; onAddFile?: never; defaultFiles: IDocumentFileWithUid[] }
| { onDelete?: never; onAddFile: (file: File) => Promise<any>; defaultFiles: IDocumentFileWithUid[] }
| { onDelete?: (fileUid: string) => Promise<any>; onAddFile?: (file: File) => Promise<any>; 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<HTMLInputElement>(null);
const [documents, setDocuments] = useState<IDocument[]>([]);
const [documentFiles, setDocumentFiles] = useState<IDocumentFile[]>([]);
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<HTMLDivElement>) => {
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<HTMLInputElement>) => {
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 (
<div
className={classNames(classes["root"], documents.length > 0 && classes["filled"])}
className={classNames(classes["root"], documentFiles.length > 0 && classes["filled"])}
onDrop={handleDrop}
onDragOver={(e) => e.preventDefault()}>
<div className={classes["content"]}>
@ -127,20 +176,22 @@ export default function DragAndDrop(props: IProps) {
</Button>
</div>
<Typography typo={ETypo.TEXT_MD_LIGHT} color={ETypoColor.TEXT_SECONDARY}>
{description}
</Typography>
{description && (
<Typography typo={ETypo.TEXT_MD_LIGHT} color={ETypoColor.TEXT_SECONDARY}>
{description}
</Typography>
)}
</div>
</div>
{documents.length > 0 && (
{documentFiles.length > 0 && (
<div className={classes["documents"]}>
{documents.map((doc) => (
<DocumentElement
key={doc.id}
isLoading={doc.isLoading}
file={doc.file}
onRemove={() => handleRemove(doc.id)}
error={doc.error}
{documentFiles.map((documentFile) => (
<DocumentFileElement
key={documentFile.id}
isLoading={documentFile.isLoading}
file={documentFile.file}
onRemove={() => handleRemove(documentFile)}
error={documentFile.error}
/>
))}
</div>
@ -152,7 +203,6 @@ export default function DragAndDrop(props: IProps) {
return (
<input
ref={fileInputRef}
name={name}
type="file"
multiple
accept={Object.keys(mimeTypesAccepted).join(",")}

View File

@ -172,6 +172,8 @@ export enum ETypoColor {
TOASTER_CONTRAST_TITLE = "--toaster-contrast-title",
TOASTER_CONTRAST_TEXT = "--toaster-contrast-text",
TABLE_COLUMN_CONTRAST = "--table-column-contrast",
}
export default function Typography(props: IProps) {

View File

@ -0,0 +1,22 @@
@import "@Themes/constants.scss";
.root {
display: flex;
width: fit-content;
padding: var(--spacing-md, 16px);
flex-direction: column;
gap: var(--spacing-md, 16px);
background: var(--primary-weak-higlight, #e5eefa);
min-width: 300px;
.header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.delete-button {
margin: auto;
}
}

View File

@ -0,0 +1,48 @@
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import { Contact as ContactCustomer, Note } from "le-coffre-resources/dist/Customer";
import { Contact as ContactNotary } from "le-coffre-resources/dist/Notary";
import classes from "./classes.module.scss";
type IProps = {
contact: ContactCustomer | ContactNotary;
note: Note | null;
};
export default function ContactBox(props: IProps) {
const { contact, note } = props;
return (
<div className={classes["root"]}>
<div className={classes["header"]}>
<Typography typo={ETypo.TEXT_LG_BOLD} color={ETypoColor.COLOR_PRIMARY_500}>
{contact?.first_name} {contact?.last_name}
</Typography>
</div>
<div>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_NEUTRAL_700}>
Numéro de téléphone
</Typography>
<Typography typo={ETypo.TEXT_LG_REGULAR} color={ETypoColor.COLOR_NEUTRAL_950}>
{contact?.cell_phone_number ?? contact?.phone_number ?? "_"}
</Typography>
</div>
<div>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_NEUTRAL_700}>
E-mail
</Typography>
<Typography typo={ETypo.TEXT_LG_REGULAR} color={ETypoColor.COLOR_NEUTRAL_950}>
{contact?.email ?? "_"}
</Typography>
</div>
<div>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_NEUTRAL_700}>
Note client
</Typography>
<Typography typo={ETypo.TEXT_LG_REGULAR} color={ETypoColor.COLOR_NEUTRAL_950}>
{note?.content ?? "-"}
</Typography>
</div>
</div>
);
}

View File

@ -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<OfficeFolder[]>([]);
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 <DefaultDashboardWithList {...props} onSelectedBlock={onSelectedBlock} blocks={getBlocks(folders)} headerConnected={false}/>;
function getBlocks(folders: OfficeFolder[]): IBlock[] {
return folders.map((folder) => {
return {
id: folder.uid!,
primaryText: folder.name!,
secondaryText: folder.folder_number!,
isActive: folderUid === folder.uid,
};
});
}
}

View File

@ -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);
}
}

View File

@ -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 (
<div className={classes["root"]}>
<div className={classes["title"]}>
<Typography typo={ETypo.TEXT_MD_SEMIBOLD} color={ETypoColor.TEXT_PRIMARY}>
{document.document_type?.name ?? "_"}
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Demandé le: {document.created_at ? new Date(document.created_at).toLocaleDateString() : "_"}
</Typography>
</div>
<DragAndDrop
title={"Drag and drop ou"}
description={document.document_type?.public_description ?? undefined}
defaultFiles={defaultFiles}
onAddFile={addFile}
onDelete={deleteFile}
/>
</div>
);
}

View File

@ -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%;
}
}

View File

@ -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<Document[] | null>(null);
const [customer, setCustomer] = useState<Customer | null>(null);
const [contact, setContact] = useState<Customer["contact"] | null>(null);
const [folder, setFolder] = useState<OfficeFolder | null>(null);
const [note, setNote] = useState<Note | null>(null);
const [isAddDocumentModalVisible, setIsAddDocumentModalVisible] = useState<boolean>(false);
const [folder, setFolder] = useState<OfficeFolderNotary | null>(null);
const getDocuments = useCallback(async () => {
const [ribUrl, setRibUrl] = useState<string | null>(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 (
<div className={classes["header"]}>
<div className={classes["text-container"]}>
{/* TODO Get name from userStore */}
<div className={classes["title-container"]}>
<Typography typo={ETypo.DISPLAY_LARGE} className={classes["title"]}>
Bonjour {customer?.contact?.first_name.concat(" ", customer?.contact?.last_name)}
</Typography>
</div>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} className={classes["folder-number"]} color={ETypoColor.COLOR_NEUTRAL_500}>
Dossier {folder?.folder_number} - {folder?.name}
</Typography>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} className={classes["office-name"]} color={ETypoColor.COLOR_NEUTRAL_500}>
{folder?.office?.name}
</Typography>
<Typography typo={ETypo.TITLE_H3} className={classes["subtitle"]}>
Documents à envoyer
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} className={classes["text"]}>
Votre notaire est dans l'attente de documents pour valider votre dossier. Voici la liste des documents.
<br /> Veuillez glisser / déposer chaque document dans la zone prévue à cet effet ou cliquez sur la zone puis
sélectionnez le document correspondant. <br /> En déposant un document, celui-ci est automatiquement enregistré et
transmis à votre notaire.
</Typography>
<div className={classes["note-box"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_NEUTRAL_500}>
{note?.content}
</Typography>
</div>
</div>
<div className={classes["contact"]}>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} className={classes["contact-text"]} color={ETypoColor.COLOR_NEUTRAL_500}>
<p>
{contact?.first_name} {contact?.last_name}
</p>
<p>{contact?.phone_number ?? contact?.cell_phone_number}</p>
<p>{contact?.email}</p>
</Typography>
<div className="separator"></div>
{folder?.office?.rib_name && (
//Div to avoid the button to be on the same line as the text
<Button className={classes["contact-button"]} onClick={downloadFile}>
Télécharger le RIB de votre notaire
</Button>
)}
</div>
</div>
);
}, [
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 (
<DepositOtherDocument
folder_uid={folderUid!}
customer_uid={customer!.uid!}
open={isAddDocumentModalVisible}
onClose={onCloseModalAddDocument}
document={Document.hydrate<Document>({
document_type: DocumentType.hydrate<DocumentType>({
name: "Autres documents",
}),
})}
/>
);
}, [customer, folderUid, isAddDocumentModalVisible, onCloseModalAddDocument]);
}, [ribUrl]);
return (
<DefaultTemplate title={"Mon compte"} isPadding={false} hasHeaderLinks={false}>
<DefaultCustomerDashboard>
<div className={classes["root"]}>
{renderHeader()}
<div className={classes["sub-container"]}>
<div className={classes["content"]}>
<div className={classes["title-container"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Dossier {folder?.folder_number} - {folder?.name}
</Typography>
<Typography typo={ETypo.TITLE_H4} color={ETypoColor.TEXT_PRIMARY}>
Bonjour {customer?.contact?.first_name.concat(" ", customer?.contact?.last_name)}
</Typography>
<Tag color={ETagColor.INFO} label={"todo"} />
<div className={classes["office-container"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Office
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_ACCENT}>
{folder?.office?.name}
</Typography>
</div>
</div>
<div className={classes["content"]}>
<div className={classes["notary"]}>
<Typography typo={ETypo.TEXT_LG_BOLD} color={ETypoColor.TABLE_COLUMN_CONTRAST}>
Votre Notaire
</Typography>
{notaryContact && <ContactBox contact={notaryContact} note={note} />}
{ribUrl && (
<Button
fullwidth
onClick={downloadRib}
variant={EButtonVariant.PRIMARY}
size={EButtonSize.LG}
styletype={EButtonstyletype.CONTAINED}
rightIcon={<ArrowDownTrayIcon />}>
Télécharger le RIB
</Button>
)}
<Button fullwidth variant={EButtonVariant.PRIMARY} size={EButtonSize.LG} styletype={EButtonstyletype.OUTLINED}>
Voir les documents reçus
</Button>
</div>
<div className={classes["documents"]}>
<Typography typo={ETypo.TEXT_LG_BOLD} color={ETypoColor.TABLE_COLUMN_CONTRAST}>
Documents à envoyer
</Typography>
{documents?.map((document) => (
<DepositDocument document={document} key={document.uid} defaultFiles={document.files ?? []} />
<DepositDocumentComponent
key={document.uid}
document={document}
onChange={() => fetchDocuments(customer?.uid)}
/>
))}
</div>
<Typography typo={ETypo.TITLE_H3}>Documents supplémentaires (facultatif)</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} className={classes["text"]}>
Vous souhaitez envoyer d'autres documents à votre notaire ?
</Typography>
<Button
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.OUTLINED}
className={classes["button"]}
onClick={onOpenModalAddDocument}>
Ajouter d'autres documents
</Button>
</div>
</div>
{isAddDocumentModalVisible && renderBox()}
</DefaultTemplate>
</DefaultCustomerDashboard>
);
}

View File

@ -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<IProps, IState> {
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 (
<DefaultTemplate title={"Mon compte"} isPadding={false} hasHeaderLinks={false}>
<div className={classes["root"]}>
{this.renderHeader()}
<div className={classes["sub-container"]}>
<div className={classes["content"]}>
{this.state.documents?.map((document) => (
<DepositDocument document={document} key={document.uid} defaultFiles={document.files ?? []} />
))}
</div>
<Typography typo={ETypo.TITLE_H3}>Documents supplémentaires (facultatif)</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} className={classes["text"]}>
Vous souhaitez envoyer d'autres documents à votre notaire ?
</Typography>
<Button
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.OUTLINED}
className={classes["button"]}
onClick={this.onOpenModalAddDocument}>
Ajouter d'autres documents
</Button>
</div>
<Confirm
isOpen={this.state.isAddDocumentModalVisible}
onClose={this.onCloseModalAddDocument}
onAccept={this.onOpenModalAddDocument}
closeBtn
header={"Ajouter un document"}
cancelText={"Annuler"}
confirmText={"Déposer le document"}>
<div className={classes["modal-content"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR} className={classes["text"]}>
Vous souhaitez envoyer un autre document à votre notaire ?
</Typography>
<TextField placeholder="Nom du document" />
<Typography typo={ETypo.TEXT_MD_REGULAR} className={classes["text"]}>
Glissez / Déposez votre document dans la zone prévue à cet effet ou cliquez sur la zone puis sélectionnez le
document correspondant.
</Typography>
<DepositDocument
document={Document.hydrate<Document>({
document_type: DocumentType.hydrate<DocumentType>({
name: "Autres documents",
}),
})}
/>
</div>
</Confirm>
</div>
</DefaultTemplate>
);
}
private renderHeader(): JSX.Element {
return (
<div className={classes["header"]}>
<div className={classes["text-container"]}>
{/* TODO Get name from userStore */}
<Typography typo={ETypo.DISPLAY_LARGE} className={classes["title"]}>
Bonjour {this.state.mockedCustomer?.contact?.first_name.concat(" ", this.state.mockedCustomer?.contact?.last_name)}
</Typography>
<Typography typo={ETypo.TITLE_H3} className={classes["subtitle"]}>
Documents à envoyer
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} className={classes["text"]}>
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.
</Typography>
</div>
</div>
);
}
// 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 });
}
}

View File

@ -131,7 +131,7 @@ export default function DesignSystem() {
Logged Out
</Button>
<Typography typo={ETypo.TEXT_LG_BOLD}>Drag and Drop</Typography>
<DragAndDrop name="test" title="Upload de document" description="Description" />
<DragAndDrop title="Upload de document" description="Description" />
<Typography typo={ETypo.TEXT_LG_BOLD}>Separators</Typography>
<div className={classes["rows"]}>
<Separator color={ESeperatorColor.DEFAULT} direction={ESeperatorDirection.HORIZONTAL} />

View File

@ -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<User | null>();
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()

View File

@ -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<OfficeFolder[]>([]);
const router = useRouter();
const [folders, setFolders] = useState<OfficeFolder[]>([]);
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],
);

View File

@ -31,6 +31,13 @@
"labelKey": "customer_login"
}
},
"ClientDashboard": {
"enabled": true,
"props": {
"path": "/client-dashboard/[folderUid]",
"labelKey": "client-dashboard"
}
},
"Folder": {
"enabled": true,
"props": {

View File

@ -31,6 +31,13 @@
"labelKey": "customer_login"
}
},
"ClientDashboard": {
"enabled": true,
"props": {
"path": "/client-dashboard/[folderUid]",
"labelKey": "client-dashboard"
}
},
"Folder": {
"enabled": true,
"props": {

View File

@ -31,6 +31,13 @@
"labelKey": "customer_login"
}
},
"ClientDashboard": {
"enabled": true,
"props": {
"path": "/client-dashboard/[folderUid]",
"labelKey": "client-dashboard"
}
},
"Folder": {
"enabled": true,
"props": {

View File

@ -31,6 +31,13 @@
"labelKey": "customer_login"
}
},
"ClientDashboard": {
"enabled": true,
"props": {
"path": "/client-dashboard/[folderUid]",
"labelKey": "client-dashboard"
}
},
"Folder": {
"enabled": true,
"props": {

View File

@ -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<User | null>();
useEffect(() => {
const decodedJwt = JwtService.getInstance().decodeJwt();
if (!decodedJwt) return;
Users.getInstance()
.getByUid(decodedJwt.userId, {
q: {
contact: true,
},
})
.then((user) => {
setUser(user);
});
}, []);
return {
user,
};
}