Merge branch 'dev' into staging

This commit is contained in:
Max S 2024-07-23 15:56:58 +02:00
commit 24fe70703a
13 changed files with 305 additions and 50 deletions

View File

@ -51,7 +51,7 @@ class ToastElementClass extends React.Component<IPropsClass, IState> {
data-clickable={toast.redirectUrl ? true : false} data-clickable={toast.redirectUrl ? true : false}
onClick={this.handleClick}> onClick={this.handleClick}>
{toast.time !== 0 && <div className={classes["loadbar"]} style={style} />} {toast.time !== 0 && <div className={classes["loadbar"]} style={style} />}
<div className={classes["header"]} onClick={this.onClose}> <div className={classes["header"]}>
<div className={classes["text-icon_row"]}> <div className={classes["text-icon_row"]}>
{toast.icon && toast.icon} {toast.icon && toast.icon}
<div className={classes["text-container"]}> <div className={classes["text-container"]}>

View File

@ -149,6 +149,10 @@ export enum ETypoColor {
INPUT_ERROR = "--input-error", INPUT_ERROR = "--input-error",
TEXT_ACCENT = "--text-accent", TEXT_ACCENT = "--text-accent",
CONTRAST_DEFAULT = "--contrast-default",
CONTRAST_HOVERED = "--contrast-hovered",
ERROR_WEAK_CONTRAST = "--error-weak-contrast",
} }
export default function Typography(props: IProps) { export default function Typography(props: IProps) {

View File

@ -0,0 +1,23 @@
.menu-item-wrapper {
width: 100%;
.menu-item {
display: flex;
padding: var(--spacing-md, 16px);
justify-content: flex-start;
align-items: center;
gap: var(--spacing-lg, 24px);
cursor: pointer;
> svg {
width: 24px;
height: 24px;
transition: all ease-in-out 0.1s;
}
}
.separator {
width: 100%;
height: 1px;
background-color: var(--separator-stroke-light, #d7dce0);
}
}

View File

@ -0,0 +1,50 @@
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import classes from "./classes.module.scss";
import { useRouter } from "next/router";
import React, { useCallback } from "react";
import useHoverable from "@Front/Hooks/useHoverable";
import { ISubElement } from "..";
type IProps = {
element: ISubElement;
closeMenuCb: () => void;
};
export default function SubMenuItem(props: IProps) {
const { element, closeMenuCb } = props;
const router = useRouter();
const handleClickElement = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
closeMenuCb();
const link = e.currentTarget.getAttribute("data-link");
if (link) router.push(link);
if (element.onClick) element.onClick();
},
[closeMenuCb, element, router],
);
const { handleMouseEnter, handleMouseLeave, isHovered } = useHoverable();
// The element has a link or an onClick but not both, if it has a link, show it has a link, if it has an onClick, show it has an onClick
const getColor = useCallback(() => {
if (isHovered && element.color !== ETypoColor.ERROR_WEAK_CONTRAST) return ETypoColor.CONTRAST_HOVERED;
if (element.color) return element.color;
return ETypoColor.CONTRAST_DEFAULT;
}, [element.color, isHovered]);
return (
<div
className={classes["menu-item-wrapper"]}
onClick={handleClickElement}
data-link={element.link}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}>
<div className={classes["menu-item"]}>
{React.cloneElement(element.icon, { color: `var(${getColor()})` })}
<Typography typo={ETypo.TEXT_LG_REGULAR} color={getColor()}>
{element.text}
</Typography>
</div>
{element.hasSeparator && <div className={classes["separator"]} />}
</div>
);
}

View File

@ -0,0 +1,29 @@
.root {
position: relative;
width: fit-content;
.sub-menu {
position: absolute;
top: 48px;
display: inline-flex;
padding: var(--spacing-05, 4px) var(--spacing-2, 16px);
flex-direction: column;
align-items: flex-start;
border-radius: var(--menu-radius, 0px);
border: 1px solid var(--menu-border, #d7dce0);
background: var(--color-generic-white, #fff);
text-wrap: nowrap;
/* shadow/sm */
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.1);
z-index: 2;
&[data-opening-side="left"] {
left: auto;
right: 0px;
}
&[data-opening-side="right"] {
left: 0px;
right: auto;
}
}
}

View File

@ -0,0 +1,78 @@
import IconButton, { EIconButtonVariant } from "@Front/Components/DesignSystem/IconButton";
import classes from "./classes.module.scss";
import { ETypoColor } from "@Front/Components/DesignSystem/Typography";
import React, { useEffect, useRef, useState } from "react";
import SubMenuItem from "./SubMenuItem";
type ISubElementBase = {
icon: JSX.Element;
text: string;
hasSeparator?: boolean;
color?: ETypoColor;
};
type ISubElementWithLink = ISubElementBase & {
link: string;
onClick?: never;
};
type ISubElementWithOnClick = ISubElementBase & {
onClick: () => void;
link?: never;
};
export type ISubElement = ISubElementWithLink | ISubElementWithOnClick;
type IProps = {
icon: React.ReactNode;
text?: string;
subElements: ISubElement[];
openingSide?: "left" | "right";
};
export default function ButtonWithSubMenu(props: IProps) {
const { openingSide = "left" } = props;
const [isSubMenuOpened, setIsSubMenuOpened] = useState(false);
const subMenuRef = useRef<HTMLDivElement>(null);
const iconRef = useRef<HTMLDivElement>(null);
const handleClick = () => {
setIsSubMenuOpened((prev) => !prev);
};
const closeMenu = () => {
setIsSubMenuOpened(false);
};
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (
subMenuRef.current &&
!subMenuRef.current.contains(e.target as Node) &&
iconRef.current &&
!iconRef.current.contains(e.target as Node)
) {
setIsSubMenuOpened(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return (
<div className={classes["root"]}>
<div className={classes["main"]} onClick={handleClick} ref={iconRef}>
<IconButton icon={props.icon} variant={EIconButtonVariant.NEUTRAL} />
</div>
{isSubMenuOpened && (
<div className={classes["sub-menu"]} ref={subMenuRef} data-opening-side={openingSide}>
{props.subElements.map((element, index) => {
return <SubMenuItem element={element} key={index} closeMenuCb={closeMenu} />;
})}
</div>
)}
</div>
);
}

View File

@ -9,15 +9,24 @@ import Modal from "@Front/Components/DesignSystem/Modal";
import Newsletter from "@Front/Components/DesignSystem/Newsletter"; import Newsletter from "@Front/Components/DesignSystem/Newsletter";
import Table from "@Front/Components/DesignSystem/Table"; import Table from "@Front/Components/DesignSystem/Table";
import Tag, { ETagColor, ETagVariant } from "@Front/Components/DesignSystem/Tag"; import Tag, { ETagColor, ETagVariant } from "@Front/Components/DesignSystem/Tag";
import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography"; import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import NumberPicker from "@Front/Components/Elements/NumberPicker"; import NumberPicker from "@Front/Components/Elements/NumberPicker";
import Tabs from "@Front/Components/Elements/Tabs"; import Tabs from "@Front/Components/Elements/Tabs";
import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate"; import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate";
import useOpenable from "@Front/Hooks/useOpenable"; import useOpenable from "@Front/Hooks/useOpenable";
import { ArrowLongLeftIcon, ArrowLongRightIcon, XMarkIcon } from "@heroicons/react/24/outline"; import {
ArchiveBoxIcon,
ArrowLongLeftIcon,
ArrowLongRightIcon,
EllipsisHorizontalIcon,
PencilSquareIcon,
UsersIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import classes from "./classes.module.scss"; import classes from "./classes.module.scss";
import ButtonWithSubMenu from "@Front/Components/Elements/ButtonWithSubMenu";
export default function DesignSystem() { export default function DesignSystem() {
const { isOpen, open, close } = useOpenable(); const { isOpen, open, close } = useOpenable();
@ -69,6 +78,30 @@ export default function DesignSystem() {
<Newsletter isOpen /> <Newsletter isOpen />
<div className={classes["root"]}> <div className={classes["root"]}>
<div className={classes["components"]}> <div className={classes["components"]}>
<Typography typo={ETypo.TEXT_LG_BOLD}>Button icon with menu</Typography>
<ButtonWithSubMenu
icon={<EllipsisHorizontalIcon />}
subElements={[
{
icon: <UsersIcon />,
text: "Modifier les collaborateurs",
onClick: () => alert("yo"),
hasSeparator: true,
},
{
icon: <PencilSquareIcon />,
text: "Modifier les informations du dossier",
link: "/",
hasSeparator: true,
},
{
icon: <ArchiveBoxIcon />,
text: "Archiver le dossier",
link: "/",
color: ETypoColor.ERROR_WEAK_CONTRAST,
},
]}
/>
<Typography typo={ETypo.TEXT_LG_BOLD}>Inputs</Typography> <Typography typo={ETypo.TEXT_LG_BOLD}>Inputs</Typography>
<Typography typo={ETypo.TEXT_SM_REGULAR}>Number picker avec min à 1 et max à 10</Typography> <Typography typo={ETypo.TEXT_SM_REGULAR}>Number picker avec min à 1 et max à 10</Typography>
<NumberPicker defaultValue={1} onChange={() => {}} min={1} max={10} /> <NumberPicker defaultValue={1} onChange={() => {}} min={1} max={10} />

View File

@ -122,7 +122,7 @@ export default function DocumentTables(props: IProps) {
label={tradDocumentStatus[document.document_status].toUpperCase()} label={tradDocumentStatus[document.document_status].toUpperCase()}
/> />
), ),
created_at: document.created_at, created_at: document.created_at ? new Date(document.created_at).toLocaleDateString() : "_",
actions: ( actions: (
<Link <Link
href={Module.getInstance() href={Module.getInstance()
@ -153,7 +153,7 @@ export default function DocumentTables(props: IProps) {
label={tradDocumentStatus[document.document_status].toUpperCase()} label={tradDocumentStatus[document.document_status].toUpperCase()}
/> />
), ),
created_at: document.created_at, created_at: document.created_at ? new Date(document.created_at).toLocaleDateString() : "_",
actions: ( actions: (
<div className={classes["actions"]}> <div className={classes["actions"]}>
<Link <Link
@ -187,7 +187,7 @@ export default function DocumentTables(props: IProps) {
label={tradDocumentStatus[document.document_status].toUpperCase()} label={tradDocumentStatus[document.document_status].toUpperCase()}
/> />
), ),
created_at: document.created_at, created_at: document.created_at ? new Date(document.created_at).toLocaleDateString() : "_",
actions: "", actions: "",
}; };
}) })

View File

@ -1,14 +1,14 @@
import CircleProgress from "@Front/Components/DesignSystem/CircleProgress"; import CircleProgress from "@Front/Components/DesignSystem/CircleProgress";
import IconButton, { EIconButtonVariant } from "@Front/Components/DesignSystem/IconButton";
import Tag, { ETagColor } from "@Front/Components/DesignSystem/Tag"; import Tag, { ETagColor } from "@Front/Components/DesignSystem/Tag";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import Module from "@Front/Config/Module"; import Module from "@Front/Config/Module";
import { ArchiveBoxIcon, PencilSquareIcon, UserGroupIcon } from "@heroicons/react/24/outline"; import { ArchiveBoxIcon, EllipsisHorizontalIcon, PencilSquareIcon, UsersIcon } from "@heroicons/react/24/outline";
import { OfficeFolder } from "le-coffre-resources/dist/Notary"; import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import Link from "next/link";
import classes from "./classes.module.scss"; import classes from "./classes.module.scss";
import { AnchorStatus } from ".."; import { AnchorStatus } from "..";
import ButtonWithSubMenu, { ISubElement } from "@Front/Components/Elements/ButtonWithSubMenu";
import { useCallback } from "react";
type IProps = { type IProps = {
folder: OfficeFolder | null; folder: OfficeFolder | null;
@ -21,6 +21,49 @@ type IProps = {
export default function InformationSection(props: IProps) { export default function InformationSection(props: IProps) {
const { folder, progress, onArchive, anchorStatus, isArchived } = props; const { folder, progress, onArchive, anchorStatus, isArchived } = props;
const getSubMenuElement = useCallback(() => {
let elements: ISubElement[] = [];
// Creating the three elements and adding them conditionnally
const modifyCollaboratorsElement = {
icon: <UsersIcon />,
text: "Modifier les collaborateurs",
link: Module.getInstance()
.get()
.modules.pages.Folder.pages.EditCollaborators.props.path.replace("[folderUid]", folder?.uid ?? ""),
hasSeparator: true,
};
const modifyInformationsElement = {
icon: <PencilSquareIcon />,
text: "Modifier les informations du dossier",
link: Module.getInstance()
.get()
.modules.pages.Folder.pages.EditInformations.props.path.replace("[folderUid]", folder?.uid ?? ""),
hasSeparator: true,
};
const archiveElement = {
icon: <ArchiveBoxIcon />,
text: "Archiver le dossier",
onClick: onArchive,
color: ETypoColor.ERROR_WEAK_CONTRAST,
};
// If the folder is not anchored, we can modify the collaborators and the informations
if (anchorStatus === AnchorStatus.NOT_ANCHORED) {
elements.push(modifyCollaboratorsElement);
// Remove the separator if it's the last item (if the folder is not archived)
if (isArchived) modifyInformationsElement.hasSeparator = false;
elements.push(modifyInformationsElement);
}
// If the folder is not archived, we can archive it
if (!isArchived) {
elements.push(archiveElement);
}
return elements;
}, [anchorStatus, folder?.uid, isArchived, onArchive]);
return ( return (
<section className={classes["root"]}> <section className={classes["root"]}>
<div className={classes["info-box1"]}> <div className={classes["info-box1"]}>
@ -43,37 +86,7 @@ export default function InformationSection(props: IProps) {
<div className={classes["progress-container"]}> <div className={classes["progress-container"]}>
<CircleProgress percentage={progress} /> <CircleProgress percentage={progress} />
<div className={classes["icon-container"]}> <div className={classes["icon-container"]}>
{anchorStatus === AnchorStatus.NOT_ANCHORED && ( <ButtonWithSubMenu icon={<EllipsisHorizontalIcon />} subElements={getSubMenuElement()} />
<>
<Link
href={Module.getInstance()
.get()
.modules.pages.Folder.pages.EditCollaborators.props.path.replace("[folderUid]", folder?.uid ?? "")}
title="Modifier les collaborateurs">
<IconButton
icon={<UserGroupIcon title="Modifier les collaborateurs" />}
variant={EIconButtonVariant.NEUTRAL}
/>
</Link>
<Link
href={Module.getInstance()
.get()
.modules.pages.Folder.pages.EditInformations.props.path.replace("[folderUid]", folder?.uid ?? "")}
title="Modifier les informations du dossiers">
<IconButton
icon={<PencilSquareIcon title="Modifier les informations du dossiers" />}
variant={EIconButtonVariant.NEUTRAL}
/>
</Link>
</>
)}
{!isArchived && (
<IconButton
onClick={onArchive}
icon={<ArchiveBoxIcon title="Archiver le dossier" />}
variant={EIconButtonVariant.ERROR}
/>
)}
</div> </div>
</div> </div>
<div className={classes["description-container"]}> <div className={classes["description-container"]}>

View File

@ -11,7 +11,7 @@ type IProps = {
isOpen: boolean; isOpen: boolean;
onClose?: () => void; onClose?: () => void;
folderUid: string; folderUid: string;
onAnchorSuccess?: () => void; onAnchorSuccess: () => void;
}; };
export default function AnchoringModal(props: IProps) { export default function AnchoringModal(props: IProps) {
@ -19,12 +19,22 @@ export default function AnchoringModal(props: IProps) {
const [isAnchoring, setIsAnchoring] = useState(false); const [isAnchoring, setIsAnchoring] = useState(false);
const anchor = useCallback(() => { const anchor = useCallback(() => {
const timeoutDelay = 9800;
const timeoutPromise = new Promise((resolve) => {
setTimeout(resolve, timeoutDelay);
});
setIsAnchoring(true); setIsAnchoring(true);
OfficeFolderAnchors.getInstance() return OfficeFolderAnchors.getInstance()
.post(folderUid) .post(folderUid)
.then(() => timeoutPromise)
.then(() => setIsAnchoring(false))
.then(onAnchorSuccess) .then(onAnchorSuccess)
.catch((e) => console.warn(e)) .then(onClose)
}, [folderUid, onAnchorSuccess]); .catch((e) => {
console.warn(e);
setIsAnchoring(false);
});
}, [folderUid, onAnchorSuccess, onClose]);
return ( return (
<Modal <Modal

View File

@ -0,0 +1,12 @@
import Alert, { EAlertVariant } from "@Front/Components/DesignSystem/Alert";
export default function AnchoringProcessingNeutral() {
return (
<Alert
title="Ancrage en cours..."
description="Vos documents sont en train d'être ancrés dans la blockchain. Cela peut prendre quelques instants. Merci de votre patience."
variant={EAlertVariant.NEUTRAL}
fullWidth
/>
);
}

View File

@ -13,6 +13,7 @@ import ClientView from "./ClientView";
import AnchoringAlertInfo from "./elements/AnchoringAlertInfo"; import AnchoringAlertInfo from "./elements/AnchoringAlertInfo";
import AnchoringAlertSuccess from "./elements/AnchoringAlertSuccess"; import AnchoringAlertSuccess from "./elements/AnchoringAlertSuccess";
import AnchoringModal from "./elements/AnchoringModal"; import AnchoringModal from "./elements/AnchoringModal";
import AnchoringProcessingNeutral from "./elements/AnchoringProcessingNeutral";
import ArchiveAlertWarning from "./elements/ArchiveAlertWarning"; import ArchiveAlertWarning from "./elements/ArchiveAlertWarning";
import ArchiveModal from "./elements/ArchiveModal"; import ArchiveModal from "./elements/ArchiveModal";
import DownloadAnchoringProofModal from "./elements/DownloadAnchoringProofModal"; import DownloadAnchoringProofModal from "./elements/DownloadAnchoringProofModal";
@ -66,6 +67,9 @@ export default function FolderInformation(props: IProps) {
include: { include: {
contact: true, contact: true,
documents: { documents: {
where: {
folder_uid: folderUid,
},
include: { include: {
folder: true, folder: true,
document_type: true, document_type: true,
@ -118,8 +122,6 @@ export default function FolderInformation(props: IProps) {
fetchData(); fetchData();
}, [fetchData]); }, [fetchData]);
const onAnchorSuccess = useCallback(() => fetchData().then(downloadAnchoringProofModal.open), [fetchData, downloadAnchoringProofModal]);
const onArchive = useCallback(() => { const onArchive = useCallback(() => {
if (anchorStatus === AnchorStatus.NOT_ANCHORED) return requireAnchoringModal.open(); if (anchorStatus === AnchorStatus.NOT_ANCHORED) return requireAnchoringModal.open();
archiveModal.open(); archiveModal.open();
@ -146,17 +148,18 @@ export default function FolderInformation(props: IProps) {
isArchived={isArchived} isArchived={isArchived}
/> />
)} )}
{!isArchived && anchorStatus === AnchorStatus.ANCHORING && <AnchoringProcessingNeutral />}
{isArchived && folderUid && ( {isArchived && folderUid && (
<ArchiveAlertWarning folderUid={folderUid} onDownloadAnchoringProof={downloadAnchoringProofModal.open} /> <ArchiveAlertWarning folderUid={folderUid} onDownloadAnchoringProof={downloadAnchoringProofModal.open} />
)} )}
{folder && !doesFolderHaveClient && <NoClientView folder={folder} anchorStatus={anchorStatus} />} {folder && !doesFolderHaveClient && <NoClientView folder={folder} anchorStatus={anchorStatus} />}
{folder && doesFolderHaveClient && <ClientView folder={folder} anchorStatus={anchorStatus} />} {folder && doesFolderHaveClient && <ClientView folder={folder} anchorStatus={anchorStatus} />}
{folderUid && anchorStatus === AnchorStatus.NOT_ANCHORED && ( {folderUid && (
<AnchoringModal <AnchoringModal
isOpen={anchoringModal.isOpen} isOpen={anchoringModal.isOpen}
onClose={anchoringModal.close} onClose={anchoringModal.close}
folderUid={folderUid} folderUid={folderUid}
onAnchorSuccess={onAnchorSuccess} onAnchorSuccess={fetchData}
/> />
)} )}
{folder && anchorStatus === AnchorStatus.VERIFIED_ON_CHAIN && ( {folder && anchorStatus === AnchorStatus.VERIFIED_ON_CHAIN && (

View File

@ -84,14 +84,14 @@ class UpdateClientClass extends BasePage<IPropsClass, IState> {
<div className={classes["content"]}> <div className={classes["content"]}>
<TextField <TextField
name="first_name" name="first_name"
placeholder="Nom" placeholder="Prénom"
onChange={this.onChangeNameInput} onChange={this.onChangeNameInput}
defaultValue={this.state.customer?.contact?.first_name} defaultValue={this.state.customer?.contact?.first_name}
validationError={this.state.validationError.find((error) => error.property === "first_name")} validationError={this.state.validationError.find((error) => error.property === "first_name")}
/> />
<TextField <TextField
name="last_name" name="last_name"
placeholder="Prénom" placeholder="Nom"
onChange={this.onChangeFirstNameInput} onChange={this.onChangeFirstNameInput}
defaultValue={this.state.customer?.contact?.last_name} defaultValue={this.state.customer?.contact?.last_name}
validationError={this.state.validationError.find((error) => error.property === "last_name")} validationError={this.state.validationError.find((error) => error.property === "last_name")}