Merge branch 'staging' into preprod

This commit is contained in:
Max S 2024-09-16 17:45:28 +02:00
commit 1efb6a85df
117 changed files with 5289 additions and 2245 deletions

849
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -25,10 +25,12 @@
"dotenv": "^16.0.3",
"eslint": "8.36.0",
"eslint-config-next": "13.2.4",
"file-saver": "^2.0.5",
"form-data": "^4.0.0",
"heroicons": "^2.1.5",
"jszip": "^3.10.1",
"jwt-decode": "^3.1.2",
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.151",
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.160",
"next": "^14.2.3",
"prettier": "^2.8.7",
"react": "18.2.0",
@ -43,6 +45,7 @@
"uuidv4": "^6.2.13"
},
"devDependencies": {
"@types/file-saver": "^2.0.7",
"@types/react-gtm-module": "^2.0.3"
}
}

View File

@ -87,4 +87,14 @@ export default class Customers extends BaseNotary {
return Promise.reject(err);
}
}
public async sendReminder(uid: string, documentsUid: string[]): Promise<void> {
const url = new URL(this.baseURl.concat(`/${uid}/send_reminder`));
try {
await this.postRequest<void>(url, { documentsUid });
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,41 @@
import { DocumentReminder } from "le-coffre-resources/dist/Notary";
import BaseNotary from "../BaseNotary";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetDocumentRemindersparams {
where?: {};
include?: {};
orderBy?: {};
}
// TODO Type getbyuid query params
export default class DocumentReminders extends BaseNotary {
private static instance: DocumentReminders;
private readonly baseURl = this.namespaceUrl.concat("/document_reminders");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
public async get(q: IGetDocumentRemindersparams): Promise<DocumentReminder[]> {
const url = new URL(this.baseURl);
const query = { q };
Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
return await this.getRequest<DocumentReminder[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,65 @@
import { DocumentNotary } from "le-coffre-resources/dist/Notary";
import BaseNotary from "../BaseNotary";
export interface IGetDocumentNotaryparams {
where?: {};
include?: {};
orderBy?: {};
}
export default class DocumentsNotary extends BaseNotary {
private static instance: DocumentsNotary;
private readonly baseURl = this.namespaceUrl.concat("/documents_notary");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
public async get(q: IGetDocumentNotaryparams): Promise<DocumentNotary[]> {
const url = new URL(this.baseURl);
const query = { q };
Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
return await this.getRequest<DocumentNotary[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Create a Document Notary
*/
public async post(body: any): Promise<DocumentNotary> {
const url = new URL(this.baseURl);
try {
return await this.postRequestFormData<DocumentNotary>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Delete a Document Notary
*/
public async delete(uid: string): Promise<void> {
const url = new URL(this.baseURl);
url.pathname = url.pathname.concat(`/${uid}`);
try {
await this.deleteRequest(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,71 @@
import { File } from "le-coffre-resources/dist/SuperAdmin";
import BaseNotary from "../BaseNotary";
export interface IGetFilesparams {
where?: {};
include?: {};
}
export type IPutFilesParams = {};
export interface IPostFilesParams {}
export default class FilesNotary extends BaseNotary {
private static instance: FilesNotary;
private readonly baseURl = this.namespaceUrl.concat("/files-notary");
private constructor() {
super();
}
public static getInstance() {
return (this.instance ??= new this());
}
public async get(q: IGetFilesparams): Promise<File[]> {
const url = new URL(this.baseURl);
const query = { q };
if (q) Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
const files = await this.getRequest<File[]>(url);
return files;
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async getByUid(uid: string, q?: any): Promise<File> {
const url = new URL(this.baseURl.concat(`/${uid}`));
const query = { q };
if (q) Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
const file = await this.getRequest<File>(url);
return file;
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async delete(uid: string): Promise<File> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.deleteRequest<File>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async download(uid: string): Promise<any> {
const url = new URL(this.baseURl.concat(`/download/${uid}`));
try {
return await this.getRequest<any>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -8,6 +8,7 @@ export interface IGetFoldersParams {
select?: {};
where?: {};
include?: {};
orderBy?: {};
};
}

View File

@ -11,6 +11,36 @@
border: 1px solid var(--alerts-info-border);
background: var(--alerts-info-background);
@media screen and (max-width: 680px) {
flex-direction: column;
}
.top {
align-self: stretch;
display: flex;
justify-content: space-between;
align-items: center;
}
.close-button {
display: block;
&.desktop {
display: block;
@media screen and (max-width: 680px) {
display: none;
}
}
&.mobile {
display: none;
@media screen and (max-width: 680px) {
display: block;
}
}
}
.content {
display: flex;
flex-direction: column;
@ -25,64 +55,34 @@
.button-container {
display: flex;
gap: var(--spacing-md, 16px);
@media screen and (max-width: 680px) {
flex-direction: column;
button {
width: 100%;
}
}
.icon {
display: flex;
padding: var(--spacing-1, 8px);
align-items: center;
align-self: flex-start;
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);
}
}
&.fullwidth {

View File

@ -7,6 +7,7 @@ import Button, { EButtonSize, EButtonstyletype, EButtonVariant, IButtonProps } f
import classNames from "classnames";
import IconButton from "../IconButton";
import useOpenable from "@Front/Hooks/useOpenable";
import BadgeIcon, { EBadgeColor } from "../BadgeIcon";
type IProps = {
variant: EAlertVariant;
@ -35,6 +36,14 @@ const variantButtonMap: Record<EAlertVariant, EButtonVariant> = {
[EAlertVariant.NEUTRAL]: EButtonVariant.NEUTRAL,
};
const variantColorMap: Record<EAlertVariant, EBadgeColor> = {
[EAlertVariant.INFO]: EBadgeColor.INFO,
[EAlertVariant.SUCCESS]: EBadgeColor.SUCCESS,
[EAlertVariant.WARNING]: EBadgeColor.WARNING,
[EAlertVariant.ERROR]: EBadgeColor.ERROR,
[EAlertVariant.NEUTRAL]: EBadgeColor.NEUTRAL,
};
export default function Alert(props: IProps) {
const { isOpen, close } = useOpenable({ defaultOpen: true });
const { variant = EAlertVariant.INFO, title, description, firstButton, secondButton, closeButton, icon, fullWidth } = props;
@ -43,7 +52,13 @@ export default function Alert(props: IProps) {
return (
<div className={classNames(classes["root"], classes[variant], fullWidth && classes["fullwidth"])}>
<span className={classes["icon"]}>{icon ?? <InformationCircleIcon />}</span>
<div className={classes["top"]}>
<BadgeIcon icon={icon ?? <InformationCircleIcon />} color={variantColorMap[variant]} />
{closeButton && (
<IconButton className={classNames(classes["close-button"], classes["mobile"])} onClick={close} icon={<XMarkIcon />} />
)}
</div>
<div className={classes["content"]}>
<div className={classes["text-container"]}>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} color={ETypoColor.COLOR_NEUTRAL_950}>
@ -75,7 +90,7 @@ export default function Alert(props: IProps) {
)}
</div>
</div>
{closeButton && <IconButton onClick={close} icon={<XMarkIcon />} />}
{closeButton && <IconButton className={classNames(classes["close-button"], classes["desktop"])} onClick={close} icon={<XMarkIcon />} />}
</div>
);
}

View File

@ -27,7 +27,6 @@ export default function Autocomplete(props: IProps) {
useEffect(() => {
if (searchValue) {
const filteredOptions = options.filter((option) => getLabel(option)?.toLowerCase().includes(searchValue.toLowerCase()));
console.log(filteredOptions);
if (filteredOptions.length === 0)
return setFilteredOptions([{ id: "no-results", label: "Aucun résulats", notSelectable: true }]);
return setFilteredOptions(filteredOptions);

View File

@ -0,0 +1,46 @@
@import "@Themes/constants.scss";
.root {
display: flex;
padding: var(--spacing-1, 8px);
align-items: center;
align-self: flex-start;
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 {
svg {
stroke: var(--alerts-badge-contrast-error);
}
}
&.warning {
svg {
stroke: var(--alerts-badge-contrast-warning);
}
}
&.success {
svg {
stroke: var(--alerts-badge-contrast-success);
}
}
&.neutral {
svg {
stroke: var(--alerts-badge-contrast-neutral);
}
}
}

View File

@ -0,0 +1,23 @@
import classNames from "classnames";
import React from "react";
import classes from "./classes.module.scss";
export enum EBadgeColor {
INFO = "info",
SUCCESS = "success",
WARNING = "warning",
ERROR = "error",
NEUTRAL = "neutral",
}
type IProps = {
icon: React.ReactNode;
color: EBadgeColor;
};
export default function BadgeIcon(props: IProps) {
const { icon, color } = props;
return <div className={classNames(classes["root"], classes[color])}>{icon}</div>;
}

View File

@ -26,8 +26,8 @@
*/
&[variant="primary"] {
color: var(--button-contained-primary-hovered-contrast);
border: 1px solid var(--button-contained-primary-default-border);
color: var(--button-contained-primary-default-contrast);
border-color: var(--button-contained-primary-default-border);
background: var(--button-contained-primary-default-background);
svg {
@ -35,7 +35,7 @@
}
&:hover {
border: 1px solid var(--button-contained-primary-hovered-border);
border-color: var(--button-contained-primary-hovered-border);
background: var(--button-contained-primary-hovered-background);
svg {
@ -46,7 +46,7 @@
&:focus,
&:active {
color: var(--button-contained-primary-default-contrast);
border: 1px solid var(--button-contained-primary-pressed-border);
border-color: var(--button-contained-primary-pressed-border);
background: var(--button-contained-primary-pressed-background);
svg {
@ -56,7 +56,7 @@
&[styletype="outlined"] {
color: var(--button-outlined-primary-default-contrast);
border: 1px solid var(--button-outlined-primary-default-border);
border-color: var(--button-outlined-primary-default-border);
background: var(--button-outlined-primary-default-background);
svg {
@ -64,7 +64,7 @@
}
&:hover {
border: 1px solid var(--button-outlined-primary-hovered-border);
border-color: var(--button-outlined-primary-hovered-border);
background: var(--button-outlined-primary-hovered-background);
svg {
@ -75,7 +75,7 @@
&:focus,
&:active {
color: var(--button-outlined-primary-pressed-contrast);
border: 1px solid var(--button-outlined-primary-pressed-border);
border-color: var(--button-outlined-primary-pressed-border);
background: var(--button-outlined-primary-pressed-background);
svg {
@ -86,16 +86,16 @@
&[styletype="text"] {
color: var(--button-text-primary-default-contrast);
border-bottom: 1px solid var(--button-text-primary-default-border);
border-color: var(--button-text-primary-default-border);
background: var(--button-text-primary-default-background);
svg {
stroke: var(--button-text-primary-default-contrast);
}
&:hover {
border-bottom: 1px solid var(--button-outlined-primary-hovered-border);
background: var(--button-outlined-primary-default-background);
color: var(--button-text-primary-hovered-contrast);
border-color: var(--button-text-primary-hovered-border);
background: var(--button-text-primary-hovered-background);
svg {
stroke: var(--button-text-primary-hovered-contrast);
@ -104,9 +104,9 @@
&:focus,
&:active {
color: var(--color-primary-800);
background: var(--button-outlined-primary-default-background);
border-bottom: 1px solid var(--color-primary-800);
color: var(--button-text-primary-pressed-contrast);
border-color: var(--button-text-primary-pressed-border);
background: var(--button-text-primary-pressed-background);
svg {
stroke: var(--button-text-primary-pressed-contrast);
@ -117,17 +117,16 @@
&[variant="secondary"] {
color: var(--button-contained-secondary-default-contrast);
background: var(--button-contained-secondary-default-background);
border-color: var(--button-contained-secondary-default-border);
background: var(--button-contained-secondary-default-background);
svg {
stroke: var(--button-contained-secondary-default-contrast);
}
&:hover {
color: var(--button-contained-secondary-hovered-contrast);
background: var(--button-contained-secondary-hovered-background);
border-color: var(--button-contained-secondary-hovered-border);
background: var(--button-contained-secondary-hovered-background);
svg {
stroke: var(--button-contained-secondary-hovered-contrast);
@ -136,9 +135,9 @@
&:focus,
&:active {
color: var(--button-contained-secondary-pressed-contrast);
background: var(--button-contained-secondary-pressed-background);
color: var(--button-contained-secondary-default-contrast);
border-color: var(--button-contained-secondary-pressed-border);
background: var(--button-contained-secondary-pressed-background);
svg {
stroke: var(--button-contained-secondary-pressed-contrast);
@ -147,14 +146,15 @@
&[styletype="outlined"] {
color: var(--button-outlined-secondary-default-contrast);
border: 1px solid var(--button-outlined-secondary-default-border);
border-color: var(--button-outlined-secondary-default-border);
background: var(--button-outlined-secondary-default-background);
svg {
stroke: var(--button-outlined-secondary-default-contrast);
}
&:hover {
border: 1px solid var(--button-outlined-secondary-hovered-border);
border-color: var(--button-outlined-secondary-hovered-border);
background: var(--button-outlined-secondary-hovered-background);
svg {
@ -165,7 +165,7 @@
&:focus,
&:active {
color: var(--button-outlined-secondary-pressed-contrast);
border: 1px solid var(--button-outlined-secondary-pressed-border);
border-color: var(--button-outlined-secondary-pressed-border);
background: var(--button-outlined-secondary-pressed-background);
svg {
@ -176,15 +176,16 @@
&[styletype="text"] {
color: var(--button-text-secondary-default-contrast);
border-bottom: 1px solid var(--button-text-secondary-default-border);
border-color: var(--button-text-secondary-default-border);
background: var(--button-text-secondary-default-background);
svg {
stroke: var(--button-text-secondary-default-contrast);
}
&:hover {
border-bottom: 1px solid var(--button-outlined-secondary-hovered-border);
background: var(--button-outlined-secondary-default-background);
color: var(--button-text-secondary-hovered-contrast);
border-color: var(--button-text-secondary-hovered-border);
background: var(--button-text-secondary-hovered-background);
svg {
stroke: var(--button-text-secondary-hovered-contrast);
@ -193,9 +194,9 @@
&:focus,
&:active {
color: var(--button-outlined-secondary-pressed-contrast);
border-bottom: 1px solid var(--button-outlined-secondary-pressed-border);
background: var(--button-outlined-secondary-pressed-background);
color: var(--button-text-secondary-pressed-contrast);
border-color: var(--button-text-secondary-pressed-border);
background: var(--button-text-secondary-pressed-background);
svg {
stroke: var(--button-text-secondary-pressed-contrast);
@ -205,8 +206,8 @@
}
&[variant="neutral"] {
color: var(--button-contained-neutral-hovered-contrast);
border: 1px solid var(--button-contained-neutral-default-border);
color: var(--button-contained-neutral-default-contrast);
border-color: var(--button-contained-neutral-default-border);
background: var(--button-contained-neutral-default-background);
svg {
@ -214,7 +215,7 @@
}
&:hover {
border: 1px solid var(--button-contained-neutral-hovered-border);
border-color: var(--button-contained-neutral-hovered-border);
background: var(--button-contained-neutral-hovered-background);
svg {
@ -225,7 +226,7 @@
&:focus,
&:active {
color: var(--button-contained-neutral-default-contrast);
border: 1px solid var(--button-contained-neutral-pressed-border);
border-color: var(--button-contained-neutral-pressed-border);
background: var(--button-contained-neutral-pressed-background);
svg {
@ -235,15 +236,17 @@
&[styletype="outlined"] {
color: var(--button-outlined-neutral-default-contrast);
border: 1px solid var(--button-outlined-neutral-default-border);
border-color: var(--button-outlined-neutral-default-border);
background: var(--button-outlined-neutral-default-background);
svg {
stroke: var(--button-outlined-neutral-default-contrast);
}
&:hover {
border: 1px solid var(--button-outlined-neutral-hovered-border);
border-color: var(--button-outlined-neutral-hovered-border);
background: var(--button-outlined-neutral-hovered-background);
svg {
stroke: var(--button-outlined-neutral-hovered-contrast);
}
@ -252,7 +255,7 @@
&:focus,
&:active {
color: var(--button-outlined-neutral-pressed-contrast);
border: 1px solid var(--button-outlined-neutral-pressed-border);
border-color: var(--button-outlined-neutral-pressed-border);
background: var(--button-outlined-neutral-pressed-background);
svg {
@ -263,15 +266,16 @@
&[styletype="text"] {
color: var(--button-text-neutral-default-contrast);
border-bottom: 1px solid var(--button-text-neutral-default-border);
border-color: var(--button-text-neutral-default-border);
background: var(--button-text-neutral-default-background);
svg {
stroke: var(--button-text-neutral-default-contrast);
}
&:hover {
border-bottom: 1px solid var(--button-outlined-neutral-hovered-border);
background: var(--button-outlined-neutral-default-background);
color: var(--button-text-neutral-hovered-contrast);
border-color: var(--button-text-neutral-hovered-border);
background: var(--button-text-neutral-hovered-background);
svg {
stroke: var(--button-text-neutral-hovered-contrast);
@ -280,9 +284,9 @@
&:focus,
&:active {
color: var(--color-primary-800);
background: var(--button-outlined-neutral-default-background);
border-bottom: 1px solid var(--color-primary-800);
color: var(--button-text-neutral-pressed-contrast);
border-color: var(--button-text-neutral-pressed-border);
background: var(--button-text-neutral-pressed-background);
svg {
stroke: var(--button-text-neutral-pressed-contrast);
@ -292,17 +296,17 @@
}
&[variant="error"] {
color: var(--color-error-600);
background: var(--color-error-600);
border-color: var(--color-error-600);
color: var(--button-contained-error-default-contrast);
border-color: var(--button-contained-error-default-border);
background: var(--button-contained-error-default-background);
svg {
stroke: var(--button-contained-error-default-contrast);
}
&:hover {
background: var(--color-error-800);
border-color: var(--color-error-800);
border-color: var(--button-contained-error-hovered-border);
background: var(--button-contained-error-hovered-background);
svg {
stroke: var(--button-contained-error-hovered-contrast);
@ -311,8 +315,9 @@
&:focus,
&:active {
background: var(--color-error-900);
border-color: var(--color-error-900);
color: var(--button-contained-error-default-contrast);
border-color: var(--button-contained-error-pressed-border);
background: var(--button-contained-error-pressed-background);
svg {
stroke: var(--button-contained-error-pressed-contrast);
@ -320,13 +325,17 @@
}
&[styletype="outlined"] {
color: var(--button-outlined-error-default-contrast);
border-color: var(--button-outlined-error-default-border);
background: var(--button-outlined-error-default-background);
svg {
stroke: var(--button-outlined-error-default-contrast);
}
&:hover {
background-color: var(--color-error-50);
border-color: var(--color-secondary-700);
border-color: var(--button-outlined-error-hovered-border);
background: var(--button-outlined-error-hovered-background);
svg {
stroke: var(--button-outlined-error-hovered-contrast);
@ -335,9 +344,9 @@
&:focus,
&:active {
background-color: var(--color-error-100);
color: var(--color-secondary-700);
border-color: var(--color-secondary-700);
color: var(--button-outlined-error-pressed-contrast);
border-color: var(--button-outlined-error-pressed-border);
background: var(--button-outlined-error-pressed-background);
svg {
stroke: var(--button-outlined-error-pressed-contrast);
@ -346,14 +355,17 @@
}
&[styletype="text"] {
color: var(--button-text-error-default-contrast);
border-color: var(--button-text-error-default-border);
background: var(--button-text-error-default-background);
svg {
stroke: var(--button-text-error-default-contrast);
}
&:hover {
background-color: transparent;
color: var(--color-error-800);
border-color: var(--color-error-800);
color: var(--button-text-error-hovered-contrast);
border-color: var(--button-text-error-hovered-border);
background: var(--button-text-error-hovered-background);
svg {
stroke: var(--button-text-error-hovered-contrast);
@ -362,9 +374,9 @@
&:focus,
&:active {
background-color: transparent;
color: var(--color-error-900);
border-color: var(--color-error-900);
color: var(--button-text-error-pressed-contrast);
border-color: var(--button-text-error-pressed-border);
background: var(--button-text-error-pressed-background);
svg {
stroke: var(--button-text-error-pressed-contrast);
@ -373,101 +385,18 @@
}
}
&[variant="warning"] {
color: var(--color-warning-600);
background: var(--color-warning-600);
border-color: var(--color-warning-600);
svg {
stroke: var(--button-contained-warning-default-contrast);
}
&:hover {
background: var(--color-warning-800);
border-color: var(--color-warning-800);
svg {
stroke: var(--button-contained-warning-hovered-contrast);
}
}
&:focus,
&:active {
background: var(--color-warning-900);
border-color: var(--color-warning-900);
svg {
stroke: var(--button-contained-warning-pressed-contrast);
}
}
&[styletype="outlined"] {
svg {
stroke: var(--button-outlined-warning-default-contrast);
}
&:hover {
background-color: var(--color-warning-50);
border-color: var(--color-warning-700);
color: var(--color-warning-700);
svg {
stroke: var(--button-outlined-warning-hovered-contrast);
}
}
&:focus,
&:active {
background-color: var(--color-warning-100);
color: var(--color-warning-700);
border-color: var(--color-warning-700);
svg {
stroke: var(--button-outlined-warning-pressed-contrast);
}
}
}
&[styletype="text"] {
svg {
stroke: var(--button-text-warning-default-contrast);
}
&:hover {
background-color: transparent;
color: var(--color-warning-800);
border-color: var(--color-warning-800);
svg {
stroke: var(--button-text-warning-hovered-contrast);
}
}
&:focus,
&:active {
background-color: transparent;
color: var(--color-warning-900);
border-color: var(--color-warning-900);
svg {
stroke: var(--button-text-warning-pressed-contrast);
}
}
}
}
&[variant="success"] {
color: var(--color-success-600);
background: var(--color-success-600);
border-color: var(--color-success-600);
color: var(--button-contained-success-default-contrast);
border-color: var(--button-contained-success-default-border);
background: var(--button-contained-success-default-background);
svg {
stroke: var(--button-contained-success-default-contrast);
}
&:hover {
background: var(--color-success-800);
border-color: var(--color-success-800);
border-color: var(--button-contained-success-hovered-border);
background: var(--button-contained-success-hovered-background);
svg {
stroke: var(--button-contained-success-hovered-contrast);
@ -476,8 +405,9 @@
&:focus,
&:active {
background: var(--color-success-900);
border-color: var(--color-success-900);
color: var(--button-contained-success-default-contrast);
border-color: var(--button-contained-success-pressed-border);
background: var(--button-contained-success-pressed-background);
svg {
stroke: var(--button-contained-success-pressed-contrast);
@ -485,14 +415,17 @@
}
&[styletype="outlined"] {
color: var(--button-outlined-success-default-contrast);
border-color: var(--button-outlined-success-default-border);
background: var(--button-outlined-success-default-background);
svg {
stroke: var(--button-outlined-success-default-contrast);
}
&:hover {
background-color: var(--color-success-50);
border-color: var(--color-success-700);
color: var(--color-success-700);
border-color: var(--button-outlined-success-hovered-border);
background: var(--button-outlined-success-hovered-background);
svg {
stroke: var(--button-outlined-success-hovered-contrast);
@ -501,9 +434,9 @@
&:focus,
&:active {
background-color: var(--color-success-100);
color: var(--color-success-700);
border-color: var(--color-success-700);
color: var(--button-outlined-success-pressed-contrast);
border-color: var(--button-outlined-success-pressed-border);
background: var(--button-outlined-success-pressed-background);
svg {
stroke: var(--button-outlined-success-pressed-contrast);
@ -512,14 +445,17 @@
}
&[styletype="text"] {
color: var(--button-text-success-default-contrast);
border-color: var(--button-text-success-default-border);
background: var(--button-text-success-default-background);
svg {
stroke: var(--button-text-success-default-contrast);
}
&:hover {
background-color: transparent;
color: var(--color-success-800);
border-color: var(--color-success-800);
color: var(--button-text-success-hovered-contrast);
border-color: var(--button-text-success-hovered-border);
background: var(--button-text-success-hovered-background);
svg {
stroke: var(--button-text-success-hovered-contrast);
@ -528,9 +464,9 @@
&:focus,
&:active {
background-color: transparent;
color: var(--color-success-900);
border-color: var(--color-success-900);
color: var(--button-text-success-pressed-contrast);
border-color: var(--button-text-success-pressed-border);
background: var(--button-text-success-pressed-background);
svg {
stroke: var(--button-text-success-pressed-contrast);
@ -539,18 +475,108 @@
}
}
&[variant="warning"] {
color: var(--button-contained-warning-default-contrast);
border-color: var(--button-contained-warning-default-border);
background: var(--button-contained-warning-default-background);
svg {
stroke: var(--button-contained-warning-default-contrast);
}
&:hover {
border-color: var(--button-contained-warning-hovered-border);
background: var(--button-contained-warning-hovered-background);
svg {
stroke: var(--button-contained-warning-hovered-contrast);
}
}
&:focus,
&:active {
color: var(--button-contained-warning-default-contrast);
border-color: var(--button-contained-warning-pressed-border);
background: var(--button-contained-warning-pressed-background);
svg {
stroke: var(--button-contained-warning-pressed-contrast);
}
}
&[styletype="outlined"] {
color: var(--button-outlined-warning-default-contrast);
border-color: var(--button-outlined-warning-default-border);
background: var(--button-outlined-warning-default-background);
svg {
stroke: var(--button-outlined-warning-default-contrast);
}
&:hover {
border-color: var(--button-outlined-warning-hovered-border);
background: var(--button-outlined-warning-hovered-background);
svg {
stroke: var(--button-outlined-warning-hovered-contrast);
}
}
&:focus,
&:active {
color: var(--button-outlined-warning-pressed-contrast);
border-color: var(--button-outlined-warning-pressed-border);
background: var(--button-outlined-warning-pressed-background);
svg {
stroke: var(--button-outlined-warning-pressed-contrast);
}
}
}
&[styletype="text"] {
color: var(--button-text-warning-default-contrast);
border-color: var(--button-text-warning-default-border);
background: var(--button-text-warning-default-background);
svg {
stroke: var(--button-text-warning-default-contrast);
}
&:hover {
color: var(--button-text-warning-hovered-contrast);
border-color: var(--button-text-warning-hovered-border);
background: var(--button-text-warning-hovered-background);
svg {
stroke: var(--button-text-warning-hovered-contrast);
}
}
&:focus,
&:active {
color: var(--button-text-warning-pressed-contrast);
border-color: var(--button-text-warning-pressed-border);
background: var(--button-text-warning-pressed-background);
svg {
stroke: var(--button-text-warning-pressed-contrast);
}
}
}
}
&[variant="info"] {
color: var(--color-info-700);
background: var(--color-info-700);
border-color: var(--color-info-700);
color: var(--button-contained-info-default-contrast);
border-color: var(--button-contained-info-default-border);
background: var(--button-contained-info-default-background);
svg {
stroke: var(--button-contained-info-default-contrast);
}
&:hover {
background: var(--color-info-900);
border-color: var(--color-info-900);
border-color: var(--button-contained-info-hovered-border);
background: var(--button-contained-info-hovered-background);
svg {
stroke: var(--button-contained-info-hovered-contrast);
@ -559,8 +585,9 @@
&:focus,
&:active {
background: var(--color-info-950);
border-color: var(--color-info-950);
color: var(--button-contained-info-default-contrast);
border-color: var(--button-contained-info-pressed-border);
background: var(--button-contained-info-pressed-background);
svg {
stroke: var(--button-contained-info-pressed-contrast);
@ -568,13 +595,17 @@
}
&[styletype="outlined"] {
color: var(--button-outlined-info-default-contrast);
border-color: var(--button-outlined-info-default-border);
background: var(--button-outlined-info-default-background);
svg {
stroke: var(--button-outlined-info-default-contrast);
}
&:hover {
background-color: var(--color-info-50);
border-color: var(--color-info-700);
color: var(--color-info-700);
border-color: var(--button-outlined-info-hovered-border);
background: var(--button-outlined-info-hovered-background);
svg {
stroke: var(--button-outlined-info-hovered-contrast);
@ -583,9 +614,9 @@
&:focus,
&:active {
background-color: var(--color-info-100);
color: var(--color-info-700);
border-color: var(--color-info-700);
color: var(--button-outlined-info-pressed-contrast);
border-color: var(--button-outlined-info-pressed-border);
background: var(--button-outlined-info-pressed-background);
svg {
stroke: var(--button-outlined-info-pressed-contrast);
@ -594,14 +625,17 @@
}
&[styletype="text"] {
color: var(--button-text-info-default-contrast);
border-color: var(--button-text-info-default-border);
background: var(--button-text-info-default-background);
svg {
stroke: var(--button-text-info-default-contrast);
}
&:hover {
background-color: transparent;
color: var(--color-info-900);
border-color: var(--color-info-900);
color: var(--button-text-info-hovered-contrast);
border-color: var(--button-text-info-hovered-border);
background: var(--button-text-info-hovered-background);
svg {
stroke: var(--button-text-info-hovered-contrast);
@ -610,9 +644,9 @@
&:focus,
&:active {
background-color: transparent;
color: var(--color-info-950);
border-color: var(--color-info-950);
color: var(--button-text-info-pressed-contrast);
border-color: var(--button-text-info-pressed-border);
background: var(--button-text-info-pressed-background);
svg {
stroke: var(--button-text-info-pressed-contrast);

View File

@ -4,11 +4,11 @@ import Tooltip from "../ToolTip";
import Typography, { ETypo, ETypoColor } from "../Typography";
import classes from "./classes.module.scss";
import classNames from "classnames";
import { IOptionOld } from "../Form/SelectFieldOld";
import { IOption } from "../Form/SelectFieldOld";
type IProps = {
name?: string;
option: IOptionOld;
option: IOption;
toolTip?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
checked: boolean;

View File

@ -1,13 +1,15 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import Typography, { ETypo, ETypoColor } from "../Typography";
import classes from "./classes.module.scss";
import classNames from "classnames";
type IProps = {
percentage: number;
className?: string;
};
export default function CircleProgress(props: IProps) {
const { percentage } = props;
const { percentage, className } = props;
const [animatedProgress, setAnimatedProgress] = useState(0);
const requestRef = useRef<number>();
@ -41,7 +43,7 @@ export default function CircleProgress(props: IProps) {
const offset = circumference - (animatedProgress / 100) * circumference;
return (
<div className={classes["root"]}>
<div className={classNames(classes["root"], className)}>
<svg xmlns="http://www.w3.org/2000/svg" width="27" height="27" viewBox="0 0 27 27" fill="none">
<circle className={classes["circleBackground"]} cx="13.5" cy="13.5" r={radius} strokeWidth="3" />
<circle

View File

@ -1,106 +0,0 @@
.container {
.root {
padding: 24px;
background-color: var(--color-generic-white);
border: 1px dashed #e7e7e7;
height: fit-content;
&[data-drag-over="true"] {
border: 1px dashed var(--color-neutral-500);
}
&.validated {
border: 1px dashed var(--color-success-600);
}
.top-container {
display: flex;
align-items: center;
.left {
margin-right: 28px;
}
.separator {
background-color: #939393;
width: 1px;
align-self: stretch;
}
.right {
margin-left: 18px;
.validated {
color: var(--color-success-600);
}
.refused-button {
font-size: 14px;
color: var(--color-error-800);
margin-left: 8px;
}
.title {
display: flex;
align-items: center;
gap: 8px;
}
}
}
.documents-container {
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 16px;
.file-container {
display: flex;
align-items: center;
justify-content: space-between;
.left-part {
display: flex;
align-items: center;
gap: 8px;
.loader {
width: 32px;
height: 32px;
}
}
.cross {
cursor: pointer;
}
}
}
.bottom-container {
margin-top: 16px;
.add-button {
.add-document {
display: flex;
align-items: center;
gap: 14px;
}
}
}
.text {
margin-bottom: 12px;
}
}
.modal-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.error-message {
color: var(--color-error-600);
margin-top: 8px;
}
}

View File

@ -1,452 +0,0 @@
import DepositDocumentIcon from "@Assets/Icons/deposit-document.svg";
import PlusIcon from "@Assets/Icons/plus.svg";
import CrossIcon from "@Assets/Icons/cross.svg";
import DocumentCheckIcon from "@Assets/Icons/document-check.svg";
import Image from "next/image";
import React from "react";
import Button, { EButtonstyletype, EButtonVariant } from "../Button";
import Tooltip from "../ToolTip";
import Typography, { ETypo, ETypoColor } from "../Typography";
import classes from "./classes.module.scss";
import { Document, DocumentHistory, File as FileCustomer } from "le-coffre-resources/dist/Customer";
import Files from "@Front/Api/LeCoffreApi/Customer/Files/Files";
import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document";
import classNames from "classnames";
import Confirm from "../OldModal/Confirm";
import Alert from "../OldModal/Alert";
import GreenCheckIcon from "@Assets/Icons/green-check.svg";
import Loader from "../Loader";
import TextAreaField from "../Form/TextareaField";
import { toast } from "react-toastify";
type IProps = {
defaultFiles?: FileCustomer[];
onChange?: (files: File[]) => void;
document: Document;
};
type IFile = {
index: number;
file: File;
uid: string;
archived: Date | null;
fileName: string;
};
type IState = {
files: IFile[];
isDragOver: boolean;
currentFiles?: FileCustomer[];
refusedReason?: string;
isShowRefusedReasonModalVisible: boolean;
showFailedUploaded: string | null;
loading: boolean;
};
type fileAccepted = {
extension: string;
size: number;
};
const filesAccepted: { [key: string]: fileAccepted } = {
"application/pdf": {
extension: "pdf",
size: 41943040,
},
"image/jpeg": {
extension: "jpeg",
size: 41943040,
},
"image/png": {
extension: "png",
size: 41943040,
},
"image/jpg": {
extension: "jpg",
size: 41943040,
},
};
export default class DepositDocument extends React.Component<IProps, IState> {
private inputRef = React.createRef<HTMLInputElement>();
private index = 0;
public constructor(props: IProps) {
super(props);
this.state = {
files: [],
isDragOver: false,
currentFiles: this.props.defaultFiles,
refusedReason: "",
isShowRefusedReasonModalVisible: false,
showFailedUploaded: null,
loading: false,
};
this.addDocument = this.addDocument.bind(this);
this.onFileChange = this.onFileChange.bind(this);
this.addFile = this.addFile.bind(this);
this.removeFile = this.removeFile.bind(this);
this.onDragOver = this.onDragOver.bind(this);
this.onDragDrop = this.onDragDrop.bind(this);
this.onDragLeave = this.onDragLeave.bind(this);
this.onCloseModalShowRefusedReason = this.onCloseModalShowRefusedReason.bind(this);
this.onOpenModalShowRefusedReason = this.onOpenModalShowRefusedReason.bind(this);
this.showRefusedReason = this.showRefusedReason.bind(this);
this.onCloseAlertUpload = this.onCloseAlertUpload.bind(this);
}
public override render(): JSX.Element {
return (
<div className={classes["container"]}>
<div
className={classNames(
classes["root"],
this.props.document.document_status === EDocumentStatus.VALIDATED && classes["validated"],
)}
onDragOver={this.onDragOver}
onDrop={this.onDragDrop}
onDragLeave={this.onDragLeave}
data-drag-over={this.state.isDragOver.toString()}>
<input
type="file"
ref={this.inputRef}
hidden
onChange={this.onFileChange}
accept={Object.keys(filesAccepted).join(",")}
/>
<div className={classes["top-container"]}>
<div className={classes["left"]}>
<Image src={DepositDocumentIcon} alt="Deposit document" />
</div>
<div className={classes["separator"]} />
<div className={classes["right"]}>
<Typography typo={ETypo.TEXT_MD_SEMIBOLD} color={ETypoColor.COLOR_GENERIC_BLACK} className={classes["title"]}>
<div
className={
this.props.document.document_status === EDocumentStatus.VALIDATED ? classes["validated"] : ""
}>
{this.props.document.document_type?.name}
</div>
{this.props.document.document_type?.public_description !== " " &&
this.props.document.document_type?.public_description !== "" &&
this.props.document.document_status !== EDocumentStatus.VALIDATED && (
<Tooltip text={this.props.document.document_type?.public_description || ""} />
)}
{this.props.document.document_status === EDocumentStatus.VALIDATED && (
<Image src={GreenCheckIcon} alt="Document check" />
)}
</Typography>
{this.props.document.document_status !== EDocumentStatus.VALIDATED && (
<Typography color={ETypoColor.COLOR_NEUTRAL_500} typo={ETypo.TEXT_SM_REGULAR}>
Sélectionnez des documents .jpg, .pdf ou .png
</Typography>
)}
{this.props.document.document_history?.map((history) => (
<div key={history.uid}>{this.renderDocumentHistory(history)}</div>
))}
</div>
</div>
<div className={classes["documents-container"]}>
{this.state.files.map((file) => {
const fileObj = file.file;
if (file.archived) return;
return (
<div className={classes["file-container"]} key={fileObj.name + file.index}>
<div className={classes["left-part"]}>
<Image src={DocumentCheckIcon} alt="Document check" />
<Typography
typo={ETypo.TEXT_MD_REGULAR}
color={ETypoColor.COLOR_NEUTRAL_500}
title={file.fileName ?? fileObj.name}>
{this.shortName(file.fileName || fileObj.name)}
</Typography>
</div>
<Image
src={CrossIcon}
alt="Cross icon"
className={classes["cross"]}
onClick={this.removeFile}
data-file={file.index}
/>
</div>
);
})}
{this.state.loading && (
<div className={classes["file-container"]}>
<div className={classes["left-part"]}>
<div className={classes["loader"]}>
<Loader />
</div>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_NEUTRAL_500}>
Chargement...
</Typography>
</div>
<div />
</div>
)}
</div>
{this.props.document.document_status !== EDocumentStatus.VALIDATED && (
<div className={classes["bottom-container"]}>
<Button
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.TEXT}
className={classes["add-button"]}
onClick={this.addDocument}>
<Typography
typo={ETypo.TEXT_MD_SEMIBOLD}
color={ETypoColor.COLOR_SECONDARY_500}
className={classes["add-document"]}>
Ajouter un document <Image src={PlusIcon} alt="Plus icon" />
</Typography>
</Button>
</div>
)}
<Confirm
isOpen={this.state.isShowRefusedReasonModalVisible}
onClose={this.onCloseModalShowRefusedReason}
showCancelButton={false}
onAccept={this.onCloseModalShowRefusedReason}
closeBtn
header={"Motif du refus"}
confirmText={"J'ai compris"}>
<div className={classes["modal-content"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR} className={classes["text"]}>
Votre document a é refusé pour la raison suivante :
</Typography>
<TextAreaField placeholder="Description" defaultValue={this.state.refusedReason} readonly />
</div>
</Confirm>
</div>
{this.props.document.document_status === EDocumentStatus.REFUSED && (
<Typography typo={ETypo.TEXT_SM_REGULAR} className={classes["error-message"]}>
Ce document n'est pas conforme. Veuillez le déposer à nouveau.
</Typography>
)}
{this.state.showFailedUploaded && (
<Alert onClose={this.onCloseAlertUpload} header={"Fichier non autorisé"} isOpen={!!this.state.showFailedUploaded}>
<div className={classes["modal-content"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR} className={classes["text"]}>
{this.state.showFailedUploaded}
</Typography>
</div>
</Alert>
)}
</div>
);
}
public override componentDidMount(): void {
if (this.props.defaultFiles) {
this.setState({
files: this.props.defaultFiles.map((file) => ({
index: this.index++,
file: new File([""], file.file_path ?? "", {}),
uid: file.uid!,
fileName: file.file_name,
archived: file.archived_at ? new Date(file.archived_at) : null,
})),
});
}
}
private openSuccessToast() {
toast.success("Document envoyé avec succès");
}
private onCloseModalShowRefusedReason() {
this.setState({
isShowRefusedReasonModalVisible: false,
});
}
private onOpenModalShowRefusedReason() {
this.setState({
isShowRefusedReasonModalVisible: true,
});
}
private renderDocumentHistory(history: DocumentHistory): JSX.Element | null {
switch (history.document_status) {
case EDocumentStatus.ASKED:
return (
<Typography color={ETypoColor.COLOR_NEUTRAL_500} typo={ETypo.TEXT_SM_REGULAR}>
Demandé par votre notaire le {this.formatDate(history.created_at!)}
</Typography>
);
case EDocumentStatus.VALIDATED:
return (
<Typography color={ETypoColor.COLOR_NEUTRAL_500} typo={ETypo.TEXT_SM_REGULAR}>
Validé par votre notaire le {this.formatDate(history.created_at!)}
</Typography>
);
case EDocumentStatus.DEPOSITED:
return (
<Typography color={ETypoColor.COLOR_NEUTRAL_500} typo={ETypo.TEXT_SM_REGULAR}>
Déposé le {this.formatDate(history.created_at!)}
</Typography>
);
case EDocumentStatus.REFUSED:
return (
<Typography typo={ETypo.TEXT_SM_REGULAR} color={ETypoColor.COLOR_ERROR_800}>
Document non conforme
{history.refused_reason && history.refused_reason.length > 0 && (
<Button
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.TEXT}
className={classes["refused-button"]}
onClick={() => this.showRefusedReason(history.refused_reason ?? "")}>
Voir le motif de refus
</Button>
)}
</Typography>
);
}
return null;
}
private shortName(name: string): string {
const maxLength = 20;
if (name.length > maxLength) {
return name.substring(0, maxLength / 2) + "..." + name.substring(name.length - maxLength / 2, name.length);
}
return name;
}
private onDragOver(event: React.DragEvent<HTMLDivElement>) {
if (!this.state.isDragOver) {
this.setState({
isDragOver: true,
});
}
event.preventDefault();
}
private showRefusedReason(refusedReason: string) {
this.setState({
refusedReason,
});
this.onOpenModalShowRefusedReason();
}
private onDragLeave(event: React.DragEvent<HTMLDivElement>) {
this.setState({
isDragOver: false,
});
event.preventDefault();
}
private async onDragDrop(event: React.DragEvent<HTMLDivElement>) {
event.preventDefault();
this.setState({
isDragOver: false,
});
const file = event.dataTransfer.files[0];
if (file) this.addFile(file);
}
private async addFile(file: File) {
const fileAccepted = filesAccepted[file.type];
if (!fileAccepted) {
alert("Le fichier déposé doit être au format .jpg .pdf .jpeg ou .png");
return false;
}
if (file.size > fileAccepted.size) {
alert("Le fichier est trop volumineux et ne doit pas dépasser 32mo");
return false;
}
this.setState({
loading: true,
});
const formData = new FormData();
formData.append("file", file, file.name);
const query = JSON.stringify({ document: { uid: this.props.document.uid } });
formData.append("q", query);
let newFile: FileCustomer;
try {
newFile = await Files.getInstance().post(formData);
} catch (e) {
this.setState({ showFailedUploaded: "Le fichier ne correspond pas aux critères demandés", loading: false });
return false;
}
const files = this.state.currentFiles ? [...this.state.currentFiles, newFile] : [newFile];
const newFileList = [
...this.state.files,
{
index: this.index++,
file: file,
uid: newFile.uid!,
archived: null,
fileName: newFile?.file_name ?? "",
},
];
this.openSuccessToast();
this.setState(
{
currentFiles: files,
loading: false,
files: newFileList,
},
() => {
if (this.props.onChange) this.props.onChange(newFileList.map((file) => file.file));
},
);
return true;
}
private async removeFile(e: any) {
const image = e.target as HTMLElement;
const indexToRemove = image.getAttribute("data-file");
if (!indexToRemove) return;
const file = this.state.files.find((file) => file.index === parseInt(indexToRemove));
if (!file) return;
this.setState({
files: this.state.files.filter((file) => file.index !== parseInt(indexToRemove)),
});
if (this.props.onChange) this.props.onChange(this.state.files.map((file) => file.file));
await Files.getInstance().delete(file.uid);
}
private async onFileChange() {
if (!this.inputRef.current) return;
const files = this.inputRef.current.files;
if (!files) {
this.setState({ loading: false });
return;
}
const file = files[0];
try {
if (file) {
await this.setState({ loading: true }, () => {
this.addFile(file);
});
}
} catch (e) {
this.setState({ loading: false });
}
}
private onCloseAlertUpload() {
this.setState({ showFailedUploaded: null });
}
private addDocument() {
if (!this.inputRef.current) return;
this.inputRef.current.value = "";
this.inputRef.current.click();
}
private formatDate(date: Date) {
const dateToConvert = new Date(date);
return dateToConvert.toLocaleDateString("fr-FR");
}
}

View File

@ -0,0 +1,38 @@
@import "@Themes/constants.scss";
.root {
display: flex;
width: 100%;
gap: var(--spacing-sm, 8px);
justify-content: space-between;
align-items: center;
.content {
display: flex;
gap: var(--spacing-sm, 8px);
align-items: center;
.file-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 277px;
}
}
svg {
min-width: var(--spacing-3, 24px);
min-height: var(--spacing-3, 24px);
width: var(--spacing-3, 24px);
height: var(--spacing-3, 24px);
stroke: var(--color-primary-500);
}
.error {
min-width: var(--spacing-3, 24px);
min-height: var(--spacing-3, 24px);
width: var(--spacing-3, 24px);
height: var(--spacing-3, 24px);
stroke: var(--color-error-500);
}
}

View File

@ -0,0 +1,37 @@
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import { CheckCircleIcon, XCircleIcon, XMarkIcon } from "@heroicons/react/24/outline";
import React from "react";
import IconButton, { EIconButtonVariant } from "../../IconButton";
import Loader from "../../Loader";
import classes from "./classes.module.scss";
type IProps = {
file: File | null;
onRemove: () => void;
isLoading?: boolean;
error?: string;
};
export default function DocumentFileElement(props: IProps) {
const { isLoading, onRemove, file, error } = props;
return (
<div className={classes["root"]}>
<div className={classes["content"]}>
{isLoading ? <Loader /> : !error ? <CheckCircleIcon /> : <XCircleIcon className={classes["error"]} />}
{error && (
<Typography typo={ETypo.TEXT_MD_LIGHT} color={ETypoColor.COLOR_ERROR_500} className={classes["file-name"]}>
{error}
</Typography>
)}
{file && !error && (
<Typography typo={ETypo.TEXT_LG_REGULAR} color={ETypoColor.TEXT_SECONDARY} className={classes["file-name"]}>
{file.name}
</Typography>
)}
</div>
<IconButton onClick={onRemove} icon={<XMarkIcon />} variant={error ? EIconButtonVariant.ERROR : EIconButtonVariant.NEUTRAL} />
</div>
);
}

View File

@ -0,0 +1,83 @@
@import "@Themes/constants.scss";
.root {
position: relative;
display: inline-flex;
flex-direction: column;
gap: var(--spacing-3, 24px);
width: fit-content;
padding: var(--spacing-2, 16px) var(--Radius-2xl, 32px) var(--spacing-2, 16px) var(--spacing-xl, 32px);
clip-path: inset(0 round var(--Radius-md, 8px));
&::before {
content: "";
position: absolute;
left: -15px;
top: -15px;
right: -15px;
bottom: -15px;
border: 16px dashed var(--dropdown-input-border-hovered);
border-radius: calc(2 * var(--Radius-md, 8px));
box-sizing: border-box;
z-index: -1;
}
&.filled {
&::before {
border-color: var(--dropdown-input-border-expanded);
}
}
&:hover {
&::before {
border-color: var(--dropdown-input-border-expanded);
}
background: var(--primary-weak-higlight, #e5eefa);
}
.content {
display: inline-flex;
align-items: center;
gap: var(--spacing-4, 32px);
.browse-document-container {
display: flex;
flex-direction: column;
gap: var(--spacing-sm, 8px);
.browse-document {
display: flex;
gap: var(--spacing-sm, 8px);
&.desktop {
@media screen and (max-width: $screen-s) {
display: none;
}
}
&.mobile {
display: none;
@media screen and (max-width: $screen-s) {
display: flex;
}
}
}
}
}
.documents {
display: flex;
flex-direction: column;
gap: var(--spacing-sm, 8px);
}
svg {
min-width: var(--spacing-3, 24px);
min-height: var(--spacing-3, 24px);
width: var(--spacing-3, 24px);
height: var(--spacing-3, 24px);
stroke: var(--color-primary-500);
}
}

View File

@ -0,0 +1,219 @@
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import { DocumentPlusIcon } from "@heroicons/react/24/outline";
import classNames from "classnames";
import React, { useCallback, useEffect, useRef, useState } from "react";
import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "../Button";
import Separator, { ESeperatorColor, ESeperatorDirection } from "../Separator";
import classes from "./classes.module.scss";
import DocumentFileElement from "./DocumentFileElement";
/**
* @description Drag and drop component to upload files
* @param {string} title - Title of the component
* @param {string} description - Description of the component
* @param {IDocumentFileWithUid[]} defaultFiles - Default files to display
* @param {(fileUid: string) => Promise<any>} onDelete - Function to delete a file (must be used with defaultFiles)
* @param {(file: File) => Promise<any>} onAddFile - Function to add a file (must be used with defaultFiles)
*/
type IProps = {
title: string;
description?: string;
defaultFiles?: IDocumentFileWithUid[];
onDelete?: (fileUid: string) => Promise<any>;
onAddFile?: (file: File) => Promise<any>;
name?: string;
onChange?: (files: File[]) => void;
} & (
| { onDelete: (fileUid: string) => Promise<any>; onAddFile?: never; defaultFiles: IDocumentFileWithUid[] }
| { onDelete?: never; onAddFile: (file: File) => Promise<any>; defaultFiles: IDocumentFileWithUid[] }
| { onDelete?: (fileUid: string) => Promise<any>; onAddFile?: (file: File) => Promise<any>; defaultFiles: IDocumentFileWithUid[] }
| { onDelete?: never; onAddFile?: never; defaultFiles?: never }
);
type IMimeTypes = {
extension: string;
size: number;
};
const mimeTypesAccepted: { [key: string]: IMimeTypes } = {
"application/pdf": {
extension: "pdf",
size: 41943040,
},
"image/jpeg": {
extension: "jpeg",
size: 41943040,
},
"image/png": {
extension: "png",
size: 41943040,
},
"image/jpg": {
extension: "jpg",
size: 41943040,
},
};
type IDocumentFileBase = {
id: string;
file: File | null;
uid?: string;
isLoading?: boolean;
error?: string;
};
export type IDocumentFileWithUid = IDocumentFileBase & {
uid: string;
};
type IDocumentFile = IDocumentFileBase | IDocumentFileWithUid;
export default function DragAndDrop(props: IProps) {
const { title, description, defaultFiles, onDelete, onAddFile, name, onChange } = props;
const fileInputRef = useRef<HTMLInputElement>(null);
const [documentFiles, setDocumentFiles] = useState<IDocumentFile[]>([]);
useEffect(() => {
if (defaultFiles) {
setDocumentFiles(defaultFiles);
}
}, [defaultFiles]);
useEffect(() => onChange?.(documentFiles.map((doc) => doc.file).filter((file) => file !== null) as File[]), [documentFiles, onChange]);
const handleAddFiles = useCallback(
(files: File[]) => {
files.forEach((file) => {
setDocumentFiles((prevDocs) => [...prevDocs, { id: file.name, file: file, isLoading: true }]);
try {
if (!mimeTypesAccepted[file.type]) {
throw new Error("Type de fichier non accepté");
}
const newDoc: IDocumentFile = { id: file.name, file, isLoading: false };
if (onAddFile) {
// As onAddFile is used along defaultFiles prop we dont need to update the state here but the parent component should update the defaultFiles prop
return onAddFile(file);
}
return setTimeout(async () => {
setDocumentFiles((prevDocs) => prevDocs.map((doc) => (doc.id === newDoc.id ? newDoc : doc)));
}, 1000);
} catch (error: any) {
const errorDoc: IDocumentFile = { id: file.name, file: null, isLoading: false, error: error.message };
return setDocumentFiles((prevDocs) => prevDocs.map((doc) => (doc.id === errorDoc.id ? errorDoc : doc)));
}
});
},
[onAddFile],
);
const handleDrop = useCallback(
(event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
const files = Array.from(event.dataTransfer.files);
handleAddFiles(files);
},
[handleAddFiles],
);
const handleRemove = useCallback(
(documentFile: IDocumentFile) => {
const loadingDoc = { ...documentFile, isLoading: true };
setDocumentFiles((prevDocs) => prevDocs.map((doc) => (doc.id === documentFile.id ? loadingDoc : doc)));
if (documentFile.uid) {
return onDelete?.(documentFile.uid);
}
return setDocumentFiles((prevDocs) => prevDocs.filter((doc) => doc.id !== documentFile.id));
},
[onDelete],
);
const handleBrowse = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files || []);
handleAddFiles(files);
},
[handleAddFiles],
);
const triggerFileInput = () => {
if (fileInputRef.current) {
fileInputRef.current.click();
}
};
return (
<div
className={classNames(classes["root"], documentFiles.length > 0 && classes["filled"])}
onDrop={handleDrop}
onDragOver={(e) => e.preventDefault()}>
<div className={classes["content"]}>
<DocumentPlusIcon />
<Separator direction={ESeperatorDirection.VERTICAL} color={ESeperatorColor.STRONG} size={64} />
<div className={classes["browse-document-container"]}>
<div className={classNames(classes["browse-document"], classes["desktop"])}>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} color={ETypoColor.TEXT_PRIMARY}>
{title}
</Typography>
<Button
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.TEXT}
size={EButtonSize.SM}
onClick={triggerFileInput}>
{inputFile()}
parcourir
</Button>
</div>
<div className={classNames(classes["browse-document"], classes["mobile"])}>
<Button
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.TEXT}
size={EButtonSize.SM}
onClick={triggerFileInput}>
{inputFile()}
Ajouter un document
</Button>
</div>
{description && (
<Typography typo={ETypo.TEXT_MD_LIGHT} color={ETypoColor.TEXT_SECONDARY}>
{description}
</Typography>
)}
</div>
</div>
{documentFiles.length > 0 && (
<div className={classes["documents"]}>
{documentFiles.map((documentFile) => (
<DocumentFileElement
key={documentFile.id}
isLoading={documentFile.isLoading}
file={documentFile.file}
onRemove={() => handleRemove(documentFile)}
error={documentFile.error}
/>
))}
</div>
)}
</div>
);
function inputFile() {
return (
<input
name={name}
ref={fileInputRef}
type="file"
multiple
accept={Object.keys(mimeTypesAccepted).join(",")}
onChange={handleBrowse}
hidden
/>
);
}
}

View File

@ -15,10 +15,11 @@ type IProps = {
disabled?: boolean;
onSelectionChange?: (option: IOption) => void;
selectedOption?: IOption | null;
className?: string;
};
export default function Dropdown(props: IProps) {
const { options, placeholder, disabled, onSelectionChange, selectedOption: selectedOptionProps, label } = props;
const { options, placeholder, disabled, onSelectionChange, selectedOption: selectedOptionProps, label, className } = props;
const [selectedOption, setSelectedOption] = useState<IOption | null>(selectedOptionProps ?? null);
const openable = useOpenable({ defaultOpen: false });
@ -40,7 +41,7 @@ export default function Dropdown(props: IProps) {
openable={openable}
onSelect={handleOnSelect}
selectedOptions={selectedOption ? [selectedOption] : []}>
<div className={classes["root"]}>
<div className={classNames(classes["root"], className)}>
{label && (
<Typography className={classes["label"]} typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.CONTRAST_DEFAULT}>
{label}

View File

@ -34,6 +34,8 @@ export default class AutocompleteField extends BaseField<IProps, IState> {
};
public override componentDidUpdate(prevProps: IProps): void {
super.componentDidUpdate(prevProps);
if (prevProps.selectedOption !== this.props.selectedOption) {
this.setState({ selectedOption: this.props.selectedOption ?? null });
}

View File

@ -34,6 +34,8 @@ export default class AutocompleteMultiSelectField extends BaseField<IProps, ISta
};
public override componentDidUpdate(prevProps: IProps): void {
super.componentDidUpdate(prevProps);
if (prevProps.selectedOptions !== this.props.selectedOptions) {
this.setState({ selectedOptions: this.props.selectedOptions ?? null });
}

View File

@ -34,6 +34,9 @@ export default class SelectField extends BaseField<IProps, IState> {
};
public override componentDidUpdate(prevProps: IProps): void {
super.componentDidUpdate(prevProps);
if (prevProps.selectedOption !== this.props.selectedOption) {
this.setState({ selectedOption: this.props.selectedOption ?? null });
}

View File

@ -10,9 +10,9 @@ import classes from "./classes.module.scss";
import { NextRouter, useRouter } from "next/router";
type IProps = {
selectedOption?: IOptionOld;
onChange?: (selectedOption: IOptionOld) => void;
options: IOptionOld[];
selectedOption?: IOption;
onChange?: (selectedOption: IOption) => void;
options: IOption[];
hasBorderRightCollapsed?: boolean;
placeholder?: string;
className?: string;
@ -21,7 +21,7 @@ type IProps = {
errors?: ValidationError;
};
export type IOptionOld = {
export type IOption = {
value: unknown;
label: string;
icon?: ReactNode;
@ -32,7 +32,7 @@ type IState = {
isOpen: boolean;
listWidth: number;
listHeight: number;
selectedOption: IOptionOld | null;
selectedOption: IOption | null;
errors: ValidationError | null;
};
@ -187,7 +187,7 @@ class SelectFieldClass extends React.Component<IPropsClass, IState> {
});
}
private onSelect(option: IOptionOld, e: React.MouseEvent<HTMLLIElement, MouseEvent>) {
private onSelect(option: IOption, e: React.MouseEvent<HTMLLIElement, MouseEvent>) {
if (this.props.disabled) return;
this.props.onChange && this.props.onChange(option);
this.setState({

View File

@ -3,6 +3,7 @@
.root {
position: relative;
&[data-is-disabled="true"] {
opacity: var(--opacity-disabled, 0.3);
.input-container {
@ -27,6 +28,8 @@
}
.input-container {
height: 56px;
display: flex;
padding: var(--spacing-2, 16px) var(--spacing-sm, 8px);
align-items: center;

View File

@ -5,6 +5,9 @@ import BaseField, { IProps as IBaseFieldProps } from "../BaseField";
import classes from "./classes.module.scss";
import classnames from "classnames";
import { XMarkIcon, Square2StackIcon } from "@heroicons/react/24/outline";
import { ToasterService } from "@Front/Components/DesignSystem/Toaster";
export type IProps = IBaseFieldProps & {
canCopy?: boolean;
password?: boolean;
@ -36,6 +39,7 @@ export default class TextField extends BaseField<IProps> {
onBlur={this.onBlur}
name={this.props.name}
disabled={this.props.disabled}
readOnly={this.props.readonly}
type={this.props.password ? "password" : "text"}
/>
{this.props.canCopy && !this.hasError() && (
@ -58,6 +62,7 @@ export default class TextField extends BaseField<IProps> {
private onCopyClick = (): void => {
if (this.props.canCopy) {
navigator.clipboard.writeText(this.state.value ?? "");
ToasterService.getInstance().success({ title: "Copié avec succès !", description: this.state.value });
}
};
}

View File

@ -5,9 +5,11 @@ import classes from "./classes.module.scss";
interface IProps {
className?: string;
width?: string | number;
color?: string;
}
export default class Loader extends React.Component<IProps> {
public override render(): JSX.Element {
return <ArrowPathIcon className={classes["root"]} />;
return <ArrowPathIcon className={classes["root"]} color={this.props.color} width={this.props.width} />;
}
}

View File

@ -7,7 +7,7 @@
left: 0;
width: 100%;
height: 100%;
z-index: 3;
z-index: 5;
.content {
position: fixed;
@ -54,7 +54,8 @@
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
opacity: 0.3;
background: var(--primary-default-deep, #013391);
overflow: hidden;
}
}

View File

@ -75,4 +75,8 @@
display: block;
}
}
&[data-fullwidth="true"] {
width: 100%;
}
}

View File

@ -14,9 +14,10 @@ export type ISearchBlockListProps = {
text: string;
link: string;
};
fullwidth?: boolean;
};
export default function SearchBlockList(props: ISearchBlockListProps) {
const { blocks, onSelectedBlock, bottomButton } = props;
const { blocks, onSelectedBlock, bottomButton, fullwidth = false } = props;
const [selectedBlock, setSelectedBlock] = useState<IBlock | null>(null);
const router = useRouter();
@ -69,7 +70,7 @@ export default function SearchBlockList(props: ISearchBlockListProps) {
}, [blocks]);
return (
<div className={classes["root"]}>
<div className={classes["root"]} data-fullwidth={fullwidth}>
<div className={classes["searchbar"]} ref={searchBarRef}>
<SearchBar placeholder="Chercher" onChange={handleSearchChange} />
{bottomButton && (

View File

@ -0,0 +1,24 @@
@import "@Themes/constants.scss";
.root {
height: 2px;
width: 100%;
background-color: var(--separator-stroke-default);
&.vertical {
width: 2px;
height: 100%;
}
&.light {
background-color: var(--separator-stroke-light);
}
&.strong {
background-color: var(--separator-stroke-strong);
}
&.contrast {
background-color: var(--separator-stroke-contrast);
}
}

View File

@ -0,0 +1,35 @@
import classNames from "classnames";
import React from "react";
import classes from "./classes.module.scss";
export enum ESeperatorColor {
LIGHT = "light",
DEFAULT = "default",
STRONG = "strong",
CONTRAST = "contrast",
}
export enum ESeperatorDirection {
HORIZONTAL = "horizontal",
VERTICAL = "vertical",
}
type IProps = {
color?: ESeperatorColor;
direction?: ESeperatorDirection;
size?: number;
thickness?: number;
className?: string;
};
export default function Separator(props: IProps) {
const { color = ESeperatorColor.DEFAULT, direction = ESeperatorDirection.HORIZONTAL, size, thickness = 1, className } = props;
return (
<div
className={classNames(classes["root"], classes[color], classes[direction], className)}
style={direction === ESeperatorDirection.HORIZONTAL ? { width: size, height: thickness } : { height: size, width: thickness }}
/>
);
}

View File

@ -8,8 +8,9 @@ import TableRow from "@mui/material/TableRow";
import Typography, { ETypo, ETypoColor } from "../../Typography";
import classes from "./classes.module.scss";
import { SxProps, Theme } from "@mui/material";
export type IRowProps = { key: string } & Record<string, React.ReactNode>;
export type IRowProps = { key: string } & Record<string, React.ReactNode | { sx: SxProps<Theme>; content: React.ReactNode }>;
type IRow = {
key?: string;
@ -29,7 +30,7 @@ export type IHead = {
type CellContent = {
key: string;
value: React.ReactNode;
value: React.ReactNode | { sx: SxProps<Theme>; content: React.ReactNode };
};
export default function MuiTable(props: IProps) {
@ -82,12 +83,14 @@ export default function MuiTable(props: IProps) {
className={classes["cell"]}
key={cell.key}
align="left"
sx={{ border: 0, padding: "4px 8px", height: "53px" }}>
sx={{ ...getCellValueStyle(cell.value), border: 0, padding: "4px 8px", height: "53px" }}>
<Typography
className={classes["content"]}
typo={ETypo.TEXT_MD_REGULAR}
color={ETypoColor.COLOR_NEUTRAL_900}>
{cell.value}
{cell.value && typeof cell.value === "object" && "content" in cell.value
? cell.value.content
: cell.value}
</Typography>
</TableCell>
))}
@ -99,4 +102,11 @@ export default function MuiTable(props: IProps) {
</TableContainer>
</InfiniteScroll>
);
function getCellValueStyle(value: React.ReactNode | { sx: SxProps<Theme>; content: React.ReactNode }) {
if (typeof value === "object" && value !== null && "sx" in value) {
return value.sx;
}
return {};
}
}

View File

@ -9,6 +9,8 @@
align-items: center;
justify-content: center;
white-space: nowrap;
&.info {
background-color: var(--tag-info-background);
}
@ -24,4 +26,8 @@
&.error {
background-color: var(--tag-error-background);
}
&.neutral {
background-color: var(--tag-neutral-background);
}
}

View File

@ -9,6 +9,7 @@ export enum ETagColor {
SUCCESS = "success",
ERROR = "error",
WARNING = "warning",
NEUTRAL = "neutral",
}
export enum ETagVariant {
@ -24,10 +25,11 @@ type IProps = {
};
const colorMap: Record<ETagColor, ETypoColor> = {
[ETagColor.INFO]: ETypoColor.COLOR_INFO_900,
[ETagColor.SUCCESS]: ETypoColor.COLOR_SUCCESS_700,
[ETagColor.ERROR]: ETypoColor.COLOR_SECONDARY_700,
[ETagColor.WARNING]: ETypoColor.COLOR_WARNING_700,
[ETagColor.INFO]: ETypoColor.TAG_INFO_CONTRAST,
[ETagColor.SUCCESS]: ETypoColor.TAG_SUCCESS_CONTRAST,
[ETagColor.ERROR]: ETypoColor.TAG_ERROR_CONTRAST,
[ETagColor.WARNING]: ETypoColor.TAG_WARNING_CONTRAST,
[ETagColor.NEUTRAL]: ETypoColor.TAG_NEUTRAL_CONTRAST,
};
const typoMap: Record<ETagVariant, ETypo> = {

View File

@ -0,0 +1,5 @@
.root {
display: flex;
align-items: center;
gap: var(--spacing-lg, 24px);
}

View File

@ -0,0 +1,28 @@
import React from "react";
import Typography, { ETypo, ETypoColor } from "../../Typography";
import classes from "./classes.module.scss";
type IProps = {
title: string;
description?: string;
icon?: React.ReactNode;
};
export default function ToastContent(props: IProps) {
const { icon, title, description } = props;
return (
<div className={classes["root"]}>
{icon}
<div className={classes["content"]}>
<Typography typo={ETypo.TEXT_MD_SEMIBOLD} color={ETypoColor.TOASTER_CONTRAST_TITLE}>
{title}
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TOASTER_CONTRAST_TEXT}>
{description}
</Typography>
</div>
</div>
);
}

View File

@ -0,0 +1,23 @@
.root {
width: fit-content !important;
.wrapper {
width: 387px;
display: flex;
align-items: center;
padding: var(--spacing-2, 16px);
border-radius: var(--toaster-radius, 0px);
border: 1px solid var(--toaster-border, #e5eefa);
background: var(--toaster-background, #fff);
/* shadow/sm */
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.1);
.body {
}
.progress {
height: 2px;
}
}
}

View File

@ -0,0 +1,111 @@
import { ArrowLeftStartOnRectangleIcon, CheckIcon, ExclamationTriangleIcon, InformationCircleIcon, XMarkIcon } from "@heroicons/react/24/outline";
import React from "react";
import { toast, ToastContainer } from "react-toastify";
import BadgeIcon, { EBadgeColor } from "../BadgeIcon";
import IconButton from "../IconButton";
import Loader from "../Loader";
import classes from "./classes.module.scss";
import ToastContent from "./ToastContent";
import "react-toastify/dist/ReactToastify.css";
export default function Toaster() {
return (
<ToastContainer
className={classes["root"]}
toastClassName={classes["wrapper"]}
bodyClassName={classes["body"]}
progressClassName={classes["progress"]}
closeButton={<IconButton icon={<XMarkIcon />} />}
/>
);
}
export class ToasterService {
private static instance: ToasterService;
private constructor() {}
public static getInstance() {
return (this.instance ??= new this());
}
public text({ title, description }: { title: string; description?: string }) {
toast.info(<ToastContent title={title} description={description} />, {
icon: false,
progressStyle: { backgroundColor: "var(--primary-default-base)" },
});
}
public info({ title, description }: { title: string; description?: string }) {
toast.info(
<ToastContent
title={title}
description={description}
icon={<BadgeIcon icon={<InformationCircleIcon />} color={EBadgeColor.INFO} />}
/>,
{
icon: false,
progressStyle: { backgroundColor: "var(--info-default-base)" },
},
);
}
public success({ title, description }: { title: string; description?: string }) {
toast.info(
<ToastContent title={title} description={description} icon={<BadgeIcon icon={<CheckIcon />} color={EBadgeColor.SUCCESS} />} />,
{
icon: false,
progressStyle: { backgroundColor: "var(--success-default-base)" },
},
);
}
public error({ title, description }: { title: string; description?: string }) {
toast.info(
<ToastContent title={title} description={description} icon={<BadgeIcon icon={<XMarkIcon />} color={EBadgeColor.ERROR} />} />,
{
icon: false,
progressStyle: { backgroundColor: "var(--error-default-base)" },
},
);
}
public warning({ title, description }: { title: string; description?: string }) {
toast.info(
<ToastContent
title={title}
description={description}
icon={<BadgeIcon icon={<ExclamationTriangleIcon />} color={EBadgeColor.WARNING} />}
/>,
{
icon: false,
progressStyle: { backgroundColor: "var(--warning-default-base)" },
},
);
}
public loading({ title, description }: { title: string; description?: string }) {
toast.info(
<ToastContent title={title} description={description} icon={<BadgeIcon icon={<Loader />} color={EBadgeColor.INFO} />} />,
{
icon: false,
autoClose: false,
},
);
}
public loggedOut({ title, description }: { title: string; description?: string }) {
toast.info(
<ToastContent
title={title}
description={description}
icon={<BadgeIcon icon={<ArrowLeftStartOnRectangleIcon />} color={EBadgeColor.NEUTRAL} />}
/>,
{
icon: false,
autoClose: false,
},
);
}
}

View File

@ -39,7 +39,6 @@ class ToastElementClass extends React.Component<IPropsClass, IState> {
public override render(): JSX.Element {
const toast = this.props.toast;
console.log(toast);
const style = {
"--data-duration": `${toast.time}ms`,

View File

@ -0,0 +1,27 @@
import { styled, Tooltip, tooltipClasses, TooltipProps } from "@mui/material";
import Typography, { ETypo, ETypoColor } from "../Typography";
type IProps = TooltipProps;
export default function TooltipElement(props: IProps) {
const CustomTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip
{...props}
title={
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
{props.title}
</Typography>
}
classes={{ popper: className }}
/>
))(({ theme }) => ({
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: "var(--tooltip-background, #FFF)",
boxShadow: theme.shadows[1],
borderRadius: "var(--tooltip-radius, 4px)",
},
}));
return <CustomTooltip {...props} />;
}

View File

@ -278,4 +278,8 @@
line-height: 15px;
letter-spacing: -0.8px;
}
&.italic {
font-style: italic;
}
}

View File

@ -10,6 +10,7 @@ type IProps = {
title?: string;
type?: "div" | "span";
onClick?: () => void;
italic?: boolean;
};
export enum ETypo {
@ -149,6 +150,8 @@ export enum ETypoColor {
INPUT_ERROR = "--input-error",
TEXT_ACCENT = "--text-accent",
TEXT_PRIMARY = "--text-primary",
TEXT_SECONDARY = "--text-secondary",
CONTRAST_DEFAULT = "--contrast-default",
CONTRAST_HOVERED = "--contrast-hovered",
@ -161,23 +164,41 @@ export enum ETypoColor {
DROPDOWN_CONTRAST_ACTIVE = "--dropdown-contrast-active",
INPUT_CHIP_CONTRAST = "--input-chip-contrast",
TAG_NEUTRAL_CONTRAST = "--tag-neutral-contrast",
TAG_INFO_CONTRAST = "--tag-info-contrast",
TAG_SUCCESS_CONTRAST = "--tag-success-contrast",
TAG_WARNING_CONTRAST = "--tag-warning-contrast",
TAG_ERROR_CONTRAST = "--tag-error-contrast",
TOASTER_CONTRAST_TITLE = "--toaster-contrast-title",
TOASTER_CONTRAST_TEXT = "--toaster-contrast-text",
TABLE_COLUMN_CONTRAST = "--table-column-contrast",
}
export default function Typography(props: IProps) {
const { typo, color, className, title, children, type = "div", onClick } = props;
const { typo, color, className, title, children, type = "div", onClick, italic } = props;
const style = color ? ({ "--data-color": `var(${color})` } as React.CSSProperties) : undefined;
if (type === "span") {
return (
<span className={classNames(classes["root"], classes[typo], className)} style={style} title={title}>
<span
className={classNames(classes["root"], classes[typo], className, italic && classes["italic"])}
style={style}
title={title}>
{children}
</span>
);
}
return (
<div className={classNames(classes["root"], classes[typo], className)} style={style} title={title} onClick={onClick}>
<div
className={classNames(classes["root"], classes[typo], className, italic && classes["italic"])}
style={style}
title={title}
onClick={onClick}>
{children}
</div>
);

View File

@ -5,6 +5,7 @@ import React from "react";
type IProps = {
url?: string;
text?: string;
};
type IPropsClass = IProps & {
@ -26,7 +27,7 @@ class BackArrowClass extends React.Component<IPropsClass, IState> {
styletype={EButtonstyletype.TEXT}
size={EButtonSize.SM}
onClick={this.handleClick}>
Retour
{this.props.text ?? "Retour"}
</Button>
);
}

View File

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

View File

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

View File

@ -0,0 +1,5 @@
.root {
display: flex;
flex-direction: column;
gap: var(--spacing-sm, 8px);
}

View File

@ -0,0 +1,39 @@
import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import classes from "./classes.module.scss";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import Link from "next/link";
export type IProps = {
title: string;
description: string;
button: { text: string; link?: string; onClick?: () => void };
};
export default function HelpBox(props: IProps) {
const { title, description, button } = props;
return (
<div className={classes["root"]}>
<Typography typo={ETypo.TEXT_MD_SEMIBOLD} color={ETypoColor.TEXT_SECONDARY}>
{title}
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
{description}
</Typography>
{button.link ? (
<Link href={button.link}>
<Button
variant={EButtonVariant.SECONDARY}
styletype={EButtonstyletype.TEXT}
size={EButtonSize.MD}
onClick={button.onClick}>
{button.text}
</Button>
</Link>
) : (
<Button variant={EButtonVariant.SECONDARY} styletype={EButtonstyletype.TEXT} size={EButtonSize.MD} onClick={button.onClick}>
{button.text}
</Button>
)}
</div>
);
}

View File

@ -80,7 +80,6 @@ export default function Tabs<T>({ onSelect, tabs: propsTabs }: IProps<T>) {
newTabs.splice(index, 1);
newTabs.unshift(tabs.current[index]!);
tabs.current = newTabs;
console.log("Updated values ; ", tabs.current);
handleSelect(value);
},
[handleSelect],

View File

@ -0,0 +1,68 @@
import Folders from "@Front/Api/LeCoffreApi/Customer/Folders/Folders";
import { IBlock } from "@Front/Components/DesignSystem/SearchBlockList/BlockList/Block";
import Module from "@Front/Config/Module";
import JwtService from "@Front/Services/JwtService/JwtService";
import { OfficeFolder } from "le-coffre-resources/dist/Customer";
import { useRouter } from "next/router";
import React, { useEffect, useState } from "react";
import DefaultDashboardWithList, { IPropsDashboardWithList } from "../DefaultDashboardWithList";
type IProps = IPropsDashboardWithList & {};
export default function DefaultCustomerDashboard(props: IProps) {
const router = useRouter();
const { folderUid } = router.query;
const [folders, setFolders] = useState<OfficeFolder[]>([]);
useEffect(() => {
const jwt = JwtService.getInstance().decodeCustomerJwt();
if (!jwt) return;
Folders.getInstance()
.get({
q: {
where: {
customers: {
some: {
contact: {
email: jwt.email,
},
},
},
},
orderBy: [
{
created_at: "desc",
},
],
include: {
customers: true,
},
},
})
.then((folders) => setFolders(folders));
}, []);
const onSelectedBlock = (block: IBlock) => {
const folder = folders.find((folder) => folder.uid === block.id);
if (!folder) return;
router.push(
Module.getInstance()
.get()
.modules.pages.ClientDashboard.props.path.replace("[folderUid]", folder.uid ?? ""),
);
};
return <DefaultDashboardWithList {...props} onSelectedBlock={onSelectedBlock} blocks={getBlocks(folders)} headerConnected={false}/>;
function getBlocks(folders: OfficeFolder[]): IBlock[] {
return folders.map((folder) => {
return {
id: folder.uid!,
primaryText: folder.name!,
secondaryText: folder.folder_number!,
isActive: folderUid === folder.uid,
};
});
}
}

View File

@ -32,6 +32,10 @@
.right-side {
min-width: 100%;
.right-side-content {
overflow-y: hidden;
}
}
}
}

View File

@ -1,8 +1,9 @@
import Head from "next/head";
import { ReactNode } from "react";
import Toaster from "../DesignSystem/Toaster";
type DefaultLayoutProps = { children: ReactNode };
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
export const DefaultLayout = ({ children }: DefaultLayoutProps) => {
return (
<>
@ -12,7 +13,7 @@ export const DefaultLayout = ({ children }: DefaultLayoutProps) => {
</Head>
<main>
{children}
<ToastContainer />
<Toaster />
</main>
</>
);

View File

@ -0,0 +1,15 @@
@import "@Themes/constants.scss";
.root {
display: flex;
justify-content: space-between;
align-items: center;
align-self: stretch;
flex-wrap: wrap;
.title {
display: flex;
flex-direction: column;
gap: var(--spacing-sm, 8px);
}
}

View File

@ -0,0 +1,73 @@
import DragAndDrop, { IDocumentFileWithUid } from "@Front/Components/DesignSystem/DragAndDrop";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import { Document } from "le-coffre-resources/dist/Customer";
import { useCallback, useMemo } from "react";
import classes from "./classes.module.scss";
import Files from "@Front/Api/LeCoffreApi/Customer/Files/Files";
import { ToasterService } from "@Front/Components/DesignSystem/Toaster";
type IProps = {
document: Document;
onChange: () => void;
};
export default function DepositDocumentComponent(props: IProps) {
const { document, onChange } = props;
const defaultFiles: IDocumentFileWithUid[] = useMemo(() => {
const filesNotArchived = document.files?.filter((file) => !file.archived_at) ?? [];
return filesNotArchived.map((file) => ({
id: file.uid!,
file: new File([""], file.file_name!, { type: file.mimetype }),
uid: file.uid!,
}));
}, [document.files]);
const addFile = useCallback(
(file: File) => {
const formData = new FormData();
formData.append("file", file, file.name);
const query = JSON.stringify({ document: { uid: document.uid } });
formData.append("q", query);
return Files.getInstance()
.post(formData)
.then(onChange)
.then(() => ToasterService.getInstance().success({ title: "Succès !", description: "Fichier uploadé avec succès!" }))
.catch((error) => ToasterService.getInstance().error({ title: "Erreur !", description: error.message }));
},
[document.uid, onChange],
);
const deleteFile = useCallback(
(filedUid: string) => {
return Files.getInstance()
.delete(filedUid)
.then(onChange)
.then(() => ToasterService.getInstance().success({ title: "Succès !", description: "Fichier supprimé avec succès!" }))
.catch((error) => ToasterService.getInstance().error({ title: "Erreur !", description: error.message }));
},
[onChange],
);
return (
<div className={classes["root"]}>
<div className={classes["title"]}>
<Typography typo={ETypo.TEXT_MD_SEMIBOLD} color={ETypoColor.TEXT_PRIMARY}>
{document.document_type?.name ?? "_"}
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Demandé le: {document.created_at ? new Date(document.created_at).toLocaleDateString() : "_"}
</Typography>
</div>
<DragAndDrop
title={"Drag and drop ou"}
description={document.document_type?.public_description ?? undefined}
defaultFiles={defaultFiles}
onAddFile={addFile}
onDelete={deleteFile}
/>
</div>
);
}

View File

@ -0,0 +1,23 @@
@import "@Themes/constants.scss";
.root {
padding: var(--spacing-3) var(--spacing-15);
max-width: 1156px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-xl, 32px);
.table{
width: 100%;
}
@media screen and (max-width: $screen-m) {
padding: var(--spacing-3);
}
@media screen and (max-width: $screen-s) {
padding: var(--spacing-2);
}
}

View File

@ -0,0 +1,124 @@
import DocumentsNotary from "@Front/Api/LeCoffreApi/Notary/DocumentsNotary/DocumentsNotary";
import FilesNotary from "@Front/Api/LeCoffreApi/Notary/FilesNotary/Files";
import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import IconButton from "@Front/Components/DesignSystem/IconButton";
import Table from "@Front/Components/DesignSystem/Table";
import { IHead, IRowProps } from "@Front/Components/DesignSystem/Table/MuiTable";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import BackArrow from "@Front/Components/Elements/BackArrow";
import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate";
import Module from "@Front/Config/Module";
import JwtService from "@Front/Services/JwtService/JwtService";
import { ArrowDownTrayIcon } from "@heroicons/react/24/outline";
import { saveAs } from "file-saver";
import JSZip from "jszip";
import { DocumentNotary } from "le-coffre-resources/dist/Notary";
import { useRouter } from "next/router";
import React, { useCallback, useEffect, useState } from "react";
import classes from "./classes.module.scss";
const header: readonly IHead[] = [
{
key: "name",
title: "Nom",
},
{
key: "sentAt",
title: "Envoyé le",
},
{
key: "actions",
title: "Action",
},
];
export default function ReceivedDocuments() {
const router = useRouter();
let { folderUid } = router.query;
const [documentsNotary, setDocumentsNotary] = useState<DocumentNotary[]>([]);
useEffect(() => {
const customerUid = JwtService.getInstance().decodeCustomerJwt()?.customerId;
if (!folderUid || !customerUid) return;
DocumentsNotary.getInstance()
.get({ where: { folder: { uid: folderUid }, customer: { uid: customerUid } }, include: { files: true } })
.then((documentsNotary) => setDocumentsNotary(documentsNotary));
}, [folderUid]);
const onDownload = useCallback((doc: DocumentNotary) => {
const file = doc.files?.[0];
if (!file || !file?.uid) return;
return FilesNotary.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 onDownloadAll = useCallback(async () => {
if (documentsNotary.length === 0) return;
const zip = new JSZip();
const folder = zip.folder("documents") || zip;
const downloadPromises = documentsNotary.map(async (doc) => {
const file = doc.files?.[0];
if (file && file.uid) {
const blob = await FilesNotary.getInstance().download(file.uid);
folder.file(file.file_name ?? "file", blob);
}
});
await Promise.all(downloadPromises);
zip.generateAsync({ type: "blob" })
.then((blob: any) => {
saveAs(blob, "documents.zip");
})
.catch((error: any) => {
console.error("Error generating ZIP file: ", error);
});
}, [documentsNotary]);
return (
<DefaultTemplate title={"Documents reçus"} isPadding={false}>
<div className={classes["root"]}>
<BackArrow
text="Retour aux dossiers"
url={Module.getInstance()
.get()
.modules.pages.ClientDashboard.props.path.replace("[folderUid]", folderUid as string)}
/>
<Typography typo={ETypo.TITLE_H1} color={ETypoColor.TEXT_PRIMARY}>
Un document vous a é envoyé
</Typography>
<Table className={classes["table"]} header={header} rows={buildRows(documentsNotary, onDownload)} />
<Button
variant={EButtonVariant.PRIMARY}
size={EButtonSize.LG}
styletype={EButtonstyletype.CONTAINED}
leftIcon={<ArrowDownTrayIcon />}
onClick={onDownloadAll}>
Tout télécharger
</Button>
</div>
</DefaultTemplate>
);
}
function buildRows(documentsNotary: DocumentNotary[], onDownloadFileNotary: (doc: DocumentNotary) => void): IRowProps[] {
return documentsNotary.map((documentNotary) => ({
key: documentNotary.uid ?? "",
name: documentNotary.files?.[0]?.file_name?.split(".")?.[0] ?? "_",
sentAt: new Date(documentNotary.created_at!).toLocaleDateString(),
actions: <IconButton onClick={() => onDownloadFileNotary(documentNotary)} icon={<ArrowDownTrayIcon />} />,
}));
}

View File

@ -1,120 +1,39 @@
@import "@Themes/constants.scss";
.root {
.header {
display: flex;
padding: 64px;
justify-content: space-between;
flex-direction: column;
gap: var(--spacing-xl, 32px);
@media (max-width: $screen-m) {
flex-wrap: wrap;
.text {
margin: 32px 0;
}
}
@media (max-width: $screen-s) {
flex-wrap: wrap;
.text {
margin: 32px 0;
}
.button {
flex: 1;
}
}
.folder-number {
margin-top: 16px;
}
.office-name {
margin-top: 8px;
text-transform: uppercase;
}
.subtitle {
margin: 64px 0 32px 0;
}
.contact {
.title-container {
flex-direction: column;
display: flex;
gap: 20px;
}
gap: var(--spacing-sm, 8px);
.contact-text {
text-align: right;
line-height: 15px;
}
.contact-button {
margin-top: 4%;
}
.separator {
width: 20px;
height: 50px; /* Adjust the height as needed */
background-color: gray;
margin: 0 20px; /* Adjust the margin as needed */
}
.note-box {
border: 1px solid #e0e0e0; /* Light grey border */
margin-top: 25px;
padding: 10px;
width: 100%;
height: 100px; /* Adjust height as needed */
box-sizing: border-box;
position: relative;
.office-container {
display: flex;
align-items: center;
gap: var(--spacing-md, 16px);
}
}
.sub-container {
background-color: var(--color-neutral-50);
padding: 64px;
.content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 32px;
margin-bottom: 64px;
display: flex;
gap: var(--spacing-lg, 24px);
align-items: flex-start;
@media (max-width: $screen-s) {
grid-template-columns: 1fr;
}
.notary {
display: flex;
width: 300px;
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-lg, 24px);
}
.component-to-replace {
min-width: 124px;
height: 98px;
background-color: white;
}
.text {
margin: 32px 0;
}
.button {
width: fit-content;
}
@media (max-width: $screen-s) {
.button {
width: 100%;
}
}
}
.modal-content {
.text {
margin: 24px 0;
}
.component-to-replace {
background-color: var(--color-neutral-50);
height: 98px;
.documents {
display: flex;
flex-direction: column;
gap: var(--spacing-xl, 32px);
width: 100%;
}
}

View File

@ -1,19 +1,27 @@
"use client";
import Documents, { IGetDocumentsparams } from "@Front/Api/LeCoffreApi/Customer/Documents/Documents";
import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import DepositDocument from "@Front/Components/DesignSystem/DepositDocument";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate";
import Customer, { Document, DocumentType, Note, OfficeFolder } from "le-coffre-resources/dist/Customer";
import React, { useCallback, useEffect, useState } from "react";
import Customer, { Document } from "le-coffre-resources/dist/Customer";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { type OfficeFolder as OfficeFolderNotary } from "le-coffre-resources/dist/Notary";
import classes from "./classes.module.scss";
import { useRouter } from "next/router";
import JwtService, { ICustomerJwtPayload } from "@Front/Services/JwtService/JwtService";
import DepositOtherDocument from "@Front/Components/DesignSystem/DepositOtherDocument";
import Folders from "@Front/Api/LeCoffreApi/Customer/Folders/Folders";
import OfficeRib from "@Front/Api/LeCoffreApi/Customer/OfficeRib/OfficeRib";
import Tag, { ETagColor } from "@Front/Components/DesignSystem/Tag";
import DefaultCustomerDashboard from "@Front/Components/LayoutTemplates/DefaultCustomerDashboard";
import ContactBox from "@Front/Components/Elements/ContactBox";
import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import { ArrowDownTrayIcon } from "@heroicons/react/24/outline";
import DepositDocumentComponent from "./DepositDocumentComponent";
import Link from "next/link";
import Module from "@Front/Config/Module";
type IProps = {};
export default function ClientDashboard(props: IProps) {
@ -22,12 +30,11 @@ export default function ClientDashboard(props: IProps) {
const [documents, setDocuments] = useState<Document[] | null>(null);
const [customer, setCustomer] = useState<Customer | null>(null);
const [contact, setContact] = useState<Customer["contact"] | null>(null);
const [folder, setFolder] = useState<OfficeFolder | null>(null);
const [note, setNote] = useState<Note | null>(null);
const [isAddDocumentModalVisible, setIsAddDocumentModalVisible] = useState<boolean>(false);
const [folder, setFolder] = useState<OfficeFolderNotary | null>(null);
const getDocuments = useCallback(async () => {
const [ribUrl, setRibUrl] = useState<string | null>(null);
const fetchFolderAndCustomer = useCallback(async () => {
let jwt: ICustomerJwtPayload | undefined;
if (typeof document !== "undefined") {
jwt = JwtService.getInstance().decodeCustomerJwt();
@ -50,25 +57,20 @@ export default function ClientDashboard(props: IProps) {
},
},
});
//Loop through the folder stakeholders, if there is at least one stakeholder that role is "Collaborateur" set contact to this stakeholders.contact, else, take the first stakeholders of the list
const contact = folder.stakeholders!.find((stakeholder) => stakeholder.office_role?.name === "Collaborateur")?.contact;
setContact(contact ?? folder.stakeholders![0]!.contact);
const actualCustomer = folder?.customers?.find((customer) => customer.contact?.email === jwt?.email);
if (!actualCustomer) throw new Error("Customer not found");
const customer = folder?.customers?.find((customer) => customer.contact?.email === jwt?.email);
if (!customer) throw new Error("Customer not found");
let note = folder.notes?.find((note) => note.customer?.uid === actualCustomer.uid);
// if (!note) throw new Error("Note not found");
if (!note) {
note = {
content: "Aucune note",
created_at: new Date(),
updated_at: new Date(),
};
}
setFolder(folder);
setCustomer(customer);
return { folder, customer };
}, [folderUid]);
const fetchDocuments = useCallback(
(customerUid: string | undefined) => {
const query: IGetDocumentsparams = {
where: { depositor: { uid: actualCustomer.uid }, folder_uid: folderUid as string },
where: { depositor: { uid: customerUid }, folder_uid: folderUid as string },
include: {
files: true,
document_history: true,
@ -86,30 +88,42 @@ export default function ClientDashboard(props: IProps) {
},
};
const documentList = await Documents.getInstance().get(query);
return Documents.getInstance()
.get(query)
.then((documents) => setDocuments(documents));
},
[folderUid],
);
//const folder = await Folders.getInstance().getByUid(folderUid as string, { q: { office: true, customers: true } });
useEffect(() => {
fetchFolderAndCustomer().then(({ customer }) => fetchDocuments(customer.uid));
}, [fetchDocuments, fetchFolderAndCustomer]);
setFolder(folder);
setDocuments(documentList);
setCustomer(actualCustomer);
setNote(note);
}, [folderUid]);
const notaryContact = useMemo(
() =>
folder?.stakeholders!.find((stakeholder) => stakeholder.office_role?.name === "Collaborateur")?.contact ??
folder?.stakeholders![0]!.contact,
[folder],
);
const onCloseModalAddDocument = useCallback(() => {
setIsAddDocumentModalVisible(false);
getDocuments();
}, [getDocuments]);
const note = useMemo(
() =>
folder?.notes?.find((note) => note.customer?.uid === customer?.uid) ?? {
content: "Aucune note",
created_at: new Date(),
updated_at: new Date(),
},
[customer?.uid, folder?.notes],
);
const onOpenModalAddDocument = useCallback(() => {
setIsAddDocumentModalVisible(true);
}, []);
const downloadFile = useCallback(async () => {
useEffect(() => {
if (!folder?.office?.uid) return;
const blob = await OfficeRib.getInstance().getRibStream(folder.office.uid);
const ribUrl = URL.createObjectURL(blob);
OfficeRib.getInstance()
.getRibStream(folder.office.uid)
.then((blob) => setRibUrl(URL.createObjectURL(blob)));
}, [folder]);
const downloadRib = useCallback(async () => {
if (!ribUrl) return;
const a = document.createElement("a");
a.style.display = "none";
@ -117,122 +131,73 @@ export default function ClientDashboard(props: IProps) {
a.download = "";
document.body.appendChild(a);
a.click();
}, [folder]);
}, [ribUrl]);
useEffect(() => {
getDocuments();
}, [folderUid, getDocuments]);
const renderHeader = useCallback(() => {
return (
<div className={classes["header"]}>
<div className={classes["text-container"]}>
{/* TODO Get name from userStore */}
<DefaultCustomerDashboard>
<div className={classes["root"]}>
<div className={classes["title-container"]}>
<Typography typo={ETypo.DISPLAY_LARGE} className={classes["title"]}>
Bonjour {customer?.contact?.first_name.concat(" ", customer?.contact?.last_name)}
</Typography>
</div>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} className={classes["folder-number"]} color={ETypoColor.COLOR_NEUTRAL_500}>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Dossier {folder?.folder_number} - {folder?.name}
</Typography>
<Typography typo={ETypo.TITLE_H4} color={ETypoColor.TEXT_PRIMARY}>
Bonjour {customer?.contact?.first_name.concat(" ", customer?.contact?.last_name)}
</Typography>
<Tag color={ETagColor.INFO} label={"todo"} />
<div className={classes["office-container"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Office
</Typography>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} className={classes["office-name"]} color={ETypoColor.COLOR_NEUTRAL_500}>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_ACCENT}>
{folder?.office?.name}
</Typography>
<Typography typo={ETypo.TITLE_H3} className={classes["subtitle"]}>
Documents à envoyer
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} className={classes["text"]}>
Votre notaire est dans l'attente de documents pour valider votre dossier. Voici la liste des documents.
<br /> Veuillez glisser / déposer chaque document dans la zone prévue à cet effet ou cliquez sur la zone puis
sélectionnez le document correspondant. <br /> En déposant un document, celui-ci est automatiquement enregistré et
transmis à votre notaire.
</Typography>
<div className={classes["note-box"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_NEUTRAL_500}>
{note?.content}
</Typography>
</div>
</div>
<div className={classes["contact"]}>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} className={classes["contact-text"]} color={ETypoColor.COLOR_NEUTRAL_500}>
<p>
{contact?.first_name} {contact?.last_name}
</p>
<p>{contact?.phone_number ?? contact?.cell_phone_number}</p>
<p>{contact?.email}</p>
<div className={classes["content"]}>
<div className={classes["notary"]}>
<Typography typo={ETypo.TEXT_LG_BOLD} color={ETypoColor.TABLE_COLUMN_CONTRAST}>
Votre Notaire
</Typography>
<div className="separator"></div>
{folder?.office?.rib_name && (
//Div to avoid the button to be on the same line as the text
<Button className={classes["contact-button"]} onClick={downloadFile}>
Télécharger le RIB de votre notaire
{notaryContact && <ContactBox contact={notaryContact} note={note} />}
{ribUrl && (
<Button
fullwidth
onClick={downloadRib}
variant={EButtonVariant.PRIMARY}
size={EButtonSize.LG}
styletype={EButtonstyletype.CONTAINED}
rightIcon={<ArrowDownTrayIcon />}>
Télécharger le RIB
</Button>
)}
<Link
href={Module.getInstance()
.get()
.modules.pages.ClientDashboard.pages.ReceiveDocuments.props.path.replace(
"[folderUid]",
folderUid as string,
)}
style={{ width: "100%" }}>
<Button fullwidth variant={EButtonVariant.PRIMARY} size={EButtonSize.LG} styletype={EButtonstyletype.OUTLINED}>
Voir les documents reçus
</Button>
</Link>
</div>
</div>
);
}, [
contact?.cell_phone_number,
contact?.email,
contact?.first_name,
contact?.last_name,
contact?.phone_number,
customer?.contact?.first_name,
customer?.contact?.last_name,
downloadFile,
folder?.folder_number,
folder?.name,
folder?.office?.name,
folder?.office?.rib_name,
note?.content,
]);
const renderBox = useCallback(() => {
return (
<DepositOtherDocument
folder_uid={folderUid!}
customer_uid={customer!.uid!}
open={isAddDocumentModalVisible}
onClose={onCloseModalAddDocument}
document={Document.hydrate<Document>({
document_type: DocumentType.hydrate<DocumentType>({
name: "Autres documents",
}),
})}
/>
);
}, [customer, folderUid, isAddDocumentModalVisible, onCloseModalAddDocument]);
return (
<DefaultTemplate title={"Mon compte"} isPadding={false} hasHeaderLinks={false}>
<div className={classes["root"]}>
{renderHeader()}
<div className={classes["sub-container"]}>
<div className={classes["content"]}>
<div className={classes["documents"]}>
<Typography typo={ETypo.TEXT_LG_BOLD} color={ETypoColor.TABLE_COLUMN_CONTRAST}>
Documents à envoyer
</Typography>
{documents?.map((document) => (
<DepositDocument document={document} key={document.uid} defaultFiles={document.files ?? []} />
<DepositDocumentComponent
key={document.uid}
document={document}
onChange={() => fetchDocuments(customer?.uid)}
/>
))}
</div>
<Typography typo={ETypo.TITLE_H3}>Documents supplémentaires (facultatif)</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} className={classes["text"]}>
Vous souhaitez envoyer d'autres documents à votre notaire ?
</Typography>
<Button
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.OUTLINED}
className={classes["button"]}
onClick={onOpenModalAddDocument}>
Ajouter d'autres documents
</Button>
</div>
</div>
{isAddDocumentModalVisible && renderBox()}
</DefaultTemplate>
</DefaultCustomerDashboard>
);
}

View File

@ -1,140 +0,0 @@
//import Customers from "@Front/Api/LeCoffreApi/Customer/Customers/Customers";
import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import DepositDocument from "@Front/Components/DesignSystem/DepositDocument";
import TextField from "@Front/Components/DesignSystem/Form/TextField";
import Confirm from "@Front/Components/DesignSystem/OldModal/Confirm";
import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography";
import Base from "@Front/Components/Layouts/Base";
import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate";
import Customer, { Document, DocumentType } from "le-coffre-resources/dist/Customer";
import React from "react";
import classes from "./classes.module.scss";
type IProps = {};
type IState = {
isAddDocumentModalVisible: boolean;
documents: Document[];
mockedCustomer: Customer | null;
};
export default class ClientDashboard extends Base<IProps, IState> {
public constructor(props: IProps) {
super(props);
this.state = {
isAddDocumentModalVisible: false,
documents: [],
mockedCustomer: null,
};
this.onCloseModalAddDocument = this.onCloseModalAddDocument.bind(this);
this.onOpenModalAddDocument = this.onOpenModalAddDocument.bind(this);
}
public override render(): JSX.Element {
return (
<DefaultTemplate title={"Mon compte"} isPadding={false} hasHeaderLinks={false}>
<div className={classes["root"]}>
{this.renderHeader()}
<div className={classes["sub-container"]}>
<div className={classes["content"]}>
{this.state.documents?.map((document) => (
<DepositDocument document={document} key={document.uid} defaultFiles={document.files ?? []} />
))}
</div>
<Typography typo={ETypo.TITLE_H3}>Documents supplémentaires (facultatif)</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} className={classes["text"]}>
Vous souhaitez envoyer d'autres documents à votre notaire ?
</Typography>
<Button
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.OUTLINED}
className={classes["button"]}
onClick={this.onOpenModalAddDocument}>
Ajouter d'autres documents
</Button>
</div>
<Confirm
isOpen={this.state.isAddDocumentModalVisible}
onClose={this.onCloseModalAddDocument}
onAccept={this.onOpenModalAddDocument}
closeBtn
header={"Ajouter un document"}
cancelText={"Annuler"}
confirmText={"Déposer le document"}>
<div className={classes["modal-content"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR} className={classes["text"]}>
Vous souhaitez envoyer un autre document à votre notaire ?
</Typography>
<TextField placeholder="Nom du document" />
<Typography typo={ETypo.TEXT_MD_REGULAR} className={classes["text"]}>
Glissez / Déposez votre document dans la zone prévue à cet effet ou cliquez sur la zone puis sélectionnez le
document correspondant.
</Typography>
<DepositDocument
document={Document.hydrate<Document>({
document_type: DocumentType.hydrate<DocumentType>({
name: "Autres documents",
}),
})}
/>
</div>
</Confirm>
</div>
</DefaultTemplate>
);
}
private renderHeader(): JSX.Element {
return (
<div className={classes["header"]}>
<div className={classes["text-container"]}>
{/* TODO Get name from userStore */}
<Typography typo={ETypo.DISPLAY_LARGE} className={classes["title"]}>
Bonjour {this.state.mockedCustomer?.contact?.first_name.concat(" ", this.state.mockedCustomer?.contact?.last_name)}
</Typography>
<Typography typo={ETypo.TITLE_H3} className={classes["subtitle"]}>
Documents à envoyer
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} className={classes["text"]}>
Votre notaire est dans l'attente de documents pour valider votre dossier. Voici la liste des documents.Veuillez
glisser / déposez chaque document dans la zone prévue à cet effet ou cliquez sur la zone puis sélectionnez le
document correspondant. Si un des documents demandés ne vous concernent pas, veuillez contacter votre notaire à
l'aide du bouton ci-dessus.
</Typography>
</div>
</div>
);
}
// public override async componentDidMount() {
// // TODO Get documents of the current customer according to userStore
// // REMOVE this mock
// const jwt = JwtService.getInstance().decodeJwt();
// const mockedCustomers = await Customers.getInstance().get({
// where: { contact: { email: jwt?.email } },
// });
// const mockedCustomer: Customer = mockedCustomers[0]!;
// const query: IGetDocumentsparams = {
// where: { depositor: { uid: mockedCustomer.uid } },
// include: {
// files: true,
// document_history: true,
// document_type: true,
// },
// };
// const documents: Document[] = await Documents.getInstance().get(query);
// this.setState({ documents, mockedCustomer });
// }
private onCloseModalAddDocument() {
this.setState({ isAddDocumentModalVisible: false });
}
private onOpenModalAddDocument() {
this.setState({ isAddDocumentModalVisible: true });
}
}

View File

@ -3,15 +3,16 @@
flex-direction: column;
gap: 32px;
.components {
display: flex;
flex-direction: column;
gap: 32px;
max-width: 600px;
.inputs {
display: flex;
flex-direction: column;
gap: 24px;
}
display: flex;
flex-direction: column;
gap: 24px;
.rows {
display: flex;

View File

@ -2,7 +2,9 @@ import Alert, { EAlertVariant } from "@Front/Components/DesignSystem/Alert";
import Autocomplete from "@Front/Components/DesignSystem/Autocomplete";
import AutocompleteMultiSelect from "@Front/Components/DesignSystem/AutocompleteMultiSelect";
import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import CheckboxesInputElement from "@Front/Components/DesignSystem/CheckBox";
import CircleProgress from "@Front/Components/DesignSystem/CircleProgress";
import DragAndDrop from "@Front/Components/DesignSystem/DragAndDrop";
import Dropdown from "@Front/Components/DesignSystem/Dropdown";
import Footer from "@Front/Components/DesignSystem/Footer";
import Form from "@Front/Components/DesignSystem/Form";
@ -12,10 +14,13 @@ import IconButton, { EIconButtonVariant } from "@Front/Components/DesignSystem/I
import Menu from "@Front/Components/DesignSystem/Menu";
import Modal from "@Front/Components/DesignSystem/Modal";
import Newsletter from "@Front/Components/DesignSystem/Newsletter";
import RadioBox from "@Front/Components/DesignSystem/RadioBox";
import SearchBlockList from "@Front/Components/DesignSystem/SearchBlockList";
import DropdownNavigation from "@Front/Components/DesignSystem/SearchBlockList/DropdownNavigation";
import Separator, { ESeperatorColor, ESeperatorDirection } from "@Front/Components/DesignSystem/Separator";
import Table from "@Front/Components/DesignSystem/Table";
import Tag, { ETagColor, ETagVariant } from "@Front/Components/DesignSystem/Tag";
import Toggle, { EToggleSize } from "@Front/Components/DesignSystem/Toggle";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import NumberPicker from "@Front/Components/Elements/NumberPicker";
import Tabs from "@Front/Components/Elements/Tabs";
@ -27,15 +32,15 @@ import {
ArrowLongRightIcon,
EllipsisHorizontalIcon,
PencilSquareIcon,
QuestionMarkCircleIcon,
UsersIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { useCallback, useMemo, useState } from "react";
import classes from "./classes.module.scss";
import CheckboxesInputElement from "@Front/Components/DesignSystem/CheckBox";
import RadioBox from "@Front/Components/DesignSystem/RadioBox";
import Toggle, { EToggleSize } from "@Front/Components/DesignSystem/Toggle";
import { ToasterService } from "@Front/Components/DesignSystem/Toaster";
import TooltipElement from "@Front/Components/DesignSystem/TooltipElement";
export default function DesignSystem() {
const { isOpen, open, close } = useOpenable();
@ -83,10 +88,78 @@ export default function DesignSystem() {
return (
<DefaultTemplate title={"DesignSystem"}>
<Typography typo={ETypo.DISPLAY_LARGE}>DesignSystem</Typography>
<Newsletter isOpen={false} />
<div className={classes["root"]}>
<div />
<Typography typo={ETypo.DISPLAY_LARGE}>DesignSystem</Typography>
<div className={classes["components"]}>
<Typography typo={ETypo.TEXT_LG_BOLD}>Tooltip</Typography>
<div className={classes["rows"]}>
<TooltipElement title="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec nec">
<QuestionMarkCircleIcon width={24} />
</TooltipElement>
<TooltipElement title="Lorem ipsum dolor">
<span style={{ cursor: "help" }}>
<Typography typo={ETypo.TEXT_XS_LIGHT}>Work for any children</Typography>
</span>
</TooltipElement>
</div>
<Typography typo={ETypo.TEXT_LG_BOLD}>Toast</Typography>
<div className={classes["rows"]}>
<Button
size={EButtonSize.SM}
onClick={() => ToasterService.getInstance().text({ title: "Title toaster", description: "Description" })}>
Send Toast
</Button>
<Button
size={EButtonSize.SM}
onClick={() => ToasterService.getInstance().info({ title: "Title toaster", description: "Description" })}>
Send Toast Info
</Button>
<Button
size={EButtonSize.SM}
onClick={() => ToasterService.getInstance().success({ title: "Title toaster", description: "Description" })}>
Send Toast Success
</Button>
</div>
<div className={classes["rows"]}>
<Button
size={EButtonSize.SM}
onClick={() => ToasterService.getInstance().warning({ title: "Title toaster", description: "Description" })}>
Send Toast Warning
</Button>
<Button
size={EButtonSize.SM}
onClick={() => ToasterService.getInstance().error({ title: "Title toaster", description: "Description" })}>
Send Toast Error
</Button>
<Button
size={EButtonSize.SM}
onClick={() => ToasterService.getInstance().loading({ title: "Title toaster", description: "Description" })}>
Send Toast Loading
</Button>
</div>
<Button
size={EButtonSize.SM}
onClick={() => ToasterService.getInstance().loggedOut({ title: "Title toaster", description: "Description" })}>
Logged Out
</Button>
<Typography typo={ETypo.TEXT_LG_BOLD}>Drag and Drop</Typography>
<DragAndDrop title="Upload de document" description="Description" />
<Typography typo={ETypo.TEXT_LG_BOLD}>Separators</Typography>
<div className={classes["rows"]}>
<Separator color={ESeperatorColor.DEFAULT} direction={ESeperatorDirection.HORIZONTAL} />
<Separator color={ESeperatorColor.LIGHT} direction={ESeperatorDirection.HORIZONTAL} />
<Separator color={ESeperatorColor.STRONG} direction={ESeperatorDirection.HORIZONTAL} />
<Separator color={ESeperatorColor.CONTRAST} direction={ESeperatorDirection.HORIZONTAL} />
</div>
<div className={classes["rows"]} style={{ height: 70, justifyContent: "space-around" }}>
<Separator color={ESeperatorColor.DEFAULT} direction={ESeperatorDirection.VERTICAL} />
<Separator color={ESeperatorColor.LIGHT} direction={ESeperatorDirection.VERTICAL} />
<Separator color={ESeperatorColor.STRONG} direction={ESeperatorDirection.VERTICAL} />
<Separator color={ESeperatorColor.CONTRAST} direction={ESeperatorDirection.VERTICAL} />
</div>
<Typography typo={ETypo.TEXT_LG_BOLD}>Checkboxes</Typography>
<div className={classes["rows"]}>
<CheckboxesInputElement
option={{
@ -125,7 +198,7 @@ export default function DesignSystem() {
disabled={true}
/>
</div>
<Typography typo={ETypo.TEXT_LG_BOLD}>Radio boxes</Typography>
<div className={classes["rows"]}>
<RadioBox name="document" value={"new client"} description="Test" label="Créer un document" toolTip="test" />
<RadioBox
@ -154,7 +227,7 @@ export default function DesignSystem() {
disabled={true}
/>
</div>
<div className={classes["components"]}>
<Typography typo={ETypo.TEXT_LG_BOLD}>Toggle</Typography>
<div className={classes["rows"]}>
<Toggle size={EToggleSize.MD} />
@ -370,6 +443,7 @@ export default function DesignSystem() {
<Tag color={ETagColor.SUCCESS} variant={ETagVariant.REGULAR} label="Success" />
<Tag color={ETagColor.WARNING} variant={ETagVariant.REGULAR} label="Warning" />
<Tag color={ETagColor.ERROR} variant={ETagVariant.REGULAR} label="Error" />
<Tag color={ETagColor.NEUTRAL} variant={ETagVariant.REGULAR} label="Envoyé" />
</div>
<Typography typo={ETypo.TEXT_LG_BOLD}>Table Tags</Typography>
@ -378,6 +452,7 @@ export default function DesignSystem() {
<Tag color={ETagColor.SUCCESS} variant={ETagVariant.SEMI_BOLD} label="SUCCESS" />
<Tag color={ETagColor.WARNING} variant={ETagVariant.SEMI_BOLD} label="WARNING" />
<Tag color={ETagColor.ERROR} variant={ETagVariant.SEMI_BOLD} label="ERROR" />
<Tag color={ETagColor.NEUTRAL} variant={ETagVariant.SEMI_BOLD} label="ENVOYÉ" />
</div>
<Typography typo={ETypo.TEXT_LG_BOLD}>Table</Typography>

View File

@ -40,7 +40,7 @@
.cancel-button {
display: flex;
margin-right: 32px;
margin-right: var(--spacing-md, 16px);
}
@media (max-width: $screen-m) {

View File

@ -1,11 +1,12 @@
.add-document-form-container {
.root {
display: flex;
flex-direction: column;
gap: 24px;
gap: var(--spacing-md, 16px);
width: 566px;
.radiobox-container {
> :not(:last-child) {
margin-bottom: 16px;
}
display: flex;
flex-direction: column;
gap: var(--spacing-xs, 8px);
}
}

View File

@ -4,7 +4,7 @@ import { IOption } from "@Front/Components/DesignSystem/Dropdown/DropdownMenu/Dr
import AutocompleteMultiSelectField from "@Front/Components/DesignSystem/Form/AutocompleteMultiSelectField";
import TextAreaField from "@Front/Components/DesignSystem/Form/TextareaField";
import TextField from "@Front/Components/DesignSystem/Form/TextField";
import Confirm from "@Front/Components/DesignSystem/OldModal/Confirm";
import Modal from "@Front/Components/DesignSystem/Modal";
import RadioBox from "@Front/Components/DesignSystem/RadioBox";
import { DocumentType, OfficeFolder } from "le-coffre-resources/dist/Notary";
import { ChangeEvent, useCallback, useEffect, useState } from "react";
@ -117,15 +117,13 @@ export default function ParameterDocuments(props: IProps) {
}, [getAvailableDocuments, props.folder]);
return (
<Confirm
<Modal
isOpen={props.isCreateDocumentModalVisible}
onClose={handleClose}
onAccept={addDocument}
closeBtn
header={"Ajouter des documents demandables"}
cancelText={"Annuler"}
confirmText={"Ajouter"}>
<div className={classes["add-document-form-container"]}>
firstButton={{ children: "Annuler", onClick: handleClose }}
secondButton={{ children: "Ajouter", onClick: addDocument }}
title={"Ajouter un document"}>
<div className={classes["root"]}>
<div className={classes["radiobox-container"]}>
<RadioBox
name="document"
@ -143,6 +141,7 @@ export default function ParameterDocuments(props: IProps) {
label="Créer un document"
/>
</div>
{addOrEditDocument === "add" && (
<>
<TextField name="document_name" placeholder="Nom du document à ajouter" onChange={onDocumentNameChange} />
@ -162,6 +161,6 @@ export default function ParameterDocuments(props: IProps) {
/>
)}
</div>
</Confirm>
</Modal>
);
}

View File

@ -33,7 +33,7 @@
.buttons-container {
display: flex;
gap: 32px;
gap: var(--spacing-md, 16px);
margin-top: 32px;
@media (max-width: $screen-s) {

View File

@ -13,14 +13,14 @@ import React, { useCallback, useEffect, useState } from "react";
import DefaultDoubleSidePage from "@Front/Components/LayoutTemplates/DefaultDoubleSidePage";
import classes from "./classes.module.scss";
import ParameterDocuments from "./ParameterDocuments";
import { IOptionOld } from "@Front/Components/DesignSystem/Form/SelectFieldOld";
import { IOption } from "@Front/Components/DesignSystem/Form/SelectFieldOld";
import backgroundImage from "@Assets/images/background_refonte.svg";
export default function AskDocuments() {
const router = useRouter();
let { folderUid, customerUid } = router.query;
const [isCreateDocumentModalVisible, setIsCreateDocumentModalVisible] = useState<boolean>(false);
const [documentTypes, setDocumentTypes] = useState<IOptionOld[]>([]);
const [documentTypes, setDocumentTypes] = useState<IOption[]>([]);
const [folder, setFolder] = useState<OfficeFolder | null>(null);
const closeModal = () => setIsCreateDocumentModalVisible(false);
@ -62,7 +62,7 @@ export default function AskDocuments() {
);
const getAvailableDocuments = useCallback(
async (folder: OfficeFolder): Promise<IOptionOld[]> => {
async (folder: OfficeFolder): Promise<IOption[]> => {
// Getting already asked documents UIDs in an array
const userDocumentTypesUids = folder
.documents!.filter((document) => document.depositor!.uid! === customerUid!)
@ -81,7 +81,7 @@ export default function AskDocuments() {
if (!documentTypes) return [];
// Else, return an array document types formatted as IOPtions
const documentTypesOptions: IOptionOld[] = documentTypes.map((documentType) => {
const documentTypesOptions: IOption[] = documentTypes.map((documentType) => {
return {
label: documentType!.name!,
value: documentType!.uid!,
@ -155,7 +155,7 @@ export default function AskDocuments() {
</div>
<div className={classes["add-document-container"]}>
<Button
rightIcon={<PlusIcon style={{ transform: "rotate(180deg)", width: "22px", height: "22px" }} />}
leftIcon={<PlusIcon style={{ transform: "rotate(180deg)", width: "22px", height: "22px" }} />}
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.OUTLINED}
size={EButtonSize.MD}
@ -169,7 +169,7 @@ export default function AskDocuments() {
Annuler
</Button>
</a>
<Button type="submit">Valider</Button>
<Button type="submit">Envoyer la demande</Button>
</div>
</div>
</Form>

View File

@ -0,0 +1,26 @@
@import "@Themes/constants.scss";
.root {
padding: var(--spacing-4) 142px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-xl, 32px);
.table{
width: 100%;
}
.customer-filter{
width: 472px;
}
@media screen and (max-width: $screen-m) {
padding: var(--spacing-3);
}
@media screen and (max-width: $screen-s) {
padding: var(--spacing-2);
}
}

View File

@ -0,0 +1,168 @@
import Customers from "@Front/Api/LeCoffreApi/Notary/Customers/Customers";
import DocumentReminders from "@Front/Api/LeCoffreApi/Notary/DocumentReminders/DocumentReminders";
import Dropdown from "@Front/Components/DesignSystem/Dropdown";
import { IOption } from "@Front/Components/DesignSystem/Dropdown/DropdownMenu/DropdownOption";
import Table from "@Front/Components/DesignSystem/Table";
import { IHead, IRowProps } from "@Front/Components/DesignSystem/Table/MuiTable";
import Tag, { ETagColor, ETagVariant } from "@Front/Components/DesignSystem/Tag";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import BackArrow from "@Front/Components/Elements/BackArrow";
import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate";
import Module from "@Front/Config/Module";
import Customer from "le-coffre-resources/dist/Customer";
import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document";
import { DocumentReminder } from "le-coffre-resources/dist/Notary";
import { useRouter } from "next/router";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import classes from "./classes.module.scss";
type IProps = {};
const tradDocumentStatus: Record<EDocumentStatus, string> = {
[EDocumentStatus.ASKED]: "DEMANDÉ",
[EDocumentStatus.DEPOSITED]: "À VALIDER",
[EDocumentStatus.VALIDATED]: "VALIDÉ",
[EDocumentStatus.REFUSED]: "REFUSÉ",
};
const header: readonly IHead[] = [
{
key: "remindedAt",
title: "Date de relance",
},
{
key: "customer",
title: "Client",
},
{
key: "document_type",
title: "Type de document",
},
{
key: "statut",
title: "Satut",
},
];
export default function DocumentsReminderHistory(props: IProps) {
const [reminders, setReminders] = useState<DocumentReminder[] | null>(null);
const [customers, setCustomers] = useState<Customer[] | null>(null);
const [customerOption, setCustomerOption] = useState<IOption | null>(null);
const router = useRouter();
let { folderUid } = router.query;
const fetchReminders = useCallback(() => {
DocumentReminders.getInstance()
.get({
...(customerOption && customerOption.id !== "tous" && { where: { document: { depositor: { uid: customerOption.id } } } }),
include: {
document: {
include: {
depositor: {
include: {
contact: true,
},
},
document_type: true,
},
},
},
orderBy: { reminder_date: "desc" },
})
.then((reminders) => setReminders(reminders))
.catch((e) => console.warn(e));
}, [customerOption]);
const fetchCustomers = useCallback(async () => {
if (!folderUid) return;
Customers.getInstance()
.get({
where: {
office_folders: {
some: {
uid: folderUid as string,
},
},
},
})
.then(setCustomers)
.catch(console.warn);
}, [folderUid]);
const customersOptions: IOption[] = useMemo(() => {
let options = [
{
id: "tous",
label: "Tous",
},
];
customers?.forEach((customer) => {
options.push({
id: customer.uid ?? "",
label: `${customer.contact?.first_name} ${customer.contact?.last_name}`,
});
});
return options;
}, [customers]);
useEffect(() => {
fetchReminders();
fetchCustomers();
}, [customerOption, customersOptions, fetchCustomers, fetchReminders]);
const onSelectionChange = useCallback((option: IOption | null) => {
setCustomerOption(option ?? null);
}, []);
return (
<DefaultTemplate title={"Historique des relances de documents"} isPadding={false}>
<div className={classes["root"]}>
<BackArrow
text="Retour"
url={Module.getInstance()
.get()
.modules.pages.Folder.pages.FolderInformation.props.path.replace("[folderUid]", folderUid as string)}
/>
<Typography typo={ETypo.TITLE_H1} color={ETypoColor.TEXT_PRIMARY}>
Historique des relances de documents
</Typography>
<Dropdown
className={classes["customer-filter"]}
options={customersOptions}
onSelectionChange={onSelectionChange}
selectedOption={customerOption ?? customersOptions?.[0]}
label="Client"
/>
<Table className={classes["table"]} header={header} rows={buildRows(reminders)} />
</div>
</DefaultTemplate>
);
}
function buildRows(reminders: DocumentReminder[] | null): IRowProps[] {
if (!reminders) return [];
return reminders.map((reminder) => ({
key: reminder.uid ?? "",
remindedAt: new Date(reminder.reminder_date!).toLocaleDateString(),
customer: `${reminder.document?.depositor?.contact?.first_name} ${reminder.document?.depositor?.contact?.last_name}`,
document_type: reminder.document?.document_type?.name,
statut: getTag(reminder.document?.document_status as EDocumentStatus),
}));
}
function getTag(status: EDocumentStatus) {
switch (status) {
case EDocumentStatus.ASKED:
return <Tag label={tradDocumentStatus[status]} color={ETagColor.INFO} variant={ETagVariant.SEMI_BOLD} />;
case EDocumentStatus.DEPOSITED:
return <Tag label={tradDocumentStatus[status]} color={ETagColor.WARNING} variant={ETagVariant.SEMI_BOLD} />;
case EDocumentStatus.VALIDATED:
return <Tag label={tradDocumentStatus[status]} color={ETagColor.SUCCESS} variant={ETagVariant.SEMI_BOLD} />;
case EDocumentStatus.REFUSED:
return <Tag label={tradDocumentStatus[status]} color={ETagColor.ERROR} variant={ETagVariant.SEMI_BOLD} />;
default:
return <Tag label={tradDocumentStatus[status]} color={ETagColor.INFO} variant={ETagVariant.SEMI_BOLD} />;
}
}

View File

@ -2,12 +2,11 @@
.root {
display: flex;
width: fit-content;
padding: var(--spacing-md, 16px);
flex-direction: column;
gap: var(--spacing-md, 16px);
background: var(--primary-weak-higlight, #e5eefa);
min-width: 300px;
width: 300px;
.header {
display: flex;

View File

@ -6,16 +6,15 @@ import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Ty
import Module from "@Front/Config/Module";
import useOpenable from "@Front/Hooks/useOpenable";
import { PencilSquareIcon, TrashIcon, UsersIcon } from "@heroicons/react/24/outline";
import { Note } from "le-coffre-resources/dist/Customer";
import Customer, { Note } from "le-coffre-resources/dist/Customer";
import { useCallback } from "react";
import { ICustomer } from "..";
import { AnchorStatus } from "../..";
import classes from "./classes.module.scss";
import DeleteCustomerModal from "./DeleteCustomerModal";
type IProps = {
customer: ICustomer;
customer: Customer;
anchorStatus: AnchorStatus;
folderUid: string | undefined;
customerNote: Note | null;

View File

@ -1,5 +1,6 @@
import Documents from "@Front/Api/LeCoffreApi/Notary/Documents/Documents";
import Modal from "@Front/Components/DesignSystem/Modal";
import { ToasterService } from "@Front/Components/DesignSystem/Toaster";
import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography";
import React, { useCallback } from "react";
@ -7,7 +8,6 @@ type IProps = {
documentUid: string;
isOpen: boolean;
onClose?: () => void;
onDeleteSuccess: (uid: string) => void;
};
@ -19,6 +19,7 @@ export default function DeleteAskedDocumentModal(props: IProps) {
Documents.getInstance()
.delete(documentUid)
.then(() => onDeleteSuccess(documentUid))
.then(() => ToasterService.getInstance().success({ title: "Succès !", description: "Le document a été supprimé avec succès." }))
.then(onClose)
.catch((error) => console.warn(error)),
[documentUid, onClose, onDeleteSuccess],

View File

@ -0,0 +1,38 @@
import DocumentsNotary from "@Front/Api/LeCoffreApi/Notary/DocumentsNotary/DocumentsNotary";
import Modal from "@Front/Components/DesignSystem/Modal";
import { ToasterService } from "@Front/Components/DesignSystem/Toaster";
import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography";
import React, { useCallback } from "react";
type IProps = {
documentUid: string;
isOpen: boolean;
onClose?: () => void;
onDeleteSuccess: (uid: string) => void;
};
export default function DeleteSentDocumentModal(props: IProps) {
const { isOpen, onClose, documentUid, onDeleteSuccess } = props;
const onDelete = useCallback(
() =>
DocumentsNotary.getInstance()
.delete(documentUid)
.then(() => onDeleteSuccess(documentUid))
.then(() => ToasterService.getInstance().success({ title: "Succès !", description: "Le document a été supprimé avec succès." }))
.then(onClose)
.catch((error) => console.warn(error)),
[documentUid, onClose, onDeleteSuccess],
);
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={"Supprimer lenvoi de document ?"}
firstButton={{ children: "Annuler", onClick: onClose }}
secondButton={{ children: "Oui, Supprimer", onClick: onDelete }}>
<Typography typo={ETypo.TEXT_MD_LIGHT}>Cette action annulera l'envoi du document.</Typography>
</Modal>
);
}

View File

@ -11,4 +11,11 @@
justify-content: space-between;
align-items: center;
}
.actions {
display: flex;
align-items: center;
gap: var(--spacing-sm, 8px);
justify-content: center;
}
}

View File

@ -1,4 +1,7 @@
import Documents from "@Front/Api/LeCoffreApi/Notary/Documents/Documents";
import DocumentsNotary from "@Front/Api/LeCoffreApi/Notary/DocumentsNotary/DocumentsNotary";
import Files from "@Front/Api/LeCoffreApi/Notary/Files/Files";
import FilesNotary from "@Front/Api/LeCoffreApi/Notary/FilesNotary/Files";
import CircleProgress from "@Front/Components/DesignSystem/CircleProgress";
import IconButton from "@Front/Components/DesignSystem/IconButton";
import Table from "@Front/Components/DesignSystem/Table";
@ -8,38 +11,23 @@ import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Ty
import Module from "@Front/Config/Module";
import useOpenable from "@Front/Hooks/useOpenable";
import { ArrowDownTrayIcon, EyeIcon, TrashIcon } from "@heroicons/react/24/outline";
import { useMediaQuery } from "@mui/material";
import { Document } from "le-coffre-resources/dist/Customer";
import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document";
import { DocumentNotary } from "le-coffre-resources/dist/Notary";
import Link from "next/link";
import { useCallback, useEffect, useMemo, useState } from "react";
import NoDocument from "../NoDocument";
import classes from "./classes.module.scss";
import DeleteAskedDocumentModal from "./DeleteAskedDocumentModal";
import DeleteSentDocumentModal from "./DeleteSentDocumentModal";
type IProps = {
documents: Document[];
customerUid: string;
folderUid: string;
};
const header: readonly IHead[] = [
{
key: "document_type",
title: "Type de document",
},
{
key: "document_status",
title: "Statut",
},
{
key: "created_at",
title: "Demandé le",
},
{
key: "actions",
title: "Actions",
},
];
const tradDocumentStatus: Record<EDocumentStatus, string> = {
[EDocumentStatus.ASKED]: "Demandé",
[EDocumentStatus.DEPOSITED]: "À valider",
@ -48,23 +36,58 @@ const tradDocumentStatus: Record<EDocumentStatus, string> = {
};
export default function DocumentTables(props: IProps) {
const { documents: documentsProps, folderUid } = props;
const [documents, setDocuments] = useState<Document[]>(documentsProps);
const [documentUid, setDocumentUid] = useState<string | null>(null);
const { folderUid, customerUid } = props;
const [documents, setDocuments] = useState<Document[]>([]);
const [documentsNotary, setDocumentsNotary] = useState<DocumentNotary[]>([]);
const [focusedDocumentUid, setFocusedDocumentUid] = useState<string | null>(null);
const deleteAskedOocumentModal = useOpenable();
const isMobile = useMediaQuery("(max-width:524px)");
const deleteAskedDocumentModal = useOpenable();
const deleteSentDocumentModal = useOpenable();
const fetchDocuments = useCallback(
() =>
Documents.getInstance()
.get({
where: { folder: { uid: folderUid }, depositor: { uid: customerUid } },
include: { files: true, document_type: true },
})
.then(setDocuments)
.catch(console.warn),
[customerUid, folderUid],
);
const fetchDocumentsNotary = useCallback(
() =>
DocumentsNotary.getInstance()
.get({ where: { folder: { uid: folderUid }, customer: { uid: customerUid } }, include: { files: true } })
.then(setDocumentsNotary)
.catch(console.warn),
[customerUid, folderUid],
);
useEffect(() => {
setDocuments(documentsProps);
}, [documentsProps]);
fetchDocuments();
fetchDocumentsNotary();
}, [fetchDocuments, fetchDocumentsNotary]);
const openDeleteAskedDocumentModal = useCallback(
(uid: string | undefined) => {
if (!uid) return;
setDocumentUid(uid);
deleteAskedOocumentModal.open();
setFocusedDocumentUid(uid);
deleteAskedDocumentModal.open();
},
[deleteAskedOocumentModal],
[deleteAskedDocumentModal],
);
const openDeleteSentDocumentModal = useCallback(
(uid: string | undefined) => {
if (!uid) return;
setFocusedDocumentUid(uid);
deleteSentDocumentModal.open();
},
[deleteSentDocumentModal],
);
const onDownload = useCallback((doc: Document) => {
@ -84,6 +107,23 @@ export default function DocumentTables(props: IProps) {
.catch((e) => console.warn(e));
}, []);
const onDownloadFileNotary = useCallback((doc: DocumentNotary) => {
const file = doc.files?.[0];
if (!file || !file?.uid) return;
return FilesNotary.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
@ -91,16 +131,29 @@ export default function DocumentTables(props: IProps) {
if (document.document_status !== EDocumentStatus.ASKED) return null;
return {
key: document.uid,
document_type: document.document_type?.name ?? "_",
document_status: (
document_type: { sx: { width: 400 }, content: document.document_type?.name ?? "_" },
document_status: {
sx: { width: 107 },
content: (
<Tag
color={ETagColor.INFO}
variant={ETagVariant.SEMI_BOLD}
label={tradDocumentStatus[document.document_status].toUpperCase()}
/>
),
created_at: document.created_at ? new Date(document.created_at).toLocaleDateString() : "_",
actions: <IconButton icon={<TrashIcon onClick={() => openDeleteAskedDocumentModal(document.uid)} />} />,
},
date: {
sx: { width: 107 },
content: document.created_at ? new Date(document.created_at).toLocaleDateString() : "_",
},
actions: {
sx: { width: 76 },
content: (
<div className={classes["actions"]}>
<IconButton icon={<TrashIcon onClick={() => openDeleteAskedDocumentModal(document.uid)} />} />
</div>
),
},
};
})
.filter((document) => document !== null) as IRowProps[],
@ -114,16 +167,25 @@ export default function DocumentTables(props: IProps) {
if (document.document_status !== EDocumentStatus.DEPOSITED) return null;
return {
key: document.uid,
document_type: document.document_type?.name ?? "_",
document_status: (
document_type: { sx: { width: 400 }, content: document.document_type?.name ?? "_" },
document_status: {
sx: { width: 107 },
content: (
<Tag
color={ETagColor.WARNING}
variant={ETagVariant.SEMI_BOLD}
label={tradDocumentStatus[document.document_status].toUpperCase()}
/>
),
created_at: document.created_at ? new Date(document.created_at).toLocaleDateString() : "_",
actions: (
},
date: {
sx: { width: 107 },
content: document.updated_at ? new Date(document.updated_at).toLocaleDateString() : "_",
},
actions: {
sx: { width: 76 },
content: (
<div className={classes["actions"]}>
<Link
href={Module.getInstance()
.get()
@ -131,7 +193,9 @@ export default function DocumentTables(props: IProps) {
.replace("[documentUid]", document.uid ?? "")}>
<IconButton icon={<EyeIcon />} />
</Link>
</div>
),
},
};
})
.filter((document) => document !== null) as IRowProps[],
@ -145,16 +209,24 @@ export default function DocumentTables(props: IProps) {
if (document.document_status !== EDocumentStatus.VALIDATED) return null;
return {
key: document.uid,
document_type: document.document_type?.name ?? "_",
document_status: (
document_type: { sx: { width: 400 }, content: document.document_type?.name ?? "_" },
document_status: {
sx: { width: 107 },
content: (
<Tag
color={ETagColor.SUCCESS}
variant={ETagVariant.SEMI_BOLD}
label={tradDocumentStatus[document.document_status].toUpperCase()}
/>
),
created_at: document.created_at ? new Date(document.created_at).toLocaleDateString() : "_",
actions: (
},
date: {
sx: { width: 107 },
content: document.updated_at ? new Date(document.updated_at).toLocaleDateString() : "_",
},
actions: {
sx: { width: 76 },
content: (
<div className={classes["actions"]}>
<Link
href={Module.getInstance()
@ -166,6 +238,7 @@ export default function DocumentTables(props: IProps) {
<IconButton onClick={() => onDownload(document)} icon={<ArrowDownTrayIcon />} />
</div>
),
},
};
})
.filter((document) => document !== null) as IRowProps[],
@ -179,15 +252,21 @@ export default function DocumentTables(props: IProps) {
if (document.document_status !== EDocumentStatus.REFUSED) return null;
return {
key: document.uid,
document_type: document.document_type?.name ?? "_",
document_status: (
document_type: { sx: { width: 400 }, content: document.document_type?.name ?? "_" },
document_status: {
sx: { width: 107 },
content: (
<Tag
color={ETagColor.ERROR}
variant={ETagVariant.SEMI_BOLD}
label={tradDocumentStatus[document.document_status].toUpperCase()}
/>
),
created_at: document.created_at ? new Date(document.created_at).toLocaleDateString() : "_",
},
date: {
sx: { width: 107 },
content: document.updated_at ? new Date(document.updated_at).toLocaleDateString() : "_",
},
actions: "",
};
})
@ -195,19 +274,46 @@ export default function DocumentTables(props: IProps) {
[documents],
);
const sentDocuments: IRowProps[] = useMemo(
() =>
documentsNotary
.map((document) => {
return {
key: document.uid,
document_type: {
sx: { width: 400 },
content: formatName(document.files?.[0]?.file_name?.split(".")?.[0] ?? "") || "_",
},
document_status: {
sx: { width: 107 },
content: <Tag color={ETagColor.NEUTRAL} variant={ETagVariant.SEMI_BOLD} label={"ENVOYÉ"} />,
},
date: {
sx: { width: 107 },
content: document.updated_at ? new Date(document.updated_at).toLocaleDateString() : "_",
},
actions: {
sx: { width: 76 },
content: (
<div className={classes["actions"]}>
<IconButton onClick={() => onDownloadFileNotary(document)} icon={<ArrowDownTrayIcon />} />
<IconButton icon={<TrashIcon onClick={() => openDeleteSentDocumentModal(document.uid)} />} />
</div>
),
},
};
})
.filter((document) => document !== null) as IRowProps[],
[documentsNotary, onDownloadFileNotary, openDeleteSentDocumentModal],
);
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]);
const handleDelete = useCallback(
(documentUid: string) => {
setDocuments(documents.filter((document) => document.uid !== documentUid));
window.location.reload();
},
[documents],
);
if (documents.length === 0 && documentsNotary.length === 0) return <NoDocument />;
return (
<div className={classes["root"]}>
@ -217,18 +323,68 @@ export default function DocumentTables(props: IProps) {
</Typography>
<CircleProgress percentage={progress} />
</div>
{askedDocuments.length > 0 && <Table header={header} rows={askedDocuments} />}
{toValidateDocuments.length > 0 && <Table header={header} rows={toValidateDocuments} />}
{validatedDocuments.length > 0 && <Table header={header} rows={validatedDocuments} />}
{refusedDocuments.length > 0 && <Table header={header} rows={refusedDocuments} />}
{documentUid && (
{askedDocuments.length > 0 && <Table header={getHeader("Demandé le", isMobile)} rows={askedDocuments} />}
{toValidateDocuments.length > 0 && <Table header={getHeader("Déposé le", isMobile)} rows={toValidateDocuments} />}
{validatedDocuments.length > 0 && <Table header={getHeader("Validé le", isMobile)} rows={validatedDocuments} />}
{refusedDocuments.length > 0 && <Table header={getHeader("Demandé le", isMobile)} rows={refusedDocuments} />}
{sentDocuments.length > 0 && <Table header={getHeader("Envoyé le", isMobile)} rows={sentDocuments} />}
{focusedDocumentUid && (
<>
<DeleteAskedDocumentModal
isOpen={deleteAskedOocumentModal.isOpen}
onClose={deleteAskedOocumentModal.close}
onDeleteSuccess={handleDelete}
documentUid={documentUid}
isOpen={deleteAskedDocumentModal.isOpen}
onClose={deleteAskedDocumentModal.close}
onDeleteSuccess={fetchDocuments}
documentUid={focusedDocumentUid}
/>
<DeleteSentDocumentModal
isOpen={deleteSentDocumentModal.isOpen}
onClose={deleteSentDocumentModal.close}
onDeleteSuccess={fetchDocumentsNotary}
documentUid={focusedDocumentUid}
/>
</>
)}
</div>
);
}
function getHeader(dateColumnTitle: string, isMobile: boolean): IHead[] {
if (isMobile) {
return [
{
key: "document_type",
title: "Type de document",
},
{
key: "document_status",
title: "Statut",
},
{
key: "actions",
title: "Action",
},
];
}
return [
{
key: "document_type",
title: "Type de document",
},
{
key: "document_status",
title: "Statut",
},
{
key: "date",
title: dateColumnTitle,
},
{
key: "actions",
title: "Action",
},
];
}
function formatName(text: string): string {
return text.replace(/[^a-zA-Z0-9 ]/g, "");
}

View File

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

View File

@ -0,0 +1,103 @@
import Customers from "@Front/Api/LeCoffreApi/Notary/Customers/Customers";
import CheckBox from "@Front/Components/DesignSystem/CheckBox";
import { IOption } from "@Front/Components/DesignSystem/Form/SelectFieldOld";
import Modal from "@Front/Components/DesignSystem/Modal";
import Separator, { ESeperatorColor } from "@Front/Components/DesignSystem/Separator";
import { ToasterService } from "@Front/Components/DesignSystem/Toaster";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import Customer from "le-coffre-resources/dist/Customer";
import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document";
import React, { useCallback, useMemo, useState } from "react";
import classes from "./classes.module.scss";
type IProps = {
isOpen: boolean;
onClose?: () => void;
onRemindSuccess: () => void;
customer: Customer;
};
export default function ReminderModal(props: IProps) {
const { isOpen, onClose, onRemindSuccess, customer } = props;
const [selectedOptions, setSelectedOptions] = useState<IOption[]>([]);
const [isAllSelected, setIsAllSelected] = useState(false);
const onRemind = useCallback(() => {
Customers.getInstance()
.sendReminder(customer.uid!, selectedOptions.map((option) => option.value) as string[])
.then(onRemindSuccess)
.then(() => ToasterService.getInstance().success({ title: "Succès !", description: "La relance a été envoyée avec succès." }))
.then(onClose);
}, [customer.uid, onClose, onRemindSuccess, selectedOptions]);
const documentsOptions: IOption[] = useMemo(
() =>
customer.documents
?.filter((document) => document.document_status !== EDocumentStatus.VALIDATED)
.map((document) => {
return {
label: document.document_type?.name ?? "",
value: document.uid ?? "",
};
}) ?? [],
[customer],
);
const handleOnChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const { value, checked } = e.target;
const optionSelected = documentsOptions.find((option) => option.value === value);
if (checked && optionSelected) {
setSelectedOptions((prev) => [...prev, optionSelected]);
} else {
setSelectedOptions((prev) => prev.filter((option) => option.value !== value));
}
},
[documentsOptions],
);
const handleSelectAll = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const { checked } = e.target;
if (checked) {
setSelectedOptions(documentsOptions);
setIsAllSelected(true);
} else {
setSelectedOptions([]);
setIsAllSelected(false);
}
},
[documentsOptions],
);
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={`Relancer votre client ${customer.contact?.last_name}`}
firstButton={{ children: "Annuler", onClick: onClose }}
secondButton={{ children: "Relancer", onClick: onRemind }}>
<div className={classes["root"]}>
<Typography typo={ETypo.TEXT_MD_LIGHT} color={ETypoColor.TEXT_SECONDARY}>
Sélectionnez le(s) document(s) pour lequel vous souhaitez relancer le client.
</Typography>
<CheckBox
option={{ label: "Tous les documents", value: "all-documents" }}
checked={isAllSelected}
onChange={handleSelectAll}
/>
<Separator color={ESeperatorColor.LIGHT} />
{documentsOptions.map((option) => (
<CheckBox
key={option.value as string}
option={option}
onChange={handleOnChange}
checked={isAllSelected}
disabled={isAllSelected}
/>
))}
</div>
</Modal>
);
}

View File

@ -0,0 +1,19 @@
@import "@Themes/constants.scss";
.root {
display: flex;
flex-direction: column;
gap: var(--spacing-md, 32px);
.reminder-info {
display: flex;
gap: 8px;
align-items: center;
.info {
display: flex;
flex-direction: column;
gap: 4px;
}
}
}

View File

@ -0,0 +1,109 @@
import DocumentReminders from "@Front/Api/LeCoffreApi/Notary/DocumentReminders/DocumentReminders";
import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import IconButton, { EIconButtonVariant } from "@Front/Components/DesignSystem/IconButton";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import Module from "@Front/Config/Module";
import useOpenable from "@Front/Hooks/useOpenable";
import { ClockIcon, EnvelopeIcon } from "@heroicons/react/24/outline";
import Customer from "le-coffre-resources/dist/Customer";
import { DocumentReminder } from "le-coffre-resources/dist/Notary";
import Link from "next/link";
import { useRouter } from "next/router";
import { useCallback, useEffect, useMemo, useState } from "react";
import classes from "./classes.module.scss";
import ReminderModal from "./ReminderModal";
import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document";
type IProps = {
customer: Customer;
isAnchored: boolean;
};
export default function EmailReminder(props: IProps) {
const { customer, isAnchored } = props;
const [reminders, setReminders] = useState<DocumentReminder[] | null>(null);
const { isOpen, open, close } = useOpenable();
const router = useRouter();
let { folderUid } = router.query;
const fetchReminders = useCallback(async () => {
if (!customer.uid || !folderUid) return;
DocumentReminders.getInstance()
.get({
where: { document: { depositor: { uid: customer.uid }, folder: { uid: folderUid } } },
include: { document: "true" },
orderBy: { reminder_date: "desc" },
})
.then((reminders) => setReminders(reminders))
.catch((e) => console.warn(e));
}, [customer.uid, folderUid]);
useEffect(() => {
fetchReminders();
}, [fetchReminders]);
const remindersLength = useMemo(() => {
// Vérifie que la liste `reminders` n'est pas vide ou null
if (!reminders || reminders.length === 0) {
return 0;
}
const remindersGroupByDate = reminders.reduce((acc, reminder) => {
// Vérifie si `reminder_date` est bien défini
if (!reminder.reminder_date) return acc;
// Normalise la date à la seconde près
const reminderDate = new Date(reminder.reminder_date).setMilliseconds(0);
// Initialise ou incrémente le compteur
acc[reminderDate] = (acc[reminderDate] || 0) + 1;
return acc;
}, {} as Record<number, number>);
// Retourne la longueur des clés, représentant le nombre de dates uniques
return Object.keys(remindersGroupByDate).length;
}, [reminders]);
const doesCustomerHaveNotValidatedDocuments = useMemo(
() => customer.documents && customer.documents.some((document) => document.document_status !== EDocumentStatus.VALIDATED),
[customer.documents],
);
return (
<div className={classes["root"]}>
{!isAnchored && (
<Button
onClick={open}
rightIcon={<EnvelopeIcon />}
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.OUTLINED}
fullwidth
disabled={!doesCustomerHaveNotValidatedDocuments}>
Relancer par mail
</Button>
)}
<div className={classes["reminder-info"]}>
<Link
title={"Voir l'historique des relances"}
href={Module.getInstance()
.get()
.modules.pages.Folder.pages.DocumentsReminderHistory.props.path.replace("[folderUid]", folderUid as string)
.replace("[customerUid]", customer.uid ?? "")}>
<IconButton icon={<ClockIcon />} variant={EIconButtonVariant.NEUTRAL} />
</Link>
<div className={classes["info"]}>
<Typography typo={ETypo.TEXT_XS_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Dernière relance:{" "}
{reminders && remindersLength > 0 ? new Date(reminders[0]!.reminder_date!).toLocaleDateString() : "-"}
</Typography>
<Typography typo={ETypo.TEXT_XS_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Nombre de relance: {remindersLength}
</Typography>
</div>
</div>
<ReminderModal isOpen={isOpen} onRemindSuccess={fetchReminders} onClose={close} customer={customer} />
</div>
);
}

View File

@ -1,5 +1,7 @@
@import "@Themes/constants.scss";
$mobile-breakpoint: 664px;
.root {
display: flex;
flex-direction: column;
@ -14,17 +16,46 @@
.tabs {
width: calc(100% - 210px);
}
border-bottom: 1px solid var(--tabs-stroke);
> :first-child {
margin-bottom: -1px;
}
> :last-child {
margin-bottom: -1px;
}
}
.content {
display: flex;
gap: var(--spacing-lg, 24px);
.client-box {
@media screen and (max-width: $screen-s) {
flex-direction: column;
}
.client-box-container {
display: flex;
flex-direction: column;
gap: var(--spacing-lg, 24px);
.button-container {
display: flex;
flex-direction: column;
gap: var(--spacing-md, 16px);
}
@media screen and (max-width: $screen-s) {
flex-direction: row;
}
@media screen and (max-width: $mobile-breakpoint) {
flex-direction: column;
margin: auto;
}
}
}
}

View File

@ -1,20 +1,19 @@
import Folders from "@Front/Api/LeCoffreApi/Notary/Folders/Folders";
import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import Tabs from "@Front/Components/Elements/Tabs";
import Module from "@Front/Config/Module";
import { DocumentIcon, UserPlusIcon } from "@heroicons/react/24/outline";
import Customer from "le-coffre-resources/dist/Customer";
import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document";
import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import Link from "next/link";
import { useCallback, useMemo, useState } from "react";
import { AnchorStatus } from "..";
import classes from "./classes.module.scss";
import ClientBox from "./ClientBox";
import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import { DocumentIcon, UserPlusIcon } from "@heroicons/react/24/outline";
import Module from "@Front/Config/Module";
import Link from "next/link";
import NoDocument from "./NoDocument";
import DocumentTables from "./DocumentTables";
import Folders from "@Front/Api/LeCoffreApi/Notary/Folders/Folders";
import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document";
import EmailReminder from "./EmailReminder";
type IProps = {
folder: OfficeFolder;
@ -57,8 +56,6 @@ export default function ClientView(props: IProps) {
[customers],
);
const doesCustomerHaveDocument = useMemo(() => customer.documents && customer.documents.length > 0, [customer]);
const handleClientDelete = useCallback(
(customerUid: string) => {
if (!folder.uid) return;
@ -95,7 +92,7 @@ export default function ClientView(props: IProps) {
)}
</div>
<div className={classes["content"]}>
<div className={classes["client-box"]}>
<div className={classes["client-box-container"]}>
<ClientBox
customer={customer}
anchorStatus={anchorStatus}
@ -103,6 +100,7 @@ export default function ClientView(props: IProps) {
onDelete={handleClientDelete}
customerNote={folder.notes!.find((value) => value.customer?.uid === customer.uid) ?? null}
/>
<div className={classes["button-container"]}>
{anchorStatus === AnchorStatus.NOT_ANCHORED && (
<Link
href={Module.getInstance()
@ -114,12 +112,11 @@ export default function ClientView(props: IProps) {
</Button>
</Link>
)}
<EmailReminder customer={customer} isAnchored={anchorStatus !== AnchorStatus.NOT_ANCHORED} />
</div>
{doesCustomerHaveDocument ? (
<DocumentTables documents={customer.documents ?? []} folderUid={folder?.uid ?? ""} />
) : (
<NoDocument />
)}
</div>
{customer.uid && folder.uid && <DocumentTables customerUid={customer.uid} folderUid={folder.uid} />}
</div>
</section>
);

View File

@ -1,18 +1,41 @@
@import "@Themes/constants.scss";
$mobile-breakpoint: 600px;
.root {
display: flex;
gap: var(--spacing-lg, 40px);
gap: var(--spacing-lg, 24px);
@media screen and (max-width: $mobile-breakpoint) {
flex-direction: column;
}
.info-box1 {
display: flex;
width: 100%;
flex-direction: column;
gap: var(--spacing-sm, 8px);
.folder-number-container {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--spacing-lg, 24px);
}
.open-date {
display: flex;
gap: var(--spacing-sm, 8px);
}
@media screen and (max-width: $screen-s) {
width: 54vw;
}
@media screen and (max-width: $mobile-breakpoint) {
width: 100%;
}
}
.info-box2 {
@ -22,15 +45,42 @@
width: 100%;
max-width: 400px;
@media screen and (max-width: $mobile-breakpoint) {
max-width: 100%;
}
.progress-container {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
@media screen and (max-width: $screen-s) {
flex-direction: column-reverse;
gap: var(--spacing-lg, 24px);
}
@media screen and (max-width: $mobile-breakpoint) {
align-items: flex-start;
}
}
}
.icon-container {
display: flex;
gap: var(--spacing-md, 8px);
&.desktop {
display: flex;
@media screen and (max-width: $mobile-breakpoint) {
display: none;
}
}
&.mobile {
display: none;
@media screen and (max-width: $mobile-breakpoint) {
display: flex;
}
}
}
@ -38,6 +88,31 @@
.text {
max-height: 60px;
overflow-y: auto;
white-space: normal;
word-wrap: break-word;
}
&.desktop {
@media screen and (max-width: $screen-s) {
display: none;
}
}
&.ipad {
display: none;
@media screen and (max-width: $screen-s) {
display: block;
}
@media screen and (max-width: $mobile-breakpoint) {
display: none;
}
}
&.mobile {
display: none;
@media screen and (max-width: $mobile-breakpoint) {
display: block;
}
}
}
@ -46,5 +121,10 @@
background-color: var(--separator-stroke-light);
width: 1px;
align-self: stretch;
@media screen and (max-width: $mobile-breakpoint) {
height: 1px;
width: 100%;
}
}
}

View File

@ -1,16 +1,18 @@
import CircleProgress from "@Front/Components/DesignSystem/CircleProgress";
import IconButton, { EIconButtonVariant } from "@Front/Components/DesignSystem/IconButton";
import Menu from "@Front/Components/DesignSystem/Menu";
import { IItem } from "@Front/Components/DesignSystem/Menu/MenuItem";
import Tag, { ETagColor } from "@Front/Components/DesignSystem/Tag";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import Module from "@Front/Config/Module";
import { ArchiveBoxIcon, EllipsisHorizontalIcon, PencilSquareIcon, UsersIcon } from "@heroicons/react/24/outline";
import { ArchiveBoxIcon, EllipsisHorizontalIcon, PaperAirplaneIcon, PencilSquareIcon, UsersIcon } from "@heroicons/react/24/outline";
import classNames from "classnames";
import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import { useCallback } from "react";
import Link from "next/link";
import { useMemo } from "react";
import { AnchorStatus } from "..";
import classes from "./classes.module.scss";
import { IItem } from "@Front/Components/DesignSystem/Menu/MenuItem";
import Menu from "@Front/Components/DesignSystem/Menu";
type IProps = {
folder: OfficeFolder | null;
@ -23,7 +25,37 @@ type IProps = {
export default function InformationSection(props: IProps) {
const { folder, progress, onArchive, anchorStatus, isArchived } = props;
const getSubMenuElement = useCallback(() => {
const menuItemsDekstop = useMemo(() => {
let elements: IItem[] = [];
// 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: false,
};
// If the folder is not anchored, we can modify the collaborators and the informations
if (anchorStatus === AnchorStatus.NOT_ANCHORED) {
elements.push(modifyCollaboratorsElement);
elements.push(modifyInformationsElement);
}
return elements;
}, [anchorStatus, folder?.uid]);
const menuItemsMobile = useMemo(() => {
let elements: IItem[] = [];
// Creating the three elements and adding them conditionnally
@ -44,33 +76,44 @@ export default function InformationSection(props: IProps) {
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
elements.push({
icon: <PaperAirplaneIcon />,
text: "Envoyer des documents",
link: Module.getInstance()
.get()
.modules.pages.Folder.pages.SendDocuments.props.path.replace("[folderUid]", folder?.uid ?? ""),
hasSeparator: !isArchived,
});
if (!isArchived) {
elements.push(archiveElement);
elements.push({
icon: <ArchiveBoxIcon />,
text: "Archiver le dossier",
onClick: onArchive,
hasSeparator: false,
});
}
return elements;
}, [anchorStatus, folder?.uid, isArchived, onArchive]);
return (
<section className={classes["root"]}>
<div className={classes["info-box1"]}>
<div>
<div className={classes["folder-number-container"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR}>{folder?.folder_number}</Typography>
<div className={classNames(classes["icon-container"], classes["mobile"])}>
<Menu items={menuItemsMobile}>
<IconButton icon={<EllipsisHorizontalIcon />} variant={EIconButtonVariant.NEUTRAL} />
</Menu>
</div>
</div>
<Typography typo={ETypo.TITLE_H4}>{folder?.name}</Typography>
</div>
@ -81,19 +124,37 @@ export default function InformationSection(props: IProps) {
{folder?.created_at ? new Date(folder.created_at).toLocaleDateString() : ""}
</Typography>
</div>
<div className={classNames(classes["description-container"], classes["ipad"])}>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_NEUTRAL_700}>
Note du dossier
</Typography>
<Typography typo={ETypo.TEXT_LG_REGULAR} className={classes["text"]}>
{folder?.description}
</Typography>
</div>
</div>
<div className={classes["separator"]} />
<div className={classes["info-box2"]}>
<div className={classes["progress-container"]}>
<CircleProgress percentage={progress} />
<div className={classes["icon-container"]}>
<Menu items={getSubMenuElement()}>
<div className={classNames(classes["icon-container"], classes["desktop"])}>
<Link
href={Module.getInstance()
.get()
.modules.pages.Folder.pages.SendDocuments.props.path.replace("[folderUid]", folder?.uid ?? "")}
title="Envoyer des documents">
<IconButton icon={<PaperAirplaneIcon />} variant={EIconButtonVariant.NEUTRAL} />
</Link>
<Menu items={menuItemsDekstop}>
<IconButton icon={<EllipsisHorizontalIcon />} variant={EIconButtonVariant.NEUTRAL} />
</Menu>
{!isArchived && <IconButton onClick={onArchive} icon={<ArchiveBoxIcon />} variant={EIconButtonVariant.ERROR} />}
</div>
</div>
<div className={classes["description-container"]}>
<div className={classNames(classes["description-container"], classes["desktop"], classes["mobile"])}>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_NEUTRAL_700}>
Note du dossier
</Typography>

View File

@ -0,0 +1,36 @@
@import "@Themes/constants.scss";
.root {
display: flex;
flex-direction: column;
gap: var(--spacing-xl, 32px);
margin: 24px auto;
width: 566px;
@media (max-width: $screen-m) {
width: 474px;
}
@media (max-width: $screen-s) {
width: 100%;
padding: var(--spacing-md, 16px);
}
.radioboxes {
display: flex;
flex-direction: column;
gap: var(--Radius-lg, 16px);
}
.form {
display: flex;
flex-direction: column;
gap: var(--spacing-xl, 32px);
.buttons-container {
display: flex;
gap: var(--spacing-md, 16px);
}
}
}

View File

@ -0,0 +1,212 @@
import backgroundImage from "@Assets/images/background_refonte.svg";
import DocumentsNotary from "@Front/Api/LeCoffreApi/Notary/DocumentsNotary/DocumentsNotary";
import Folders from "@Front/Api/LeCoffreApi/Notary/Folders/Folders";
import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import DragAndDrop from "@Front/Components/DesignSystem/DragAndDrop";
import Form from "@Front/Components/DesignSystem/Form";
import AutocompleteMultiSelectField from "@Front/Components/DesignSystem/Form/AutocompleteMultiSelectField";
import RadioBox from "@Front/Components/DesignSystem/RadioBox";
import { ToasterService } from "@Front/Components/DesignSystem/Toaster";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import BackArrow from "@Front/Components/Elements/BackArrow";
import DefaultDoubleSidePage from "@Front/Components/LayoutTemplates/DefaultDoubleSidePage";
import Module from "@Front/Config/Module";
import { PaperAirplaneIcon } from "@heroicons/react/24/outline";
import { ValidationError } from "class-validator";
import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import { useRouter } from "next/router";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import classes from "./classes.module.scss";
enum EClientSelection {
ALL_CLIENTS = "all_clients",
SELECTED_CLIENTS = "selected_clients",
}
export default function SendDocuments() {
const router = useRouter();
let { folderUid } = router.query;
const [folder, setFolder] = useState<OfficeFolder | null>(null);
const [clientSelection, setClientSelection] = useState<EClientSelection | null>(null);
const [selectedClients, setSelectedClients] = useState<string[]>([]);
const [files, setFiles] = useState<File[]>([]);
const [isSending, setIsSending] = useState(false);
const [validationError, setValidationError] = useState<ValidationError | null>(null);
const onFormSubmit = useCallback(
async (
_e: React.FormEvent<HTMLFormElement> | null,
_values: {
[key: string]: any;
},
) => {
if (!files.length) {
console.error("No files to send");
return;
}
try {
setIsSending(true);
if (selectedClients.length === 0) {
setValidationError({
property: "clients",
constraints: {
isEmpty: "Veuillez sélectionner au moins un client",
},
});
throw new Error("No clients selected");
}
await Promise.all(
selectedClients.map(async (customer) => {
const promises = files.map(async (file) => {
const formData = new FormData();
formData.append("customerUid", customer as string);
formData.append("folderUid", folderUid as string);
formData.append("name", file.name);
formData.append("file", file);
// Envoi de chaque fichier pour chaque client
return DocumentsNotary.getInstance().post(formData);
});
// Attendre que tous les fichiers pour un client soient envoyés
await Promise.all(promises);
}),
);
router.push(
Module.getInstance()
.get()
.modules.pages.Folder.pages.FolderInformation.props.path.replace("[folderUid]", folderUid as string),
);
setIsSending(false);
ToasterService.getInstance().success({ title: "Succès !", description: "Votre document a été envoyée avec succès." });
} catch (error) {
setIsSending(false);
console.warn("Error while sending files: ", error);
}
},
[files, folderUid, router, selectedClients],
);
const fetchFolder = useCallback(async () => {
Folders.getInstance()
.getByUid(folderUid as string, {
q: {
customers: {
include: {
contact: true,
},
},
},
})
.then((folder) => setFolder(folder))
.catch((e) => console.warn(e));
}, [folderUid]);
const onClientSelectionChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const selection = e.target.value as EClientSelection;
setClientSelection(selection);
if (selection === EClientSelection.ALL_CLIENTS && folder?.customers) {
const allClientIds = folder.customers.map((customer) => customer.uid ?? "");
setSelectedClients(allClientIds);
} else {
setSelectedClients([]);
}
},
[folder],
);
const handleClientSelectionChange = useCallback((selectedOptions: any) => {
setSelectedClients(selectedOptions.map((option: any) => option.id));
}, []);
useEffect(() => {
fetchFolder();
}, [fetchFolder]);
const backUrl = useMemo(
() =>
Module.getInstance()
.get()
.modules.pages.Folder.pages.FolderInformation.props.path.replace("[folderUid]", folderUid as string),
[folderUid],
);
const handleFileChange = useCallback((files: File[]) => {
setFiles(files);
}, []);
const clientsOptions = useMemo(() => {
if (!folder?.customers) return [];
return folder.customers.map((customer) => ({
id: customer.uid ?? "",
label: `${customer.contact?.first_name} ${customer.contact?.last_name}`,
}));
}, [folder]);
return (
<DefaultDoubleSidePage title={"Demander des documents"} image={backgroundImage} showHeader>
<div className={classes["root"]}>
<BackArrow url={backUrl} />
<Typography typo={ETypo.TITLE_H1} color={ETypoColor.TEXT_PRIMARY}>
Envoyer des documents, sélectionnez les destinataires :
</Typography>
<Typography typo={ETypo.TEXT_MD_LIGHT} color={ETypoColor.TEXT_SECONDARY}>
Voulez-vous envoyer ce document à tous les clients du dossier ou sélectionner certains clients ?{" "}
</Typography>
<div className={classes["radioboxes"]}>
<RadioBox
name="clients"
value={EClientSelection.ALL_CLIENTS}
label="Sélectionner tous les clients du dossier"
onChange={onClientSelectionChange}
/>
<RadioBox
name="clients"
value={EClientSelection.SELECTED_CLIENTS}
label="Sélectionner certains clients"
onChange={onClientSelectionChange}
/>
</div>
<Form onSubmit={onFormSubmit} className={classes["form"]}>
{clientSelection === EClientSelection.SELECTED_CLIENTS && (
<AutocompleteMultiSelectField
name="clients"
label="Choisir le ou les clients: "
options={clientsOptions}
onSelectionChange={handleClientSelectionChange}
validationError={validationError ?? undefined}
/>
)}
{clientSelection && (
<>
<DragAndDrop
name="files"
title="Glisser ou déposer ou"
description="Formats acceptés : PDF, JPG Taille maximale : 5 Mo"
onChange={handleFileChange}
/>
<div className={classes["buttons-container"]}>
<a href={backUrl}>
<Button variant={EButtonVariant.PRIMARY} styletype={EButtonstyletype.OUTLINED}>
Annuler
</Button>
</a>
<Button type="submit" rightIcon={<PaperAirplaneIcon />} isLoading={isSending}>
Envoyer
</Button>
</div>
</>
)}
</Form>
</div>
</DefaultDoubleSidePage>
);
}

View File

@ -1,7 +1,7 @@
import Folders from "@Front/Api/LeCoffreApi/Notary/Folders/Folders";
import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import Form from "@Front/Components/DesignSystem/Form";
import Select, { IOptionOld } from "@Front/Components/DesignSystem/Form/SelectFieldOld";
import Select, { IOption } from "@Front/Components/DesignSystem/Form/SelectFieldOld";
import TextField from "@Front/Components/DesignSystem/Form/TextField";
import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography";
import BackArrow from "@Front/Components/Elements/BackArrow";
@ -81,7 +81,7 @@ export default function UpdateFolderMetadata() {
const deedOption = {
label: selectedFolder?.deed?.deed_type?.name,
value: selectedFolder?.deed?.deed_type?.uid,
} as IOptionOld;
} as IOption;
const openingDate = new Date(selectedFolder?.created_at ?? "");
if (!selectedFolder?.created_at) return <></>;
const defaultValue = openingDate.toISOString().split("T")[0];

View File

@ -3,7 +3,7 @@
.root {
.content {
display: flex;
width: 648px;
max-width: 648px;
flex-direction: column;
justify-content: center;
gap: var(--spacing-2xl, 40px);
@ -14,8 +14,8 @@
gap: var(--spacing-md, 16px);
align-self: stretch;
.logo{
fill: "red"
.logo {
fill: "red";
}
}
@ -39,17 +39,23 @@
display: flex;
gap: var(--spacing-lg, 24px);
.box {
display: flex;
@media screen and (max-width: 600px) {
flex-direction: column;
gap: var(--spacing-sm, 8px);
}
}
}
.separator {
background-color: var(--separator-stroke-light);
width: 1px;
align-self: stretch;
.mobile {
display: none;
@media screen and (max-width: 600px) {
display: flex;
}
}
.desktop {
display: flex;
@media screen and (max-width: 600px) {
display: none;
}
}
}

View File

@ -1,50 +1,36 @@
import LogoIcon from "@Assets/logo_small_blue.svg";
import Users from "@Front/Api/LeCoffreApi/Notary/Users/Users";
import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import Folders from "@Front/Api/LeCoffreApi/Notary/Folders/Folders";
import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button";
import Separator, { ESeperatorColor, ESeperatorDirection } from "@Front/Components/DesignSystem/Separator";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import HelpBox from "@Front/Components/Elements/HelpBox";
import DefaultNotaryDashboard from "@Front/Components/LayoutTemplates/DefaultNotaryDashboard";
import Module from "@Front/Config/Module";
import JwtService from "@Front/Services/JwtService/JwtService";
import useUser from "@Front/Hooks/useUser";
import { DocumentIcon } from "@heroicons/react/24/outline";
import User from "le-coffre-resources/dist/Notary";
import EFolderStatus from "le-coffre-resources/dist/Customer/EFolderStatus";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import classes from "./classes.module.scss";
import Folders from "@Front/Api/LeCoffreApi/Notary/Folders/Folders";
import EFolderStatus from "le-coffre-resources/dist/Customer/EFolderStatus";
import { useRouter } from "next/router";
export default function Folder() {
const [_isArchivedModalOpen, _setIsArchivedModalOpen] = useState(true);
const router = useRouter();
const [activeUser, setActiveUser] = useState<User | null>();
useEffect(() => {
const decodedJwt = JwtService.getInstance().decodeJwt();
if (!decodedJwt) return;
Users.getInstance()
.getByUid(decodedJwt.userId, {
q: {
contact: true,
},
})
.then((user) => {
setActiveUser(user);
});
}, []);
const { user: activeUser } = useUser();
useEffect(() => {
Folders.getInstance()
.get({
q: {
where: { status: EFolderStatus.LIVE },
orderBy: { created_at: "desc" },
},
})
.then((folders) => {
console.log(folders);
if (folders.length > 0)
router.push(
Module.getInstance()
@ -53,7 +39,6 @@ export default function Folder() {
);
});
}, [router]);
return (
<DefaultNotaryDashboard title={"Dossier"} mobileBackText={"Liste des dossiers"}>
<div className={classes["root"]}>
@ -62,13 +47,13 @@ export default function Folder() {
<Image src={LogoIcon} alt="logo" />
{activeUser && activeUser.contact && (
<Typography typo={ETypo.TITLE_H1} color={ETypoColor.COLOR_PRIMARY_500}>
<Typography typo={ETypo.TITLE_H1} color={ETypoColor.TEXT_ACCENT}>
Bonjour {activeUser.contact.first_name}, bienvenue sur LeCoffre.io
</Typography>
)}
{!activeUser ||
(!activeUser.contact && (
<Typography typo={ETypo.TITLE_H1} color={ETypoColor.COLOR_PRIMARY_500}>
<Typography typo={ETypo.TITLE_H1} color={ETypoColor.TEXT_ACCENT}>
Bonjour, bienvenue sur LeCoffre.io
</Typography>
))}
@ -96,22 +81,25 @@ export default function Folder() {
</div>
<div className={classes["help-container"]}>
<div className={classes["box"]}>
<Typography typo={ETypo.TEXT_MD_SEMIBOLD}>Besoin d'aide ?</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR}>Consultez nos guides pour bien démarrer.</Typography>
<Button variant={EButtonVariant.SECONDARY} styletype={EButtonstyletype.TEXT} size={EButtonSize.MD}>
Accéder aux guides
</Button>
</div>
<div className={classes["separator"]} />
<HelpBox
title="Besoin d'aide ?"
description="Consultez nos guides pour bien démarrer."
button={{ text: "Accéder aux guides" }}
/>
<div className={classes["box"]}>
<Typography typo={ETypo.TEXT_MD_SEMIBOLD}>Vous avez des questions ?</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR}>Notre équipe de support est pour vous aider.</Typography>
<Button variant={EButtonVariant.SECONDARY} styletype={EButtonstyletype.TEXT} size={EButtonSize.MD}>
Contactez le support
</Button>
</div>
<Separator
className={classes["desktop"]}
direction={ESeperatorDirection.VERTICAL}
size={128}
color={ESeperatorColor.LIGHT}
/>
<Separator className={classes["mobile"]} direction={ESeperatorDirection.HORIZONTAL} color={ESeperatorColor.LIGHT} />
<HelpBox
title="Vous avez des questions ?"
description="Notre équipe de support est là pour vous aider."
button={{ text: "Contactez le support" }}
/>
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import Form from "@Front/Components/DesignSystem/Form";
import Select, { IOptionOld } from "@Front/Components/DesignSystem/Form/SelectFieldOld";
import Select, { IOption } from "@Front/Components/DesignSystem/Form/SelectFieldOld";
import TextField from "@Front/Components/DesignSystem/Form/TextField";
import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography";
import BackArrow from "@Front/Components/Elements/BackArrow";
@ -18,7 +18,7 @@ type IProps = {
};
type IState = {
selectedFolder: OfficeFolder | null;
selectedOption?: IOptionOld;
selectedOption?: IOption;
};
class UpdateFolderMetadataClass extends BasePage<IProps, IState> {
constructor(props: IProps) {
@ -74,7 +74,7 @@ class UpdateFolderMetadataClass extends BasePage<IProps, IState> {
);
}
private onSelectedOption(option: IOptionOld) {
private onSelectedOption(option: IOption) {
this.setState({
selectedOption: option,
});

View File

@ -0,0 +1,14 @@
.root {
width: 1056px;
padding: 24px;
display: flex;
flex-direction: column;
gap: var(--spacing-xl, 32px);
section {
display: flex;
flex-direction: column;
gap: var(--spacing-md, 16px);
}
}

View File

@ -0,0 +1,597 @@
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import Dot from "@Front/Components/Layouts/Legal/LegalInformations/elements/Dot";
import React from "react";
import classes from "./classes.module.scss";
import Module from "@Front/Config/Module";
import { ELegalOptions } from "@Front/Components/LayoutTemplates/DefaultLegalDashboard";
import LinkInline from "../elements/LinkInline";
export default function CGU() {
return (
<div className={classes["root"]}>
<section>
<Typography typo={ETypo.TITLE_H4} color={ETypoColor.TEXT_PRIMARY}>
Conditions Générales d'Utilisation
</Typography>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} color={ETypoColor.TEXT_SECONDARY} italic>
Version en vigueur à compter du 24 avril 2024
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Les présentes Conditions Générales dUtilisation sont conclues entre LEcoffre.io, 2 Mail Anne-Catherine 35000 Rennes,
Numéro Siret 92747436100015, TVA intracommunautaire FR17927474361, et lInternaute.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Tout Internaute qui souhaite utiliser le Site
<LinkInline text="https://www.lecoffre.io" href="https://www.lecoffre.io" />
(ci-après, le « Site ») dans sa version ordinateur ou mobile est réputé avoir pris connaissance des présentes conditions
générales dutilisation (ci-après, les « CGU »).
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Toute utilisation du Site nécessite la consultation et lacceptation des présentes conditions générales. LInternaute
reconnaît que lutilisation du Site nécessite le respect de lensemble des prescriptions définies au sein des présentes.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Il est tenu à chaque visite de prendre connaissance déventuelles évolutions de ces dernières, LEcoffre.io se réservant
le droit de les modifier à tout moment.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Ces conditions générales dutilisation ne sont pas des conditions générales de vente ou des conditions générales de
prestations de services.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Elles ont vocation à constituer la règle que doit respecter tout Internaute se rendant sur le Site LEcoffre.io
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY} italic>
Dernière mise à jour le 24 avril 2024.
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
1. Définitions
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Les termes ci-dessous définis auront entre les Parties la signification suivante :
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
<Dot>« Internaute » : toute personne accédant et mettant en œuvre les fonctionnalités du Site ;</Dot>
<Dot>
« Parties » : désigne au singulier indistinctement Lecoffre.io ou lInternaute, et au pluriel le LEcoffre.io et
lInternaute ensemble ;
</Dot>
<Dot>
« Règlementation relative à la protection des données à caractère personnel » : ° la loi relative à linformatique,
aux fichiers et aux libertés n° 78-17 du 6 janvier 1978 modifiée et ses mises à jour ou modification ; et
</Dot>
<Dot>
le Règlement (UE) 2016/679 du Parlement européen et du Conseil du 27 avril 2016 (règlement général sur la protection
des données) ;
</Dot>
<Dot>
« Site » : site Internet édité par LEcoffre.io accessible à
<LinkInline text="https://www.lecoffre.io" href="https://www.lecoffre.io" />
</Dot>
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
2. Description des services
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Le Site permet un accès au service LECoffre.io qui est une plateforme en ligne, par abonnement, permettant le transfert
sécurisé de données et documents à destination des notaires et de leurs offices.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Le fait de déposer un document sur la plateforme entraîne donc la conservation temporaire des documents transmis.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Une fois le dossier complété et l'ensemble des documents nécessaires à la préparation d'un acte notarié réalisé, le
notaire peut obtenir l'ensemble des documents après que la plateforme lecoffre.io ait certifié l'origine du document via
l'identification de son émetteur et verrouillé l'état des documents par l'établissement d'un jeton unique
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
La technologie utilisée repose sur l'utilisation dune technologie blockchain.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LInternaute est invité à se référer aux
<LinkInline
text="Conditions Générales de Service"
href={Module.getInstance()
.get()
.modules.pages.Legal.pages.LegalInformations.props.path.replace("[legalUid]", ELegalOptions.CGS)}
/>
concernant lutilisation de la plateforme.
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
3. Entrée en vigueur Durée
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Les présentes conditions générales dutilisation entrent en vigueur à la date de leur mise en ligne et seront opposables
à la date de la première utilisation du Site par lInternaute.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Les présentes conditions dutilisation sont opposables pendant toute la durée dutilisation du Site et jusquà ce que de
nouvelles conditions générales dutilisation remplacent les présentes.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LInternaute peut à tout moment renoncer à utiliser le Site mais reste responsable de toute utilisation antérieure.
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
4. Objet des CGU
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Les présentes CGU ont pour objet de présenter et de fixer les conditions dutilisation du Site, et lensemble des pages
du Site.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LInternaute déclare avoir pris connaissance et accepté les présentes conditions générales dutilisation
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
5. Conditions d'accès
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Laccès au Site est libre et gratuit pour tout Internaute disposant d'un accès à internet ou à un réseau mobile. Tous
les coûts afférents à l'accès, que ce soit les frais matériels, logiciels ou d'accès à internet sont exclusivement à la
charge de l'Utilisateur. Il est seul responsable du bon fonctionnement de son équipement mobile ainsi que de son accès à
internet.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LEcoffre.io sefforce de fournir un accès de qualité et de permettre aux Internautes d'utiliser les moyens de
communication mis à leur disposition dans les meilleures conditions possibles.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
En raison de la nature et de la complexité du réseau de lInternet, et en particulier, de ses performances techniques et
des temps de réponse pour consulter, interroger ou transférer les données dinformations, LEcoffre.io fait ses meilleurs
efforts, conformément aux règles de lart, pour permettre laccès et lutilisation du Site. LEcoffre.io ne saurait en
effet assurer une accessibilité ou une disponibilité absolue du Site.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Toutefois, LEcoffre.io se réserve le droit, sans préavis, ni indemnité, de fermer temporairement ou définitivement tout
ou partie du service notamment pour effectuer une mise à jour, des opérations de maintenance, des modifications ou
changements sur les méthodes opérationnelles et les serveurs, sans que cette liste ne soit limitative.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LEcoffre.io n'est pas responsable des dommages de toute nature qui peuvent résulter de ces changements ou d'une
indisponibilité temporaire ou encore de la fermeture définitive de tout ou partie du Site ou des services qui y sont
associés.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LEcoffre.io se réserve le droit de compléter ou de modifier, à tout moment, le Site et les services qui y sont
disponibles en fonction de lévolution des technologies.{" "}
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Il appartient à lInternaute de veiller aux possibilités dévolution des moyens informatiques et de transmission à sa
disposition pour que ces moyens puissent sadapter aux évolutions du service.
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
6. Liens hypertextes
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LEcoffre.io se réserve la possibilité de mettre en place des hyperliens sur son Site, donnant accès à des pages web
autres que celles de son Site sur lesquels LEcoffre.io nexerce aucun contrôle.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LEcoffre.io décline toute responsabilité quant aux conditions daccès à ces pages web, à leur fonctionnement, à leur
utilisation des données à caractère personnel des Internautes et quant au contenu des informations fournies sur ces
pages web au titre de lactivation des hyperliens.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
L'Internaute qui dispose dun site internet à titre personnel et désire placer, à des fins personnelles, sur son site un
lien simple renvoyant directement sur la page daccueil du Site du LEcoffre.io, doit obtenir lautorisation expresse de
LEcoffre.io pour établir ce lien.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
En aucun cas, cette autorisation ne pourra être qualifiée de convention implicite daffiliation.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
En toute hypothèse, les liens hypertextes renvoyant au Site sans lautorisation de LEcoffre.io devront être retirés à
première demande de LEcoffre.io.
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
7. Sécurité
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LEcoffre.io fait ses meilleurs efforts, conformément aux règles de lart, pour sécuriser le Site au regard du risque
encouru et de la nature des données traitées. Toutefois, LEcoffre.io ne saurait être responsable, sagissant de ce
point, quen cas de faute prouvée imputable à cette dernière. Il ne saurait assurer une sécurité absolue.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Il est interdit à lInternaute d'accéder ou de se maintenir, frauduleusement, dans tout ou partie du Site. Il s'interdit
dutiliser une méthode daccès autre que linterface mise à disposition par LEcoffre.io. En cas de découverte d'une
telle méthode ou si lInternaute entre dans un espace réservé, sans droit, par inadvertance, celui-ci s'engage à en
informer sans délai LEcoffre.io par courrier électronique à ladresse (contact@lecoffre.io ) afin quil puisse prendre
les mesures nécessaires.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Il est interdit à lInternaute de supprimer ou modifier des données contenues sur le Site qui nauraient pas é
publiées par lui-même, ou dy introduire frauduleusement des données ou même dopérer une altération du fonctionnement
du Site. Il veille notamment à ne pas introduire de virus, code malveillant ou toute autre technologie nuisible au Site
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Tout accès à un espace interdit sera considéré comme un accès frauduleux au sens des dispositions du Code pénal.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LInternaute sengage à considérer que toutes les données dont il aura eu connaissance à loccasion dun tel accès à un
espace non autorisé sont des données confidentielles et sengage, en conséquence, à ne pas les divulguer.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LInternaute sinterdit notamment de réaliser toute opération visant à saturer une page, les opérations de rebond ou
toute opération ayant pour conséquence d'entraver ou de fausser le fonctionnement du Site
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LInternaute sengage à ne pas utiliser de dispositifs ou de logiciels de toutes sortes qui auraient pour conséquence de
perturber le bon fonctionnement du Site.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LInternaute sengage à ne pas engager d'action qui imposerait une charge disproportionnée sur les infrastructures du
Site.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LInternaute accepte les caractéristiques et limites de linternet. Il a conscience que les données circulant sur
linternet ne sont pas nécessairement protégées, notamment contre les détournements éventuels.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LInternaute prend les mesures appropriées pour assurer la sécurité de ses propres données et / ou logiciels de la
contamination par des éventuels virus sur le réseau internet.
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
8. Données à caractère personnel
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LEcoffre.io déclare quil traite les données à caractère personnel conformément aux dispositions de la loi n°78-17 du 6
janvier 1978 modifiée et du Règlement (UE) 2016/679 du 27 avril 2016 (RGPD).
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LInternaute est invité à se référer aux
<LinkInline
text="Politique de confidentialité"
href={Module.getInstance()
.get()
.modules.pages.Legal.pages.LegalInformations.props.path.replace(
"[legalUid]",
ELegalOptions.POLITIQUE_DE_CONFIDENTIALITE,
)}
/>
concernant lutilisation par LECoffre.io des données à caractère personnel.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LInternaute est invité à se référer aux
<LinkInline
text=" Politiques de Cookies"
href={Module.getInstance()
.get()
.modules.pages.Legal.pages.LegalInformations.props.path.replace(
"[legalUid]",
ELegalOptions.POLITIQUE_DE_GESTION_DES_COOKIES,
)}
/>
concernant lutilisation par LECoffre.io des cookies.
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
9. Responsabilité de LEcoffre.io
</Typography>
</section>
<section>
<Typography typo={ETypo.TEXT_LG_REGULAR} color={ETypoColor.TEXT_ACCENT}>
9.1. Responsabilité générale
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
La responsabilité de LECoffre.io ne pourra pas être retenue en cas de non-compatibilité du système dinformation de
lOffice avec les Services proposés.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Pour des raisons de maintenance, ou du fait de tiers, laccès et le fonctionnement, partiel ou total des Services
LECoffre.io, pourront être suspendus, sans que cette interruption puisse ouvrir droit à une quelconque indemnité au
bénéfice de lOffice ou de tout tiers ou constituer une cause de résiliation.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
En cas dinterruption des Services LECoffre.io, léditeur mettra tous les moyens nécessaires en œuvre pour rétablir le
plus rapidement possible laccès aux services, sauf si linterruption résulte dun défaut qui ne lui est pas imputable.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
La responsabilité de LECoffre.io ne pourra être engagée sur le fondement dune garantie de délai de traitement des
échanges dématérialisés.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LECoffre.io est un prestataire technique mettant en œuvre un processus déchanges et darchivage dématérialisés de
documents et de données.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Dans le cas de traitement danomalies techniques, LECoffre.io peut être amenée à analyser les aspects techniques
contenus dans les flux à lexception de tout autre aspect tenant notamment à la validité de lopération réalisée, aux
effets juridiques liés à lopération, etc.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Dans lhypothèse la responsabilité de LECoffre.io se trouverait engagée, pour quelque cause que ce soit, au titre des
présentes conditions, la responsabilité de LECoffre.io est expressément limitée la sécurisation des données qui lui ont
é transmises par les utilisateurs, professionnels ou non, à lexclusion de tous dommages indirects (notamment dommages
commerciaux et moraux, perte de chiffre daffaires, perte de clientèle) qui seront exclus de la réparation due par
LECoffre.io.
</Typography>
</section>
<section>
<Typography typo={ETypo.TEXT_LG_REGULAR} color={ETypoColor.TEXT_ACCENT}>
9.2. Responsabilité à légard des informations présentes sur le Site
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LEcoffre.io fait ses meilleurs efforts pour proposer sur son Site des informations à jour.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Toutes les informations portées à la connaissance des personnes accédant au Site ont é sélectionnées à une date
déterminée. Compte tenu de la nature et de la complexité des technologies qui sont mises en œuvre, chaque Partie
reconnaît que le Site ne peut être exempte dAnomalies, de défaillances, dinterruptions et dindisponibilités.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
En cas de dysfonctionnement ou dincident de quelque nature que ce soit lors de lutilisation du Site, lInternaute doit
en informer immédiatement LEcoffre.io.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LEcoffre.io ne saurait être tenue responsable de toute erreur ou omission.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Toute mise à jour, nouvelle prestation ou nouvelle caractéristique qui améliore ou augmente un ou plusieurs contenus
dinformations existants sera soumis aux présentes conditions.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Les équipements (notamment ordinateur, téléphone, logiciels, moyens de communication électronique) permettant laccès et
lutilisation au Site sont à la charge exclusive des Internautes, de même que les frais de communications électroniques
(notamment coûts téléphoniques, coûts daccès à Internet) résultant de leur utilisation.
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
10. Responsabilités de lInternaute
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Dans le cadre des présentes CGU, lInternaute a é informé quil devait sassurer :
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
<Dot>du respect des préconisations techniques requises ;</Dot>
<Dot>
quil dispose de la compétence nécessaire et des informations suffisantes pour une utilisation optimale du Site.
</Dot>
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LInternaute demeure intégralement et exclusivement responsable de lexploitation des informations obtenues sur le Site.{" "}
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
De manière générale, chaque Internaute sengage à utiliser le Site :{" "}
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
<Dot>dans le respect des lois, réglementation et droits des tiers ;</Dot>
<Dot>
de manière loyale et conformément à sa destination, dans le respect des présentes CGU et de la Réglementation.
</Dot>
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
11. Obligations de lInternaute
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LInternaute sengage à nutiliser le Site ainsi que lensemble des informations auxquelles il pourra avoir accès que
pour obtenir des informations et dans un but conforme à lordre public, aux bonnes mœurs et aux droits des tiers.{" "}
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Ainsi, il lui est interdit de publier ou de transmettre via le Site tout élément illicite, préjudiciable, diffamatoire,
pornographique, haineux, raciste ou autrement attentatoire à la dignité humaine notamment. Au cas LEcoffre.io serait
informé de la présence de contenus illicites sur le Site, LEcoffre.io serait en droit d'en supprimer immédiatement le
contenu et empêcher laccès au Site à lInternaute.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LInternaute sengage, par ailleurs, à ne pas perturber lusage que pourraient faire les autres Internautes du Site de
ne pas accéder à des Parties du Site dont laccès est réservé.
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
12. Propriété intellectuelle
</Typography>
</section>
<section>
<Typography typo={ETypo.TEXT_LG_REGULAR} color={ETypoColor.TEXT_ACCENT}>
12.1 Propriété intellectuelle de LEcoffre.io
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Le contenu du Site, la structure générale ainsi que les logiciels, textes, images animées ou non, photographies, son
savoir-faire et tous les autres éléments composant le Site sont la propriété exclusive de LEcoffre.io ou des tiers qui
lui ont concédé une licence.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Lensemble des éléments du le Site, sont protégés par le droit dauteur, le droit des marques, des dessins et modèles
et/ou tous autres droits de propriété intellectuelle.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Aucun titre ni droit quelconque sur aucun élément ou logiciel ne sera obtenu par téléchargement ou copie déléments de
ce Site. Il est formellement interdit à lInternaute de reproduire (à part pour son utilisation personnelle et non
commerciale), publier, 7 éditer, transmettre, distribuer, montrer, enlever, supprimer, ajouter à ce Site et aux éléments
et logiciels quils contiennent, pas plus que les modifier ou effectuer un quelconque travail en les prenant pour base,
ni vendre ou participer à aucune vente en rapport avec ce Site, les éléments de ce Site ni aucun logiciel y afférant.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
L'Internaute s'interdit notamment de modifier, copier, reproduire, télécharger, diffuser, transmettre, exploiter
commercialement et/ou distribuer de quelque façon que ce soit les pages du Site, ou les codes informatiques des éléments
composant le Site.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Les présentes conditions générales dutilisation nemportent aucune cession daucune sorte de droit de propriété
intellectuelle sur les éléments appartenant à LEcoffre.io ou ayants droit tels que les sons, photographies, images,
textes littéraires, travaux artistiques, logiciels, marques, chartes graphiques, logos au bénéfice du consommateur.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LEcoffre.io concède à lInternaute une licence non exclusive pour utiliser le Site. Cette licence est strictement
personnelle et ne peut en aucun cas être cédée ou transférée à quel que tiers que ce soit. La licence est concédée pour
la durée dutilisation du Site.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Toute reproduction et/ou représentation, totale ou partielle dun de ces droits, sans lautorisation expresse de
LEcoffre.io, est interdite et constituerait une contrefaçon susceptible d'engager la responsabilité civile et pénale du
contrefacteur.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
En conséquence, l'Internaute s'interdit tout agissement et tout acte susceptible de porter atteinte directement ou non
aux droits de propriété intellectuelle de LEcoffre.io.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Il en est de même des bases de données figurant, le cas échéant, sur le Site qui sont protégées par les articles du Code
de la propriété intellectuelle. Toute extraction ou réutilisation, totale ou partielle desdites bases et de leur contenu
est ainsi interdite sans lautorisation préalable expresse de LEcoffre.io.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Seule une utilisation conforme à la destination de ce Site est autorisée.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Toute autre utilisation, non expressément autorisée par écrit et au préalable par LEcoffre.io, est prohibée et
constitutive de contrefaçon.
</Typography>
</section>
<section>
<Typography typo={ETypo.TEXT_LG_REGULAR} color={ETypoColor.TEXT_ACCENT}>
12.2 Propriété intellectuelle des tiers
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Les éléments appartenant à des tiers, tels quextrait de films, marques, logo, images, textes, sons, sans que cette
liste ne soit exhaustive, sont la propriété exclusive de leur auteur et sont protégés à ce titre par le droit dauteur,
le droit des marques ou tout autre droit reconnu par les lois en vigueur.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
L'Utilisateur s'interdit de porter atteinte, directement ou indirectement, au droit de propriété de tiers, dont les
contenus sont présents sur le Site et s'interdit d'exploiter ces éléments de quelque manière que ce soit.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
L'Utilisateur s'engage à respecter l'intégralité des droits de tiers, dont les contenus sont présents sur le Site.
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
13. Tolérance
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LUtilisateur convient que le fait pour LEcoffre.io de tolérer une situation na pas pour effet daccorder à
lutilisateur des droits acquis.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
De plus, une telle tolérance ne peut être interprétée comme une renonciation à faire valoir les droits en cause.
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
14. Titres
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
En cas de difficultés dinterprétation résultant dune contradiction entre lun quelconque des titres figurant en tête
des clauses et lune quelconque des clauses, les titres seront déclarés inexistants.
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
15. Nullité
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Si une ou plusieurs stipulations des présentes conditions générales dutilisation sont tenues pour non valides ou
déclarées comme telles en application dune loi, dun règlement ou à la suite dune décision passée en force de chose
jugée dune juridiction compétente, les autres stipulations garderont toute leur force et leur portée.
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
16. Domiciliation
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Pour lexécution de la présente convention et sauf dispositions particulières, lInternaute convient dadresser toute
correspondance à ladresse postale de LEcoffre.io : 2, Mail Anne Catherine, 35000 Rennes
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
17. Langues et compétences
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Les présentes Conditions générales dutilisation ont é initialement rédigées en français.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
La version française prévaudra sur toute autre traduction en cas de contestation, litige, difficulté d'interprétation ou
d'exécution des présentes Conditions.
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
18. Litige
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Compte tenu de la dimension mondiale du réseau Internet, nous vous informons qu'il faut vous conformer à toutes les
règles applicables dans le pays dans lequel vous résidez.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Les présentes conditions générales dutilisation du Site sont régies par la loi française.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
En cas de litige lié à linterprétation, la validité et les conséquences des présentes CGU et, à défaut de solution
amiable préalable, les tribunaux de Rennes seront seuls compétents.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Il en est ainsi pour les règles de fond et les règles de forme et ce, nonobstant les lieux dexécution des obligations
substantielles ou accessoires.
</Typography>
</section>
<section>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} color={ETypoColor.TEXT_SECONDARY} italic>
QUESTIONS ? PROBLÈMES ? SUGGESTIONS ?
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Nous vous prions de nous contacter à : (contact@lecoffre.io) ou par courrier à LEcoffre.io, 2, Mail Anne-Catherine
35000 Rennes, France, afin de signaler toute violation des conditions générales dutilisation ou pour poser toute
question concernant les CGU,
<LinkInline
text="Politique de confidentialité"
href={Module.getInstance()
.get()
.modules.pages.Legal.pages.LegalInformations.props.path.replace(
"[legalUid]",
ELegalOptions.POLITIQUE_DE_CONFIDENTIALITE,
)}
/>
et/ou le Site.
</Typography>
</section>
</div>
);
}

View File

@ -0,0 +1,14 @@
.root {
width: 1056px;
padding: 24px;
display: flex;
flex-direction: column;
gap: var(--spacing-xl, 32px);
section {
display: flex;
flex-direction: column;
gap: var(--spacing-md, 16px);
}
}

View File

@ -0,0 +1,48 @@
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import React from "react";
import classes from "./classes.module.scss";
export default function LegalNotice() {
return (
<div className={classes["root"]}>
<section>
<Typography typo={ETypo.TITLE_H4} color={ETypoColor.TEXT_PRIMARY}>
Mentions Légales
</Typography>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} color={ETypoColor.TEXT_SECONDARY} italic>
Version en vigueur à compter du 10 avril 2024
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Léditeur du Site est SMARTCHAIN, dont le siège social est situé au 23 RUE TAITBOUT, 75009 PARIS 9 avec un capital
social de 10 000,00 Euros.
<br />
N° RCS : 851 103 267 R.C.S. Paris
<br />
Tel : 06 63 84 83 45
<br />
N° individuel / didentification de TVA : FR56851103267
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Directeur de la publication : Konstantin Grouzdev
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Hébergement du Site : SMARTCHAIN
<br />
Adresse : 23 RUE TAITBOUT, 75009 PARIS 9
<br />
Représentée par Konstantin Grouzdev - Téléphone 06 63 84 83 45 - Site Internet
<br />
<a href="https://www.lecoffre.io/" target="_blank" rel="noreferrer">
<Typography type="span" typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_ACCENT}>
https://www.lecoffre.io/
</Typography>
</a>
</Typography>
</section>
</div>
);
}

View File

@ -0,0 +1,14 @@
.root {
width: 1056px;
padding: 24px;
display: flex;
flex-direction: column;
gap: var(--spacing-xl, 32px);
section {
display: flex;
flex-direction: column;
gap: var(--spacing-md, 16px);
}
}

View File

@ -0,0 +1,376 @@
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import Dot from "@Front/Components/Layouts/Legal/LegalInformations/elements/Dot";
import React from "react";
import classes from "./classes.module.scss";
import Module from "@Front/Config/Module";
import { ELegalOptions } from "@Front/Components/LayoutTemplates/DefaultLegalDashboard";
import LinkInline from "../elements/LinkInline";
export default function PrivacyPolicy() {
return (
<div className={classes["root"]}>
<section>
<Typography typo={ETypo.TITLE_H4} color={ETypoColor.TEXT_PRIMARY}>
Politique de confidentialité
</Typography>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} color={ETypoColor.TEXT_SECONDARY} italic>
Version en vigueur à compter du 24 avril 2024
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Les informations suivantes vous sont communiquées afin que vous puissiez prendre connaissance des engagements en matière
de protection des données à caractère personnel de LEcoffre.io, qui agit en tant que responsable du traitement pour les
traitements de données à caractère personnel évoqués dans le présent document.
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
1. Les données à caractère personnel que nous traitons
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Dans le cadre des traitements de données à caractère personnel, LEcoffre.io collecte et traite les données suivantes en
qualité de responsable du traitement :
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
<Dot>Données d'identité (nom, prénom), d'identification (courriel et numéro de téléphone);</Dot>
<Dot>Données de paiement;</Dot>
<Dot>Données de connexion et de navigation.</Dot>
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Il peut également arriver que des Internautes communiquent dautres données en contactant LEcoffre.io à laide de son
formulaire de contact.
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
2. Les finalités de nos traitements
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Les traitements que nous mettons en œuvre le sont pour assurer les finalités suivantes :
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
<Dot>répondre à vos demandes de contacts;</Dot>
<Dot>la gestion des demandes de droit d'accès, de suppression, de rectification et d'opposition;</Dot>
<Dot>vous permettre daccéder au Site et à la plateforme LECoffre.io;</Dot>
<Dot>vous envoyer par courriel nos offres relatives aux Services, des actualités et bonnes pratiques.</Dot>
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
3. Les fondements juridiques de nos traitements
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Nous ne mettons en œuvre des traitements de données que si au moins lune des conditions suivantes est remplie :
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
<Dot>votre consentement aux opérations de traitement a é recueilli;</Dot>
<Dot>
lexistence de notre intérêt légitime, ou de celui dun tiers, qui justifie que nous mettions en œuvre le traitement
de données à caractère personnel concerné;
</Dot>
<Dot>
lexécution dun contrat qui nous lie à vous nécessite que nous mettions en œuvre le traitement de données à
caractère personnel concerné.
</Dot>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Les intérêts légitimes qui sont poursuivis par LEcoffre.io peuvent notamment consister dans le management de son
activité, le suivi de ses contrats et la réalisation de ses prestations.
</Typography>
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
4. Les destinataires de vos données
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Les données à caractère personnel que nous collectons, de même que celles qui sont recueillies ultérieurement, nous sont
destinées en notre qualité de responsable du traitement.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Nous veillons à ce que seules les personnes habilitées puissent avoir accès à ces données. Nos prestataires de services
peuvent être destinataires de ces données pour réaliser les prestations que nous leur confions. Certaines données
personnelles peuvent être adressées à des tiers ou à des autorités légalement habilitées et ce pour satisfaire nos
obligations légales, réglementaires ou conventionnelles.
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
5. La durée de conservation de vos données
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Les durées de conservation que nous appliquons à vos données à caractère personnel sont proportionnées aux finalités
pour lesquelles elles ont é collectées. En conséquence, nous organisons notre politique de conservation des données de
la manière suivante :
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
<u>Durée de conservation des données techniques</u>
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Les données techniques sont conservées pour la durée strictement nécessaire à la réalisation des finalités visées dans
la présente politique de confidentialité.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
<u>Conservation des données pendant la durée de la relation contractuelle</u>
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Les données à caractère personnel faisant l'objet d'un traitement ne sont pas conservées au-delà du temps nécessaire à
l'exécution des obligations définies lors de la conclusion du contrat ou de la durée prédéfinie de la relation
contractuelle.
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
6. La sécurité de vos données
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LEcoffre.io a mis en œuvre des mesures pour protéger la confidentialité, la sécurité et lintégrité de vos données à
caractère personnel. Laccès aux données à caractère personnel est restreint aux employés et fournisseurs de service qui
ont besoin de connaître ces informations et qui ont é formés pour se conformer aux règles en matière de
confidentialité.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LEcoffre.io sengage à ce que vos données à caractère personnel ne soient pas altérées ou endommagées et que des tiers
non autorisés ny aient pas accès.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Nous disposons des mesures de protection physiques, techniques, électroniques, procédurales et organisationnelles
appropriées pour protéger les informations personnelles recueillies contre tout traitement non autorisé.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Toutefois, LEcoffre.io ne saurait vous garantir contre toute perte, destruction ou dommages de vos données à caractère
personnel.
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
7. Les droits qui vous sont reconnus
</Typography>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} color={ETypoColor.TEXT_SECONDARY}>
Modalités dexercice de vos droits
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Vous pouvez exercer vos droits par voie électronique à ladresse suivante (contact@lecoffre.io) ou par courrier postal à
ladresse LEcoffre.io - 2, Mail Anne-Catherine 35000 Rennes, en justifiant de votre identité.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Pour ce faire, vous devez indiquer clairement vos nom(s) et prénom(s), ladresse à laquelle vous souhaitez que la
réponse vous soit envoyée et y joindre la photocopie dun titre didentité portant votre signature.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Par principe, vous pouvez exercer sans frais lensemble de vos droits. Cependant en matière de droit daccès, il pourra
vous être demandé le paiement de frais raisonnables basés sur les coûts administratifs pour toute copie des données que
vous demanderez.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Concernant le droit dinformation, LEcoffre.io naura pas lobligation dy donner suite lorsque vous disposez déjà des
informations dont vous sollicitez la communication.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LEcoffre.io vous informera si elle ne peut donner suite à vos demandes.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
LEcoffre.io tient à vous informer que le non-renseignement ou la modification de vos données sont susceptibles davoir
des conséquences dans le traitement de certaines demandes dans le cadre de lexécution des relations contractuelles et
que votre demande au titre de lexercice de vos droits sera conservée à des fins de suivi.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Lensemble des droits dont vous bénéficiez est détaillé ci-dessous :
</Typography>
</section>
<section>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} color={ETypoColor.TEXT_SECONDARY}>
Droit à linformation
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Vous pouvez exercer vos droits par voie électronique à ladresse suivante (contact@lecoffre.io) ou par courrier postal à
ladresse LEcoffre.io - 2, Mail Anne-Catherine 35000 Rennes, en justifiant de votre identité.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Si nous décidons de traiter des données pour des finalités autres que celles indiquées, toutes les informations
relatives à ces nouvelles finalités vous seront communiquées.
</Typography>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} color={ETypoColor.TEXT_SECONDARY}>
Droit daccès et à la rectification de vos données
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Vous disposez du droit daccéder et de faire rectifier vos données personnelles, que vous pouvez exercer auprès de
LEcoffre.io par voie électronique à ladresse
<LinkInline text="contact@lecoffre.io" href="mailto:contact@lecoffre.io" />
ou par courrier postal à ladresse LEcoffre.io 2, Mail Anne-Catherine 35000 Rennes.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
A ce titre, vous avez la confirmation que vos données personnelles sont ou ne sont pas traitées et lorsquelles le sont,
et disposez de laccès à vos données ainsi quaux informations concernant :
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
<Dot>les finalités du traitement ;</Dot>
<Dot>les catégories de données personnelles concernées ;</Dot>
<Dot>
les destinataires ou catégories de destinataires ainsi que les organisations internationales auxquels les données
personnelles ont é ou seront communiquées, en particulier les destinataires qui sont établis dans des pays tiers ;
</Dot>
<Dot>
lorsque cela est possible, la durée de conservation des données personnelles envisagée ou, lorsque ce nest pas
possible, les critères utilisés pour déterminer cette durée ;
</Dot>
<Dot>
lexistence du droit de demander au responsable du traitement la rectification ou leffacement de vos données
personnelles, du droit de demander une limitation du traitement de vos données personnelles, du droit de vous
opposer à ce traitement ;
</Dot>
<Dot>le droit dintroduire une réclamation auprès dune autorité de contrôle ;</Dot>
<Dot>
des informations relatives à la source des données quand elles ne sont pas collectées directement auprès des
personnes concernées ;
</Dot>
<Dot>
lexistence dune prise de décision automatisée, y compris de profilage, et dans ce dernier cas, des informations
utiles concernant la logique sous-jacente, ainsi que limportance et les conséquences prévues de ce traitement pour
les personnes concernées.
</Dot>
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Vous pouvez nous demander que vos données personnelles soient, selon les cas, rectifiées, complétées si elles sont
inexactes, incomplètes, équivoques, périmées.
</Typography>
</section>
<section>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} color={ETypoColor.TEXT_SECONDARY}>
Droit à leffacement de vos données
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Vous pouvez nous demander leffacement de vos données personnelles lorsque lun des motifs suivants sapplique :
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
<Dot>
les données personnelles ne sont plus nécessaires au regard des finalités pour lesquelles elles ont é collectées
ou traitées dune autre manière ;
</Dot>
<Dot>vous retirez le consentement préalablement donné ;</Dot>
<Dot>
vous vous opposez au traitement de vos données personnelles lorsquil nexiste pas de motif légal audit traitement ;
</Dot>
<Dot>
le traitement de données personnelles nest pas conforme aux dispositions de la législation et de la réglementation
applicable ;
</Dot>
<Dot>
vos données personnelles ont é collectées dans le cadre de loffre de services de la société de linformation aux
enfants âgés de moins de 16 ans.
</Dot>
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Néanmoins, lexercice de ce droit ne sera pas possible lorsque la conservation de vos données personnelles est
nécessaire au regard de la législation ou de la réglementation et notamment par exemple pour la constatation, lexercice
ou la défense de droits en justice.
</Typography>
</section>
<section>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} color={ETypoColor.TEXT_SECONDARY}>
Droit à la limitation des traitements de données
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Vous pouvez demander la limitation du traitement de vos données personnelles dans les cas prévus par la législation et
la réglementation.
</Typography>
</section>
<section>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} color={ETypoColor.TEXT_SECONDARY}>
Droit de vous opposer aux traitements de données
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Vous avez le droit de vous opposer à un traitement de données personnelles vous concernant lorsque le traitement est
fondé sur lintérêt légitime du responsable du traitement.
</Typography>
</section>
<section>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} color={ETypoColor.TEXT_SECONDARY}>
Droit à la portabilité de vos données
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Vous disposez du droit à la portabilité de vos données personnelles, lorsque :
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
<Dot>
le traitement est fondé sur le consentement en application de larticle 6, paragraphe 1, point a), ou de larticle
9, paragraphe 2, point a), ou sur un contrat en application de larticle 6, paragraphe 1, point b) du RGPD ;
</Dot>
<Dot>le traitement est effectué à l'aide de procédés automatisés.</Dot>
</Typography>
</section>
<section>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} color={ETypoColor.TEXT_SECONDARY}>
Droit de retirer votre consentement
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Lorsque les traitements de données que nous mettons en œuvre sont fondés sur votre consentement, vous pouvez le retirer
à nimporte quel moment. Nous cessons alors de traiter vos données à caractère personnel sans que les opérations
antérieures pour lesquelles vous aviez consenti ne soient remises en cause.
</Typography>
</section>
<section>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} color={ETypoColor.TEXT_SECONDARY}>
Droit dintroduire un recours
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Si vous considérez que LEcoffre.io ne respecte pas ses obligations au regard de vos données à caractère personnel, vous
pouvez adresser une plainte ou une demande auprès de lautorité compétente. En France, lautorité compétente est la Cnil
à laquelle vous pouvez adresser une demande par voie électronique en cliquant sur le lien suivant{" "}
<LinkInline text="https://www.cnil.fr/fr/plaintes/internet" href="https://www.cnil.fr/fr/plaintes/internet" />
</Typography>
</section>
<section>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} color={ETypoColor.TEXT_SECONDARY}>
Droit de définir des directives post-mortem
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Vous avez la possibilité de définir des directives relatives à la conservation, à leffacement et à la communication de
vos données personnelles après votre décès et ce auprès dun tiers de confiance, certifié et chargé de faire respecter
la volonté du défunt, conformément aux exigences du cadre juridique applicable.
</Typography>
</section>
<section>
<Typography typo={ETypo.TITLE_H6} color={ETypoColor.TEXT_ACCENT}>
8. Cookies
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
En naviguant sur notre site, des cookies sont déposés sur votre terminal, soit directement, soit après avoir recueilli
votre consentement lorsque la réglementation en matière de cookies le requiert.
</Typography>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
Vous êtes invité à vous référer aux
<LinkInline
text="Politiques cookies"
href={Module.getInstance()
.get()
.modules.pages.Legal.pages.LegalInformations.props.path.replace(
"[legalUid]",
ELegalOptions.POLITIQUE_DE_GESTION_DES_COOKIES,
)}
/>
concernant lutilisation par LECoffre.io des cookies.
</Typography>
</section>
</div>
);
}

View File

@ -0,0 +1,10 @@
import React from "react";
export default function Dot({ children }: { children: React.ReactNode }) {
return (
<>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{children}
<br />
</>
);
}

View File

@ -0,0 +1,13 @@
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import React from "react";
export default function LinkInline({ text, href }: { text: string; href: string }) {
return (
<a href={href} target="_blank" rel="noreferrer">
<Typography type="span" typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.TEXT_ACCENT}>
{" "}
{text}{" "}
</Typography>
</a>
);
}

View File

@ -1,9 +1,13 @@
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import DefaultLegalDashboard, { ELegalOptions } from "@Front/Components/LayoutTemplates/DefaultLegalDashboard";
import Link from "next/link";
import { useRouter } from "next/router";
import React from "react";
import CGU from "./CGU";
import classes from "./classes.module.scss";
import DefaultLegalDashboard, { ELegalOptions } from "@Front/Components/LayoutTemplates/DefaultLegalDashboard";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import Link from "next/link";
import LegalNotice from "./LegalNotice";
import PrivacyPolicy from "./PrivacyPolicy";
const pdfLinks: Record<ELegalOptions, string> = {
"mentions-legales": "https://s3.fr-par.scw.cloud/lecoffre.io-bucket/footer/mentions_legales.pdf",
@ -16,19 +20,45 @@ const pdfLinks: Record<ELegalOptions, string> = {
export default function LegalInformations() {
const router = useRouter();
let { legalUid } = router.query;
const legalUidTyped = legalUid as ELegalOptions;
const legalType = legalUid as ELegalOptions;
return <DefaultLegalDashboard>{getLegalInformationContent(legalType)}</DefaultLegalDashboard>;
}
function getLegalInformationContent(legalType: ELegalOptions) {
switch (legalType) {
case ELegalOptions.LEGAL_MENTIONS:
return <LegalNotice />;
case ELegalOptions.CGU:
return <CGU />;
case ELegalOptions.CGS:
return (
<DefaultLegalDashboard>
<object data={pdfLinks[legalUidTyped]} type="application/pdf" width="100%" height="100%" className={classes["pdf-viewer"]}>
<object data={pdfLinks[legalType]} type="application/pdf" width="100%" height="100%" className={classes["pdf-viewer"]}>
<Typography typo={ETypo.TEXT_LG_LIGHT} className={classes["no-pdf-text"]}>
Votre navigateur ne prend pas en charge l'affichage des pdf&nbsp;
<Typography typo={ETypo.TEXT_LG_LIGHT} type="span">
<Link href={pdfLinks[legalUidTyped]} target="_blank" style={{ color: `var(${ETypoColor.COLOR_PRIMARY_500})` }}>
<Link href={pdfLinks[legalType]} target="_blank" style={{ color: `var(${ETypoColor.COLOR_PRIMARY_500})` }}>
cliquez ici pour télécharger le pdf.
</Link>
</Typography>
</Typography>
</object>
</DefaultLegalDashboard>
);
case ELegalOptions.POLITIQUE_DE_CONFIDENTIALITE:
return <PrivacyPolicy />;
case ELegalOptions.POLITIQUE_DE_GESTION_DES_COOKIES:
return (
<object data={pdfLinks[legalType]} type="application/pdf" width="100%" height="100%" className={classes["pdf-viewer"]}>
<Typography typo={ETypo.TEXT_LG_LIGHT} className={classes["no-pdf-text"]}>
Votre navigateur ne prend pas en charge l'affichage des pdf&nbsp;
<Typography typo={ETypo.TEXT_LG_LIGHT} type="span">
<Link href={pdfLinks[legalType]} target="_blank" style={{ color: `var(${ETypoColor.COLOR_PRIMARY_500})` }}>
cliquez ici pour télécharger le pdf.
</Link>
</Typography>
</Typography>
</object>
);
default:
return null;
}
}

View File

@ -1,29 +1,22 @@
@import "@Themes/constants.scss";
.root {
margin: 80px auto;
width: 474px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 100%;
max-width: 530px;
margin: auto;
gap: var(--spacing-xl, 32px);
.title-container {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.title {
margin: 32px 0;
text-align: center;
@media (max-width: $screen-s) {
font-family: 48px;
}
}
.loader {
width: 32px;
height: 32px;
}
.forget-password {
margin-top: 32px;
margin-bottom: 8px;
width: 100%;
margin: auto;
padding: var(--spacing-md, 16px);
}
}

Some files were not shown because too many files have changed in this diff Show More