deploy v2.5 (#184)

Co-authored-by: Vins <vincent.alamelle@smart-chain.fr>
This commit is contained in:
Maxime Sallerin 2024-09-23 15:05:33 +02:00 committed by GitHub
parent 3ce0ec1a62
commit 9d83616c14
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
120 changed files with 5510 additions and 2553 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);
}
}
.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);
@media screen and (max-width: 680px) {
flex-direction: column;
button {
width: 100%;
}
}
}
}
&.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
@ -59,4 +61,4 @@ export default function CircleProgress(props: IProps) {
</Typography>
</div>
);
}
}

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

@ -24,7 +24,6 @@
word-wrap: break-word;
.content {
max-width: 270px;
width: 100%;
word-break: break-word;
}

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) {
@ -54,7 +55,13 @@ export default function MuiTable(props: IProps) {
className={classes["root"]}
sx={{ maxHeight: "80vh", overflowY: "auto", overflowX: "hidden", backgroundColor: "var(--table-background-default)" }}>
<Table aria-label="simple table" sx={{ border: "0" }}>
<TableHead sx={{ position: "sticky", top: "0", borderBottom: "1px solid var(--table-header-border)" }}>
<TableHead
sx={{
position: "sticky",
top: "0",
borderBottom: "1px solid var(--table-header-border)",
backgroundColor: "var(--table-background-default)",
}}>
<TableRow>
{props.header.map((column) => (
<TableCell key={column.key} align={"left"} sx={{ border: 0, padding: "4px 8px" }}>
@ -82,12 +89,15 @@ 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}
color={ETypoColor.COLOR_NEUTRAL_900}
>
{cell.value && typeof cell.value === "object" && "content" in cell.value
? cell.value.content
: cell.value}
</Typography>
</TableCell>
))}
@ -99,4 +109,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;
flex-direction: column;
gap: var(--spacing-xl, 32px);
.title-container {
flex-direction: column;
display: flex;
padding: 64px;
justify-content: space-between;
gap: var(--spacing-sm, 8px);
@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 {
.office-container {
display: flex;
gap: 20px;
}
.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;
align-items: center;
gap: var(--spacing-md, 16px);
}
}
.sub-container {
background-color: var(--color-neutral-50);
padding: 64px;
.content {
display: flex;
gap: var(--spacing-lg, 24px);
align-items: flex-start;
.content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 32px;
margin-bottom: 64px;
@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,66 +57,73 @@ 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);
const query: IGetDocumentsparams = {
where: { depositor: { uid: actualCustomer.uid }, folder_uid: folderUid as string },
include: {
files: true,
document_history: true,
document_type: true,
depositor: true,
folder: {
include: {
customers: {
include: {
contact: true,
return { folder, customer };
}, [folderUid]);
const fetchDocuments = useCallback(
(customerUid: string | undefined) => {
const query: IGetDocumentsparams = {
where: { depositor: { uid: customerUid }, folder_uid: folderUid as string },
include: {
files: true,
document_history: true,
document_type: true,
depositor: true,
folder: {
include: {
customers: {
include: {
contact: true,
},
},
},
},
},
};
return Documents.getInstance()
.get(query)
.then((documents) => setDocuments(documents));
},
[folderUid],
);
useEffect(() => {
fetchFolderAndCustomer().then(({ customer }) => fetchDocuments(customer.uid));
}, [fetchDocuments, fetchFolderAndCustomer]);
const notaryContact = useMemo(
() =>
folder?.stakeholders!.find((stakeholder) => stakeholder.office_role?.name === "Collaborateur")?.contact ??
folder?.stakeholders![0]!.contact,
[folder],
);
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 documentList = await Documents.getInstance().get(query);
//const folder = await Folders.getInstance().getByUid(folderUid as string, { q: { office: true, customers: true } });
setFolder(folder);
setDocuments(documentList);
setCustomer(actualCustomer);
setNote(note);
}, [folderUid]);
const onCloseModalAddDocument = useCallback(() => {
setIsAddDocumentModalVisible(false);
getDocuments();
}, [getDocuments]);
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]);
useEffect(() => {
getDocuments();
}, [folderUid, getDocuments]);
const renderHeader = useCallback(() => {
return (
<div className={classes["header"]}>
<div className={classes["text-container"]}>
{/* TODO Get name from userStore */}
<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}>
Dossier {folder?.folder_number} - {folder?.name}
</Typography>
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} className={classes["office-name"]} color={ETypoColor.COLOR_NEUTRAL_500}>
{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>
</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
</Button>
)}
</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]);
}, [ribUrl]);
return (
<DefaultTemplate title={"Mon compte"} isPadding={false} hasHeaderLinks={false}>
<DefaultCustomerDashboard>
<div className={classes["root"]}>
{renderHeader()}
<div className={classes["sub-container"]}>
<div className={classes["content"]}>
<div className={classes["title-container"]}>
<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_MD_REGULAR} color={ETypoColor.TEXT_ACCENT}>
{folder?.office?.name}
</Typography>
</div>
</div>
<div className={classes["content"]}>
<div className={classes["notary"]}>
<Typography typo={ETypo.TEXT_LG_BOLD} color={ETypoColor.TABLE_COLUMN_CONTRAST}>
Votre Notaire
</Typography>
{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 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,78 +88,146 @@ export default function DesignSystem() {
return (
<DefaultTemplate title={"DesignSystem"}>
<Typography typo={ETypo.DISPLAY_LARGE}>DesignSystem</Typography>
<Newsletter isOpen={false} />
<div className={classes["root"]}>
<div />
<div className={classes["rows"]}>
<CheckboxesInputElement
option={{
label: "Default",
value: "all",
description: "Description",
}}
toolTip="test"
/>
<CheckboxesInputElement
option={{
label: "Checked",
value: "all",
description: "Description",
}}
toolTip="test"
checked={true}
/>
<CheckboxesInputElement
option={{
label: "Disabled",
value: "all",
description: "Description",
}}
toolTip="test"
disabled={true}
/>
<CheckboxesInputElement
option={{
label: "Checked & Disabled",
value: "all",
description: "Description",
}}
toolTip="test"
checked={true}
disabled={true}
/>
</div>
<div className={classes["rows"]}>
<RadioBox name="document" value={"new client"} description="Test" label="Créer un document" toolTip="test" />
<RadioBox
name="document"
value={"new client"}
description="Test"
label="Créer un document"
toolTip="test"
defaultChecked={true}
/>
<RadioBox
name="document"
value={"new client"}
description="Test"
label="Créer un document"
toolTip="test"
disabled={true}
/>
<RadioBox
name="document"
value={"new client"}
description="Test"
label="Créer un document"
toolTip="test"
checked={true}
disabled={true}
/>
</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={{
label: "Default",
value: "all",
description: "Description",
}}
toolTip="test"
/>
<CheckboxesInputElement
option={{
label: "Checked",
value: "all",
description: "Description",
}}
toolTip="test"
checked={true}
/>
<CheckboxesInputElement
option={{
label: "Disabled",
value: "all",
description: "Description",
}}
toolTip="test"
disabled={true}
/>
<CheckboxesInputElement
option={{
label: "Checked & Disabled",
value: "all",
description: "Description",
}}
toolTip="test"
checked={true}
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
name="document"
value={"new client"}
description="Test"
label="Créer un document"
toolTip="test"
defaultChecked={true}
/>
<RadioBox
name="document"
value={"new client"}
description="Test"
label="Créer un document"
toolTip="test"
disabled={true}
/>
<RadioBox
name="document"
value={"new client"}
description="Test"
label="Créer un document"
toolTip="test"
checked={true}
disabled={true}
/>
</div>
<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,182 @@
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();
}, [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: { sx: { width: 220 }, content: formatDateWithHours(reminder.reminder_date) },
customer: {
sx: { width: 220 },
content: `${reminder.document?.depositor?.contact?.first_name} ${reminder.document?.depositor?.contact?.last_name}`,
},
document_type: reminder.document?.document_type?.name,
statut: { sx: { width: 220 }, content: 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} />;
}
}
function formatDateWithHours(date: Date | null) {
if (!date) return "-";
return new Date(date).toLocaleDateString("fr-FR", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
}

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: (
<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)} />} />,
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()}
/>
),
},
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,24 +167,35 @@ 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: (
<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: (
<Link
href={Module.getInstance()
.get()
.modules.pages.Folder.pages.ViewDocuments.props.path.replace("[folderUid]", folderUid)
.replace("[documentUid]", document.uid ?? "")}>
<IconButton icon={<EyeIcon />} />
</Link>
),
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()}
/>
),
},
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()
.modules.pages.Folder.pages.ViewDocuments.props.path.replace("[folderUid]", folderUid)
.replace("[documentUid]", document.uid ?? "")}>
<IconButton icon={<EyeIcon />} />
</Link>
</div>
),
},
};
})
.filter((document) => document !== null) as IRowProps[],
@ -145,27 +209,36 @@ 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: (
<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: (
<div className={classes["actions"]}>
<Link
href={Module.getInstance()
.get()
.modules.pages.Folder.pages.ViewDocuments.props.path.replace("[folderUid]", folderUid)
.replace("[documentUid]", document.uid ?? "")}>
<IconButton icon={<EyeIcon />} />
</Link>
<IconButton onClick={() => onDownload(document)} icon={<ArrowDownTrayIcon />} />
</div>
),
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()}
/>
),
},
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()
.modules.pages.Folder.pages.ViewDocuments.props.path.replace("[folderUid]", folderUid)
.replace("[documentUid]", document.uid ?? "")}>
<IconButton icon={<EyeIcon />} />
</Link>
<IconButton onClick={() => onDownload(document)} icon={<ArrowDownTrayIcon />} />
</div>
),
},
};
})
.filter((document) => document !== null) as IRowProps[],
@ -179,28 +252,69 @@ 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: (
<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() : "_",
actions: "",
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()}
/>
),
},
date: {
sx: { width: 107 },
content: document.updated_at ? new Date(document.updated_at).toLocaleDateString() : "_",
},
actions: { sx: { width: 76 }, content: "" },
};
})
.filter((document) => document !== null) as IRowProps[],
[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]);
if (documents.length === 0 && documentsNotary.length === 0) return <NoDocument />;
return (
<div className={classes["root"]}>
<div className={classes["title"]}>
@ -209,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 && (
<DeleteAskedDocumentModal
isOpen={deleteAskedOocumentModal.isOpen}
onClose={deleteAskedOocumentModal.close}
onDeleteSuccess={(uid: string) => setDocuments(documents.filter((document) => document.uid !== uid))}
documentUid={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={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,117 @@
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 ? formatDateWithHours(reminders[0]!.reminder_date) : "-"}
</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>
);
}
function formatDateWithHours(date: Date | null) {
if (!date) return "-";
return new Date(date).toLocaleDateString("fr-FR", {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
}

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,23 +100,23 @@ export default function ClientView(props: IProps) {
onDelete={handleClientDelete}
customerNote={folder.notes!.find((value) => value.customer?.uid === customer.uid) ?? null}
/>
{anchorStatus === AnchorStatus.NOT_ANCHORED && (
<Link
href={Module.getInstance()
.get()
.modules.pages.Folder.pages.AskDocument.props.path.replace("[folderUid]", folder.uid ?? "")
.replace("[customerUid]", customer.uid ?? "")}>
<Button rightIcon={<DocumentIcon />} variant={EButtonVariant.PRIMARY} fullwidth>
Demander un document
</Button>
</Link>
)}
<div className={classes["button-container"]}>
{anchorStatus === AnchorStatus.NOT_ANCHORED && (
<Link
href={Module.getInstance()
.get()
.modules.pages.Folder.pages.AskDocument.props.path.replace("[folderUid]", folder.uid ?? "")
.replace("[customerUid]", customer.uid ?? "")}>
<Button rightIcon={<DocumentIcon />} variant={EButtonVariant.PRIMARY} fullwidth>
Demander un document
</Button>
</Link>
)}
<EmailReminder customer={customer} isAnchored={anchorStatus !== AnchorStatus.NOT_ANCHORED} />
</div>
</div>
{doesCustomerHaveDocument ? (
<DocumentTables documents={customer.documents ?? []} folderUid={folder?.uid ?? ""} />
) : (
<NoDocument />
)}
{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,22 +45,74 @@
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;
.icon-container {
display: flex;
gap: var(--spacing-md, 8px);
@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;
}
}
.description-container {
.text {
max-height: 60px;
overflow-y: auto;
&.mobile {
display: none;
@media screen and (max-width: $mobile-breakpoint) {
display: flex;
}
}
}
.description-container {
.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>
<Typography typo={ETypo.TEXT_MD_REGULAR}>{folder?.folder_number}</Typography>
<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,57 +1,37 @@
@import "@Themes/constants.scss";
.root {
margin: 24px auto;
width: 566px;
@media (max-width: $screen-m) {
width: 474px;
}
@media (max-width: $screen-s) {
width: 100%;
padding: var(--spacing-md, 16px);
}
display: flex;
flex-direction: column;
min-height: 100%;
align-items: flex-start;
width: fit-content;
.back-arrow {
margin-bottom: 24px;
}
gap: var(--spacing-xl, 32px);
.form {
width: 100%;
display: flex;
flex-direction: column;
gap: var(--spacing-md, 16px);
}
.content {
margin-top: 32px;
.button-container {
display: flex;
gap: var(--spacing-md, 16px);
>:not(:last-child) {
margin-bottom: 24px;
}
}
.button-container {
width: 100%;
display: flex;
text-align: center;
margin-top: 24px;
.cancel-button {
display: flex;
margin-right: 32px;
}
@media (max-width: $screen-m) {
display: flex;
flex-direction: column-reverse;
.cancel-button {
margin-left: 0;
margin-top: 12px;
>* {
flex: 1;
}
}
>* {
width: 100%;
}
@media (max-width: $screen-xs) {
button {
width: 100%;
}
flex-direction: column-reverse;
}
}
}
}

View File

@ -1,282 +1,179 @@
import backgroundImage from "@Assets/images/background_refonte.svg";
import Customers from "@Front/Api/LeCoffreApi/Notary/Customers/Customers";
import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import Form from "@Front/Components/DesignSystem/Form";
import Form, { IBaseField } from "@Front/Components/DesignSystem/Form";
import TextField from "@Front/Components/DesignSystem/Form/TextField";
import Confirm from "@Front/Components/DesignSystem/OldModal/Confirm";
import Modal from "@Front/Components/DesignSystem/Modal";
import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography";
import BackArrow from "@Front/Components/Elements/BackArrow";
import DefaultNotaryDashboard from "@Front/Components/LayoutTemplates/DefaultNotaryDashboard";
import DefaultDoubleSidePage from "@Front/Components/LayoutTemplates/DefaultDoubleSidePage";
import Module from "@Front/Config/Module";
import { Contact, Customer, OfficeFolder } from "le-coffre-resources/dist/Notary";
import Link from "next/link";
import { NextRouter, useRouter } from "next/router";
import { ChangeEvent } from "react";
import BasePage from "../../Base";
import classes from "./classes.module.scss";
import { Address } from "le-coffre-resources/dist/Customer";
import useOpenable from "@Front/Hooks/useOpenable";
import { ValidationError } from "class-validator";
import { Address } from "le-coffre-resources/dist/Customer";
import { Contact, Customer } from "le-coffre-resources/dist/Notary";
import Link from "next/link";
import { useRouter } from "next/router";
import { useCallback, useEffect, useState } from "react";
type IProps = {};
import classes from "./classes.module.scss";
type IPropsClass = IProps & {
selectedFolderUid: string;
router: NextRouter;
customerUid: string;
};
type IState = {
selectedFolder: OfficeFolder | null;
inputNameValue: string;
inputFirstNameValue: string;
inputEmailValue: string;
inputPhoneNumberValue: string;
isOpenLeavingModal: boolean;
doesInputHaveValues: boolean;
inputBirthdate: Date | null;
inputAddress: string;
folder: OfficeFolder | null;
customer: Customer | null;
validationError: ValidationError[];
};
class UpdateClientClass extends BasePage<IPropsClass, IState> {
constructor(props: IPropsClass) {
super(props);
this.state = {
selectedFolder: null,
inputNameValue: "",
inputFirstNameValue: "",
inputEmailValue: "",
inputPhoneNumberValue: "",
isOpenLeavingModal: false,
doesInputHaveValues: false,
inputBirthdate: null,
inputAddress: "",
folder: null,
customer: null,
validationError: [],
};
this.onSelectedFolder = this.onSelectedFolder.bind(this);
this.onChangeNameInput = this.onChangeNameInput.bind(this);
this.onChangeFirstNameInput = this.onChangeFirstNameInput.bind(this);
this.onChangeEmailInput = this.onChangeEmailInput.bind(this);
this.onChangePhoneNumberInput = this.onChangePhoneNumberInput.bind(this);
this.openLeavingModal = this.openLeavingModal.bind(this);
this.closeLeavingModal = this.closeLeavingModal.bind(this);
this.leavePage = this.leavePage.bind(this);
this.onChangeBirthDateInput = this.onChangeBirthDateInput.bind(this);
this.onChangeAddressInput = this.onChangeAddressInput.bind(this);
this.onFormSubmit = this.onFormSubmit.bind(this);
}
export default function UpdateClient() {
const router = useRouter();
const { folderUid, customerUid } = router.query;
private backwardPath = Module.getInstance()
const [doesInputHasChanged, setDoesInputHasChanged] = useState<boolean>(false);
const [customer, setCustomer] = useState<Customer | null>(null);
const [validationError, setValidationError] = useState<ValidationError[]>([]);
const { isOpen, open, close } = useOpenable();
const backwardPath = Module.getInstance()
.get()
.modules.pages.Folder.pages.FolderInformation.props.path.replace("[folderUid]", this.props.selectedFolderUid);
.modules.pages.Folder.pages.FolderInformation.props.path.replace("[folderUid]", folderUid as string);
public override render(): JSX.Element {
return (
<DefaultNotaryDashboard title={"Ajouter client(s)"}>
<div className={classes["root"]}>
<div className={classes["back-arrow"]}>
<BackArrow url={this.backwardPath} />
</div>
<Typography typo={ETypo.TITLE_H1}>Modifier les informations du client</Typography>
<Form className={classes["form"]} onSubmit={this.onFormSubmit}>
<div className={classes["content"]}>
<TextField
name="first_name"
placeholder="Prénom"
onChange={this.onChangeNameInput}
defaultValue={this.state.customer?.contact?.first_name}
validationError={this.state.validationError.find((error) => error.property === "first_name")}
/>
<TextField
name="last_name"
placeholder="Nom"
onChange={this.onChangeFirstNameInput}
defaultValue={this.state.customer?.contact?.last_name}
validationError={this.state.validationError.find((error) => error.property === "last_name")}
/>
<TextField
name="email"
placeholder="E-mail"
onChange={this.onChangeEmailInput}
defaultValue={this.state.customer?.contact?.email}
validationError={this.state.validationError.find((error) => error.property === "email")}
/>
<TextField
name="cell_phone_number"
placeholder="Numéro de téléphone"
onChange={this.onChangePhoneNumberInput}
defaultValue={this.state.customer?.contact?.cell_phone_number ?? ""}
validationError={this.state.validationError.find((error) => error.property === "cell_phone_number")}
/>
<TextField
name="birthdate"
placeholder="Date de naissance"
required={false}
onChange={this.onChangeBirthDateInput}
defaultValue={this.state.customer?.contact?.birthdate?.toString() ?? ""}
validationError={this.state.validationError.find((error) => error.property === "birthdate")}
/>
<TextField
name="address"
placeholder="Adresse"
required={false}
onChange={this.onChangeAddressInput}
defaultValue={this.state.customer?.contact?.address?.address ?? ""}
/>
</div>
useEffect(() => {
const fetchCustomer = async () => {
try {
const customerData = await Customers.getInstance().getByUid(customerUid as string, {
contact: {
include: {
address: true,
},
},
});
if (customerData) {
setCustomer(customerData);
}
} catch (error) {
console.error("Failed to fetch customer", error);
}
};
fetchCustomer();
}, [customerUid]);
<div className={classes["button-container"]}>
{!this.doesInputsHaveValues() ? (
<Link href={this.backwardPath} className={classes["cancel-button"]}>
<Button variant={EButtonVariant.PRIMARY} styletype={EButtonstyletype.OUTLINED}>
Annuler
</Button>
</Link>
) : (
<Button
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.OUTLINED}
onClick={this.openLeavingModal}
className={classes["cancel-button"]}>
const onFormSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => {
if (!values["cell_phone_number"]) return;
let phoneNumber = values["cell_phone_number"].replace(/\s/g, "").replace(/\./g, "");
if (phoneNumber.length === 10 && phoneNumber.startsWith("0")) {
phoneNumber = "+33" + phoneNumber.substring(1);
}
const contact = Contact.hydrate<Contact>({
first_name: values["first_name"],
last_name: values["last_name"],
email: values["email"],
cell_phone_number: phoneNumber,
birthdate: values["birthdate"] === "" ? null : new Date(values["birthdate"]!),
civility: "-",
address: values["address"] ? Address.hydrate<Address>({ address: values["address"], city: "-", zip_code: 0 }) : undefined,
});
try {
await contact.validateOrReject?.({ groups: ["createCustomer"], forbidUnknownValues: false });
await Customers.getInstance().put(customerUid as string, { contact });
router.push(backwardPath);
} catch (validationErrors) {
if (Array.isArray(validationErrors)) {
setValidationError(validationErrors as ValidationError[]);
}
}
},
[backwardPath, customerUid, router],
);
const leavePage = useCallback(() => {
router.push(backwardPath);
}, [backwardPath, router]);
const onFieldChange = useCallback((name: string, field: IBaseField) => {
if (field.props.value !== field.props.defaultValue) {
setDoesInputHasChanged(true);
} else {
setDoesInputHasChanged(false);
}
}, []);
return (
<DefaultDoubleSidePage title={"Ajouter client(s)"} image={backgroundImage}>
<div className={classes["root"]}>
<div className={classes["back-arrow"]}>
<BackArrow url={backwardPath} />
</div>
<Typography typo={ETypo.TITLE_H1}>Modifier les informations du client</Typography>
<Form className={classes["form"]} onSubmit={onFormSubmit} onFieldChange={onFieldChange}>
<TextField
name="first_name"
placeholder="Prénom"
label="Prénom"
defaultValue={customer?.contact?.first_name}
validationError={validationError.find((error) => error.property === "first_name")}
/>
<TextField
name="last_name"
placeholder="Nom"
label="Nom"
defaultValue={customer?.contact?.last_name}
validationError={validationError.find((error) => error.property === "last_name")}
/>
<TextField
name="email"
placeholder="E-mail"
label="E-mail"
defaultValue={customer?.contact?.email}
validationError={validationError.find((error) => error.property === "email")}
/>
<TextField
name="cell_phone_number"
placeholder="Numéro de téléphone"
label="Numéro de téléphone"
defaultValue={customer?.contact?.cell_phone_number ?? ""}
validationError={validationError.find((error) => error.property === "cell_phone_number")}
/>
<TextField
name="birthdate"
placeholder="Date de naissance"
label="Date de naissance (facultatif)"
required={false}
defaultValue={customer?.contact?.birthdate?.toString() ?? ""}
validationError={validationError.find((error) => error.property === "birthdate")}
/>
<TextField
name="address"
placeholder="Adresse"
label="Adresse (facultatif)"
required={false}
defaultValue={customer?.contact?.address?.address ?? ""}
/>
<div className={classes["button-container"]}>
{!doesInputHasChanged ? (
<Link href={backwardPath} className={classes["cancel-button"]}>
<Button variant={EButtonVariant.PRIMARY} styletype={EButtonstyletype.OUTLINED}>
Annuler
</Button>
)}
<Button type="submit">Enregistrer</Button>
</div>
</Form>
<Confirm
isOpen={this.state.isOpenLeavingModal}
onClose={this.closeLeavingModal}
closeBtn
header={"Êtes-vous sur de vouloir quitter sans enregistrer ?"}
cancelText={"Annuler"}
confirmText={"Quitter"}
onAccept={this.leavePage}>
Si vous quittez, toutes les modifications que vous avez effectuées ne seront pas enregistrées.{" "}
</Confirm>
</div>
</DefaultNotaryDashboard>
);
}
public override async componentDidMount() {
const customer = await Customers.getInstance().getByUid(this.props.customerUid, {
contact: {
include: {
address: true,
},
},
});
if (customer) {
this.setState({
customer,
});
}
}
private async onFormSubmit(
e: React.FormEvent<HTMLFormElement> | null,
values: {
[key: string]: string;
},
) {
if (!values["cell_phone_number"]) return;
// remove every space from the phone number
values["cell_phone_number"] = values["cell_phone_number"].replace(/\s/g, "");
values["cell_phone_number"] = values["cell_phone_number"].replace(/\./g, "");
if (values["cell_phone_number"] && values["cell_phone_number"].length === 10) {
// get the first digit of the phone number
const firstDigit = values["cell_phone_number"].charAt(0);
// if the first digit is a 0 replace it by +33
if (firstDigit === "0") {
values["cell_phone_number"] = "+33" + values["cell_phone_number"].substring(1);
}
}
const contact = Contact.hydrate<Contact>({
first_name: values["first_name"],
last_name: values["last_name"],
email: values["email"],
cell_phone_number: values["cell_phone_number"],
birthdate: values["birthdate"] === "" ? null : new Date(values["birthdate"]!),
civility: "-",
address: values["address"] ? Address.hydrate<Address>({ address: values["address"], city: "-", zip_code: 0 }) : undefined,
});
try {
await contact.validateOrReject?.({ groups: ["createCustomer"], forbidUnknownValues: false });
} catch (validationErrors) {
this.setState({
validationError: validationErrors as ValidationError[],
});
return;
}
try {
await Customers.getInstance().put(this.props.customerUid, { contact });
this.props.router.push(this.backwardPath);
} catch (backError) {
if (!Array.isArray(backError)) return;
this.setState({
validationError: backError as ValidationError[],
});
return;
}
}
private leavePage() {
this.props.router.push(this.backwardPath);
}
private openLeavingModal(): void {
this.setState({ isOpenLeavingModal: true });
}
private closeLeavingModal(): void {
this.setState({ isOpenLeavingModal: false });
}
private onChangeBirthDateInput(event: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) {
this.setState({ inputBirthdate: new Date(event.target.value) });
}
private onChangeAddressInput(event: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) {
this.setState({ inputAddress: event.target.value });
}
private onChangeNameInput(event: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) {
this.setState({ inputNameValue: event.target.value });
}
private onChangeFirstNameInput(event: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) {
this.setState({ inputFirstNameValue: event.target.value });
}
private onChangeEmailInput(event: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) {
this.setState({ inputEmailValue: event.target.value });
}
private onChangePhoneNumberInput(event: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) {
this.setState({ inputPhoneNumberValue: event.target.value });
}
private onSelectedFolder(folder: OfficeFolder): void {
this.setState({ selectedFolder: folder });
}
private doesInputsHaveValues(): boolean {
const doesInputsHaveValues: boolean =
this.state.inputNameValue !== "" ||
this.state.inputFirstNameValue !== "" ||
this.state.inputEmailValue !== "" ||
this.state.inputPhoneNumberValue !== "";
return doesInputsHaveValues;
}
}
export default function UpdateClient(props: IProps) {
const router = useRouter();
let { folderUid, customerUid } = router.query;
folderUid = folderUid as string;
customerUid = customerUid as string;
return <UpdateClientClass {...props} router={router} selectedFolderUid={folderUid} customerUid={customerUid} />;
</Link>
) : (
<Button
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.OUTLINED}
onClick={open}
className={classes["cancel-button"]}>
Annuler
</Button>
)}
<Button type="submit">Enregistrer les modifications</Button>
</div>
</Form>
<Modal
title={"Quitter sans enregistrer ?"}
isOpen={isOpen}
onClose={close}
firstButton={{ children: "Annuler", onClick: close }}
secondButton={{ children: "Oui, quitter sans enregistrer", onClick: leavePage }}>
Si vous quittez, toutes les modifications que vous avez effectuées ne seront pas enregistrées.
</Modal>
</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];