Refacto/folder information wip (#172)

This commit is contained in:
Maxime Sallerin 2024-07-19 16:58:13 +02:00 committed by GitHub
parent 9ec14f0202
commit 608b9583ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 1379 additions and 568 deletions

View File

@ -0,0 +1,20 @@
@import "@Themes/constants.scss";
.root {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: var(--spacing-3, 24px);
gap: var(--spacing-lg, 24px);
border-radius: var(--radius-minimal, 8px);
background: var(--primary-weak-higlight, #e5eefa);
text-align: center;
svg {
width: 32px;
stroke: var(--primary-weak-contrast);
}
}

View File

@ -0,0 +1,28 @@
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import React from "react";
import classes from "./classes.module.scss";
type IProps = {
icon: React.ReactNode;
title: string;
description: string;
footer?: React.ReactNode;
};
export default function EmptyAlert(props: IProps) {
const { icon, title, description, footer } = props;
return (
<div className={classes["root"]}>
{icon}
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} color={ETypoColor.COLOR_NEUTRAL_950}>
{title}
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_NEUTRAL_700}>
{description}
</Typography>
{footer}
</div>
);
}

View File

@ -21,7 +21,7 @@
.header { .header {
display: flex; display: flex;
align-items: center; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
padding: var(--spacing-md, 16px) var(--modal-spacing, 16px); padding: var(--spacing-md, 16px) var(--modal-spacing, 16px);
gap: var(--spacing-md, 16px); gap: var(--spacing-md, 16px);

View File

@ -8,7 +8,7 @@ import useOpenable from "@Front/Hooks/useOpenable";
export type ITabValue<T> = T & { export type ITabValue<T> = T & {
id: unknown; id: unknown;
}; }
type ITabInternal<T> = ITab & { type ITabInternal<T> = ITab & {
key?: string; key?: string;

View File

@ -0,0 +1,11 @@
@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;
}

View File

@ -0,0 +1,57 @@
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import { ICustomer } from "..";
import classes from "./classes.module.scss";
import { PencilSquareIcon, TrashIcon } from "@heroicons/react/24/outline";
import IconButton, { EIconButtonVariant } from "@Front/Components/DesignSystem/IconButton";
import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
type IProps = {
customer: ICustomer;
};
export default function ClientBox(props: IProps) {
const { customer } = props;
return (
<div className={classes["root"]}>
<div className={classes["header"]}>
<Typography typo={ETypo.TEXT_LG_BOLD} color={ETypoColor.COLOR_PRIMARY_500}>
{customer.contact?.last_name}
</Typography>
<IconButton variant={EIconButtonVariant.NEUTRAL} icon={<PencilSquareIcon />} />
</div>
<div>
<Typography typo={ETypo.TEXT_LG_BOLD} color={ETypoColor.COLOR_NEUTRAL_700}>
Numéro de téléphone
</Typography>
<Typography typo={ETypo.TEXT_LG_REGULAR} color={ETypoColor.COLOR_PRIMARY_500}>
{customer.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}>
{customer.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}>
TODO
</Typography>
</div>
<Button
className={classes["delete-button"]}
variant={EButtonVariant.ERROR}
styletype={EButtonstyletype.TEXT}
rightIcon={<TrashIcon />}>
Supprimer le client
</Button>
</div>
);
}

View File

@ -0,0 +1,14 @@
@import "@Themes/constants.scss";
.root {
width: 100%;
display: flex;
flex-direction: column;
gap: var(--spacing-xl, 32px);
.title {
display: flex;
justify-content: space-between;
align-items: center;
}
}

View File

@ -0,0 +1,155 @@
import Documents from "@Front/Api/LeCoffreApi/Notary/Documents/Documents";
import CircleProgress from "@Front/Components/DesignSystem/CircleProgress";
import IconButton from "@Front/Components/DesignSystem/IconButton";
import Table from "@Front/Components/DesignSystem/Table";
import { IHead, IRowProps } from "@Front/Components/DesignSystem/Table/MuiTable";
import Tag, { ETagColor, ETagVariant } from "@Front/Components/DesignSystem/Tag";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import { ArrowDownTrayIcon, EyeIcon, TrashIcon } from "@heroicons/react/24/outline";
import { Document } from "le-coffre-resources/dist/Customer";
import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document";
import { useCallback, useEffect, useMemo, useState } from "react";
import classes from "./classes.module.scss";
type IProps = {
documents: Document[];
totalOfDocumentTypes: number;
};
const header: readonly IHead[] = [
{
key: "document_type",
title: "Type de document",
},
{
key: "document_status",
title: "Statut",
},
{
key: "created_at",
title: "Demandé le",
},
{
key: "actions",
title: "Actions",
},
];
export default function DocumentTables(props: IProps) {
const { documents: documentsProps, totalOfDocumentTypes } = props;
const [documents, setDocuments] = useState<Document[]>(documentsProps);
useEffect(() => {
setDocuments(documentsProps);
}, [documentsProps]);
const deleteAskedDocument = useCallback(
(uid: string | undefined) => {
if (!uid) return;
return Documents.getInstance()
.delete(uid)
.then(() => setDocuments(documents.filter((document) => document.uid !== uid)))
.catch((error) => console.warn(error));
},
[documents],
);
const askDocuments: IRowProps[] = useMemo(
() =>
documents
.map((document) => {
if (document.document_status !== EDocumentStatus.ASKED) return null;
return {
key: document.uid,
document_type: document.document_type?.name ?? "_",
document_status: (
<Tag color={ETagColor.INFO} variant={ETagVariant.SEMI_BOLD} label={document.document_status.toUpperCase()} />
),
created_at: document.created_at ? new Date(document.created_at).toLocaleDateString() : "_",
actions: <IconButton icon={<TrashIcon onClick={() => deleteAskedDocument(document.uid)} />} />,
};
})
.filter((document) => document !== null) as IRowProps[],
[deleteAskedDocument, documents],
);
const toValidateDocuments: IRowProps[] = useMemo(
() =>
documents
.map((document) => {
if (document.document_status !== EDocumentStatus.DEPOSITED) return null;
return {
key: document.uid,
document_type: document.document_type?.name ?? "_",
document_status: (
<Tag color={ETagColor.WARNING} variant={ETagVariant.SEMI_BOLD} label={document.document_status.toUpperCase()} />
),
created_at: document.created_at,
actions: <IconButton icon={<EyeIcon />} />,
};
})
.filter((document) => document !== null) as IRowProps[],
[documents],
);
const validatedDocuments: IRowProps[] = useMemo(
() =>
documents
.map((document) => {
if (document.document_status !== EDocumentStatus.VALIDATED) return null;
return {
key: document.uid,
document_type: document.document_type?.name ?? "_",
document_status: (
<Tag color={ETagColor.SUCCESS} variant={ETagVariant.SEMI_BOLD} label={document.document_status.toUpperCase()} />
),
created_at: document.created_at,
actions: (
<div className={classes["actions"]}>
<IconButton icon={<EyeIcon />} />
<IconButton icon={<ArrowDownTrayIcon />} />
</div>
),
};
})
.filter((document) => document !== null) as IRowProps[],
[documents],
);
const refusedDocuments: IRowProps[] = useMemo(
() =>
documents
.map((document) => {
if (document.document_status !== EDocumentStatus.REFUSED) return null;
return {
key: document.uid,
document_type: document.document_type?.name ?? "_",
document_status: (
<Tag color={ETagColor.ERROR} variant={ETagVariant.SEMI_BOLD} label={document.document_status.toUpperCase()} />
),
created_at: document.created_at,
actions: "",
};
})
.filter((document) => document !== null) as IRowProps[],
[documents],
);
const progressValidated = useMemo(() => validatedDocuments.length / totalOfDocumentTypes, [validatedDocuments, totalOfDocumentTypes]);
return (
<div className={classes["root"]}>
<div className={classes["title"]}>
<Typography typo={ETypo.TEXT_LG_BOLD} color={ETypoColor.COLOR_NEUTRAL_950}>
Documents
</Typography>
<CircleProgress percentage={progressValidated} />
</div>
<Table header={header} rows={askDocuments} />
{toValidateDocuments.length > 0 && <Table header={header} rows={toValidateDocuments} />}
{validatedDocuments.length > 0 && <Table header={header} rows={validatedDocuments} />}
{refusedDocuments.length > 0 && <Table header={header} rows={refusedDocuments} />}
</div>
);
}

View File

@ -0,0 +1,7 @@
@import "@Themes/constants.scss";
.root {
display: flex;
flex-direction: column;
gap: var(--spacing-xl, 32px);
}

View File

@ -0,0 +1,20 @@
import EmptyAlert from "@Front/Components/DesignSystem/EmptyAlert";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import { DocumentIcon } from "@heroicons/react/24/outline";
import classes from "./classes.module.scss";
export default function NoDocument() {
return (
<div className={classes["root"]}>
<Typography typo={ETypo.TEXT_LG_BOLD} color={ETypoColor.COLOR_NEUTRAL_950}>
Documents
</Typography>
<EmptyAlert
icon={<DocumentIcon />}
title="Aucune demande de document"
description="Vous n'avez encore demandé aucun document pour ce client. Pour commencer, cliquez sur le bouton ci-dessous pour créer une nouvelle demande de document."
/>
</div>
);
}

View File

@ -0,0 +1,26 @@
@import "@Themes/constants.scss";
.root {
display: flex;
flex-direction: column;
gap: var(--spacing-xl, 32px);
.tab-container {
display: flex;
gap: var(--spacing-md, 16px);
justify-content: space-between;
align-items: center;
}
.content {
display: flex;
gap: var(--spacing-lg, 24px);
.client-box {
display: flex;
flex-direction: column;
gap: var(--spacing-lg, 24px);
}
}
}

View File

@ -0,0 +1,92 @@
import Tabs from "@Front/Components/Elements/Tabs";
import Customer from "le-coffre-resources/dist/Customer";
import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import { useMemo, useState } from "react";
import { AnchorStatus } from "..";
import classes from "./classes.module.scss";
import ClientBox from "./ClientBox";
import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import { DocumentIcon, UserPlusIcon } from "@heroicons/react/24/outline";
import Module from "@Front/Config/Module";
import Link from "next/link";
import NoDocument from "./NoDocument";
import DocumentTables from "./DocumentTables";
type IProps = {
folder: OfficeFolder;
anchorStatus: AnchorStatus;
};
export type ICustomer = Customer & { id: string };
export default function ClientView(props: IProps) {
const { folder, anchorStatus } = props;
const customers: ICustomer[] = useMemo(
() =>
folder?.customers?.map((customer) => ({
id: customer.uid ?? "",
...customer,
})) ?? [],
[folder],
);
const [customer, setCustomer] = useState<(typeof customers)[number]>(customers[0]!);
const tabs = useMemo(
() =>
customers.map((customer) => ({
label: `${customer.contact?.first_name} ${customer.contact?.last_name}`,
key: customer.uid,
value: customer,
})),
[customers],
);
const doesCustomerHaveDocument = useMemo(() => customer.documents && customer.documents.length > 0, [customer]);
const totalOfDocumentTypes = useMemo(() => folder.deed?.document_types?.length ?? 0, [folder]);
return (
<section className={classes["root"]}>
<div className={classes["tab-container"]}>
{tabs && <Tabs<ICustomer> tabs={tabs} onSelect={setCustomer} />}
{anchorStatus === AnchorStatus.NOT_ANCHORED && (
<Link
href={Module.getInstance()
.get()
.modules.pages.Folder.pages.AddClient.props.path.replace("[folderUid]", folder.uid ?? "")}>
<Button
size={EButtonSize.MD}
rightIcon={<UserPlusIcon />}
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.TEXT}>
Ajouter un client
</Button>
</Link>
)}
</div>
<div className={classes["content"]}>
<div className={classes["client-box"]}>
<ClientBox customer={customer} />
<Link
href={Module.getInstance()
.get()
.modules.pages.Folder.pages.AskDocument.props.path.replace("[folderUid]", folder.uid ?? "")
.replace("[customerUid]", customer.uid ?? "")}>
<Button rightIcon={<DocumentIcon />} variant={EButtonVariant.PRIMARY} fullwidth>
Demander un document
</Button>
</Link>
</div>
{doesCustomerHaveDocument ? (
<DocumentTables documents={customer.documents ?? []} totalOfDocumentTypes={totalOfDocumentTypes} />
) : (
<NoDocument />
)}
</div>
</section>
);
}

View File

@ -0,0 +1,113 @@
@import "@Themes/constants.scss";
.root {
display: flex;
align-items: center;
flex-direction: column;
min-height: 100%;
.no-folder-selected {
width: 100%;
.choose-a-folder {
margin-top: 96px;
text-align: center;
}
}
.folder-informations {
width: 100%;
min-height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: column;
flex-grow: 1;
.folder-header {
width: 100%;
.header {
margin-bottom: 32px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
@media (max-width: $screen-m) {
flex-wrap: wrap;
.title {
margin-bottom: 24px;
}
}
}
}
}
.second-box {
margin-top: 24px;
margin-bottom: 32px;
}
.progress-bar {
margin-bottom: 32px;
}
.button-container {
width: 100%;
display: flex;
gap: 16px;
text-align: center;
justify-content: center;
.delete-folder {
display: flex;
margin-left: 12px;
}
@media (max-width: $screen-m) {
display: flex;
flex-direction: column;
.delete-folder {
margin-left: 0;
margin-top: 12px;
> * {
flex: 1;
}
}
> * {
width: 100%;
}
}
}
.modal-title {
margin-bottom: 24px;
}
}
.validate-document-container {
.document-validating-container {
.validate-gif {
width: 100%;
height: 100%;
object-fit: contain;
}
}
}
.loader-container {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
height: 100%;
.loader {
width: 21px;
height: 21px;
}
}

View File

@ -0,0 +1,541 @@
import ChevronIcon from "@Assets/Icons/chevron.svg";
import Folders from "@Front/Api/LeCoffreApi/Notary/Folders/Folders";
import OfficeFolderAnchors from "@Front/Api/LeCoffreApi/Notary/OfficeFolderAnchors/OfficeFolderAnchors";
import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import FolderBoxInformation, { EFolderBoxInformationType } from "@Front/Components/DesignSystem/FolderBoxInformation";
import TextAreaField from "@Front/Components/DesignSystem/Form/TextareaField";
import QuantityProgressBar from "@Front/Components/DesignSystem/QuantityProgressBar";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import DefaultNotaryDashboard from "@Front/Components/LayoutTemplates/DefaultNotaryDashboard";
import Module from "@Front/Config/Module";
import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import { EDocumentStatus } from "le-coffre-resources/dist/Notary/Document";
import Link from "next/link";
import { NextRouter, useRouter } from "next/router";
import { ChangeEvent, useCallback, useEffect, useState } from "react";
import Image from "next/image";
import ValidateAnchoringGif from "@Front/Assets/images/validate_anchoring.gif";
import BasePage from "../../../Base";
import classes from "./classes.module.scss";
import ClientSection from "./ClientSection";
import Loader from "@Front/Components/DesignSystem/Loader";
import Newsletter from "@Front/Components/DesignSystem/Newsletter";
import Confirm from "@Front/Components/DesignSystem/OldModal/Confirm";
export enum AnchorStatus {
"VERIFIED_ON_CHAIN" = "VERIFIED_ON_CHAIN",
"ANCHORING" = "ANCHORING",
"NOT_ANCHORED" = "NOT_ANCHORED",
}
type IProps = {};
type IPropsClass = IProps & {
router: NextRouter;
selectedFolderUid: string;
isAnchored: AnchorStatus;
isLoading: boolean;
selectedFolder: OfficeFolder | null;
getAnchoringStatus: () => Promise<void>;
getFolderCallback: () => Promise<void>;
openedCustomer?: string;
};
type IState = {
isArchivedModalOpen: boolean;
inputArchivedDescripton: string;
isValidateModalVisible: boolean;
hasValidateAnchoring: boolean;
isVerifDeleteModalVisible: boolean;
isPreventArchiveModalOpen: boolean;
loadingAnchoring: boolean;
};
class FolderInformationClass extends BasePage<IPropsClass, IState> {
public constructor(props: IPropsClass) {
super(props);
this.state = {
isArchivedModalOpen: false,
inputArchivedDescripton: "",
isValidateModalVisible: false,
hasValidateAnchoring: false,
isVerifDeleteModalVisible: false,
isPreventArchiveModalOpen: false,
loadingAnchoring: false,
};
this.openArchivedModal = this.openArchivedModal.bind(this);
this.closeArchivedModal = this.closeArchivedModal.bind(this);
this.onArchivedModalAccepted = this.onArchivedModalAccepted.bind(this);
this.onPreventArchiveModalAccepted = this.onPreventArchiveModalAccepted.bind(this);
this.getCompletionNumber = this.getCompletionNumber.bind(this);
this.onArchivedDescriptionInputChange = this.onArchivedDescriptionInputChange.bind(this);
this.deleteFolder = this.deleteFolder.bind(this);
this.closeModal = this.closeModal.bind(this);
this.validateAnchoring = this.validateAnchoring.bind(this);
this.openValidateModal = this.openValidateModal.bind(this);
this.openVerifDeleteFolder = this.openVerifDeleteFolder.bind(this);
this.closeVerifDeleteFolder = this.closeVerifDeleteFolder.bind(this);
this.closePreventArchiveModal = this.closePreventArchiveModal.bind(this);
}
// TODO: Message if the user has not created any folder yet
// TODO: get the selected folder from the api in componentDidMount
public override render(): JSX.Element {
const redirectPathEditCollaborators = Module.getInstance()
.get()
.modules.pages.Folder.pages.EditCollaborators.props.path.replace("[folderUid]", this.props.selectedFolderUid);
return (
<DefaultNotaryDashboard title={"Dossier"} isArchived={false} mobileBackText="Retour aux dossiers">
{!this.props.isLoading && (
<div className={classes["root"]}>
{this.props.selectedFolder ? (
<div className={classes["folder-informations"]}>
<div className={classes["folder-header"]}>
<div className={classes["header"]}>
<div className={classes["title"]}>
<Typography typo={ETypo.TITLE_H1}>Informations du dossier</Typography>
</div>
<Link href={redirectPathEditCollaborators}>
<Button
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.TEXT}
rightIcon={ChevronIcon}>
Modifier les collaborateurs
</Button>
</Link>
</div>
<FolderBoxInformation
anchorStatus={this.props.isAnchored}
folder={this.props.selectedFolder}
type={EFolderBoxInformationType.INFORMATIONS}
/>
<div className={classes["second-box"]}>
<FolderBoxInformation
anchorStatus={this.props.isAnchored}
folder={this.props.selectedFolder}
type={EFolderBoxInformationType.DESCRIPTION}
/>
</div>
<div className={classes["progress-bar"]}>
<QuantityProgressBar
title="Complétion du dossier"
total={100}
currentNumber={this.getCompletionNumber()}
/>
</div>
{this.doesFolderHaveCustomer() && (
<ClientSection
folder={this.props.selectedFolder}
anchorStatus={this.props.isAnchored}
getFolderCallback={this.props.getFolderCallback}
openedCustomer={this.props.openedCustomer}
/>
)}
</div>
{!this.doesFolderHaveCustomer() && (
<ClientSection
folder={this.props.selectedFolder}
anchorStatus={this.props.isAnchored}
getFolderCallback={this.props.getFolderCallback}
openedCustomer={this.props.openedCustomer}
/>
)}
<div className={classes["button-container"]}>
<Button
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.OUTLINED}
onClick={this.openArchivedModal}>
Archiver le dossier
</Button>
{this.everyDocumentValidated() && !this.props.isLoading && (
<>
{this.props.isAnchored === AnchorStatus.NOT_ANCHORED && (
<Button variant={EButtonVariant.PRIMARY} onClick={this.openValidateModal}>
Valider et ancrer
</Button>
)}
{this.props.isAnchored === AnchorStatus.ANCHORING && (
<Button variant={EButtonVariant.PRIMARY} disabled>
Demande d'ancrage envoyée...&nbsp;&nbsp;
<div className={classes["loader-container"]}>
<div className={classes["loader"]}>
<Loader />
</div>
</div>
</Button>
)}
{this.props.isAnchored === AnchorStatus.VERIFIED_ON_CHAIN && (
<Button
variant={EButtonVariant.PRIMARY}
onClick={() => this.downloadAnchoringProof(this.props.selectedFolder?.uid)}
disabled={this.state.loadingAnchoring}>
Télécharger la preuve d'ancrage
{this.state.loadingAnchoring && (
<div className={classes["loader-container"]}>
<div className={classes["loader"]}>
<Loader />
</div>
</div>
)}
</Button>
)}
</>
)}
{this.canDeleteFolder() && (
<span className={classes["delete-folder"]} onClick={this.openVerifDeleteFolder}>
<Button variant={EButtonVariant.SECONDARY}>Supprimer le dossier</Button>
</span>
)}
</div>
<Confirm
isOpen={this.state.isArchivedModalOpen}
onAccept={this.onArchivedModalAccepted}
onClose={this.closeArchivedModal}
closeBtn
header={"Archiver le dossier ?"}
cancelText={"Annuler"}
confirmText={"Archiver"}>
<div className={classes["modal-title"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR}>Souhaitez-vous vraiment archiver le dossier ?</Typography>
</div>
<TextAreaField
name="archived_description"
placeholder="Description"
onChange={this.onArchivedDescriptionInputChange}
/>
</Confirm>
<Confirm
isOpen={this.state.isPreventArchiveModalOpen}
onAccept={this.onPreventArchiveModalAccepted}
onClose={this.closePreventArchiveModal}
closeBtn
header={"Archiver le dossier"}
cancelText={"Annuler"}
confirmText={"Archiver"}>
<div className={classes["modal-title"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR}>
Vous êtes en train darchiver le dossier sans avoir lancré, êtes-vous sûr de vouloir le faire ?
</Typography>
</div>
<TextAreaField
name="archived_description"
placeholder="Description"
onChange={this.onArchivedDescriptionInputChange}
/>
</Confirm>
<Confirm
isOpen={this.state.isVerifDeleteModalVisible}
onAccept={this.deleteFolder}
onClose={this.closeVerifDeleteFolder}
closeBtn
header={"Êtes-vous sûr de vouloir supprimer ce dossier ?"}
cancelText={"Annuler"}
confirmText={"Confirmer"}>
<div className={classes["modal-title"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR}>Cette action sera irréversible.</Typography>
</div>
</Confirm>
<Newsletter isOpen={false} />
</div>
) : (
<div className={classes["no-folder-selected"]}>
<Typography typo={ETypo.TITLE_H1}>Informations du dossier</Typography>
<div className={classes["choose-a-folder"]}>
<Typography typo={ETypo.TEXT_LG_REGULAR} color={ETypoColor.COLOR_NEUTRAL_500}>
Sélectionnez un dossier
</Typography>
</div>
</div>
)}
</div>
)}
{this.props.isLoading && (
<div className={classes["loader-container"]}>
<div className={classes["loader"]}>
<Loader />
</div>
</div>
)}
<Confirm
isOpen={this.state.isValidateModalVisible}
onClose={this.closeModal}
onAccept={this.validateAnchoring}
closeBtn={true}
hasContainerClosable={true}
header={
this.state.hasValidateAnchoring
? "Dossier en cours de certification"
: "Êtes-vous sûr de vouloir ancrer et certifier ce dossier ?"
}
cancelText={"Annuler"}
confirmText={"Confirmer"}
showButtons={!this.state.hasValidateAnchoring}>
<div className={classes["validate-document-container"]}>
{!this.state.hasValidateAnchoring && (
<Typography
typo={ETypo.TEXT_MD_REGULAR}
color={ETypoColor.COLOR_GENERIC_BLACK}
className={classes["validate-text"]}>
Les documents du dossier seront certifiés sur la blockchain. Pensez à bien télécharger l'ensemble des
documents du dossier ainsi que le fichier de preuve d'ancrage pour les mettre dans la GED de votre logiciel
de rédaction d'actes.
</Typography>
)}
{this.state.hasValidateAnchoring && (
<div className={classes["document-validating-container"]}>
<Typography
typo={ETypo.TEXT_MD_REGULAR}
color={ETypoColor.COLOR_GENERIC_BLACK}
className={classes["validate-text"]}>
Veuillez revenir sur le dossier dans 5 minutes et rafraîchir la page pour télécharger le dossier de
preuve d'ancrage et le glisser dans la GED de votre logiciel de rédaction d'acte.
</Typography>
<Image src={ValidateAnchoringGif} alt="Anchoring animation" className={classes["validate-gif"]} />
</div>
)}
</div>
</Confirm>
</DefaultNotaryDashboard>
);
}
private closePreventArchiveModal() {
this.setState({
isPreventArchiveModalOpen: false,
});
}
public openVerifDeleteFolder() {
this.setState({
isVerifDeleteModalVisible: true,
});
}
public closeVerifDeleteFolder() {
this.setState({
isVerifDeleteModalVisible: false,
});
}
private closeModal() {
this.setState({
isValidateModalVisible: false,
});
}
private openValidateModal() {
this.setState({
isValidateModalVisible: true,
});
}
private async validateAnchoring() {
this.setState({
hasValidateAnchoring: true,
});
try {
const timeoutDelay = 9800;
await this.anchorFolder();
setTimeout(() => {
this.setState({
isValidateModalVisible: false,
});
}, timeoutDelay);
setTimeout(() => {
this.setState({
hasValidateAnchoring: false,
});
}, timeoutDelay + 1000);
} catch (e) {
this.setState({
isValidateModalVisible: false,
hasValidateAnchoring: false,
});
console.error(e);
}
}
private async anchorFolder() {
if (!this.props.selectedFolder?.uid) return;
await OfficeFolderAnchors.getInstance().post(this.props.selectedFolder.uid);
this.props.getAnchoringStatus();
}
private async downloadAnchoringProof(uid?: string) {
if (!uid) return;
this.setState({ loadingAnchoring: true });
try {
const file: Blob = await OfficeFolderAnchors.getInstance().download(uid);
const url = window.URL.createObjectURL(file);
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
// the filename you want
a.download = `anchoring_proof_${this.props.selectedFolder?.folder_number}_${this.props.selectedFolder?.name}.zip`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
this.setState({ loadingAnchoring: false });
} catch (e) {
this.setState({ loadingAnchoring: false });
console.error(e);
}
}
private everyDocumentValidated(): boolean {
if (!this.props.selectedFolder?.documents) return false;
return (
this.props.selectedFolder?.documents?.length >= 1 &&
this.props.selectedFolder?.documents.every((document) => document.document_status === EDocumentStatus.VALIDATED)
);
}
private async deleteFolder() {
if (!this.props.selectedFolder?.uid) return;
await Folders.getInstance().delete(this.props.selectedFolder.uid);
this.props.router.push(Module.getInstance().get().modules.pages.Folder.props.path);
}
private getCompletionNumber() {
const documents = this.props.selectedFolder?.documents;
if (!documents) return 0;
const totalDocuments = documents.length;
const refusedDocuments = documents.filter((document) => document.document_status === EDocumentStatus.REFUSED).length ?? 0;
const askedDocuments =
documents.filter(
(document) => document.document_status === EDocumentStatus.ASKED || document.document_status === EDocumentStatus.DEPOSITED,
).length ?? 0;
const depositedDocuments = totalDocuments - askedDocuments - refusedDocuments;
const percentage = (depositedDocuments / totalDocuments) * 100;
return isNaN(percentage) ? 0 : percentage;
}
private doesFolderHaveCustomer(): boolean {
if (!this.props.selectedFolder?.customers) return false;
return this.props.selectedFolder?.customers!.length > 0;
}
private canDeleteFolder(): boolean {
return (this.props.selectedFolder?.customers?.length ?? 0) === 0 && (this.props.selectedFolder?.documents?.length ?? 0) === 0;
}
private openArchivedModal(): void {
if (this.everyDocumentValidated() && this.props.isAnchored === AnchorStatus.VERIFIED_ON_CHAIN) {
this.setState({ isArchivedModalOpen: true });
} else {
this.setState({ isPreventArchiveModalOpen: true });
}
}
private closeArchivedModal(): void {
this.setState({ isArchivedModalOpen: false });
}
private onArchivedDescriptionInputChange(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) {
this.setState({ inputArchivedDescripton: e.target.value });
}
private async onArchivedModalAccepted() {
if (!this.props.selectedFolder) return;
await Folders.getInstance().archive(this.props.selectedFolder.uid ?? "", this.state.inputArchivedDescripton);
this.closeArchivedModal();
this.props.router.push(Module.getInstance().get().modules.pages.Folder.props.path);
}
private async onPreventArchiveModalAccepted() {
if (!this.props.selectedFolder) return;
await Folders.getInstance().archive(this.props.selectedFolder.uid ?? "", this.state.inputArchivedDescripton);
this.closePreventArchiveModal();
this.props.router.push(Module.getInstance().get().modules.pages.Folder.props.path);
}
}
export default function FolderInformation(props: IProps) {
const router = useRouter();
const [isAnchored, setIsAnchored] = useState<AnchorStatus>(AnchorStatus.NOT_ANCHORED);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [selectedFolder, setSelectedFolder] = useState<OfficeFolder | null>(null);
let { folderUid } = router.query;
const customerUid = router.query["customerUid"] as string | undefined;
folderUid = folderUid as string;
const getAnchoringStatus = useCallback(async () => {
if (!folderUid) return;
try {
const anchorStatus = await OfficeFolderAnchors.getInstance().getByUid(folderUid as string);
setIsAnchored(anchorStatus.status === "VERIFIED_ON_CHAIN" ? AnchorStatus.VERIFIED_ON_CHAIN : AnchorStatus.ANCHORING);
} catch (e) {
setIsAnchored(AnchorStatus.NOT_ANCHORED);
}
}, [folderUid]);
const getFolder = useCallback(async () => {
if (!folderUid) return;
setIsLoading(true);
const query = {
q: {
deed: { include: { deed_type: true } },
office: true,
customers: {
include: {
contact: true,
documents: {
include: {
folder: true,
document_type: true,
files: true,
},
},
},
},
documents: {
include: {
depositor: {
include: {
contact: true,
},
},
},
},
folder_anchor: true,
notes: {
include: {
customer: true,
},
},
},
};
const folder = await Folders.getInstance().getByUid(folderUid as string, query);
if (folder) {
setSelectedFolder(folder);
getAnchoringStatus();
}
setIsLoading(false);
}, [folderUid, getAnchoringStatus]);
useEffect(() => {
setIsLoading(true);
getFolder();
}, [getFolder]);
return (
<FolderInformationClass
{...props}
selectedFolderUid={folderUid}
router={router}
isAnchored={isAnchored}
isLoading={isLoading}
selectedFolder={selectedFolder}
getAnchoringStatus={getAnchoringStatus}
getFolderCallback={getFolder}
openedCustomer={customerUid}
/>
);
}

View File

@ -0,0 +1,39 @@
@import "@Themes/constants.scss";
.root {
display: flex;
gap: var(--spacing-lg, 40px);
.info-box1 {
display: flex;
width: 648px;
flex-direction: column;
gap: var(--spacing-sm, 8px);
.open-date {
display: flex;
gap: var(--spacing-sm, 8px);
}
}
.info-box2 {
display: flex;
flex-direction: column;
gap: var(--spacing-lg, 24px);
.progress-container {
display: flex;
justify-content: space-between;
align-items: center;
.icon-container {
display: flex;
gap: var(--spacing-md, 8px);
}
}
}
.separator {
background-color: var(--separator-stroke-light);
width: 1px;
align-self: stretch;
}
}

View File

@ -0,0 +1,71 @@
import CircleProgress from "@Front/Components/DesignSystem/CircleProgress";
import Tag, { ETagColor } from "@Front/Components/DesignSystem/Tag";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import { UserGroupIcon, PencilSquareIcon, ArchiveBoxIcon } from "@heroicons/react/24/outline";
import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import { EDocumentStatus } from "le-coffre-resources/dist/Notary/Document";
import { useCallback } from "react";
import classes from "./classes.module.scss";
import IconButton, { EIconButtonVariant } from "@Front/Components/DesignSystem/IconButton";
type IProps = {
folder: OfficeFolder | null;
};
export default function InformationSection(props: IProps) {
const { folder } = props;
const getCompletionNumber = useCallback(() => {
const documents = folder?.documents;
if (!documents) return 0;
const totalDocuments = documents.length;
const refusedDocuments = documents.filter((document) => document.document_status === EDocumentStatus.REFUSED).length ?? 0;
const askedDocuments =
documents.filter(
(document) => document.document_status === EDocumentStatus.ASKED || document.document_status === EDocumentStatus.DEPOSITED,
).length ?? 0;
const depositedDocuments = totalDocuments - askedDocuments - refusedDocuments;
const percentage = (depositedDocuments / totalDocuments) * 100;
return isNaN(percentage) ? 0 : percentage;
}, [folder]);
return (
<section className={classes["root"]}>
<div className={classes["info-box1"]}>
<div>
<Typography typo={ETypo.TEXT_MD_REGULAR}>{folder?.folder_number}</Typography>
<Typography typo={ETypo.TITLE_H4}>{folder?.name}</Typography>
</div>
<Tag color={ETagColor.INFO} label={folder?.deed?.deed_type?.name ?? ""} />
<div className={classes["open-date"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR}>Ouverture du dossier</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_PRIMARY_500}>
{folder?.created_at ? new Date(folder.created_at).toLocaleDateString() : ""}
</Typography>
</div>
</div>
<div className={classes["separator"]} />
<div className={classes["info-box2"]}>
<div className={classes["progress-container"]}>
<CircleProgress percentage={getCompletionNumber()} />
<div className={classes["icon-container"]}>
<IconButton icon={<UserGroupIcon title="Modifier les collaborateurs" />} variant={EIconButtonVariant.NEUTRAL} />
<IconButton
icon={<PencilSquareIcon title="Modifier les informations du dossiers" />}
variant={EIconButtonVariant.NEUTRAL}
/>
<IconButton icon={<ArchiveBoxIcon title="Archiver le dossier" />} variant={EIconButtonVariant.ERROR} />
</div>
</div>
<div>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_NEUTRAL_700}>
Notre dossier
</Typography>
<Typography typo={ETypo.TEXT_LG_REGULAR}>Travaux de rénovation en cours. </Typography>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,34 @@
import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import EmptyAlert from "@Front/Components/DesignSystem/EmptyAlert";
import Module from "@Front/Config/Module";
import { UserPlusIcon } from "@heroicons/react/24/outline";
import Link from "next/link";
import React, { useMemo } from "react";
type IProps = {
folderUid: string;
};
export default function AddClientSection(props: IProps) {
const { folderUid } = props;
const addClientPath = useMemo(() => {
if (!folderUid) return "";
return Module.getInstance().get().modules.pages.Folder.pages.AddClient.props.path.replace("[folderUid]", folderUid);
}, [folderUid]);
return (
<EmptyAlert
icon={<UserPlusIcon />}
title="Ajouter des clients au dossier"
description="Pour pouvoir faire une demande de document, vous devez d'abord ajouter un ou plusieurs clients à ce dossier. Cette étape est essentielle pour assurer le suivi et la gestion des documents."
footer={
<Link href={addClientPath}>
<Button variant={EButtonVariant.PRIMARY} styletype={EButtonstyletype.OUTLINED}>
Ajouter un client
</Button>
</Link>
}
/>
);
}

View File

@ -0,0 +1,42 @@
import Folders from "@Front/Api/LeCoffreApi/Notary/Folders/Folders";
import Modal from "@Front/Components/DesignSystem/Modal";
import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography";
import Module from "@Front/Config/Module";
import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import { useRouter } from "next/router";
import React, { useCallback } from "react";
type IProps = {
isOpen: boolean;
onClose?: () => void;
folder: OfficeFolder;
};
export default function DeleteFolderModal(props: IProps) {
const { isOpen, onClose, folder } = props;
const navigate = useRouter();
const onDelete = useCallback(() => {
if (!folder.uid) return;
if ((folder?.customers?.length ?? 0) > 0 || (folder?.documents?.length ?? 0) > 0)
return console.warn("Cannot delete folder with customers or documents");
return Folders.getInstance()
.delete(folder.uid)
.then(() => navigate.push(Module.getInstance().get().modules.pages.Folder.props.path))
.then(onClose);
}, [folder, navigate, onClose]);
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={"Êtes-vous sûr de vouloir supprimer ce dossier ?"}
firstButton={{ text: "Annuler", onClick: onClose }}
secondButton={{ text: "Supprimer le dossier", onClick: onDelete }}>
<Typography typo={ETypo.TEXT_MD_light}>
Cette action est irréversible. En supprimant ce dossier, toutes les informations associées seront définitivement perdues.
</Typography>
</Modal>
);
}

View File

@ -0,0 +1,11 @@
@import "@Themes/constants.scss";
.root {
display: flex;
flex-direction: column;
gap: var(--spacing-xl, 32px);
.delete-button {
align-self: flex-end;
}
}

View File

@ -0,0 +1,41 @@
import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import { TrashIcon } from "@heroicons/react/24/outline";
import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import { useMemo } from "react";
import AddClientSection from "./AddClientSection";
import classes from "./classes.module.scss";
import DeleteFolderModal from "./DeleteFolderModal";
import useOpenable from "@Front/Hooks/useOpenable";
import { AnchorStatus } from "..";
type IProps = {
folder: OfficeFolder;
anchorStatus: AnchorStatus;
};
export default function NoClientView(props: IProps) {
const { folder, anchorStatus } = props;
const deleteFolderModal = useOpenable();
const canDeleteFolder = useMemo(() => folder.documents?.length === 0 && folder.customers?.length === 0, [folder]);
return (
<section className={classes["root"]}>
{anchorStatus === AnchorStatus.NOT_ANCHORED && <AddClientSection folderUid={folder?.uid ?? ""} />}
{canDeleteFolder && (
<>
<Button
className={classes["delete-button"]}
variant={EButtonVariant.ERROR}
styletype={EButtonstyletype.TEXT}
rightIcon={<TrashIcon />}
onClick={deleteFolderModal.open}>
Supprimer le dossier
</Button>
<DeleteFolderModal isOpen={deleteFolderModal.isOpen} onClose={deleteFolderModal.close} folder={folder} />
</>
)}
</section>
);
}

View File

@ -2,91 +2,13 @@
.root { .root {
display: flex; display: flex;
align-items: center;
flex-direction: column; flex-direction: column;
min-height: 100%; gap: var(--spacing-xl, 32px);
.no-folder-selected { .separator {
width: 100%; background-color: var(--separator-stroke-light);
width: 1px;
.choose-a-folder { align-self: stretch;
margin-top: 96px;
text-align: center;
}
}
.folder-informations {
width: 100%;
min-height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: column;
flex-grow: 1;
.folder-header {
width: 100%;
.header {
margin-bottom: 32px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
@media (max-width: $screen-m) {
flex-wrap: wrap;
.title {
margin-bottom: 24px;
}
}
}
}
}
.second-box {
margin-top: 24px;
margin-bottom: 32px;
}
.progress-bar {
margin-bottom: 32px;
}
.button-container {
width: 100%;
display: flex;
gap: 16px;
text-align: center;
justify-content: center;
.delete-folder {
display: flex;
margin-left: 12px;
}
@media (max-width: $screen-m) {
display: flex;
flex-direction: column;
.delete-folder {
margin-left: 0;
margin-top: 12px;
> * {
flex: 1;
}
}
> * {
width: 100%;
}
}
}
.modal-title {
margin-bottom: 24px;
} }
} }

View File

@ -1,27 +1,15 @@
import ChevronIcon from "@Assets/Icons/chevron.svg";
import Folders from "@Front/Api/LeCoffreApi/Notary/Folders/Folders"; import Folders from "@Front/Api/LeCoffreApi/Notary/Folders/Folders";
import OfficeFolderAnchors from "@Front/Api/LeCoffreApi/Notary/OfficeFolderAnchors/OfficeFolderAnchors"; import OfficeFolderAnchors from "@Front/Api/LeCoffreApi/Notary/OfficeFolderAnchors/OfficeFolderAnchors";
import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import FolderBoxInformation, { EFolderBoxInformationType } from "@Front/Components/DesignSystem/FolderBoxInformation";
import Confirm from "@Front/Components/DesignSystem/OldModal/Confirm";
import QuantityProgressBar from "@Front/Components/DesignSystem/QuantityProgressBar";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import DefaultNotaryDashboard from "@Front/Components/LayoutTemplates/DefaultNotaryDashboard";
import Module from "@Front/Config/Module";
import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import { EDocumentStatus } from "le-coffre-resources/dist/Notary/Document";
import Link from "next/link";
import { NextRouter, useRouter } from "next/router";
import { ChangeEvent, useCallback, useEffect, useState } from "react";
import Image from "next/image";
import ValidateAnchoringGif from "@Front/Assets/images/validate_anchoring.gif";
import BasePage from "../../Base";
import classes from "./classes.module.scss";
import ClientSection from "./ClientSection";
import Loader from "@Front/Components/DesignSystem/Loader"; import Loader from "@Front/Components/DesignSystem/Loader";
import Newsletter from "@Front/Components/DesignSystem/Newsletter"; import DefaultNotaryDashboard from "@Front/Components/LayoutTemplates/DefaultNotaryDashboard";
import TextAreaField from "@Front/Components/DesignSystem/Form/TextareaField"; import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import { useParams } from "next/navigation";
import { useCallback, useEffect, useMemo, useState } from "react";
import classes from "./classes.module.scss";
import InformationSection from "./InformationSection";
import NoClientView from "./NoClientView";
import ClientView from "./ClientView";
export enum AnchorStatus { export enum AnchorStatus {
"VERIFIED_ON_CHAIN" = "VERIFIED_ON_CHAIN", "VERIFIED_ON_CHAIN" = "VERIFIED_ON_CHAIN",
@ -31,455 +19,19 @@ export enum AnchorStatus {
type IProps = {}; type IProps = {};
type IPropsClass = IProps & {
router: NextRouter;
selectedFolderUid: string;
isAnchored: AnchorStatus;
isLoading: boolean;
selectedFolder: OfficeFolder | null;
getAnchoringStatus: () => Promise<void>;
getFolderCallback: () => Promise<void>;
openedCustomer?: string;
};
type IState = {
isArchivedModalOpen: boolean;
inputArchivedDescripton: string;
isValidateModalVisible: boolean;
hasValidateAnchoring: boolean;
isVerifDeleteModalVisible: boolean;
isPreventArchiveModalOpen: boolean;
loadingAnchoring: boolean;
};
class FolderInformationClass extends BasePage<IPropsClass, IState> {
public constructor(props: IPropsClass) {
super(props);
this.state = {
isArchivedModalOpen: false,
inputArchivedDescripton: "",
isValidateModalVisible: false,
hasValidateAnchoring: false,
isVerifDeleteModalVisible: false,
isPreventArchiveModalOpen: false,
loadingAnchoring: false,
};
this.openArchivedModal = this.openArchivedModal.bind(this);
this.closeArchivedModal = this.closeArchivedModal.bind(this);
this.onArchivedModalAccepted = this.onArchivedModalAccepted.bind(this);
this.onPreventArchiveModalAccepted = this.onPreventArchiveModalAccepted.bind(this);
this.getCompletionNumber = this.getCompletionNumber.bind(this);
this.onArchivedDescriptionInputChange = this.onArchivedDescriptionInputChange.bind(this);
this.deleteFolder = this.deleteFolder.bind(this);
this.closeModal = this.closeModal.bind(this);
this.validateAnchoring = this.validateAnchoring.bind(this);
this.openValidateModal = this.openValidateModal.bind(this);
this.openVerifDeleteFolder = this.openVerifDeleteFolder.bind(this);
this.closeVerifDeleteFolder = this.closeVerifDeleteFolder.bind(this);
this.closePreventArchiveModal = this.closePreventArchiveModal.bind(this);
}
// TODO: Message if the user has not created any folder yet
// TODO: get the selected folder from the api in componentDidMount
public override render(): JSX.Element {
const redirectPathEditCollaborators = Module.getInstance()
.get()
.modules.pages.Folder.pages.EditCollaborators.props.path.replace("[folderUid]", this.props.selectedFolderUid);
return (
<DefaultNotaryDashboard title={"Dossier"} isArchived={false} mobileBackText="Retour aux dossiers">
{!this.props.isLoading && (
<div className={classes["root"]}>
{this.props.selectedFolder ? (
<div className={classes["folder-informations"]}>
<div className={classes["folder-header"]}>
<div className={classes["header"]}>
<div className={classes["title"]}>
<Typography typo={ETypo.TITLE_H1}>Informations du dossier</Typography>
</div>
<Link href={redirectPathEditCollaborators}>
<Button
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.TEXT}
rightIcon={<ChevronIcon />}>
Modifier les collaborateurs
</Button>
</Link>
</div>
<FolderBoxInformation
anchorStatus={this.props.isAnchored}
folder={this.props.selectedFolder}
type={EFolderBoxInformationType.INFORMATIONS}
/>
<div className={classes["second-box"]}>
<FolderBoxInformation
anchorStatus={this.props.isAnchored}
folder={this.props.selectedFolder}
type={EFolderBoxInformationType.DESCRIPTION}
/>
</div>
<div className={classes["progress-bar"]}>
<QuantityProgressBar
title="Complétion du dossier"
total={100}
currentNumber={this.getCompletionNumber()}
/>
</div>
{this.doesFolderHaveCustomer() && (
<ClientSection
folder={this.props.selectedFolder}
anchorStatus={this.props.isAnchored}
getFolderCallback={this.props.getFolderCallback}
openedCustomer={this.props.openedCustomer}
/>
)}
</div>
{!this.doesFolderHaveCustomer() && (
<ClientSection
folder={this.props.selectedFolder}
anchorStatus={this.props.isAnchored}
getFolderCallback={this.props.getFolderCallback}
openedCustomer={this.props.openedCustomer}
/>
)}
<div className={classes["button-container"]}>
<Button
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.OUTLINED}
onClick={this.openArchivedModal}>
Archiver le dossier
</Button>
{this.everyDocumentValidated() && !this.props.isLoading && (
<>
{this.props.isAnchored === AnchorStatus.NOT_ANCHORED && (
<Button variant={EButtonVariant.PRIMARY} onClick={this.openValidateModal}>
Valider et ancrer
</Button>
)}
{this.props.isAnchored === AnchorStatus.ANCHORING && (
<Button variant={EButtonVariant.PRIMARY} disabled>
Demande d'ancrage envoyée...&nbsp;&nbsp;
<div className={classes["loader-container"]}>
<div className={classes["loader"]}>
<Loader />
</div>
</div>
</Button>
)}
{this.props.isAnchored === AnchorStatus.VERIFIED_ON_CHAIN && (
<Button
variant={EButtonVariant.PRIMARY}
onClick={() => this.downloadAnchoringProof(this.props.selectedFolder?.uid)}
disabled={this.state.loadingAnchoring}>
Télécharger la preuve d'ancrage
{this.state.loadingAnchoring && (
<div className={classes["loader-container"]}>
<div className={classes["loader"]}>
<Loader />
</div>
</div>
)}
</Button>
)}
</>
)}
{this.canDeleteFolder() && (
<span className={classes["delete-folder"]} onClick={this.openVerifDeleteFolder}>
<Button variant={EButtonVariant.SECONDARY}>Supprimer le dossier</Button>
</span>
)}
</div>
<Confirm
isOpen={this.state.isArchivedModalOpen}
onAccept={this.onArchivedModalAccepted}
onClose={this.closeArchivedModal}
closeBtn
header={"Archiver le dossier ?"}
cancelText={"Annuler"}
confirmText={"Archiver"}>
<div className={classes["modal-title"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR}>Souhaitez-vous vraiment archiver le dossier ?</Typography>
</div>
<TextAreaField
name="archived_description"
placeholder="Description"
onChange={this.onArchivedDescriptionInputChange}
/>
</Confirm>
<Confirm
isOpen={this.state.isPreventArchiveModalOpen}
onAccept={this.onPreventArchiveModalAccepted}
onClose={this.closePreventArchiveModal}
closeBtn
header={"Archiver le dossier"}
cancelText={"Annuler"}
confirmText={"Archiver"}>
<div className={classes["modal-title"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR}>
Vous êtes en train darchiver le dossier sans avoir lancré, êtes-vous sûr de vouloir le faire ?
</Typography>
</div>
<TextAreaField
name="archived_description"
placeholder="Description"
onChange={this.onArchivedDescriptionInputChange}
/>
</Confirm>
<Confirm
isOpen={this.state.isVerifDeleteModalVisible}
onAccept={this.deleteFolder}
onClose={this.closeVerifDeleteFolder}
closeBtn
header={"Êtes-vous sûr de vouloir supprimer ce dossier ?"}
cancelText={"Annuler"}
confirmText={"Confirmer"}>
<div className={classes["modal-title"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR}>Cette action sera irréversible.</Typography>
</div>
</Confirm>
<Newsletter isOpen={false} />
</div>
) : (
<div className={classes["no-folder-selected"]}>
<Typography typo={ETypo.TITLE_H1}>Informations du dossier</Typography>
<div className={classes["choose-a-folder"]}>
<Typography typo={ETypo.TEXT_LG_REGULAR} color={ETypoColor.COLOR_NEUTRAL_500}>
Sélectionnez un dossier
</Typography>
</div>
</div>
)}
</div>
)}
{this.props.isLoading && (
<div className={classes["loader-container"]}>
<div className={classes["loader"]}>
<Loader />
</div>
</div>
)}
<Confirm
isOpen={this.state.isValidateModalVisible}
onClose={this.closeModal}
onAccept={this.validateAnchoring}
closeBtn={true}
hasContainerClosable={true}
header={
this.state.hasValidateAnchoring
? "Dossier en cours de certification"
: "Êtes-vous sûr de vouloir ancrer et certifier ce dossier ?"
}
cancelText={"Annuler"}
confirmText={"Confirmer"}
showButtons={!this.state.hasValidateAnchoring}>
<div className={classes["validate-document-container"]}>
{!this.state.hasValidateAnchoring && (
<Typography
typo={ETypo.TEXT_MD_REGULAR}
color={ETypoColor.COLOR_GENERIC_BLACK}
className={classes["validate-text"]}>
Les documents du dossier seront certifiés sur la blockchain. Pensez à bien télécharger l'ensemble des
documents du dossier ainsi que le fichier de preuve d'ancrage pour les mettre dans la GED de votre logiciel
de rédaction d'actes.
</Typography>
)}
{this.state.hasValidateAnchoring && (
<div className={classes["document-validating-container"]}>
<Typography
typo={ETypo.TEXT_MD_REGULAR}
color={ETypoColor.COLOR_GENERIC_BLACK}
className={classes["validate-text"]}>
Veuillez revenir sur le dossier dans 5 minutes et rafraîchir la page pour télécharger le dossier de
preuve d'ancrage et le glisser dans la GED de votre logiciel de rédaction d'acte.
</Typography>
<Image src={ValidateAnchoringGif} alt="Anchoring animation" className={classes["validate-gif"]} />
</div>
)}
</div>
</Confirm>
</DefaultNotaryDashboard>
);
}
private closePreventArchiveModal() {
this.setState({
isPreventArchiveModalOpen: false,
});
}
public openVerifDeleteFolder() {
this.setState({
isVerifDeleteModalVisible: true,
});
}
public closeVerifDeleteFolder() {
this.setState({
isVerifDeleteModalVisible: false,
});
}
private closeModal() {
this.setState({
isValidateModalVisible: false,
});
}
private openValidateModal() {
this.setState({
isValidateModalVisible: true,
});
}
private async validateAnchoring() {
this.setState({
hasValidateAnchoring: true,
});
try {
const timeoutDelay = 9800;
await this.anchorFolder();
setTimeout(() => {
this.setState({
isValidateModalVisible: false,
});
}, timeoutDelay);
setTimeout(() => {
this.setState({
hasValidateAnchoring: false,
});
}, timeoutDelay + 1000);
} catch (e) {
this.setState({
isValidateModalVisible: false,
hasValidateAnchoring: false,
});
console.error(e);
}
}
private async anchorFolder() {
if (!this.props.selectedFolder?.uid) return;
await OfficeFolderAnchors.getInstance().post(this.props.selectedFolder.uid);
this.props.getAnchoringStatus();
}
private async downloadAnchoringProof(uid?: string) {
if (!uid) return;
this.setState({ loadingAnchoring: true });
try {
const file: Blob = await OfficeFolderAnchors.getInstance().download(uid);
const url = window.URL.createObjectURL(file);
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
// the filename you want
a.download = `anchoring_proof_${this.props.selectedFolder?.folder_number}_${this.props.selectedFolder?.name}.zip`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
this.setState({ loadingAnchoring: false });
} catch (e) {
this.setState({ loadingAnchoring: false });
console.error(e);
}
}
private everyDocumentValidated(): boolean {
if (!this.props.selectedFolder?.documents) return false;
return (
this.props.selectedFolder?.documents?.length >= 1 &&
this.props.selectedFolder?.documents.every((document) => document.document_status === EDocumentStatus.VALIDATED)
);
}
private async deleteFolder() {
if (!this.props.selectedFolder?.uid) return;
await Folders.getInstance().delete(this.props.selectedFolder.uid);
this.props.router.push(Module.getInstance().get().modules.pages.Folder.props.path);
}
private getCompletionNumber() {
const documents = this.props.selectedFolder?.documents;
if (!documents) return 0;
const totalDocuments = documents.length;
const refusedDocuments = documents.filter((document) => document.document_status === EDocumentStatus.REFUSED).length ?? 0;
const askedDocuments =
documents.filter(
(document) => document.document_status === EDocumentStatus.ASKED || document.document_status === EDocumentStatus.DEPOSITED,
).length ?? 0;
const depositedDocuments = totalDocuments - askedDocuments - refusedDocuments;
const percentage = (depositedDocuments / totalDocuments) * 100;
return isNaN(percentage) ? 0 : percentage;
}
private doesFolderHaveCustomer(): boolean {
if (!this.props.selectedFolder?.customers) return false;
return this.props.selectedFolder?.customers!.length > 0;
}
private canDeleteFolder(): boolean {
return (this.props.selectedFolder?.customers?.length ?? 0) === 0 && (this.props.selectedFolder?.documents?.length ?? 0) === 0;
}
private openArchivedModal(): void {
if (this.everyDocumentValidated() && this.props.isAnchored === AnchorStatus.VERIFIED_ON_CHAIN) {
this.setState({ isArchivedModalOpen: true });
} else {
this.setState({ isPreventArchiveModalOpen: true });
}
}
private closeArchivedModal(): void {
this.setState({ isArchivedModalOpen: false });
}
private onArchivedDescriptionInputChange(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) {
this.setState({ inputArchivedDescripton: e.target.value });
}
private async onArchivedModalAccepted() {
if (!this.props.selectedFolder) return;
await Folders.getInstance().archive(this.props.selectedFolder.uid ?? "", this.state.inputArchivedDescripton);
this.closeArchivedModal();
this.props.router.push(Module.getInstance().get().modules.pages.Folder.props.path);
}
private async onPreventArchiveModalAccepted() {
if (!this.props.selectedFolder) return;
await Folders.getInstance().archive(this.props.selectedFolder.uid ?? "", this.state.inputArchivedDescripton);
this.closePreventArchiveModal();
this.props.router.push(Module.getInstance().get().modules.pages.Folder.props.path);
}
}
export default function FolderInformation(props: IProps) { export default function FolderInformation(props: IProps) {
const router = useRouter(); const [anchorStatus, setAnchorStatus] = useState<AnchorStatus>(AnchorStatus.NOT_ANCHORED);
const [isAnchored, setIsAnchored] = useState<AnchorStatus>(AnchorStatus.NOT_ANCHORED);
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
const [selectedFolder, setSelectedFolder] = useState<OfficeFolder | null>(null); const [folder, setFolder] = useState<OfficeFolder | null>(null);
let { folderUid } = router.query; const params = useParams();
const customerUid = router.query["customerUid"] as string | undefined; const folderUid = params["folderUid"] as string;
folderUid = folderUid as string;
const getAnchoringStatus = useCallback(async () => { const fetchFolder = useCallback(async () => {
if (!folderUid) return; if (!folderUid) return;
try {
const anchorStatus = await OfficeFolderAnchors.getInstance().getByUid(folderUid as string);
setIsAnchored(anchorStatus.status === "VERIFIED_ON_CHAIN" ? AnchorStatus.VERIFIED_ON_CHAIN : AnchorStatus.ANCHORING);
} catch (e) {
setIsAnchored(AnchorStatus.NOT_ANCHORED);
}
}, [folderUid]);
const getFolder = useCallback(async () => {
if (!folderUid) return;
setIsLoading(true);
const query = { const query = {
q: { q: {
deed: { include: { deed_type: true } }, deed: { include: { deed_type: true, document_types: true } },
office: true, office: true,
customers: { customers: {
include: { include: {
@ -511,31 +63,46 @@ export default function FolderInformation(props: IProps) {
}, },
}; };
const folder = await Folders.getInstance().getByUid(folderUid as string, query); return Folders.getInstance()
if (folder) { .getByUid(folderUid, query)
setSelectedFolder(folder); .then((folder) => setFolder(folder));
getAnchoringStatus(); }, [folderUid]);
}
setIsLoading(false); const fetchAnchorStatus = useCallback(() => {
}, [folderUid, getAnchoringStatus]); return OfficeFolderAnchors.getInstance()
.getByUid(folderUid)
.then((anchorStatus) =>
setAnchorStatus(anchorStatus.status === "VERIFIED_ON_CHAIN" ? AnchorStatus.VERIFIED_ON_CHAIN : AnchorStatus.ANCHORING),
)
.catch(() => setAnchorStatus(AnchorStatus.NOT_ANCHORED));
}, [folderUid]);
useEffect(() => { useEffect(() => {
setIsLoading(true); setIsLoading(true);
getFolder(); fetchFolder()
}, [getFolder]); .then(() => fetchAnchorStatus())
.catch((e) => console.error(e))
.finally(() => setIsLoading(false));
}, [fetchAnchorStatus, fetchFolder, folderUid]);
const doesFolderHaveClient = useMemo(() => folder?.customers?.length !== 0, [folder]);
return ( return (
<FolderInformationClass <DefaultNotaryDashboard title={"Dossier"} isArchived={false} mobileBackText="Retour aux dossiers">
{...props} {!isLoading && (
selectedFolderUid={folderUid} <div className={classes["root"]}>
router={router} <InformationSection folder={folder} />
isAnchored={isAnchored} {folder && !doesFolderHaveClient && <NoClientView folder={folder} anchorStatus={anchorStatus} />}
isLoading={isLoading} {folder && doesFolderHaveClient && <ClientView folder={folder} anchorStatus={anchorStatus} />}
selectedFolder={selectedFolder} </div>
getAnchoringStatus={getAnchoringStatus} )}
getFolderCallback={getFolder} {isLoading && (
openedCustomer={customerUid} <div className={classes["loader-container"]}>
/> <div className={classes["loader"]}>
<Loader />
</div>
</div>
)}
</DefaultNotaryDashboard>
); );
} }