diff --git a/src/front/Components/DesignSystem/Header/Notifications/NotificationModal/classes.module.scss b/src/front/Components/DesignSystem/Header/Notifications/NotificationModal/classes.module.scss index ad7cd54d..1e7bcdf7 100644 --- a/src/front/Components/DesignSystem/Header/Notifications/NotificationModal/classes.module.scss +++ b/src/front/Components/DesignSystem/Header/Notifications/NotificationModal/classes.module.scss @@ -32,10 +32,12 @@ } .notification-subheader { - width: 100%; - display: inline-flex; - justify-content: space-between; + display: flex; + align-items: center; + gap: 8px; text-decoration: underline; + text-underline-offset: 2px; + color: var(--pink-flash); cursor: pointer; } diff --git a/src/front/Components/DesignSystem/Header/Notifications/NotificationModal/index.tsx b/src/front/Components/DesignSystem/Header/Notifications/NotificationModal/index.tsx index 0d8d8ba6..56c15f11 100644 --- a/src/front/Components/DesignSystem/Header/Notifications/NotificationModal/index.tsx +++ b/src/front/Components/DesignSystem/Header/Notifications/NotificationModal/index.tsx @@ -5,6 +5,7 @@ import CloseIcon from "@Assets/Icons/cross.svg"; import Image from "next/image"; import ToastHandler from "@Front/Components/DesignSystem/Toasts/ToastsHandler"; import Toasts, { IToast } from "@Front/Stores/Toasts"; +import Check from "@Front/Components/Elements/Icons/Check"; type IProps = { isOpen: boolean; @@ -23,6 +24,7 @@ export default class NotificationModal extends React.Component { toastList: Toasts.getInstance().toasts, }; this.handleToastChange = this.handleToastChange.bind(this); + this.readAllNotifications = this.readAllNotifications.bind(this); } public override render(): JSX.Element | null { @@ -38,7 +40,10 @@ export default class NotificationModal extends React.Component {
- Tout marquer comme lu + + Tout marquer comme lu + +
<> @@ -68,6 +73,7 @@ export default class NotificationModal extends React.Component { private readAllNotifications() { Toasts.getInstance().closeAll(); + this.props.closeModal(); } private handleToastChange(toastList: IToast[] | null) { diff --git a/src/front/Components/DesignSystem/UserFolder/classes.module.scss b/src/front/Components/DesignSystem/UserFolder/classes.module.scss index ecaffe07..453861ac 100644 --- a/src/front/Components/DesignSystem/UserFolder/classes.module.scss +++ b/src/front/Components/DesignSystem/UserFolder/classes.module.scss @@ -54,7 +54,8 @@ .content { display: grid; grid-template-columns: 1fr 1fr; - gap: 64px; + column-gap: 64px; + row-gap: 16px; margin-top: 32px; @media (max-width: $screen-s) { @@ -63,7 +64,7 @@ flex-direction: column; } - .documents-asked{ + .documents-asked { order: -1; } @@ -73,7 +74,7 @@ gap: 32px; margin-top: 16px; - @media(max-width: $screen-s){ + @media (max-width: $screen-s) { order: -1; } } diff --git a/src/front/Components/DesignSystem/UserFolder/index.tsx b/src/front/Components/DesignSystem/UserFolder/index.tsx index fb964193..0ff66682 100644 --- a/src/front/Components/DesignSystem/UserFolder/index.tsx +++ b/src/front/Components/DesignSystem/UserFolder/index.tsx @@ -26,6 +26,7 @@ type IProps = { isOpened: boolean; onChange: (id: string) => void; anchorStatus: AnchorStatus; + getFolderCallback: () => Promise; }; type IState = { isOpenDeletionModal: boolean; @@ -95,6 +96,9 @@ export default class UserFolder extends React.Component { { private async deleteAskedDocument() { try { await Documents.getInstance().delete(this.state.selectedDocumentToDelete); - window.location.reload(); + await this.props.getFolderCallback(); } catch (e) { console.error(e); } diff --git a/src/front/Components/Elements/Icons/Check/index.tsx b/src/front/Components/Elements/Icons/Check/index.tsx new file mode 100644 index 00000000..b3ec1825 --- /dev/null +++ b/src/front/Components/Elements/Icons/Check/index.tsx @@ -0,0 +1,19 @@ +import { ITypoColor } from "@Front/Components/DesignSystem/Typography"; + +type IProps = { + color?: ITypoColor; +}; + +export default function Check(props: IProps) { + return ( + + + + ); +} diff --git a/src/front/Components/Layouts/Folder/AddClientToFolder/index.tsx b/src/front/Components/Layouts/Folder/AddClientToFolder/index.tsx index 8aa0c509..ddf1418d 100644 --- a/src/front/Components/Layouts/Folder/AddClientToFolder/index.tsx +++ b/src/front/Components/Layouts/Folder/AddClientToFolder/index.tsx @@ -10,13 +10,14 @@ import BackArrow from "@Front/Components/Elements/BackArrow"; import DefaultNotaryDashboard from "@Front/Components/LayoutTemplates/DefaultNotaryDashboard"; import Module from "@Front/Config/Module"; import { ECivility } from "le-coffre-resources/dist/Customer/Contact"; -import { Customer, OfficeFolder } from "le-coffre-resources/dist/Notary"; +import { Contact, Customer, OfficeFolder } from "le-coffre-resources/dist/Notary"; import Link from "next/link"; import { NextRouter, useRouter } from "next/router"; import BasePage from "../../Base"; import classes from "./classes.module.scss"; import TextField from "@Front/Components/DesignSystem/Form/TextField"; +import { ValidationError } from "class-validator"; enum ESelectedOption { EXISTING_CUSTOMER = "existing_customer", @@ -29,6 +30,7 @@ type IState = { selectedCustomers: readonly IOption[]; existingCustomers: IOption[]; isLoaded: boolean; + validationError: ValidationError[]; }; type IPropsClass = IProps & { @@ -44,6 +46,7 @@ class AddClientToFolderClass extends BasePage { selectedCustomers: [], existingCustomers: [], isLoaded: false, + validationError: [], }; this.onExistingClientSelected = this.onExistingClientSelected.bind(this); this.onNewClientSelected = this.onNewClientSelected.bind(this); @@ -105,10 +108,28 @@ class AddClientToFolderClass extends BasePage { {this.state.selectedOption === "new_customer" && (
- - - - + error.property === "last_name")} + /> + error.property === "first_name")} + /> + error.property === "email")} + /> + error.property === "cell_phone_number", + )} + />
@@ -207,11 +228,29 @@ class AddClientToFolderClass extends BasePage { }) as Partial[]; if (this.state.selectedOption === "new_customer") { - const customer: Customer = await Customers.getInstance().post({ - contact: values, - }); - if (!customer.uid) return; - customersToLink?.push({ uid: customer.uid } as Partial); + try { + const contactToCreate = Contact.hydrate(values); + await contactToCreate.validateOrReject?.({ groups: ["createCustomer"], forbidUnknownValues: false }); + } catch (validationErrors) { + console.log(validationErrors); + this.setState({ + validationError: validationErrors as ValidationError[], + }); + return; + } + + try { + const customer: Customer = await Customers.getInstance().post({ + contact: values, + }); + if (!customer.uid) return; + customersToLink?.push({ uid: customer.uid } as Partial); + } catch (backError) { + if (!Array.isArray(backError)) return; + this.setState({ + validationError: backError as ValidationError[], + }); + } } if (customersToLink) { diff --git a/src/front/Components/Layouts/Folder/FolderInformation/ClientSection/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/ClientSection/index.tsx index b4920f84..15b06256 100644 --- a/src/front/Components/Layouts/Folder/FolderInformation/ClientSection/index.tsx +++ b/src/front/Components/Layouts/Folder/FolderInformation/ClientSection/index.tsx @@ -13,6 +13,7 @@ import { AnchorStatus } from ".."; type IProps = { folder: OfficeFolder; anchorStatus: AnchorStatus; + getFolderCallback: () => Promise; }; type IState = { openedCustomer: string; @@ -76,6 +77,7 @@ export default class ClientSection extends React.Component { isOpened={this.state.openedCustomer === customer.uid} onChange={this.changeUserFolder} anchorStatus={this.props.anchorStatus} + getFolderCallback={this.props.getFolderCallback} /> ); }); diff --git a/src/front/Components/Layouts/Folder/FolderInformation/index.tsx b/src/front/Components/Layouts/Folder/FolderInformation/index.tsx index 654aa40b..2a17a3b7 100644 --- a/src/front/Components/Layouts/Folder/FolderInformation/index.tsx +++ b/src/front/Components/Layouts/Folder/FolderInformation/index.tsx @@ -37,6 +37,7 @@ type IPropsClass = IProps & { isLoading: boolean; selectedFolder: OfficeFolder | null; getAnchoringStatus: () => Promise; + getFolderCallback: () => Promise; }; type IState = { @@ -115,12 +116,20 @@ class FolderInformationClass extends BasePage { />
{this.doesFolderHaveCustomer() && ( - + )}
{!this.doesFolderHaveCustomer() && ( - + )}
@@ -209,7 +218,7 @@ class FolderInformationClass extends BasePage { onAccept={this.closePreventArchiveModal} onClose={this.closePreventArchiveModal} closeBtn - header={"Vous devez valider et certifier un dossier avant de pouvoir l'archiver"} + header={"Vous devez valider et ancrer un dossier avant de pouvoir l'archiver"} cancelText={"Annuler"} confirmText={"J'ai compris"}>
@@ -374,7 +383,7 @@ class FolderInformationClass extends BasePage { } private openArchivedModal(): void { - if (this.everyDocumentValidated() && this.props.isAnchored) { + if (this.everyDocumentValidated() && this.props.isAnchored === AnchorStatus.VERIFIED_ON_CHAIN) { this.setState({ isArchivedModalOpen: true }); } else { this.setState({ isPreventArchiveModalOpen: true }); @@ -471,6 +480,7 @@ export default function FolderInformation(props: IProps) { isLoading={isLoading} selectedFolder={selectedFolder} getAnchoringStatus={getAnchoringStatus} + getFolderCallback={getFolder} /> ); } diff --git a/src/front/Components/Layouts/Folder/UpdateClient/index.tsx b/src/front/Components/Layouts/Folder/UpdateClient/index.tsx index df51d182..2c19e02b 100644 --- a/src/front/Components/Layouts/Folder/UpdateClient/index.tsx +++ b/src/front/Components/Layouts/Folder/UpdateClient/index.tsx @@ -5,7 +5,7 @@ import TextField from "@Front/Components/DesignSystem/Form/TextField"; import Confirm from "@Front/Components/DesignSystem/Modal/Confirm"; import Typography, { ITypo } from "@Front/Components/DesignSystem/Typography"; import BackArrow from "@Front/Components/Elements/BackArrow"; -import DefaultNotaryDashboard from "@Front/Components/LayoutTemplates/DefaultNotaryDashboard"; +import DefaultNotaryDashboard from "@Front/Components/LayoutTemplates/DefaultNotaryDashboard"; import Module from "@Front/Config/Module"; import { Contact, Customer, OfficeFolder } from "le-coffre-resources/dist/Notary"; import Link from "next/link"; @@ -14,6 +14,8 @@ import { ChangeEvent } from "react"; import BasePage from "../../Base"; import classes from "./classes.module.scss"; +import { Address } from "le-coffre-resources/dist/Customer"; +import { ValidationError } from "class-validator"; type IProps = {}; @@ -34,6 +36,7 @@ type IState = { inputAddress: string; folder: OfficeFolder | null; customer: Customer | null; + validationError: ValidationError[]; }; class UpdateClientClass extends BasePage { constructor(props: IPropsClass) { @@ -50,6 +53,7 @@ class UpdateClientClass extends BasePage { inputAddress: "", folder: null, customer: null, + validationError: [], }; this.onSelectedFolder = this.onSelectedFolder.bind(this); this.onChangeNameInput = this.onChangeNameInput.bind(this); @@ -83,24 +87,28 @@ class UpdateClientClass extends BasePage { placeholder="Nom" onChange={this.onChangeNameInput} defaultValue={this.state.customer?.contact?.first_name} + validationError={this.state.validationError.find((error) => error.property === "first_name")} /> error.property === "last_name")} /> error.property === "email")} /> error.property === "cell_phone_number")} /> { required={false} onChange={this.onChangeBirthDateInput} defaultValue={this.state.customer?.contact?.birthdate?.toString() ?? ""} + validationError={this.state.validationError.find((error) => error.property === "birthdate")} /> { [key: string]: string; }, ) { - const contact = { + const contact = Contact.hydrate({ first_name: values["first_name"], last_name: values["last_name"], email: values["email"], cell_phone_number: values["cell_phone_number"], - birthdate: values["birthdate"] === "" ? null : values["birthdate"], - address: - values["address"] === "" - ? null - : { - address: values["address"], - }, - } as Contact; + birthdate: values["birthdate"] === "" ? null : new Date(values["birthdate"]!), + address: values["address"] ? Address.hydrate
({ address: values["address"] }) : undefined, + }); + + try { + await contact.validateOrReject?.({ groups: ["createCustomer"], forbidUnknownValues: false }); + } catch (validationErrors) { + console.log(validationErrors); + this.setState({ + validationError: validationErrors as ValidationError[], + }); + return; + } try { await Customers.getInstance().put(this.props.customerUid, { contact }); this.props.router.push(this.backwardPath); - } catch (e) { - console.error(e); + } catch (backError) { + if (!Array.isArray(backError)) return; + this.setState({ + validationError: backError as ValidationError[], + }); } } diff --git a/src/front/Components/Layouts/FolderArchived/FolderInformation/ClientSection/index.tsx b/src/front/Components/Layouts/FolderArchived/FolderInformation/ClientSection/index.tsx index 3052c261..bd084620 100644 --- a/src/front/Components/Layouts/FolderArchived/FolderInformation/ClientSection/index.tsx +++ b/src/front/Components/Layouts/FolderArchived/FolderInformation/ClientSection/index.tsx @@ -8,6 +8,7 @@ import { AnchorStatus } from "@Front/Components/Layouts/Folder/FolderInformation type IProps = { folder: OfficeFolder; anchorStatus: AnchorStatus; + getFolderCallback: () => Promise; }; type IState = { openedCustomer: string; @@ -53,6 +54,7 @@ export default class ClientSection extends React.Component { onChange={this.changeUserFolder} anchorStatus={this.props.anchorStatus} isArchived + getFolderCallback={this.props.getFolderCallback} /> ); }); diff --git a/src/front/Components/Layouts/FolderArchived/FolderInformation/index.tsx b/src/front/Components/Layouts/FolderArchived/FolderInformation/index.tsx index 28a8a6e2..453b86ea 100644 --- a/src/front/Components/Layouts/FolderArchived/FolderInformation/index.tsx +++ b/src/front/Components/Layouts/FolderArchived/FolderInformation/index.tsx @@ -26,6 +26,7 @@ type IPropsClass = IProps & { isLoading: boolean; selectedFolder: OfficeFolder | null; isAnchored: AnchorStatus; + getFolderCallback: () => Promise; }; type IState = { @@ -54,68 +55,80 @@ class FolderInformationClass extends BasePage { return ( {!this.props.isLoading && ( -
- {this.state.selectedFolder ? ( -
-
-
-
- Informations du dossier +
+ {this.state.selectedFolder ? ( +
+
+
+
+ Informations du dossier +
+ + +
- - - -
- -
+
+ +
+
+ +
+ +
+ +
+ {this.doesFolderHaveCustomer() && ( + + )}
-
- + )} + +
+
- -
- +
+ ) : ( +
+ Informations du dossier +
+ + Aucun dossier sélectionné +
- {this.doesFolderHaveCustomer() && }
- - {!this.doesFolderHaveCustomer() && } - -
- -
-
- ) : ( -
- Informations du dossier -
- - Aucun dossier sélectionné - -
-
- )} -
+ )} +
)} {this.props.isLoading && (
@@ -181,7 +194,7 @@ export default function FolderInformation(props: IProps) { folderUid = folderUid as string; const getAnchoringStatus = useCallback(async () => { - if(!folderUid) return; + if (!folderUid) return; setIsLoading(true); try { const anchorStatus = await OfficeFolderAnchors.getInstance().getByUid(folderUid as string); @@ -237,5 +250,15 @@ export default function FolderInformation(props: IProps) { getFolder(); }, [getFolder]); - return ; + return ( + + ); } diff --git a/src/front/Components/Layouts/Login/index.tsx b/src/front/Components/Layouts/Login/index.tsx index e6e60bd7..36609094 100644 --- a/src/front/Components/Layouts/Login/index.tsx +++ b/src/front/Components/Layouts/Login/index.tsx @@ -5,15 +5,19 @@ import Typography, { ITypo } from "@Front/Components/DesignSystem/Typography"; import DefaultDoubleSidePage from "@Front/Components/LayoutTemplates/DefaultDoubleSidePage"; import Image from "next/image"; import { useRouter } from "next/router"; -import { useCallback } from "react"; +import { useCallback, useEffect, useState } from "react"; import classes from "./classes.module.scss"; import LandingImage from "./landing-connect.jpeg"; import { FrontendVariables } from "@Front/Config/VariablesFront"; import Link from "next/link"; +import Confirm from "@Front/Components/DesignSystem/Modal/Confirm"; export default function Login() { const router = useRouter(); + const error = router.query["error"]; + + const [isErrorModalOpen, setIsErrorModalOpen] = useState(false); const redirectUserOnConnection = useCallback(() => { const variables = FrontendVariables.getInstance(); @@ -24,6 +28,18 @@ export default function Login() { ); }, [router]); + const openErrorModal = useCallback(() => { + setIsErrorModalOpen(true); + }, []); + + const closeErrorModal = useCallback(() => { + setIsErrorModalOpen(false); + }, []); + + useEffect(() => { + if (error === "1") openErrorModal(); + }, [error, openErrorModal]); + return (
@@ -41,6 +57,20 @@ export default function Login() {
+ +
+ + Une erreur est survenue lors de la connexion. Veuillez réessayer. + +
+
); } diff --git a/src/front/Components/Layouts/LoginCallback/index.tsx b/src/front/Components/Layouts/LoginCallback/index.tsx index 86b3a6e2..3ca30a34 100644 --- a/src/front/Components/Layouts/LoginCallback/index.tsx +++ b/src/front/Components/Layouts/LoginCallback/index.tsx @@ -26,7 +26,7 @@ export default function LoginCallBack() { await UserStore.instance.connect(token.accessToken, token.refreshToken); return router.push(Module.getInstance().get().modules.pages.Folder.props.path); } catch (e) { - console.error(e); + router.push(Module.getInstance().get().modules.pages.Login.props.path + "?error=1"); return; } } diff --git a/src/front/Components/Layouts/Users/UserInformations/classes.module.scss b/src/front/Components/Layouts/Users/UserInformations/classes.module.scss index 6dd2a010..cc483aa2 100644 --- a/src/front/Components/Layouts/Users/UserInformations/classes.module.scss +++ b/src/front/Components/Layouts/Users/UserInformations/classes.module.scss @@ -83,3 +83,15 @@ .remove-my-vote { margin-top: 16px; } + +.loader-container { + display: flex; + flex: 1; + align-items: center; + justify-content: center; + height: 100%; + .loader { + width: 40px; + height: 40px; + } +} diff --git a/src/front/Components/Layouts/Users/UserInformations/index.tsx b/src/front/Components/Layouts/Users/UserInformations/index.tsx index 82158ea4..b55e45d5 100644 --- a/src/front/Components/Layouts/Users/UserInformations/index.tsx +++ b/src/front/Components/Layouts/Users/UserInformations/index.tsx @@ -18,6 +18,7 @@ import { useCallback, useEffect, useState } from "react"; import classes from "./classes.module.scss"; import OfficeRoles from "@Front/Api/LeCoffreApi/Admin/OfficeRoles/OfficeRoles"; +import Loader from "@Front/Components/DesignSystem/Loader"; type IProps = {}; export default function UserInformations(props: IProps) { @@ -36,10 +37,12 @@ export default function UserInformations(props: IProps) { const [currentAppointment, setCurrentAppointment] = useState(null); + const [isLoading, setIsLoading] = useState(true); /** When page change, get the user of the page */ const getUser = useCallback(async () => { if (!userUid) return; + setIsLoading(true); const user = await Users.getInstance().getByUid(userUid as string, { q: { contact: true, @@ -66,6 +69,7 @@ export default function UserInformations(props: IProps) { }, }); if (!roles) return; + setIsLoading(false); setUserSelected(user); }, [userUid]); @@ -201,135 +205,146 @@ export default function UserInformations(props: IProps) { return ( -
-
- {userSelected?.contact?.first_name + " " + userSelected?.contact?.last_name} - - Office {userSelected?.office_membership?.name.toLocaleUpperCase()} - -
-
-
- - Nom + {!isLoading && ( +
+
+ + {userSelected?.contact?.first_name + " " + userSelected?.contact?.last_name} - {userSelected?.contact?.first_name} -
-
- - Prénom + + Office {userSelected?.office_membership?.name.toLocaleUpperCase()} - {userSelected?.contact?.last_name}
-
- - Numéro de téléphone - - {userSelected?.contact?.phone_number} -
-
- - Email - - {userSelected?.contact?.email} -
-
- -
-
-
- Rôle au sein de son office +
+
+ + Nom + + {userSelected?.contact?.first_name}
-
- - {userSelected?.office_role ? userSelected?.office_role?.name : "Utilisateur restreint"} +
+ + Prénom + + {userSelected?.contact?.last_name} +
+
+ + Numéro de téléphone + + {userSelected?.contact?.phone_number} +
+
+ + Email + + {userSelected?.contact?.email} +
+
+ +
+
+
+ Rôle au sein de son office +
+
+ + {userSelected?.office_role ? userSelected?.office_role?.name : "Utilisateur restreint"} + +
+
+
+
+ Attribuer un titre +
+
+ {!isSuperAdminChecked && !currentAppointment && ( + + )} + + + {currentAppointment && ( +
+
+ warning +
+
+
+ {currentAppointment.votes?.length}/3 +
+
+ + {currentAppointment.choice === EVote.NOMINATE + ? `Un ou des collaborateurs souhaitent attribuer le titre de Super Admin à ce collaborateur. Il manque ${ + 3 - currentAppointment.votes?.length! + } vote(s) pour que le collaborateur se voit attribuer le titre.` + : `Un ou des collaborateurs souhaitent retirer le titre de Super Admin à ce collaborateur. Il manque ${ + 3 - currentAppointment.votes?.length! + } vote(s) pour que le collaborateur se voit retirer le titre.`} + +
+ {userHasVoted() && ( +
+ +
+ )} +
+
+ )} +
+
+
+ +
+ + {superAdminModalType === "add" ? "Nommer" : "Retirer"} une personne Super Administrateur nécessite 3 votes + de super administrateurs existants. Souhaitez-vous attribuer un vote ?
-
-
-
- Attribuer un titre -
-
- {!isSuperAdminChecked && !currentAppointment && ( - - )} - - - {currentAppointment && ( -
-
- warning -
-
-
- {currentAppointment.votes?.length}/3 -
-
- - {currentAppointment.choice === EVote.NOMINATE - ? `Un ou des collaborateurs souhaitent attribuer le titre de Super Admin à ce collaborateur. Il manque ${ - 3 - currentAppointment.votes?.length! - } vote(s) pour que le collaborateur se voit attribuer le titre.` - : `Un ou des collaborateurs souhaitent retirer le titre de Super Admin à ce collaborateur. Il manque ${ - 3 - currentAppointment.votes?.length! - } vote(s) pour que le collaborateur se voit retirer le titre.`} - -
- {userHasVoted() && ( -
- -
- )} -
-
- )} -
+ + +
+
+
+ )} + {isLoading && ( +
+
+
- -
- - {superAdminModalType === "add" ? "Nommer" : "Retirer"} une personne Super Administrateur nécessite 3 votes de - super administrateurs existants. Souhaitez-vous attribuer un vote ? - -
-
- -
-
-
+ )} ); }