deploy v2.5 (#184)
Co-authored-by: Vins <vincent.alamelle@smart-chain.fr>
This commit is contained in:
parent
3ce0ec1a62
commit
9d83616c14
849
package-lock.json
generated
849
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/front/Api/LeCoffreApi/Notary/FilesNotary/Files.ts
Normal file
71
src/front/Api/LeCoffreApi/Notary/FilesNotary/Files.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ export interface IGetFoldersParams {
|
||||
select?: {};
|
||||
where?: {};
|
||||
include?: {};
|
||||
orderBy?: {};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/front/Components/DesignSystem/BadgeIcon/index.tsx
Normal file
23
src/front/Components/DesignSystem/BadgeIcon/index.tsx
Normal 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>;
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 été 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");
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
219
src/front/Components/DesignSystem/DragAndDrop/index.tsx
Normal file
219
src/front/Components/DesignSystem/DragAndDrop/index.tsx
Normal 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
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -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} />;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,4 +75,8 @@
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-fullwidth="true"] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
35
src/front/Components/DesignSystem/Separator/index.tsx
Normal file
35
src/front/Components/DesignSystem/Separator/index.tsx
Normal 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 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -24,7 +24,6 @@
|
||||
word-wrap: break-word;
|
||||
|
||||
.content {
|
||||
max-width: 270px;
|
||||
width: 100%;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@ -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 {};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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> = {
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-lg, 24px);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
111
src/front/Components/DesignSystem/Toaster/index.tsx
Normal file
111
src/front/Components/DesignSystem/Toaster/index.tsx
Normal 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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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`,
|
||||
|
||||
27
src/front/Components/DesignSystem/TooltipElement/index.tsx
Normal file
27
src/front/Components/DesignSystem/TooltipElement/index.tsx
Normal 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} />;
|
||||
}
|
||||
@ -278,4 +278,8 @@
|
||||
line-height: 15px;
|
||||
letter-spacing: -0.8px;
|
||||
}
|
||||
|
||||
&.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
22
src/front/Components/Elements/ContactBox/classes.module.scss
Normal file
22
src/front/Components/Elements/ContactBox/classes.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
48
src/front/Components/Elements/ContactBox/index.tsx
Normal file
48
src/front/Components/Elements/ContactBox/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm, 8px);
|
||||
}
|
||||
39
src/front/Components/Elements/HelpBox/index.tsx
Normal file
39
src/front/Components/Elements/HelpBox/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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],
|
||||
|
||||
@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -32,6 +32,10 @@
|
||||
|
||||
.right-side {
|
||||
min-width: 100%;
|
||||
|
||||
.right-side-content {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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 été 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 />} />,
|
||||
}));
|
||||
}
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
|
||||
.cancel-button {
|
||||
display: flex;
|
||||
margin-right: 32px;
|
||||
margin-right: var(--spacing-md, 16px);
|
||||
}
|
||||
|
||||
@media (max-width: $screen-m) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
|
||||
.buttons-container {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
gap: var(--spacing-md, 16px);
|
||||
margin-top: 32px;
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
});
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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 l’envoi 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>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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, "");
|
||||
}
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md, 16px);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
});
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
212
src/front/Components/Layouts/Folder/SendDocuments/index.tsx
Normal file
212
src/front/Components/Layouts/Folder/SendDocuments/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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];
|
||||
|
||||