diff --git a/src/front/Components/DesignSystem/Alert/classes.module.scss b/src/front/Components/DesignSystem/Alert/classes.module.scss new file mode 100644 index 00000000..b230faf6 --- /dev/null +++ b/src/front/Components/DesignSystem/Alert/classes.module.scss @@ -0,0 +1,86 @@ +@import "@Themes/constants.scss"; + +.root { + width: fit-content; + display: inline-flex; + padding: var(--spacing-2, 16px); + align-items: center; + gap: var(--spacing-lg, 24px); + + border-radius: var(--alerts-radius, 0px); + border: 1px solid var(--alerts-info-border); + background: var(--alerts-info-background); + + .content { + display: flex; + flex-direction: column; + gap: var(--spacing-md, 16px); + + .text-container { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); + } + + .button-container { + display: flex; + gap: var(--spacing-md, 16px); + } + } + + .icon { + display: flex; + padding: var(--spacing-1, 8px); + align-items: center; + + border-radius: var(--alerts-badge-radius, 360px); + border: 1px solid var(--alerts-badge-border, rgba(0, 0, 0, 0)); + background: var(--alerts-badge-background, #fff); + box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.1); + + svg { + width: 24px; + height: 24px; + min-width: 24px; + min-height: 24px; + + stroke: var(--alerts-badge-contrast-info); + } + } + + &.error { + border-color: var(--alerts-error-border); + background: var(--alerts-error-background); + + .icon svg { + stroke: var(--alerts-badge-contrast-error); + } + } + + &.warning { + border-color: var(--alerts-warning-border); + background: var(--alerts-warning-background); + + .icon svg { + stroke: var(--alerts-badge-contrast-warning); + } + } + + &.success { + border-color: var(--alerts-success-border); + background: var(--alerts-success-background); + + .icon svg { + stroke: var(--alerts-badge-contrast-success); + } + } + + &.neutral { + border-color: var(--alerts-neutral-border); + background: var(--alerts-neutral-background); + + .icon svg { + stroke: var(--alerts-badge-contrast-neutral); + } + } +} diff --git a/src/front/Components/DesignSystem/Alert/index.tsx b/src/front/Components/DesignSystem/Alert/index.tsx new file mode 100644 index 00000000..4605961f --- /dev/null +++ b/src/front/Components/DesignSystem/Alert/index.tsx @@ -0,0 +1,80 @@ +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; +import React from "react"; +import { InformationCircleIcon, XMarkIcon } from "@heroicons/react/24/outline"; + +import classes from "./classes.module.scss"; +import Button, { EButtonSize, EButtonstyletype, EButtonVariant, IButtonProps } from "../Button"; +import classNames from "classnames"; +import IconButton from "../IconButton"; +import useOpenable from "@Front/Hooks/useOpenable"; + +type IProps = { + variant: EAlertVariant; + title: string; + description: string; + icon?: React.ReactNode; + firstButton?: IButtonProps; + secondButton?: IButtonProps; + closeButton?: boolean; +}; + +export enum EAlertVariant { + INFO = "info", + SUCCESS = "success", + WARNING = "warning", + ERROR = "error", + NEUTRAL = "neutral", +} + +const variantButtonMap: Record = { + [EAlertVariant.INFO]: EButtonVariant.PRIMARY, + [EAlertVariant.SUCCESS]: EButtonVariant.SUCCESS, + [EAlertVariant.WARNING]: EButtonVariant.WARNING, + [EAlertVariant.ERROR]: EButtonVariant.ERROR, + [EAlertVariant.NEUTRAL]: EButtonVariant.NEUTRAL, +}; + +export default function Alert(props: IProps) { + const { isOpen, close } = useOpenable({ defaultOpen: true }); + const { variant = EAlertVariant.INFO, title, description, firstButton, secondButton, closeButton, icon } = props; + + if (!isOpen) return null; + + return ( +
+ {icon ?? } +
+
+ + {title} + + + {description} + +
+ +
+ {firstButton && ( + + )} + {secondButton && ( + + )} +
+
+ {closeButton && } />} +
+ ); +} diff --git a/src/front/Components/DesignSystem/Button/index.tsx b/src/front/Components/DesignSystem/Button/index.tsx index d63c01cf..0bdb4974 100644 --- a/src/front/Components/DesignSystem/Button/index.tsx +++ b/src/front/Components/DesignSystem/Button/index.tsx @@ -25,7 +25,7 @@ export enum EButtonstyletype { TEXT = "text", } -type IProps = { +export type IButtonProps = { onClick?: React.MouseEventHandler | undefined; children?: React.ReactNode; variant?: EButtonVariant; @@ -40,7 +40,7 @@ type IProps = { className?: string; }; -export default function Button(props: IProps) { +export default function Button(props: IButtonProps) { let { variant = EButtonVariant.PRIMARY, size = EButtonSize.LG, diff --git a/src/front/Components/DesignSystem/CircleProgress/index.tsx b/src/front/Components/DesignSystem/CircleProgress/index.tsx index 741dd5f0..4b66e985 100644 --- a/src/front/Components/DesignSystem/CircleProgress/index.tsx +++ b/src/front/Components/DesignSystem/CircleProgress/index.tsx @@ -1,11 +1,11 @@ import React, { useCallback, useEffect, useRef, useState } from "react"; - import Typography, { ETypo, ETypoColor } from "../Typography"; import classes from "./classes.module.scss"; type IProps = { percentage: number; }; + export default function CircleProgress(props: IProps) { const { percentage } = props; @@ -27,13 +27,14 @@ export default function CircleProgress(props: IProps) { }, [percentage]); useEffect(() => { + setAnimatedProgress(0); // Reset progress requestRef.current = requestAnimationFrame(animate); return () => { if (requestRef.current) { cancelAnimationFrame(requestRef.current); } }; - }, [animate, percentage]); + }, [percentage, animate]); const radius = 11; const circumference = 2 * Math.PI * radius; @@ -58,4 +59,4 @@ export default function CircleProgress(props: IProps) { ); -} +} \ No newline at end of file diff --git a/src/front/Components/DesignSystem/Modal/classes.module.scss b/src/front/Components/DesignSystem/Modal/classes.module.scss index f674201d..4d58af84 100644 --- a/src/front/Components/DesignSystem/Modal/classes.module.scss +++ b/src/front/Components/DesignSystem/Modal/classes.module.scss @@ -12,7 +12,7 @@ .content { position: fixed; max-width: 600px; - max-height: 85vh; + max-height: 75vh; border-radius: var(--modal-radius, 0px); background: var(--modal-background, #fff); box-shadow: 0px 4px 18px 0px rgba(0, 0, 0, 0.15); diff --git a/src/front/Components/DesignSystem/Modal/index.tsx b/src/front/Components/DesignSystem/Modal/index.tsx index f0b436ad..8f165f99 100644 --- a/src/front/Components/DesignSystem/Modal/index.tsx +++ b/src/front/Components/DesignSystem/Modal/index.tsx @@ -1,11 +1,11 @@ -import classNames from "classnames"; -import classes from "./classes.module.scss"; -import Button, { EButtonstyletype, EButtonVariant } from "../Button"; -import React from "react"; -import Typography, { ETypo } from "../Typography"; - import { XMarkIcon } from "@heroicons/react/24/outline"; +import classNames from "classnames"; +import React from "react"; + +import Button, { EButtonstyletype, EButtonVariant, IButtonProps } from "../Button"; import IconButton, { EIconButtonVariant } from "../IconButton"; +import Typography, { ETypo } from "../Typography"; +import classes from "./classes.module.scss"; type IProps = { className?: string; @@ -13,18 +13,8 @@ type IProps = { onClose?: () => void; children?: React.ReactNode; title?: string; - firstButton?: { - text: string; - onClick?: () => void; - rightIcon?: React.ReactNode; - leftIcon?: React.ReactNode; - }; - secondButton?: { - text: string; - onClick?: () => void; - rightIcon?: React.ReactNode; - leftIcon?: React.ReactNode; - }; + firstButton?: IButtonProps; + secondButton?: IButtonProps; fullwidth?: boolean; fullscreen?: boolean; }; @@ -54,22 +44,18 @@ export default function Modal(props: IProps) {
{firstButton && ( )} {secondButton && ( )}
diff --git a/src/front/Components/Layouts/DesignSystem/index.tsx b/src/front/Components/Layouts/DesignSystem/index.tsx index 3e5eb232..0e475bdf 100644 --- a/src/front/Components/Layouts/DesignSystem/index.tsx +++ b/src/front/Components/Layouts/DesignSystem/index.tsx @@ -1,22 +1,23 @@ +import Alert, { EAlertVariant } from "@Front/Components/DesignSystem/Alert"; import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button"; import CircleProgress from "@Front/Components/DesignSystem/CircleProgress"; +import Form from "@Front/Components/DesignSystem/Form"; +import TextAreaField from "@Front/Components/DesignSystem/Form/TextareaField"; +import TextField from "@Front/Components/DesignSystem/Form/TextField"; +import IconButton, { EIconButtonVariant } from "@Front/Components/DesignSystem/IconButton"; +import Modal from "@Front/Components/DesignSystem/Modal"; import Newsletter from "@Front/Components/DesignSystem/Newsletter"; import Table from "@Front/Components/DesignSystem/Table"; import Tag, { ETagColor, ETagVariant } from "@Front/Components/DesignSystem/Tag"; import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography"; +import NumberPicker from "@Front/Components/Elements/NumberPicker"; import Tabs from "@Front/Components/Elements/Tabs"; import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate"; +import useOpenable from "@Front/Hooks/useOpenable"; import { ArrowLongLeftIcon, ArrowLongRightIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { useCallback, useMemo, useState } from "react"; import classes from "./classes.module.scss"; -import useOpenable from "@Front/Hooks/useOpenable"; -import Modal from "@Front/Components/DesignSystem/Modal"; -import IconButton, { EIconButtonVariant } from "@Front/Components/DesignSystem/IconButton"; -import Form from "@Front/Components/DesignSystem/Form"; -import TextField from "@Front/Components/DesignSystem/Form/TextField"; -import TextAreaField from "@Front/Components/DesignSystem/Form/TextareaField"; -import NumberPicker from "@Front/Components/Elements/NumberPicker"; export default function DesignSystem() { const { isOpen, open, close } = useOpenable(); @@ -85,8 +86,8 @@ export default function DesignSystem() { isOpen={isOpen} onClose={close} title={"Modal"} - firstButton={{ text: "Annuler", leftIcon: , rightIcon: }} - secondButton={{ text: "Confirmer", leftIcon: , rightIcon: }}> + firstButton={{ children: "Annuler", leftIcon: , rightIcon: }} + secondButton={{ children: "Confirmer", leftIcon: , rightIcon: }}> Modal Content @@ -704,6 +705,64 @@ export default function DesignSystem() { } variant={EIconButtonVariant.INFO} /> } variant={EIconButtonVariant.INFO} disabled /> + + Alerts + , + rightIcon: , + }} + secondButton={{ children: "Button", leftIcon: , rightIcon: }} + variant={EAlertVariant.INFO} + closeButton + /> + , + rightIcon: , + }} + secondButton={{ children: "Button", leftIcon: , rightIcon: }} + variant={EAlertVariant.ERROR} + /> + , + rightIcon: , + }} + secondButton={{ children: "Button", leftIcon: , rightIcon: }} + variant={EAlertVariant.WARNING} + /> + , + rightIcon: , + }} + secondButton={{ children: "Button", leftIcon: , rightIcon: }} + variant={EAlertVariant.SUCCESS} + /> + , + rightIcon: , + }} + secondButton={{ children: "Button", leftIcon: , rightIcon: }} + variant={EAlertVariant.NEUTRAL} + /> diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/ClientBox/DeleteCustomerModal/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/ClientBox/DeleteCustomerModal/index.tsx index bb44529a..559ba8ca 100644 --- a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/ClientBox/DeleteCustomerModal/index.tsx +++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/ClientBox/DeleteCustomerModal/index.tsx @@ -28,8 +28,8 @@ export default function DeleteCustomerModal(props: IProps) { isOpen={isOpen} onClose={onClose} title={"Êtes-vous sûr de vouloir supprimer ce client du dossier ?"} - firstButton={{ text: "Annuler", onClick: onClose }} - secondButton={{ text: "Supprimer le client", onClick: onDelete }}> + firstButton={{ children: "Annuler", onClick: onClose }} + secondButton={{ children: "Supprimer le client", onClick: onDelete }}> Cette action retirera le client de ce dossier. Vous ne pourrez plus récupérer les informations associées à ce client dans ce dossier une fois supprimées. diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/ClientBox/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/ClientBox/index.tsx index 264a79e7..5de56af7 100644 --- a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/ClientBox/index.tsx +++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/ClientBox/index.tsx @@ -5,15 +5,17 @@ import useOpenable from "@Front/Hooks/useOpenable"; import { PencilSquareIcon, TrashIcon } from "@heroicons/react/24/outline"; import { ICustomer } from ".."; +import { AnchorStatus } from "../.."; import classes from "./classes.module.scss"; import DeleteCustomerModal from "./DeleteCustomerModal"; type IProps = { customer: ICustomer; + anchorStatus: AnchorStatus; }; export default function ClientBox(props: IProps) { - const { customer } = props; + const { customer, anchorStatus } = props; const { isOpen, open, close } = useOpenable(); @@ -23,7 +25,9 @@ export default function ClientBox(props: IProps) { {customer.contact?.last_name} - } /> + {anchorStatus === AnchorStatus.NOT_ANCHORED && ( + } /> + )}
@@ -46,19 +50,23 @@ export default function ClientBox(props: IProps) { Note client - TODO + {customer.notes?.[0]?.content ?? "_"}
- - {}} /> + {anchorStatus === AnchorStatus.NOT_ANCHORED && ( + <> + + {}} /> + + )} ); } diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/DeleteAskedDocumentModal/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/DeleteAskedDocumentModal/index.tsx index 08f66894..901780d9 100644 --- a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/DeleteAskedDocumentModal/index.tsx +++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/DeleteAskedDocumentModal/index.tsx @@ -29,8 +29,8 @@ export default function DeleteAskedDocumentModal(props: IProps) { isOpen={isOpen} onClose={onClose} title={"Êtes-vous sûr de vouloir supprimer cette demande de document ?"} - firstButton={{ text: "Annuler", onClick: onClose }} - secondButton={{ text: "Supprimer la demande", onClick: onDelete }}> + firstButton={{ children: "Annuler", onClick: onClose }} + secondButton={{ children: "Supprimer la demande", onClick: onDelete }}> Cette action annulera la demande du document en cours. ); diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/FilePreviewModal/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/FilePreviewModal/index.tsx new file mode 100644 index 00000000..6ef96822 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/FilePreviewModal/index.tsx @@ -0,0 +1,20 @@ +import Modal from "@Front/Components/DesignSystem/Modal"; +import { File } from "le-coffre-resources/dist/Customer"; +import React from "react"; + +type IProps = { + file: File; + url: string; + isOpen: boolean; + onClose?: () => void; +}; + +export default function FilePreviewModal(props: IProps) { + const { isOpen, onClose, file, url } = props; + + return ( + + + + ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/index.tsx index 642ed5e6..4d6cbb20 100644 --- a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/index.tsx +++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/DocumentTables/index.tsx @@ -1,3 +1,4 @@ +import Files from "@Front/Api/LeCoffreApi/Notary/Files/Files"; import CircleProgress from "@Front/Components/DesignSystem/CircleProgress"; import IconButton from "@Front/Components/DesignSystem/IconButton"; import Table from "@Front/Components/DesignSystem/Table"; @@ -6,16 +7,16 @@ import Tag, { ETagColor, ETagVariant } from "@Front/Components/DesignSystem/Tag" import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; import useOpenable from "@Front/Hooks/useOpenable"; import { ArrowDownTrayIcon, EyeIcon, TrashIcon } from "@heroicons/react/24/outline"; -import { Document } from "le-coffre-resources/dist/Customer"; +import { Document, File } 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"; import DeleteAskedDocumentModal from "./DeleteAskedDocumentModal"; +import FilePreviewModal from "./FilePreviewModal"; type IProps = { documents: Document[]; - totalOfDocumentTypes: number; }; const header: readonly IHead[] = [ @@ -37,10 +38,19 @@ const header: readonly IHead[] = [ }, ]; +const tradDocumentStatus: Record = { + [EDocumentStatus.ASKED]: "Demandé", + [EDocumentStatus.DEPOSITED]: "À valider", + [EDocumentStatus.VALIDATED]: "Validé", + [EDocumentStatus.REFUSED]: "Refusé", +}; + export default function DocumentTables(props: IProps) { - const { documents: documentsProps, totalOfDocumentTypes } = props; + const { documents: documentsProps } = props; const [documents, setDocuments] = useState(documentsProps); const [documentUid, setDocumentUid] = useState(null); + const previewModal = useOpenable(); + const [file, setFile] = useState<{ file: File; blob: Blob } | null>(null); const deleteAskedOocumentModal = useOpenable(); @@ -57,7 +67,37 @@ export default function DocumentTables(props: IProps) { [deleteAskedOocumentModal], ); - const askDocuments: IRowProps[] = useMemo( + const onPreview = useCallback( + (document: Document) => { + const file = document.files?.[0]; + if (!file || !file?.uid) return; + return Files.getInstance() + .download(file.uid) + .then((blob) => setFile({ file, blob })) + .then(() => previewModal.open()) + .catch((e) => console.warn(e)); + }, + [previewModal], + ); + + const onDownload = useCallback((doc: Document) => { + const file = doc.files?.[0]; + if (!file || !file?.uid) return; + + return Files.getInstance() + .download(file.uid) + .then((blob) => { + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = file.file_name ?? "file"; + a.click(); + URL.revokeObjectURL(url); + }) + .catch((e) => console.warn(e)); + }, []); + + const askedDocuments: IRowProps[] = useMemo( () => documents .map((document) => { @@ -66,7 +106,11 @@ export default function DocumentTables(props: IProps) { key: document.uid, document_type: document.document_type?.name ?? "_", document_status: ( - + ), created_at: document.created_at ? new Date(document.created_at).toLocaleDateString() : "_", actions: openDeleteAskedDocumentModal(document.uid)} />} />, @@ -85,14 +129,18 @@ export default function DocumentTables(props: IProps) { key: document.uid, document_type: document.document_type?.name ?? "_", document_status: ( - + ), created_at: document.created_at, - actions: } />, + actions: onPreview(document)} icon={} />, }; }) .filter((document) => document !== null) as IRowProps[], - [documents], + [documents, onPreview], ); const validatedDocuments: IRowProps[] = useMemo( @@ -104,19 +152,23 @@ export default function DocumentTables(props: IProps) { key: document.uid, document_type: document.document_type?.name ?? "_", document_status: ( - + ), created_at: document.created_at, actions: (
- } /> - } /> + onPreview(document)} icon={} /> + onDownload(document)} icon={} />
), }; }) .filter((document) => document !== null) as IRowProps[], - [documents], + [documents, onDownload, onPreview], ); const refusedDocuments: IRowProps[] = useMemo( @@ -128,7 +180,11 @@ export default function DocumentTables(props: IProps) { key: document.uid, document_type: document.document_type?.name ?? "_", document_status: ( - + ), created_at: document.created_at, actions: "", @@ -138,11 +194,11 @@ export default function DocumentTables(props: IProps) { [documents], ); - const progressValidated = useMemo(() => { - if (totalOfDocumentTypes === 0) return 100; - if (validatedDocuments.length === 0) return 0; - return (validatedDocuments.length / totalOfDocumentTypes) * 100; - }, [validatedDocuments, totalOfDocumentTypes]); + const progress = useMemo(() => { + const total = askedDocuments.length + toValidateDocuments.length + validatedDocuments.length + refusedDocuments.length; + if (total === 0) return 0; + return (validatedDocuments.length / total) * 100; + }, [askedDocuments.length, refusedDocuments.length, toValidateDocuments.length, validatedDocuments.length]); return (
@@ -150,9 +206,9 @@ export default function DocumentTables(props: IProps) { Documents - +
- +
{toValidateDocuments.length > 0 &&
} {validatedDocuments.length > 0 &&
} {refusedDocuments.length > 0 &&
} @@ -164,6 +220,14 @@ export default function DocumentTables(props: IProps) { documentUid={documentUid} /> )} + {file && ( + + )} ); } diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/index.tsx index 71119c08..1b3fc3bd 100644 --- a/src/front/Components/Layouts/Folder/FolderInformation/ClientView/index.tsx +++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientView/index.tsx @@ -47,8 +47,6 @@ export default function ClientView(props: IProps) { const doesCustomerHaveDocument = useMemo(() => customer.documents && customer.documents.length > 0, [customer]); - const totalOfDocumentTypes = useMemo(() => folder.deed?.document_types?.length ?? 0, [folder]); - return (
@@ -70,22 +68,20 @@ export default function ClientView(props: IProps) {
- - - - + + {anchorStatus === AnchorStatus.NOT_ANCHORED && ( + + + + )}
- {doesCustomerHaveDocument ? ( - - ) : ( - - )} + {doesCustomerHaveDocument ? : }
); diff --git a/src/front/Components/Layouts/Folder/FolderInformation/FolderInformationOld/ClientSection/classes.module.scss b/src/front/Components/Layouts/Folder/FolderInformation/FolderInformationOld/ClientSection/classes.module.scss deleted file mode 100644 index 1ac3bba8..00000000 --- a/src/front/Components/Layouts/Folder/FolderInformation/FolderInformationOld/ClientSection/classes.module.scss +++ /dev/null @@ -1,25 +0,0 @@ -@import "@Themes/constants.scss"; - -.root { - width: 100%; - padding-bottom: 32px; - - .no-client { - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - padding: 72px; - - .title { - margin-bottom: 16px; - text-align: center; - } - } - - .client { - display: grid; - gap: 32px; - margin-bottom: 32px; - } -} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/FolderInformationOld/ClientSection/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/FolderInformationOld/ClientSection/index.tsx deleted file mode 100644 index f0277861..00000000 --- a/src/front/Components/Layouts/Folder/FolderInformation/FolderInformationOld/ClientSection/index.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import PlusIcon from "@Assets/Icons/plus.svg"; -import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button"; -import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; -import UserFolder from "@Front/Components/DesignSystem/UserFolder"; -import { OfficeFolder } from "le-coffre-resources/dist/Notary"; -import Module from "@Front/Config/Module"; -import Link from "next/link"; -import React from "react"; - -import classes from "./classes.module.scss"; -import { AnchorStatus } from ".."; - -type IProps = { - folder: OfficeFolder; - anchorStatus: AnchorStatus; - getFolderCallback: () => Promise; - openedCustomer?: string; -}; -type IState = { - openedCustomer: string; -}; - -export default class ClientSection extends React.Component { - public constructor(props: IProps) { - super(props); - this.state = { - openedCustomer: this.props.openedCustomer ?? "", - }; - this.changeUserFolder = this.changeUserFolder.bind(this); - this.renderCustomerFolders = this.renderCustomerFolders.bind(this); - } - - public override render(): JSX.Element { - const navigatePath = Module.getInstance() - .get() - .modules.pages.Folder.pages.AddClient.props.path.replace("[folderUid]", this.props.folder.uid ?? ""); - return ( -
- {this.doesFolderHaveCustomer() ? ( - <> -
{this.renderCustomerFolders()}
- {this.props.anchorStatus === AnchorStatus.NOT_ANCHORED && ( - - - - )} - - ) : ( -
-
- - Aucun client n'est associé au dossier. - -
- {this.props.anchorStatus === AnchorStatus.NOT_ANCHORED && ( - - - - )} -
- )} -
- ); - } - - private renderCustomerFolders() { - const output = this.props.folder.customers?.map((customer) => { - if (!customer) return null; - return ( - - ); - }); - return output ?? null; - } - - private changeUserFolder(uid: string) { - this.setState({ - openedCustomer: uid === this.state.openedCustomer ? "" : uid, - }); - } - - private doesFolderHaveCustomer(): boolean { - if (!this.props.folder?.customers) return false; - return this.props.folder?.customers!.length > 0; - } -} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/FolderInformationOld/classes.module.scss b/src/front/Components/Layouts/Folder/FolderInformation/FolderInformationOld/classes.module.scss deleted file mode 100644 index a274d91e..00000000 --- a/src/front/Components/Layouts/Folder/FolderInformation/FolderInformationOld/classes.module.scss +++ /dev/null @@ -1,113 +0,0 @@ -@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; - } -} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/FolderInformationOld/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/FolderInformationOld/index.tsx deleted file mode 100644 index 7d943dd7..00000000 --- a/src/front/Components/Layouts/Folder/FolderInformation/FolderInformationOld/index.tsx +++ /dev/null @@ -1,541 +0,0 @@ -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; - getFolderCallback: () => Promise; - openedCustomer?: string; -}; - -type IState = { - isArchivedModalOpen: boolean; - inputArchivedDescripton: string; - isValidateModalVisible: boolean; - hasValidateAnchoring: boolean; - isVerifDeleteModalVisible: boolean; - isPreventArchiveModalOpen: boolean; - loadingAnchoring: boolean; -}; -class FolderInformationClass extends BasePage { - 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 ( - - {!this.props.isLoading && ( -
- {this.props.selectedFolder ? ( -
-
-
-
- Informations du dossier -
- - - -
- -
- -
-
- -
- {this.doesFolderHaveCustomer() && ( - - )} -
- - {!this.doesFolderHaveCustomer() && ( - - )} - -
- - {this.everyDocumentValidated() && !this.props.isLoading && ( - <> - {this.props.isAnchored === AnchorStatus.NOT_ANCHORED && ( - - )} - {this.props.isAnchored === AnchorStatus.ANCHORING && ( - - )} - {this.props.isAnchored === AnchorStatus.VERIFIED_ON_CHAIN && ( - - )} - - )} - {this.canDeleteFolder() && ( - - - - )} -
- -
- Souhaitez-vous vraiment archiver le dossier ? -
- -
- -
- - Vous êtes en train d’archiver le dossier sans avoir l’ancré, êtes-vous sûr de vouloir le faire ? - -
- -
- -
- Cette action sera irréversible. -
-
- -
- ) : ( -
- Informations du dossier -
- - Sélectionnez un dossier - -
-
- )} -
- )} - {this.props.isLoading && ( -
-
- -
-
- )} - -
- {!this.state.hasValidateAnchoring && ( - - 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. - - )} - {this.state.hasValidateAnchoring && ( -
- - 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. - - -
- )} -
-
-
- ); - } - - 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) { - 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.NOT_ANCHORED); - const [isLoading, setIsLoading] = useState(true); - const [selectedFolder, setSelectedFolder] = useState(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 ( - - ); -} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/index.tsx index 4575828b..925a1b3f 100644 --- a/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/index.tsx +++ b/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/index.tsx @@ -5,31 +5,21 @@ import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Ty import Module from "@Front/Config/Module"; import { ArchiveBoxIcon, PencilSquareIcon, UserGroupIcon } from "@heroicons/react/24/outline"; import { OfficeFolder } from "le-coffre-resources/dist/Notary"; -import { EDocumentStatus } from "le-coffre-resources/dist/Notary/Document"; import Link from "next/link"; -import { useCallback } from "react"; import classes from "./classes.module.scss"; +import { AnchorStatus } from ".."; type IProps = { folder: OfficeFolder | null; + progress: number; + onArchive: () => void; + anchorStatus: AnchorStatus; + isArchived: boolean; }; 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]); + const { folder, progress, onArchive, anchorStatus, isArchived } = props; return (
@@ -51,26 +41,39 @@ export default function InformationSection(props: IProps) {
- +
- - } variant={EIconButtonVariant.NEUTRAL} /> - - + {anchorStatus === AnchorStatus.NOT_ANCHORED && ( + <> + + } + variant={EIconButtonVariant.NEUTRAL} + /> + + + } + variant={EIconButtonVariant.NEUTRAL} + /> + + + )} + {!isArchived && ( } - variant={EIconButtonVariant.NEUTRAL} + onClick={onArchive} + icon={} + variant={EIconButtonVariant.ERROR} /> - - } variant={EIconButtonVariant.ERROR} /> + )}
diff --git a/src/front/Components/Layouts/Folder/FolderInformation/NoClientView/DeleteFolderModal/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/NoClientView/DeleteFolderModal/index.tsx index 4e2e74c1..814f5d56 100644 --- a/src/front/Components/Layouts/Folder/FolderInformation/NoClientView/DeleteFolderModal/index.tsx +++ b/src/front/Components/Layouts/Folder/FolderInformation/NoClientView/DeleteFolderModal/index.tsx @@ -14,7 +14,7 @@ type IProps = { export default function DeleteFolderModal(props: IProps) { const { isOpen, onClose, folder } = props; - const navigate = useRouter(); + const router = useRouter(); const onDelete = useCallback(() => { if (!folder.uid) return; @@ -23,17 +23,17 @@ export default function DeleteFolderModal(props: IProps) { return Folders.getInstance() .delete(folder.uid) - .then(() => navigate.push(Module.getInstance().get().modules.pages.Folder.props.path)) + .then(() => router.push(Module.getInstance().get().modules.pages.Folder.props.path)) .then(onClose); - }, [folder, navigate, onClose]); + }, [folder, router, onClose]); return ( + firstButton={{ children: "Annuler", onClick: onClose }} + secondButton={{ children: "Supprimer le dossier", onClick: onDelete }}> Cette action est irréversible. En supprimant ce dossier, toutes les informations associées seront définitivement perdues. diff --git a/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringAlertInfo/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringAlertInfo/index.tsx new file mode 100644 index 00000000..b8d243ed --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringAlertInfo/index.tsx @@ -0,0 +1,24 @@ +import Alert, { EAlertVariant } from "@Front/Components/DesignSystem/Alert"; +import { EButtonstyletype } from "@Front/Components/DesignSystem/Button"; +import { LockClosedIcon } from "@heroicons/react/24/outline"; + +type IProps = { + onAnchor: () => void; +}; + +export default function AnchoringAlertInfo(props: IProps) { + const { onAnchor } = props; + return ( + , + onClick: onAnchor, + }} + variant={EAlertVariant.INFO} + /> + ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringAlertSuccess/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringAlertSuccess/index.tsx new file mode 100644 index 00000000..3cef33fb --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringAlertSuccess/index.tsx @@ -0,0 +1,35 @@ +import Alert, { EAlertVariant } from "@Front/Components/DesignSystem/Alert"; +import { EButtonstyletype } from "@Front/Components/DesignSystem/Button"; +import { ArrowDownOnSquareIcon, CheckIcon } from "@heroicons/react/24/outline"; + +type IProps = { + onDownloadAnchoringProof: () => void; + onArchive: () => void; + isArchived: boolean; +}; + +export default function AnchoringAlertSuccess(props: IProps) { + const { onDownloadAnchoringProof, onArchive, isArchived } = props; + return ( + , + onClick: onDownloadAnchoringProof, + }} + secondButton={ + isArchived + ? undefined + : { + children: "Archiver le dossier", + onClick: onArchive, + } + } + variant={EAlertVariant.SUCCESS} + icon={} + /> + ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringModal/classes.module.scss b/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringModal/classes.module.scss new file mode 100644 index 00000000..f0139309 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringModal/classes.module.scss @@ -0,0 +1,11 @@ +.anchoring { + display: flex; + flex-direction: column; + gap: 24px; + + .validate-gif { + width: 100%; + height: 100%; + object-fit: contain; + } +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringModal/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringModal/index.tsx new file mode 100644 index 00000000..6710946a --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/elements/AnchoringModal/index.tsx @@ -0,0 +1,52 @@ +import OfficeFolderAnchors from "@Front/Api/LeCoffreApi/Notary/OfficeFolderAnchors/OfficeFolderAnchors"; +import ValidateAnchoringGif from "@Front/Assets/images/validate_anchoring.gif"; +import Modal from "@Front/Components/DesignSystem/Modal"; +import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography"; +import Image from "next/image"; +import React, { useCallback, useState } from "react"; + +import classes from "./classes.module.scss"; + +type IProps = { + isOpen: boolean; + onClose?: () => void; + folderUid: string; + onAnchorSuccess?: () => void; +}; + +export default function AnchoringModal(props: IProps) { + const { isOpen, onClose, folderUid, onAnchorSuccess } = props; + const [isAnchoring, setIsAnchoring] = useState(false); + + const anchor = useCallback(() => { + setIsAnchoring(true); + OfficeFolderAnchors.getInstance() + .post(folderUid) + .then(onAnchorSuccess) + .catch((e) => console.warn(e)) + }, [folderUid, onAnchorSuccess]); + + return ( + + {!isAnchoring ? ( + + La certification et l'ancrage de ce dossier dans la blockchain sont des actions définitives et garantiront la sécurité + et l'authenticité de tous les documents. Veuillez confirmer que vous souhaitez continuer. + + ) : ( +
+ + Vos documents sont en train d'être ancrés dans la blockchain. Cela peut prendre quelques instants. Merci de votre + patience. + + +
+ )} +
+ ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/elements/ArchiveModal/classes.module.scss b/src/front/Components/Layouts/Folder/FolderInformation/elements/ArchiveModal/classes.module.scss new file mode 100644 index 00000000..455f1de2 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/elements/ArchiveModal/classes.module.scss @@ -0,0 +1,5 @@ +.root { + display: flex; + flex-direction: column; + gap: var(--spacing-lg); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/elements/ArchiveModal/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/elements/ArchiveModal/index.tsx new file mode 100644 index 00000000..441b71b3 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/elements/ArchiveModal/index.tsx @@ -0,0 +1,49 @@ +import Folders from "@Front/Api/LeCoffreApi/Notary/Folders/Folders"; +import TextAreaField from "@Front/Components/DesignSystem/Form/TextareaField"; +import Modal from "@Front/Components/DesignSystem/Modal"; +import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography"; +import Module from "@Front/Config/Module"; +import { useRouter } from "next/router"; +import React, { useCallback } from "react"; +import classes from "./classes.module.scss"; + +type IProps = { + isOpen: boolean; + onClose?: () => void; + folderUid: string; +}; + +export default function ArchiveModal(props: IProps) { + const { isOpen, onClose, folderUid } = props; + const router = useRouter(); + + const archive = useCallback(() => { + if (!folderUid) return; + const description = (document.querySelector("textarea[name='archived_description']") as HTMLTextAreaElement).value ?? ""; + + Folders.getInstance() + .archive(folderUid, description) + .then(onClose) + .then(() => router.push(Module.getInstance().get().modules.pages.Folder.props.path)) + .catch((e) => { + console.warn(e); + }); + }, [folderUid, onClose, router]); + + return ( + +
+ + Archiver ce dossier le déplacera dans la section des dossiers archivés. Vous pouvez ajouter une note de dossier avant + d'archiver si vous le souhaitez. + + +
+
+ ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/elements/DownloadAnchoringProofModal/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/elements/DownloadAnchoringProofModal/index.tsx new file mode 100644 index 00000000..910076a4 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/elements/DownloadAnchoringProofModal/index.tsx @@ -0,0 +1,47 @@ +import OfficeFolderAnchors from "@Front/Api/LeCoffreApi/Notary/OfficeFolderAnchors/OfficeFolderAnchors"; +import Modal from "@Front/Components/DesignSystem/Modal"; +import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography"; +import { OfficeFolder } from "le-coffre-resources/dist/Notary"; +import React, { useCallback } from "react"; + +type IProps = { + isOpen: boolean; + onClose?: () => void; + folder: OfficeFolder; +}; + +export default function DownloadAnchoringProofModal(props: IProps) { + const { isOpen, onClose, folder } = props; + + const downloadAnchoringProof = useCallback(async () => { + if (!folder?.uid) return; + try { + const file = await OfficeFolderAnchors.getInstance().download(folder.uid); + const url = window.URL.createObjectURL(file); + const a = document.createElement("a"); + a.style.display = "none"; + a.href = url; + a.download = `anchoring_proof_${folder?.folder_number}_${folder?.name}.zip`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + onClose?.(); + } catch (e) { + console.warn(e); + } + }, [folder?.folder_number, folder?.name, folder.uid, onClose]); + + return ( + + + Votre dossier a été validé et ancré dans la blockchain. Vous pouvez maintenant télécharger la preuve d'ancrage pour vos + archives. + + + ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/elements/RequireAnchoringModal/classes.module.scss b/src/front/Components/Layouts/Folder/FolderInformation/elements/RequireAnchoringModal/classes.module.scss new file mode 100644 index 00000000..f0139309 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/elements/RequireAnchoringModal/classes.module.scss @@ -0,0 +1,11 @@ +.anchoring { + display: flex; + flex-direction: column; + gap: 24px; + + .validate-gif { + width: 100%; + height: 100%; + object-fit: contain; + } +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/elements/RequireAnchoringModal/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/elements/RequireAnchoringModal/index.tsx new file mode 100644 index 00000000..d6273927 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/elements/RequireAnchoringModal/index.tsx @@ -0,0 +1,32 @@ +import Modal from "@Front/Components/DesignSystem/Modal"; +import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography"; +import React, { useCallback } from "react"; + +type IProps = { + isOpen: boolean; + onClose: () => void; + onAnchor: () => void; +}; + +export default function RequireAnchoringModal(props: IProps) { + const { isOpen, onClose, onAnchor: onAnchorProps } = props; + + const onAnchor = useCallback(() => { + onAnchorProps(); + onClose(); + }, [onAnchorProps, onClose]); + + return ( + + + Pour archiver ce dossier, il est nécessaire de l'ancrer dans la blockchain afin de garantir la sécurité et l'authenticité + des documents. Veuillez procéder à l'ancrage avant de continuer. + + + ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/index.tsx index 07c80935..c9770a16 100644 --- a/src/front/Components/Layouts/Folder/FolderInformation/index.tsx +++ b/src/front/Components/Layouts/Folder/FolderInformation/index.tsx @@ -3,13 +3,21 @@ import OfficeFolderAnchors from "@Front/Api/LeCoffreApi/Notary/OfficeFolderAncho import Loader from "@Front/Components/DesignSystem/Loader"; import DefaultNotaryDashboard from "@Front/Components/LayoutTemplates/DefaultNotaryDashboard"; import { OfficeFolder } from "le-coffre-resources/dist/Notary"; +import { EDocumentStatus } from "le-coffre-resources/dist/Notary/Document"; import { useParams } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; import classes from "./classes.module.scss"; +import ClientView from "./ClientView"; import InformationSection from "./InformationSection"; import NoClientView from "./NoClientView"; -import ClientView from "./ClientView"; +import AnchoringAlertInfo from "./elements/AnchoringAlertInfo"; +import AnchoringModal from "./elements/AnchoringModal"; +import useOpenable from "@Front/Hooks/useOpenable"; +import AnchoringAlertSuccess from "./elements/AnchoringAlertSuccess"; +import DownloadAnchoringProofModal from "./elements/DownloadAnchoringProofModal"; +import RequireAnchoringModal from "./elements/RequireAnchoringModal"; +import ArchiveModal from "./elements/ArchiveModal"; export enum AnchorStatus { "VERIFIED_ON_CHAIN" = "VERIFIED_ON_CHAIN", @@ -17,16 +25,36 @@ export enum AnchorStatus { "NOT_ANCHORED" = "NOT_ANCHORED", } -type IProps = {}; +type IProps = { isArchived?: boolean }; export default function FolderInformation(props: IProps) { + const { isArchived = false } = props; const [anchorStatus, setAnchorStatus] = useState(AnchorStatus.NOT_ANCHORED); const [isLoading, setIsLoading] = useState(true); const [folder, setFolder] = useState(null); + const anchoringModal = useOpenable(); + const downloadAnchoringProofModal = useOpenable(); + const requireAnchoringModal = useOpenable(); + const archiveModal = useOpenable(); const params = useParams(); const folderUid = params["folderUid"] as string; + const progress = useMemo(() => { + let total = 0; + let validatedDocuments = 0; + folder?.customers?.forEach((customer) => { + const documents = customer.documents; + total += documents?.length ?? 0; + validatedDocuments += documents?.filter((document) => document.document_status === EDocumentStatus.VALIDATED).length ?? 0; + }); + if (total === 0) return 0; + const percentage = (validatedDocuments / total) * 100; + return isNaN(percentage) ? 0 : percentage; + }, [folder]); + + const doesFolderHaveClient = useMemo(() => folder?.customers?.length !== 0, [folder]); + const fetchFolder = useCallback(async () => { if (!folderUid) return; const query = { @@ -77,23 +105,69 @@ export default function FolderInformation(props: IProps) { .catch(() => setAnchorStatus(AnchorStatus.NOT_ANCHORED)); }, [folderUid]); - useEffect(() => { + const fetchData = useCallback(() => { setIsLoading(true); - fetchFolder() + return fetchFolder() .then(() => fetchAnchorStatus()) .catch((e) => console.error(e)) .finally(() => setIsLoading(false)); - }, [fetchAnchorStatus, fetchFolder, folderUid]); + }, [fetchAnchorStatus, fetchFolder]); - const doesFolderHaveClient = useMemo(() => folder?.customers?.length !== 0, [folder]); + useEffect(() => { + fetchData(); + }, [fetchData]); + + const onAnchorSuccess = useCallback(() => fetchData().then(downloadAnchoringProofModal.open), [fetchData, downloadAnchoringProofModal]); + + const onArchive = useCallback(() => { + if (anchorStatus === AnchorStatus.NOT_ANCHORED) return requireAnchoringModal.open(); + archiveModal.open(); + }, [anchorStatus, archiveModal, requireAnchoringModal]); return ( - + {!isLoading && (
- + + {progress === 100 && anchorStatus === AnchorStatus.NOT_ANCHORED && ( + + )} + {anchorStatus === AnchorStatus.VERIFIED_ON_CHAIN && ( + + )} {folder && !doesFolderHaveClient && } {folder && doesFolderHaveClient && } + {folderUid && anchorStatus === AnchorStatus.NOT_ANCHORED && ( + + )} + {folder && anchorStatus === AnchorStatus.VERIFIED_ON_CHAIN && ( + + )} + + {folderUid && }
)} {isLoading && ( diff --git a/src/front/Components/Layouts/FolderArchived/FolderInformation/ClientSection/classes.module.scss b/src/front/Components/Layouts/FolderArchived/FolderInformation/ClientSection/classes.module.scss deleted file mode 100644 index 1ac3bba8..00000000 --- a/src/front/Components/Layouts/FolderArchived/FolderInformation/ClientSection/classes.module.scss +++ /dev/null @@ -1,25 +0,0 @@ -@import "@Themes/constants.scss"; - -.root { - width: 100%; - padding-bottom: 32px; - - .no-client { - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - padding: 72px; - - .title { - margin-bottom: 16px; - text-align: center; - } - } - - .client { - display: grid; - gap: 32px; - margin-bottom: 32px; - } -} diff --git a/src/front/Components/Layouts/FolderArchived/FolderInformation/ClientSection/index.tsx b/src/front/Components/Layouts/FolderArchived/FolderInformation/ClientSection/index.tsx deleted file mode 100644 index e3d4caae..00000000 --- a/src/front/Components/Layouts/FolderArchived/FolderInformation/ClientSection/index.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React from "react"; -import classes from "./classes.module.scss"; -import { OfficeFolder } from "le-coffre-resources/dist/Notary"; -import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; -import UserFolder from "@Front/Components/DesignSystem/UserFolder"; -import { AnchorStatus } from "@Front/Components/Layouts/Folder/FolderInformation"; - -type IProps = { - folder: OfficeFolder; - anchorStatus: AnchorStatus; - getFolderCallback: () => Promise; -}; -type IState = { - openedCustomer: string; -}; - -export default class ClientSection extends React.Component { - public constructor(props: IProps) { - super(props); - this.state = { - openedCustomer: "", - }; - - this.changeUserFolder = this.changeUserFolder.bind(this); - } - - public override render(): JSX.Element { - return ( -
- {this.doesFolderHaveCustomer() ? ( - <> -
{this.renderCustomerFolders()}
- - ) : ( -
- - Aucun client dans ce dossier - -
- )} -
- ); - } - - private renderCustomerFolders() { - const output = this.props.folder.customers?.map((customer) => { - if (!customer) return null; - return ( - - ); - }); - return output ?? null; - } - - private changeUserFolder(uid: string) { - this.setState({ - openedCustomer: uid === this.state.openedCustomer ? "" : uid, - }); - } - - private doesFolderHaveCustomer(): boolean { - if (!this.props.folder?.customers) return false; - return this.props.folder?.customers!.length > 0; - } -} diff --git a/src/front/Components/Layouts/FolderArchived/FolderInformation/index.tsx b/src/front/Components/Layouts/FolderArchived/FolderInformation/index.tsx index 600ee8f6..6140b167 100644 --- a/src/front/Components/Layouts/FolderArchived/FolderInformation/index.tsx +++ b/src/front/Components/Layouts/FolderArchived/FolderInformation/index.tsx @@ -1,309 +1,5 @@ -import ChevronIcon from "@Assets/Icons/chevron.svg"; -import Folders from "@Front/Api/LeCoffreApi/Notary/Folders/Folders"; -import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button"; -import FolderBoxInformation, { EFolderBoxInformationType } from "@Front/Components/DesignSystem/FolderBoxInformation"; -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 { NextRouter, useRouter } from "next/router"; +import FolderInformation from "../../Folder/FolderInformation"; -import BasePage from "../../Base"; -import classes from "./classes.module.scss"; -import ClientSection from "./ClientSection"; -import Link from "next/link"; -import { AnchorStatus } from "../../Folder/FolderInformation"; -import { useCallback, useEffect, useState } from "react"; -import OfficeFolderAnchors from "@Front/Api/LeCoffreApi/Notary/OfficeFolderAnchors/OfficeFolderAnchors"; -import Loader from "@Front/Components/DesignSystem/Loader"; - -type IProps = {}; - -type IPropsClass = IProps & { - router: NextRouter; - selectedFolderUid: string; - isLoading: boolean; - selectedFolder: OfficeFolder | null; - isAnchored: AnchorStatus; - getFolderCallback: () => Promise; -}; - -type IState = { - selectedFolder: OfficeFolder | null; - isArchivedModalOpen: boolean; - loadingAnchoring: boolean; -}; -class FolderInformationClass extends BasePage { - public constructor(props: IPropsClass) { - super(props); - this.state = { - selectedFolder: null, - isArchivedModalOpen: false, - loadingAnchoring: false, - }; - this.onSelectedFolder = this.onSelectedFolder.bind(this); - this.openArchivedModal = this.openArchivedModal.bind(this); - this.closeArchivedModal = this.closeArchivedModal.bind(this); - this.restoreFolder = this.restoreFolder.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 ( - - {!this.props.isLoading && ( -
- {this.state.selectedFolder ? ( -
-
-
-
- Informations du dossier -
- - - -
- -
- -
-
- -
- -
- -
- {this.doesFolderHaveCustomer() && ( - - )} -
- - {!this.doesFolderHaveCustomer() && ( - - )} - -
- - {this.props.isAnchored === AnchorStatus.VERIFIED_ON_CHAIN && ( - - )} -
-
- ) : ( -
- Informations du dossier -
- - Aucun dossier sélectionné - -
-
- )} -
- )} - {this.props.isLoading && ( -
-
- -
-
- )} -
- ); - } - public override async componentDidMount() { - const folder = await this.getFolder(); - this.setState({ selectedFolder: folder }); - } - - private doesFolderHaveCustomer(): boolean { - return this.state.selectedFolder?.customers !== undefined; - } - - private onSelectedFolder(folder: OfficeFolder): void { - this.setState({ selectedFolder: folder }); - } - - private async restoreFolder() { - if (!this.state.selectedFolder) return; - await Folders.getInstance().restore(this.state.selectedFolder.uid ?? ""); - this.props.router.push( - Module.getInstance() - .get() - .modules.pages.Folder.pages.FolderInformation.props.path.replace("[folderUid]", this.props.selectedFolderUid), - ); - } - - private openArchivedModal(): void { - this.setState({ isArchivedModalOpen: true }); - } - - private closeArchivedModal(): void { - this.setState({ isArchivedModalOpen: false }); - } - - 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 async getFolder(): Promise { - const query = { - q: { - deed: { include: { deed_type: "true" } }, - office: "true", - customers: { include: { contact: true } }, - notes: "true", - }, - }; - const folder = await Folders.getInstance().getByUid(this.props.selectedFolderUid, query); - return folder; - } -} - -export default function FolderInformation(props: IProps) { - const router = useRouter(); - const [isAnchored, setIsAnchored] = useState(AnchorStatus.NOT_ANCHORED); - const [isLoading, setIsLoading] = useState(true); - const [selectedFolder, setSelectedFolder] = useState(null); - - let { folderUid } = router.query; - folderUid = folderUid as string; - - const getAnchoringStatus = useCallback(async () => { - if (!folderUid) return; - setIsLoading(true); - 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); - } - setIsLoading(false); - }, [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, - }, - }; - 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 ( - - ); +export default function FolderInformationArchived() { + return ; } diff --git a/src/front/Hooks/useOpenable.ts b/src/front/Hooks/useOpenable.ts index ad7a6e24..6527e414 100644 --- a/src/front/Hooks/useOpenable.ts +++ b/src/front/Hooks/useOpenable.ts @@ -1,26 +1,26 @@ import { useCallback, useState } from "react"; -function useOpenable() { - const [isOpen, setIsOpen] = useState(false); +function useOpenable({ defaultOpen } = { defaultOpen: false }) { + const [isOpen, setIsOpen] = useState(defaultOpen); - const open = useCallback(() => { - setIsOpen(true); - }, []); + const open = useCallback(() => { + setIsOpen(true); + }, []); - const close = useCallback(() => { - setIsOpen(false); - }, []); + const close = useCallback(() => { + setIsOpen(false); + }, []); - const toggle = useCallback(() => { - setIsOpen((prev) => !prev); - }, []); + const toggle = useCallback(() => { + setIsOpen((prev) => !prev); + }, []); - return { - isOpen, - open, - close, - toggle, - }; + return { + isOpen, + open, + close, + toggle, + }; } export default useOpenable;