453 lines
13 KiB
TypeScript
453 lines
13 KiB
TypeScript
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");
|
|
}
|
|
}
|