Merge branch 'dev-without-id-not' into dev

This commit is contained in:
Maxime Lalo 2023-09-21 17:30:03 +02:00
commit 26d597411f
18 changed files with 363 additions and 95 deletions

View File

@ -75,6 +75,11 @@ class FolderListContainerClass extends React.Component<IPropsClass, IState> {
);
}
public override componentDidUpdate(prevProps: Readonly<IPropsClass>, prevState: Readonly<IState>, snapshot?: any): void {
if (prevProps.selectedFolder !== this.props.selectedFolder) {
this.setState({ filteredFolders: this.props.folders, blocks: this.getBlocks(this.props.folders) });
}
}
private getBlocks(folders: OfficeFolder[]): IBlock[] {
const pendingFolders = folders
.filter((folder) => {

View File

@ -0,0 +1,102 @@
@import "@Themes/constants.scss";
.root {
position: relative;
.input {
z-index: 1;
display: flex;
flex-direction: row;
align-items: center;
padding: 24px;
gap: 10px;
width: 100%;
height: 70px;
border: 1px solid var(--grey-medium);
&:disabled {
cursor: not-allowed;
}
&:focus {
~ .fake-placeholder {
transform: translateY(-35px);
}
}
&:not([data-value=""]) {
~ .fake-placeholder {
transform: translateY(-35px);
}
}
&[type="number"] {
&:focus {
~ .fake-placeholder {
transform: translateY(-35px);
}
}
&:not([data-value=""]) {
~ .fake-placeholder {
transform: translateY(-35px);
}
}
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* For Chrome, Safari, and Opera */
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* For IE 10+ */
&::-ms-inner-spin-button,
&::-ms-outer-spin-button {
display: none;
}
}
&:not([data-value=""]) {
~ .fake-placeholder {
transform: translateY(-35px);
}
}
~ .fake-placeholder {
z-index: 2;
top: 35%;
margin-left: 8px;
padding: 0 16px;
pointer-events: none;
position: absolute;
background: $white;
transition: transform 0.3s ease-in-out;
}
}
&[data-is-errored="true"] {
.input {
border: 1px solid var(--red-flash);
~ .fake-placeholder {
color: var(--red-flash);
}
}
}
.copy-icon {
cursor: pointer;
height: 24px;
width: 24px;
position: absolute;
top: 50%;
right: 24px;
transform: translate(0, -50%);
}
}

View File

@ -0,0 +1,56 @@
import React from "react";
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
import { ReactNode } from "react";
import CopyIcon from "@Assets/Icons/copy.svg";
import BaseField, { IProps as IBaseFieldProps } from "../BaseField";
import classes from "./classes.module.scss";
import classnames from "classnames";
import Image from "next/image";
export type IProps = IBaseFieldProps & {
canCopy?: boolean;
};
export default class DateField extends BaseField<IProps> {
constructor(props: IProps) {
super(props);
this.state = this.getDefaultState();
}
public override render(): ReactNode {
const value = this.state.value ?? "";
return (
<Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}>
<div className={classes["root"]} data-is-errored={this.hasError().toString()}>
<input
onChange={this.onChange}
data-value={value}
data-has-validation-errors={(this.state.validationError === null).toString()}
className={classnames(classes["input"], this.props.className)}
value={value}
onFocus={this.onFocus}
onBlur={this.onBlur}
name={this.props.name}
disabled={this.props.disabled}
type={"date"}
/>
<div className={classes["fake-placeholder"]}>
{this.props.placeholder} {!this.props.required && " (Facultatif)"}
</div>
{this.props.canCopy && (
<div className={classes["copy-icon"]} onClick={this.onCopyClick}>
<Image src={CopyIcon} alt="Copy icon" />
</div>
)}
</div>
{this.hasError() && <div className={classes["errors-container"]}>{this.renderErrors()}</div>}
</Typography>
);
}
private onCopyClick = (): void => {
if (this.props.canCopy) {
navigator.clipboard.writeText(this.state.value ?? "");
}
};
}

View File

@ -7,6 +7,7 @@ import React, { FormEvent, ReactNode } from "react";
import Typography, { ITypo, ITypoColor } from "../../Typography";
import classes from "./classes.module.scss";
import { NextRouter, useRouter } from "next/router";
type IProps = {
selectedOption?: IOption;
@ -16,7 +17,7 @@ type IProps = {
placeholder?: string;
className?: string;
name: string;
disabled: boolean;
disabled?: boolean;
errors?: ValidationError;
};
@ -35,7 +36,11 @@ type IState = {
errors: ValidationError | null;
};
export default class SelectField extends React.Component<IProps, IState> {
type IPropsClass = IProps & {
router: NextRouter;
};
class SelectFieldClass extends React.Component<IPropsClass, IState> {
private contentRef = React.createRef<HTMLUListElement>();
private rootRef = React.createRef<HTMLDivElement>();
private removeOnresize = () => {};
@ -44,7 +49,7 @@ export default class SelectField extends React.Component<IProps, IState> {
disabled: false,
};
constructor(props: IProps) {
constructor(props: IPropsClass) {
super(props);
this.state = {
isOpen: false,
@ -64,7 +69,7 @@ export default class SelectField extends React.Component<IProps, IState> {
<div
className={classNames(classes["root"], this.props.className)}
ref={this.rootRef}
data-disabled={this.props.disabled.toString()}
data-disabled={this.props.disabled?.toString()}
data-errored={(this.state.errors !== null).toString()}>
{selectedOption && <input type="text" defaultValue={selectedOption.value as string} name={this.props.name} hidden />}
<label
@ -116,6 +121,23 @@ export default class SelectField extends React.Component<IProps, IState> {
</div>
);
}
public override componentDidMount(): void {
this.onResize();
this.removeOnresize = WindowStore.getInstance().onResize(() => this.onResize());
this.props.router.events.on("routeChangeStart", () => {
this.setState({
isOpen: false,
selectedOption: null,
listHeight: 0,
listWidth: 0,
});
});
}
public override componentWillUnmount() {
this.removeOnresize();
}
public override componentDidUpdate(prevProps: IProps) {
if (this.props.errors !== prevProps.errors) {
@ -140,15 +162,6 @@ export default class SelectField extends React.Component<IProps, IState> {
return null;
}
public override componentDidMount(): void {
this.onResize();
this.removeOnresize = WindowStore.getInstance().onResize(() => this.onResize());
}
public override componentWillUnmount() {
this.removeOnresize();
}
private onResize() {
let listHeight = 0;
let listWidth = 0;
@ -192,3 +205,8 @@ export default class SelectField extends React.Component<IProps, IState> {
);
}
}
export default function SelectField(props: IProps) {
const router = useRouter();
return <SelectFieldClass {...props} router={router} />;
}

View File

@ -1,8 +1,8 @@
import React, { useCallback, useEffect } from "react";
import { useRouter } from "next/router";
import Module from "@Front/Config/Module";
import JwtService from "@Front/Services/JwtService/JwtService";
import { IAppRule } from "@Front/Api/Entities/rule";
import { useRouter } from "next/router";
import Module from "@Front/Config/Module";
export enum RulesMode {
OPTIONAL = "optional",
@ -10,16 +10,18 @@ export enum RulesMode {
}
type IProps = {
isPage?: boolean;
mode: RulesMode;
rules: IAppRule[];
no?: boolean;
children: JSX.Element;
isPage?: boolean;
};
export default function Rules(props: IProps) {
const router = useRouter();
const [isShowing, setIsShowing] = React.useState(false);
const [hasJwt, setHasJwt] = React.useState(false);
const getShowValue = useCallback(() => {
if (props.mode === RulesMode.NECESSARY) {
return props.rules.every((rule) => JwtService.getInstance().hasRule(rule.name, rule.action));
@ -27,19 +29,18 @@ export default function Rules(props: IProps) {
return !!props.rules.find((rule) => JwtService.getInstance().hasRule(rule.name, rule.action));
}, [props.mode, props.rules]);
const show = getShowValue();
const [isShowing, setIsShowing] = React.useState(props.no ? !show : show);
useEffect(() => {
setIsShowing(props.no ? !show : show);
}, [props.no, show]);
if (!JwtService.getInstance().decodeJwt()) return;
setHasJwt(true);
setIsShowing(getShowValue());
}, [getShowValue, isShowing]);
if (!isShowing && props.isPage) {
router.push(Module.getInstance().get().modules.pages.Home.props.path);
}
if (!JwtService.getInstance().decodeJwt() || !isShowing) {
if (props.isPage && !isShowing) {
router.push(Module.getInstance().get().modules.pages[404].props.path);
return null;
}
if (!hasJwt || !isShowing) return null;
return props.children;
}

View File

@ -45,9 +45,9 @@ export default function CollaboratorInformations(props: IProps) {
setRoleModalOpened(false);
setSelectedOption({
value: userSelected?.office_role ? userSelected?.office_role?.uid : userSelected?.role?.uid,
label: userSelected?.office_role ? userSelected?.office_role?.name : userSelected?.role?.name!,
label: userSelected?.office_role ? userSelected?.office_role?.name : "Utilisateur restreint",
});
}, [userSelected?.office_role, userSelected?.role?.name, userSelected?.role?.uid]);
}, [userSelected?.office_role, userSelected?.role?.uid]);
const changeRole = useCallback(async () => {
await Users.getInstance().put(
@ -133,7 +133,7 @@ export default function CollaboratorInformations(props: IProps) {
setUserSelected(user);
setSelectedOption({
value: user?.office_role ? user?.office_role?.uid : user?.role?.uid,
label: user?.office_role ? user?.office_role?.name : user?.role?.name!,
label: user?.office_role ? user?.office_role?.name : "Utilisateur restreint",
});
}
@ -197,7 +197,6 @@ export default function CollaboratorInformations(props: IProps) {
})}
selectedOption={selectedOption!}
onChange={handleRoleChange}
disabled={userSelected?.role?.name === "super-admin"}
/>
</div>
{userSelected?.role?.name !== "super-admin" && (

View File

@ -72,6 +72,9 @@
border: 1px solid var(--grey);
.container-title {
display: flex;
gap: 8px;
align-items: center;
}
.documents {

View File

@ -19,6 +19,7 @@ import { useCallback, useEffect, useState } from "react";
import { MultiValue } from "react-select";
import classes from "./classes.module.scss";
import Tooltip from "@Front/Components/DesignSystem/ToolTip";
type IProps = {};
export default function DeedTypesInformations(props: IProps) {
@ -30,6 +31,7 @@ export default function DeedTypesInformations(props: IProps) {
const [selectedDocuments, setSelectedDocuments] = useState<IOption[]>([]);
const [isDeleteModalOpened, setIsDeleteModalOpened] = useState<boolean>(false);
const [isSaveModalOpened, setIsSaveModalOpened] = useState<boolean>(false);
const openDeleteModal = useCallback(() => {
setIsDeleteModalOpened(true);
@ -39,6 +41,14 @@ export default function DeedTypesInformations(props: IProps) {
setIsDeleteModalOpened(false);
}, []);
const openSaveModal = useCallback(() => {
setIsSaveModalOpened(true);
}, []);
const closeSaveModal = useCallback(() => {
setIsSaveModalOpened(false);
}, []);
const deleteDeedType = useCallback(async () => {
await DeedTypes.getInstance().put(
deedTypeUid as string,
@ -81,13 +91,18 @@ export default function DeedTypesInformations(props: IProps) {
const onSubmitHandler = useCallback(
async (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => {
openSaveModal();
},
[openSaveModal],
);
const saveDocumentTypes = useCallback(async () => {
await DeedTypes.getInstance().put(deedTypeUid as string, {
uid: deedTypeUid as string,
document_types: selectedDocuments.map((document) => DocumentType.hydrate<DocumentType>({ uid: document.value as string })),
});
},
[deedTypeUid, selectedDocuments],
);
closeSaveModal();
}, [closeSaveModal, deedTypeUid, selectedDocuments]);
const onDocumentChangeHandler = useCallback((values: MultiValue<IOption>) => {
setSelectedDocuments(values as IOption[]);
@ -137,7 +152,8 @@ export default function DeedTypesInformations(props: IProps) {
<div className={classes["documents-container"]}>
<Form onSubmit={onSubmitHandler}>
<div className={classes["container-title"]}>
<Typography typo={ITypo.P_SB_18}>Documents paramétrés</Typography>
<Typography typo={ITypo.P_SB_18}>Sélectionner les documents associés à ce type d'acte</Typography>
<Tooltip text="Si vous ne trouvez pas le document que vous souhaitez dans la liste, cliquez sur « Modifier la liste des documents » pour créer ce type de document à la liste" />
</div>
<div className={classes["documents"]}>
<MultiSelect
@ -171,6 +187,20 @@ export default function DeedTypesInformations(props: IProps) {
</Typography>
</div>
</Confirm>
<Confirm
isOpen={isSaveModalOpened}
onClose={closeSaveModal}
onAccept={saveDocumentTypes}
closeBtn
header={"Enregistrer les modifications ?"}
confirmText={"Enregistrer"}
cancelText={"Annuler"}>
<div className={classes["modal-content"]}>
<Typography typo={ITypo.P_16} className={classes["text"]}>
Les documents seront associés à ce type d'acte.
</Typography>
</div>
</Confirm>
</div>
</DefaultDeedTypesDashboard>
);

View File

@ -1,6 +1,18 @@
@import "@Themes/constants.scss";
.root {
.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
@media (max-width: $screen-l) {
flex-direction: column;
align-items: flex-start;
gap: 24px;
}
}
.document-infos {
display: flex;
align-items: flex-start;

View File

@ -1,3 +1,4 @@
import ChevronIcon from "@Assets/Icons/chevron.svg";
import PenICon from "@Assets/Icons/pen.svg";
import DocumentTypes from "@Front/Api/LeCoffreApi/Notary/DocumentTypes/DocumentTypes";
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
@ -10,6 +11,7 @@ import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import classes from "./classes.module.scss";
import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button";
export default function DocumentTypesInformations() {
const router = useRouter();
@ -33,8 +35,14 @@ export default function DocumentTypesInformations() {
return (
<DefaultDocumentTypesDashboard mobileBackText={"Liste des collaborateurs"}>
<div className={classes["root"]}>
<div className={classes["folder-header"]}>
<Typography typo={ITypo.H1Bis}>Paramétrage des documents</Typography>
<div className={classes["header"]}>
<Typography typo={ITypo.H1Bis}>Paramétrage des listes de pièces</Typography>
<Link href={Module.getInstance().get().modules.pages.DeedTypes.props.path}>
<Button variant={EButtonVariant.LINE}>
Retour au paramétrage des types d'actes
<Image src={ChevronIcon} alt="Chevron" />
</Button>
</Link>
</div>
<div className={classes["document-infos"]}>
<div className={classes["left"]}>

View File

@ -9,7 +9,6 @@ import Typography, { ITypo } from "@Front/Components/DesignSystem/Typography";
import BackArrow from "@Front/Components/Elements/BackArrow";
import DefaultNotaryDashboard from "@Front/Components/LayoutTemplates/DefaultNotaryDashboard";
import Module from "@Front/Config/Module";
import { TextField } from "@mui/material";
import { ECivility } from "le-coffre-resources/dist/Customer/Contact";
import { Customer, OfficeFolder } from "le-coffre-resources/dist/Notary";
import Link from "next/link";
@ -17,6 +16,7 @@ import { NextRouter, useRouter } from "next/router";
import BasePage from "../../Base";
import classes from "./classes.module.scss";
import TextField from "@Front/Components/DesignSystem/Form/TextField";
enum ESelectedOption {
EXISTING_CUSTOMER = "existing_customer",

View File

@ -34,6 +34,7 @@ type IState = {
inputArchivedDescripton: string;
isValidateModalVisible: boolean;
hasValidateAnchoring: boolean;
isVerifDeleteModalVisible: boolean;
};
class FolderInformationClass extends BasePage<IPropsClass, IState> {
public constructor(props: IPropsClass) {
@ -44,6 +45,7 @@ class FolderInformationClass extends BasePage<IPropsClass, IState> {
inputArchivedDescripton: "",
isValidateModalVisible: false,
hasValidateAnchoring: false,
isVerifDeleteModalVisible: false,
};
this.onSelectedFolder = this.onSelectedFolder.bind(this);
this.openArchivedModal = this.openArchivedModal.bind(this);
@ -55,6 +57,8 @@ class FolderInformationClass extends BasePage<IPropsClass, IState> {
this.closeModal = this.closeModal.bind(this);
this.validateAnchoring = this.validateAnchoring.bind(this);
this.openValidateModal = this.openValidateModal.bind(this);
this.openVerifDeleteFolder = this.openVerifDeleteFolder.bind(this);
this.closeVerifDeleteFolder = this.closeVerifDeleteFolder.bind(this);
}
// TODO: Message if the user has not created any folder yet
@ -109,7 +113,7 @@ class FolderInformationClass extends BasePage<IPropsClass, IState> {
</Button>
)}
{!this.doesFolderHaveCustomer() && (
<span className={classes["delete-folder"]} onClick={this.deleteFolder}>
<span className={classes["delete-folder"]} onClick={this.openVerifDeleteFolder}>
<Button variant={EButtonVariant.SECONDARY}>Supprimer le dossier</Button>
</span>
)}
@ -131,6 +135,18 @@ class FolderInformationClass extends BasePage<IPropsClass, IState> {
onChange={this.onArchivedDescriptionInputChange}
/>
</Confirm>
<Confirm
isOpen={this.state.isVerifDeleteModalVisible}
onAccept={this.deleteFolder}
onClose={this.closeVerifDeleteFolder}
closeBtn
header={"Êtes-vous sûr de vouloir supprimer ce dossier ?"}
cancelText={"Annuler"}
confirmText={"Confirmer"}>
<div className={classes["modal-title"]}>
<Typography typo={ITypo.P_16}>Cette action sera irréversible.</Typography>
</div>
</Confirm>
</div>
) : (
<div className={classes["no-folder-selected"]}>
@ -187,6 +203,18 @@ class FolderInformationClass extends BasePage<IPropsClass, IState> {
});
}
public openVerifDeleteFolder() {
this.setState({
isVerifDeleteModalVisible: true,
});
}
public closeVerifDeleteFolder() {
this.setState({
isVerifDeleteModalVisible: false,
});
}
private closeModal() {
this.setState({
isValidateModalVisible: false,

View File

@ -13,6 +13,7 @@ import { NextRouter, useRouter } from "next/router";
import BasePage from "../../Base";
import classes from "./classes.module.scss";
import DateField from "@Front/Components/DesignSystem/Form/DateField";
type IProps = {};
@ -43,6 +44,8 @@ class UpdateFolderMetadataClass extends BasePage<IPropsClass, IState> {
value: this.state.selectedFolder?.deed?.deed_type?.uid,
} as IOption;
const openingDate = new Date(this.state.selectedFolder?.created_at ?? "");
if (!this.state.selectedFolder?.created_at) return <></>;
const defaultValue = openingDate.toISOString().split("T")[0];
return (
<DefaultNotaryDashboard title={"Ajouter client(s)"} onSelectedFolder={this.onSelectedFolder}>
<div className={classes["root"]}>
@ -60,7 +63,7 @@ class UpdateFolderMetadataClass extends BasePage<IPropsClass, IState> {
defaultValue={this.state.selectedFolder?.folder_number}
/>
<Select name="deed" options={[]} placeholder={"Type d'acte"} selectedOption={deedOption} disabled />
<TextField placeholder="Ouverture du dossier" defaultValue={openingDate.toLocaleDateString("fr-FR")} disabled />
<DateField name="opening_date" placeholder="Ouverture du dossier" defaultValue={defaultValue} disabled />
</div>
<div className={classes["button-container"]}>

View File

@ -49,6 +49,7 @@ export default class ClientSection extends React.Component<IProps, IState> {
key={customer.uid}
isOpened={this.state.openedCustomer === customer.uid}
onChange={this.changeUserFolder}
isArchived
/>
);
});

View File

@ -12,6 +12,8 @@ import { useCallback, useState } from "react";
import classes from "./classes.module.scss";
import JwtService from "@Front/Services/JwtService/JwtService";
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
import { AppRuleActions, AppRuleNames } from "@Front/Api/Entities/rule";
type IProps = {};
export default function RolesCreate(props: IProps) {
@ -61,6 +63,14 @@ export default function RolesCreate(props: IProps) {
}, [hasChanged, redirect]);
return (
<Rules
mode={RulesMode.NECESSARY}
rules={[
{
action: AppRuleActions.create,
name: AppRuleNames.officeRoles,
},
]}>
<DefaultRolesDashboard mobileBackText={"Liste des rôles"} hasBackArrow title="Créer un rôle">
<div className={classes["root"]}>
<div className={classes["header"]}>
@ -91,5 +101,6 @@ export default function RolesCreate(props: IProps) {
</Confirm>
</div>
</DefaultRolesDashboard>
</Rules>
);
}

View File

@ -81,7 +81,11 @@ export default function UserInformations(props: IProps) {
useEffect(() => {
if (!userSelected) return;
setCurrentAppointment(userSelected?.appointment?.find((appointment) => appointment.status === EAppointmentStatus.OPEN && appointment.votes?.length != 0) ?? null);
setCurrentAppointment(
userSelected?.appointment?.find(
(appointment) => appointment.status === EAppointmentStatus.OPEN && appointment.votes?.length != 0,
) ?? null,
);
}, [userSelected]);
/** Functions for the admin modal */
@ -271,7 +275,7 @@ export default function UserInformations(props: IProps) {
onChange={handleRoleChange}
selectedOption={{
value: userSelected?.office_role ? userSelected?.office_role?.uid : userSelected?.role?.uid,
label: userSelected?.office_role ? userSelected?.office_role?.name : userSelected?.role?.label!,
label: userSelected?.office_role ? userSelected?.office_role?.name : "Utilisateur restreint",
}}
/>
</div>
@ -282,7 +286,12 @@ export default function UserInformations(props: IProps) {
</div>
<div className={classes["second-line"]}>
<Switch label="Admin de son office" checked={isAdminChecked} onChange={handleAdminChanged} />
<Switch label="Super-admin LeCoffre.io" checked={isSuperAdminChecked} disabled={userHasVoted()} onChange={handleSuperAdminChanged} />
<Switch
label="Super-admin LeCoffre.io"
checked={isSuperAdminChecked}
disabled={userHasVoted()}
onChange={handleSuperAdminChanged}
/>
{currentAppointment && (
<div className={classes["votes-block"]}>
<div className={classes["left"]}>

View File

@ -68,10 +68,8 @@ export default class JwtService {
}
public hasRule(name: string, action: string) {
const accessToken = CookieService.getInstance().getCookie("leCoffreAccessToken");
if (!accessToken) return false;
const decodedToken = this.decodeJwt();
if (!decodedToken) return false;
return decodedToken?.rules?.some((rule: string) => rule === `${action} ${name}`);
const token = this.decodeJwt();
if (!token) return false;
return token?.rules?.some((rule: string) => rule === `${action} ${name}`);
}
}

View File

@ -22,22 +22,6 @@ export async function middleware(request: NextRequest) {
return NextResponse.redirect(new URL("/login", request.url));
}
const requestUrlPath = request.nextUrl.pathname;
if (
requestUrlPath.startsWith("/collaborators") ||
requestUrlPath.startsWith("/deed-types") ||
requestUrlPath.startsWith("/customer") ||
requestUrlPath.startsWith("/offices") ||
requestUrlPath.startsWith("/roles") ||
requestUrlPath.startsWith("/users")
) {
if (userDecodedToken.role !== "admin" && userDecodedToken.role !== "super-admin")
return NextResponse.redirect(new URL("/404", request.url));
}
if ((requestUrlPath.startsWith("/my-account") || requestUrlPath.startsWith("/document-types")) && !userDecodedToken)
return NextResponse.redirect(new URL("/404", request.url));
if (requestUrlPath.startsWith("/client-dashboard") && !customerDecodedToken) return NextResponse.redirect(new URL("/404", request.url));
return NextResponse.next();
}