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/AnchoringAlertInfo/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/AnchoringAlertInfo/index.tsx new file mode 100644 index 00000000..b8d243ed --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/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/AnchoringAlertSuccess/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/AnchoringAlertSuccess/index.tsx new file mode 100644 index 00000000..3ee5c80e --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/AnchoringAlertSuccess/index.tsx @@ -0,0 +1,30 @@ +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; +}; + +export default function AnchoringAlertSuccess(props: IProps) { + const { onDownloadAnchoringProof, onArchive } = props; + return ( + , + onClick: onDownloadAnchoringProof, + }} + secondButton={{ + children: "Archiver le dossier", + onClick: onArchive, + }} + variant={EAlertVariant.SUCCESS} + icon={} + /> + ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/AnchoringModal/classes.module.scss b/src/front/Components/Layouts/Folder/FolderInformation/AnchoringModal/classes.module.scss new file mode 100644 index 00000000..f0139309 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/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/AnchoringModal/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/AnchoringModal/index.tsx new file mode 100644 index 00000000..4d8abae4 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/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. + + Anchoring animation +
+ )} +
+ ); +} diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ArchiveModal/classes.module.scss b/src/front/Components/Layouts/Folder/FolderInformation/ArchiveModal/classes.module.scss new file mode 100644 index 00000000..455f1de2 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/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/ArchiveModal/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/ArchiveModal/index.tsx new file mode 100644 index 00000000..441b71b3 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/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/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..2f4e24d6 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[] = [ @@ -38,9 +39,11 @@ const header: readonly IHead[] = [ ]; 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 +60,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) => { @@ -88,11 +121,11 @@ export default function DocumentTables(props: IProps) { ), created_at: document.created_at, - actions: } />, + actions: onPreview(document)} icon={} />, }; }) .filter((document) => document !== null) as IRowProps[], - [documents], + [documents, onPreview], ); const validatedDocuments: IRowProps[] = useMemo( @@ -109,14 +142,14 @@ export default function DocumentTables(props: IProps) { 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( @@ -138,11 +171,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 +183,9 @@ export default function DocumentTables(props: IProps) { Documents - +
- +
{toValidateDocuments.length > 0 &&
} {validatedDocuments.length > 0 &&
} {refusedDocuments.length > 0 &&
} @@ -164,6 +197,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/DownloadAnchoringProofModal/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/DownloadAnchoringProofModal/index.tsx new file mode 100644 index 00000000..910076a4 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/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/InformationSection/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/index.tsx index 4575828b..dd3ef56d 100644 --- a/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/index.tsx +++ b/src/front/Components/Layouts/Folder/FolderInformation/InformationSection/index.tsx @@ -5,31 +5,20 @@ 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; }; 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 } = props; return (
@@ -51,26 +40,38 @@ export default function InformationSection(props: IProps) {
- +
- - } variant={EIconButtonVariant.NEUTRAL} /> - - - } - variant={EIconButtonVariant.NEUTRAL} - /> - - } variant={EIconButtonVariant.ERROR} /> + {anchorStatus === AnchorStatus.NOT_ANCHORED && ( + <> + + } + variant={EIconButtonVariant.NEUTRAL} + /> + + + } + variant={EIconButtonVariant.NEUTRAL} + /> + + + )} + + } + 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/RequireAnchoringModal/classes.module.scss b/src/front/Components/Layouts/Folder/FolderInformation/RequireAnchoringModal/classes.module.scss new file mode 100644 index 00000000..f0139309 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/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/RequireAnchoringModal/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/RequireAnchoringModal/index.tsx new file mode 100644 index 00000000..d6273927 --- /dev/null +++ b/src/front/Components/Layouts/Folder/FolderInformation/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..2808c41b 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 "./AnchoringAlertInfo"; +import AnchoringModal from "./AnchoringModal"; +import useOpenable from "@Front/Hooks/useOpenable"; +import AnchoringAlertSuccess from "./AnchoringAlertSuccess"; +import DownloadAnchoringProofModal from "./DownloadAnchoringProofModal"; +import RequireAnchoringModal from "./RequireAnchoringModal"; +import ArchiveModal from "./ArchiveModal"; export enum AnchorStatus { "VERIFIED_ON_CHAIN" = "VERIFIED_ON_CHAIN", @@ -23,10 +31,26 @@ export default function FolderInformation(props: IProps) { 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(() => { + const documents = folder?.documents; + if (!documents) return 0; + const total = documents.length; + const 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 +101,59 @@ 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/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;