Merge branch 'staging' into preprod
This commit is contained in:
commit
a22123256e
947
package-lock.json
generated
947
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "PORT=5005 next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@heroicons/react": "^2.1.3",
|
||||
"@mui/material": "^5.11.13",
|
||||
"@types/node": "18.15.1",
|
||||
"@types/react": "18.0.28",
|
||||
@ -24,7 +25,7 @@
|
||||
"eslint-config-next": "13.2.4",
|
||||
"form-data": "^4.0.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.119",
|
||||
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.130",
|
||||
"next": "13.2.4",
|
||||
"prettier": "^2.8.7",
|
||||
"react": "18.2.0",
|
||||
|
@ -19,7 +19,7 @@ export default class Auth extends BaseApiService {
|
||||
try {
|
||||
return await fetch(url);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
this.onError(err);
|
||||
return Promise.reject(err);
|
||||
}
|
||||
@ -27,7 +27,11 @@ export default class Auth extends BaseApiService {
|
||||
|
||||
public async loginWithIdNot() {
|
||||
const variables = FrontendVariables.getInstance();
|
||||
const url = new URL(`${variables.IDNOT_BASE_URL + variables.IDNOT_AUTHORIZE_ENDPOINT}?client_id=${variables.IDNOT_CLIENT_ID}&redirect_uri=${variables.FRONT_APP_HOST}/authorized-client&scope=openid,profile&response_type=code`);
|
||||
const url = new URL(
|
||||
`${variables.IDNOT_BASE_URL + variables.IDNOT_AUTHORIZE_ENDPOINT}?client_id=${variables.IDNOT_CLIENT_ID}&redirect_uri=${
|
||||
variables.FRONT_APP_HOST
|
||||
}/authorized-client&scope=openid,profile&response_type=code`,
|
||||
);
|
||||
try {
|
||||
return await this.getRequest(url);
|
||||
} catch (err) {
|
||||
@ -36,16 +40,15 @@ export default class Auth extends BaseApiService {
|
||||
}
|
||||
}
|
||||
|
||||
public async getIdnotJwt(autorizationCode: string | string[]): Promise<{accessToken: string, refreshToken: string}> {
|
||||
public async getIdnotJwt(autorizationCode: string | string[]): Promise<{ accessToken: string; refreshToken: string }> {
|
||||
const variables = FrontendVariables.getInstance();
|
||||
const baseBackUrl = variables.BACK_API_PROTOCOL + variables.BACK_API_HOST;
|
||||
const url = new URL(`${baseBackUrl}/api/v1/idnot/user/${autorizationCode}`);
|
||||
try {
|
||||
return await this.postRequest<{accessToken: string, refreshToken: string}>(url);
|
||||
return await this.postRequest<{ accessToken: string; refreshToken: string }>(url);
|
||||
} catch (err) {
|
||||
this.onError(err);
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -18,4 +18,6 @@ export enum AppRuleNames {
|
||||
offices = "offices",
|
||||
documents = "documents",
|
||||
rib = "rib",
|
||||
subscriptions = "subscriptions",
|
||||
stripe = "stripe",
|
||||
}
|
||||
|
67
src/front/Api/LeCoffreApi/Admin/Stripe/Stripe.ts
Normal file
67
src/front/Api/LeCoffreApi/Admin/Stripe/Stripe.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { EType } from "le-coffre-resources/dist/Admin/Subscription";
|
||||
import BaseAdmin from "../BaseAdmin";
|
||||
|
||||
export interface IPostStripeParams {
|
||||
type: EType;
|
||||
nb_seats: number;
|
||||
}
|
||||
|
||||
export type IPostStripeResponse = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type IGetClientPortalSessionResponse = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export interface IGetCustomerBySubscriptionIdParams {
|
||||
email: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class Stripe extends BaseAdmin {
|
||||
private static instance: Stripe;
|
||||
private readonly baseURl = this.namespaceUrl.concat("/stripe");
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public static getInstance() {
|
||||
if (!this.instance) {
|
||||
return new Stripe();
|
||||
} else {
|
||||
return this.instance;
|
||||
}
|
||||
}
|
||||
|
||||
public async post(body: IPostStripeParams) {
|
||||
const url = new URL(this.baseURl);
|
||||
try {
|
||||
return await this.postRequest<IPostStripeResponse>(url, body as any);
|
||||
} catch (err) {
|
||||
this.onError(err);
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
public async getClientPortalSession(stripe_subscription_id: string) {
|
||||
const url = new URL(this.baseURl.concat(`/${stripe_subscription_id}`));
|
||||
try {
|
||||
return await this.getRequest<IGetClientPortalSessionResponse>(url);
|
||||
} catch (err) {
|
||||
this.onError(err);
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
public async getCustomerBySubscriptionId(subscriptionId: string) {
|
||||
const url = new URL(this.baseURl.concat(`/${subscriptionId}/customer`));
|
||||
try {
|
||||
return await this.getRequest<IGetCustomerBySubscriptionIdParams>(url);
|
||||
} catch (err) {
|
||||
this.onError(err);
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import { Subscription } from "le-coffre-resources/dist/Admin";
|
||||
import BaseAdmin from "../../../../../common/Api/LeCoffreApi/Admin/BaseAdmin";
|
||||
|
||||
export interface IPostSubscriptionsParams {
|
||||
emails: string[];
|
||||
}
|
||||
|
||||
export interface IPutSubscriptionsParams {
|
||||
seats: any;
|
||||
}
|
||||
|
||||
export default class Subscriptions extends BaseAdmin {
|
||||
private static instance: Subscriptions;
|
||||
private readonly baseURl = this.namespaceUrl.concat("/subscriptions");
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public static getInstance() {
|
||||
if (!this.instance) {
|
||||
return new this();
|
||||
} else {
|
||||
return this.instance;
|
||||
}
|
||||
}
|
||||
|
||||
public async get(q: any) {
|
||||
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 {
|
||||
return await this.getRequest<Subscription[]>(url);
|
||||
} catch (err) {
|
||||
this.onError(err);
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description : Invite collaborators to a subscription
|
||||
*/
|
||||
public async post(body: IPostSubscriptionsParams) {
|
||||
const url = new URL(this.baseURl.concat(`/invite`));
|
||||
try {
|
||||
return await this.postRequest(url, body as any);
|
||||
} catch (err) {
|
||||
this.onError(err);
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description : Update a Subscription
|
||||
*/
|
||||
public async put(uid: string, body: IPutSubscriptionsParams) {
|
||||
const url = new URL(this.baseURl.concat(`/${uid}`));
|
||||
try {
|
||||
return await this.putRequest(url, body as any);
|
||||
} catch (err) {
|
||||
this.onError(err);
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ export type IBlock = {
|
||||
name: string;
|
||||
id: string;
|
||||
selected: boolean;
|
||||
rightIcon?: JSX.Element;
|
||||
hasFlag?: boolean;
|
||||
};
|
||||
|
||||
@ -35,6 +36,7 @@ export default function BlockList({ blocks, onSelectedBlock }: IProps) {
|
||||
<div className={classes["right-side"]}>
|
||||
{folder.hasFlag && <WarningBadge />}
|
||||
<Image alt="chevron" src={ChevronIcon} />
|
||||
{folder.rightIcon && folder.rightIcon}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -96,6 +96,10 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&[fullwidthattr="false"] {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
&[touppercase="false"] {
|
||||
text-transform: inherit;
|
||||
}
|
||||
|
@ -4,6 +4,9 @@
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
appearance: none;
|
||||
@ -15,6 +18,10 @@
|
||||
margin-right: 16px;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"]::before {
|
||||
|
@ -4,6 +4,7 @@ import { IOption } from "../Form/SelectField";
|
||||
import Tooltip from "../ToolTip";
|
||||
import Typography, { ITypo, ITypoColor } from "../Typography";
|
||||
import classes from "./classes.module.scss";
|
||||
import classNames from "classnames";
|
||||
|
||||
type IProps = {
|
||||
name?: string;
|
||||
@ -11,6 +12,7 @@ type IProps = {
|
||||
toolTip?: string;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
checked: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type IState = {
|
||||
@ -21,6 +23,7 @@ export default class CheckBox extends React.Component<IProps, IState> {
|
||||
static defaultProps = {
|
||||
toolTip: "",
|
||||
checked: false,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
constructor(props: IProps) {
|
||||
@ -35,13 +38,14 @@ export default class CheckBox extends React.Component<IProps, IState> {
|
||||
public override render(): JSX.Element {
|
||||
return (
|
||||
<Typography typo={ITypo.P_ERR_16} color={ITypoColor.BLACK}>
|
||||
<label className={classes["root"]}>
|
||||
<label className={classNames(classes["root"], this.props.disabled && classes["disabled"])}>
|
||||
<input
|
||||
type="checkbox"
|
||||
name={this.props.name ?? (this.props.option.value as string)}
|
||||
value={this.props.option.value as string}
|
||||
onChange={this.onChange}
|
||||
checked={this.state.checked}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
{this.props.option.label}
|
||||
{this.props.toolTip && <Tooltip className={classes["tooltip"]} text={this.props.toolTip} />}
|
||||
|
@ -421,7 +421,6 @@ export default class DepositDocument extends React.Component<IProps, IState> {
|
||||
}
|
||||
} catch (e) {
|
||||
this.setState({ loading: false });
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,14 +85,12 @@ export default class DepositRib extends React.Component<IProps, IState> {
|
||||
// formData.append("file", this.state.documents[0]!, this.state.documents[0]!.name);
|
||||
|
||||
// const sentFile = await Bucket.getInstance().post(formData);
|
||||
// console.log("Sent file:", sentFile);
|
||||
|
||||
// // Reset documents state
|
||||
// this.setState({ documents: [] });
|
||||
// };
|
||||
|
||||
// handleCancel = () => {
|
||||
// console.log("Cancel:", this.state.documents);
|
||||
// // Reset documents state
|
||||
// this.setState({ documents: [] });
|
||||
// };
|
||||
|
@ -0,0 +1,18 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
.content {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sub-menu {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
gap: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
import classNames from "classnames";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import classes from "./classes.module.scss";
|
||||
import { IAppRule } from "@Front/Api/Entities/rule";
|
||||
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
|
||||
import { IHeaderLinkProps } from "../../../HeaderLink";
|
||||
import Typography, { ITypo } from "@Front/Components/DesignSystem/Typography";
|
||||
import HeaderSubmenuLink from "../../../HeaderSubmenu/HeaderSubmenuLink";
|
||||
import useToggle from "@Front/Hooks/useToggle";
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
type IProps = {
|
||||
text: string | JSX.Element;
|
||||
links: (IHeaderLinkProps & {
|
||||
rules?: IAppRule[];
|
||||
})[];
|
||||
};
|
||||
|
||||
export default function HeaderSubmenu(props: IProps) {
|
||||
const router = useRouter();
|
||||
const { pathname } = router;
|
||||
const [isActive, setIsActive] = useState(true);
|
||||
const { active: isSubmenuOpened, toggle } = useToggle();
|
||||
|
||||
useEffect(() => {
|
||||
setIsActive(false);
|
||||
if (props.links.some((link) => link.path === pathname)) setIsActive(true);
|
||||
if (props.links.some((link) => link.routesActive?.some((routeActive) => pathname.includes(routeActive)))) setIsActive(true);
|
||||
}, [isActive, pathname, props.links]);
|
||||
|
||||
return (
|
||||
<Rules mode={RulesMode.OPTIONAL} rules={props.links.flatMap((link) => link.rules ?? [])}>
|
||||
<div className={classes["container"]}>
|
||||
<div className={classNames(classes["root"], (isActive || isSubmenuOpened) && classes["active"])}>
|
||||
<div className={classes["content"]} onClick={toggle}>
|
||||
<Typography typo={isActive || isSubmenuOpened ? ITypo.P_SB_18 : ITypo.NAV_HEADER_18}>{props.text}</Typography>
|
||||
{isSubmenuOpened ? <ChevronUpIcon height="20" width="20" /> : <ChevronDownIcon height="20" width="20" />}
|
||||
</div>
|
||||
<div className={classes["underline"]} data-active={(isActive || isSubmenuOpened).toString()} />
|
||||
{isSubmenuOpened && (
|
||||
<div className={classes["sub-menu"]}>
|
||||
{props.links.map((link) => (
|
||||
<Rules mode={RulesMode.NECESSARY} rules={link.rules ?? []} key={link.path}>
|
||||
<HeaderSubmenuLink {...link} />
|
||||
</Rules>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Rules>
|
||||
);
|
||||
}
|
@ -11,6 +11,8 @@
|
||||
width: 100%;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
max-height: calc(100vh - 83px);
|
||||
overflow: auto;
|
||||
> *:not(:last-child) {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
@ -4,8 +4,8 @@ import React from "react";
|
||||
|
||||
import NavigationLink from "../../NavigationLink";
|
||||
import classes from "./classes.module.scss";
|
||||
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
|
||||
import { AppRuleActions, AppRuleNames } from "@Front/Api/Entities/rule";
|
||||
import BurgerModalSubmenu from "./BurgerModalSubmenu";
|
||||
|
||||
type IProps = {
|
||||
isOpen: boolean;
|
||||
@ -34,111 +34,120 @@ export default class BurgerModal extends React.Component<IProps, IState> {
|
||||
text="Dossiers archivés"
|
||||
routesActive={[Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path]}
|
||||
/>
|
||||
<Rules
|
||||
mode={RulesMode.NECESSARY}
|
||||
rules={[
|
||||
<div className={classes["separator"]} />
|
||||
|
||||
<BurgerModalSubmenu
|
||||
text={"Espace super admin"}
|
||||
links={[
|
||||
{
|
||||
text: "Gestion des utilisateurs",
|
||||
path: Module.getInstance().get().modules.pages.Users.props.path,
|
||||
routesActive: [
|
||||
Module.getInstance().get().modules.pages.Users.pages.UsersInformations.props.path,
|
||||
Module.getInstance().get().modules.pages.Users.props.path,
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.offices,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Gestion des offices",
|
||||
path: Module.getInstance().get().modules.pages.Offices.props.path,
|
||||
routesActive: [
|
||||
Module.getInstance().get().modules.pages.Offices.pages.OfficesInformations.props.path,
|
||||
Module.getInstance().get().modules.pages.Offices.props.path,
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.offices,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<BurgerModalSubmenu
|
||||
text="Espace office"
|
||||
links={[
|
||||
{
|
||||
text: "Collaborateurs",
|
||||
path: Module.getInstance().get().modules.pages.Collaborators.props.path,
|
||||
routesActive: [
|
||||
Module.getInstance().get().modules.pages.Collaborators.pages.CollaboratorInformations.props.path,
|
||||
Module.getInstance().get().modules.pages.Collaborators.props.path,
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
action: AppRuleActions.read,
|
||||
name: AppRuleNames.users,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Gestion des rôles",
|
||||
path: Module.getInstance().get().modules.pages.Roles.props.path,
|
||||
routesActive: [
|
||||
Module.getInstance().get().modules.pages.Roles.pages.Create.props.path,
|
||||
Module.getInstance().get().modules.pages.Roles.pages.RolesInformations.props.path,
|
||||
Module.getInstance().get().modules.pages.Roles.props.path,
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.officeRoles,
|
||||
},
|
||||
]}>
|
||||
<NavigationLink
|
||||
path={Module.getInstance().get().modules.pages.Collaborators.props.path}
|
||||
text="Collaborateurs"
|
||||
routesActive={[Module.getInstance().get().modules.pages.Collaborators.props.path]}
|
||||
/>
|
||||
</Rules>
|
||||
<Rules
|
||||
mode={RulesMode.NECESSARY}
|
||||
rules={[
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Paramétrage des listes de pièces",
|
||||
path: Module.getInstance().get().modules.pages.DeedTypes.props.path,
|
||||
routesActive: [
|
||||
Module.getInstance().get().modules.pages.DeedTypes.pages.Create.props.path,
|
||||
Module.getInstance().get().modules.pages.DeedTypes.pages.Edit.props.path,
|
||||
Module.getInstance().get().modules.pages.DeedTypes.props.path,
|
||||
Module.getInstance().get().modules.pages.DocumentTypes.props.path,
|
||||
Module.getInstance().get().modules.pages.DocumentTypes.pages.Create.props.path,
|
||||
Module.getInstance().get().modules.pages.DocumentTypes.pages.Edit.props.path,
|
||||
Module.getInstance().get().modules.pages.DocumentTypes.pages.DocumentTypesInformations.props.path,
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.deedTypes,
|
||||
},
|
||||
]}>
|
||||
<NavigationLink
|
||||
path={Module.getInstance().get().modules.pages.DeedTypes.props.path}
|
||||
text="Paramétrage des listes de pièces"
|
||||
routesActive={[
|
||||
Module.getInstance().get().modules.pages.DeedTypes.props.path,
|
||||
Module.getInstance().get().modules.pages.DeedTypes.pages.Create.props.path,
|
||||
Module.getInstance().get().modules.pages.DeedTypes.pages.DeedTypesInformations.props.path,
|
||||
Module.getInstance().get().modules.pages.DeedTypes.pages.Edit.props.path,
|
||||
Module.getInstance().get().modules.pages.DocumentTypes.pages.Edit.props.path,
|
||||
Module.getInstance().get().modules.pages.DocumentTypes.pages.Create.props.path,
|
||||
Module.getInstance().get().modules.pages.DocumentTypes.pages.DocumentTypesInformations.props.path,
|
||||
Module.getInstance().get().modules.pages.DocumentTypes.props.path,
|
||||
]}
|
||||
/>
|
||||
</Rules>
|
||||
<Rules
|
||||
mode={RulesMode.NECESSARY}
|
||||
rules={[
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.officeRoles,
|
||||
],
|
||||
},
|
||||
]}>
|
||||
<NavigationLink
|
||||
path={Module.getInstance().get().modules.pages.Roles.props.path}
|
||||
text="Gestion des rôles"
|
||||
routesActive={[
|
||||
Module.getInstance().get().modules.pages.Roles.props.path,
|
||||
Module.getInstance().get().modules.pages.Roles.pages.RolesInformations.props.path,
|
||||
]}
|
||||
/>
|
||||
</Rules>
|
||||
<Rules
|
||||
mode={RulesMode.NECESSARY}
|
||||
rules={[
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.offices,
|
||||
},
|
||||
]}>
|
||||
<NavigationLink
|
||||
path={Module.getInstance().get().modules.pages.Users.props.path}
|
||||
text="Gestion des utilisateurs"
|
||||
routesActive={[
|
||||
Module.getInstance().get().modules.pages.Users.props.path,
|
||||
Module.getInstance().get().modules.pages.Users.pages.UsersInformations.props.path,
|
||||
]}
|
||||
/>
|
||||
</Rules>
|
||||
<Rules
|
||||
mode={RulesMode.NECESSARY}
|
||||
rules={[
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.offices,
|
||||
},
|
||||
]}>
|
||||
<NavigationLink
|
||||
path={Module.getInstance().get().modules.pages.Offices.props.path}
|
||||
text="Gestion des offices"
|
||||
routesActive={[
|
||||
Module.getInstance().get().modules.pages.Offices.props.path,
|
||||
Module.getInstance().get().modules.pages.Offices.pages.OfficesInformations.props.path,
|
||||
]}
|
||||
/>
|
||||
</Rules>
|
||||
<Rules
|
||||
mode={RulesMode.NECESSARY}
|
||||
rules={[
|
||||
text: "RIB Office",
|
||||
path: Module.getInstance().get().modules.pages.OfficesRib.props.path,
|
||||
rules: [
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.rib,
|
||||
},
|
||||
]}>
|
||||
<NavigationLink
|
||||
path={Module.getInstance().get().modules.pages.OfficesRib.props.path}
|
||||
text="Gestion du RIB"
|
||||
routesActive={[Module.getInstance().get().modules.pages.OfficesRib.props.path]}
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Abonnement",
|
||||
path: Module.getInstance().get().modules.pages.Subscription.pages.Manage.props.path,
|
||||
routesActive: [
|
||||
Module.getInstance().get().modules.pages.Subscription.pages.Error.props.path,
|
||||
Module.getInstance().get().modules.pages.Subscription.pages.Success.props.path,
|
||||
Module.getInstance().get().modules.pages.Subscription.pages.Invite.props.path,
|
||||
Module.getInstance().get().modules.pages.Subscription.pages.Manage.props.path,
|
||||
Module.getInstance().get().modules.pages.Subscription.pages.ManageCollaborators.props.path,
|
||||
Module.getInstance().get().modules.pages.Subscription.pages.New.props.path,
|
||||
Module.getInstance().get().modules.pages.Subscription.pages.Subscribe.props.path,
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Rules>
|
||||
<div className={classes["separator"]} />
|
||||
<NavigationLink path={Module.getInstance().get().modules.pages.MyAccount.props.path} text="Mon compte" />
|
||||
<NavigationLink target="_blank" path="/CGU_LeCoffre_io.pdf" text="CGU" />
|
||||
<div className={classes["separator"]} />
|
||||
<LogOutButton />
|
||||
</div>
|
||||
</>
|
||||
|
@ -14,10 +14,14 @@
|
||||
.underline {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: $black;
|
||||
background-color: $white;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
&[data-active="true"] {
|
||||
background-color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
&.desactivated {
|
||||
|
@ -1,54 +1,54 @@
|
||||
import classNames from "classnames";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import Typography, { ITypo } from "../../Typography";
|
||||
import classes from "./classes.module.scss";
|
||||
import useHoverable from "@Front/Hooks/useHoverable";
|
||||
|
||||
type IProps = {
|
||||
export type IHeaderLinkProps = {
|
||||
text: string | JSX.Element;
|
||||
path: string;
|
||||
isActive?: boolean;
|
||||
routesActive?: string[];
|
||||
};
|
||||
|
||||
type IPropsClass = IProps;
|
||||
export default function HeaderLink(props: IHeaderLinkProps) {
|
||||
const router = useRouter();
|
||||
const { pathname } = router;
|
||||
const [isActive, setIsActive] = React.useState(props.path === pathname);
|
||||
const { handleMouseLeave, handleMouseEnter, isHovered } = useHoverable();
|
||||
|
||||
type IStateClass = {};
|
||||
useEffect(() => {
|
||||
if (props.path === pathname) setIsActive(true);
|
||||
if (props.routesActive) {
|
||||
for (const routeActive of props.routesActive) {
|
||||
if (isActive) break;
|
||||
if (pathname.includes(routeActive)) setIsActive(true);
|
||||
}
|
||||
}
|
||||
}, [isActive, pathname, props.path, props.routesActive]);
|
||||
|
||||
class HeaderLinkClass extends React.Component<IPropsClass, IStateClass> {
|
||||
public override render(): JSX.Element {
|
||||
if (this.props.path !== "" && this.props.path !== undefined) {
|
||||
if (props.path !== "" && props.path !== undefined) {
|
||||
return (
|
||||
<Link href={this.props.path} className={classNames(classes["root"], this.props.isActive && classes["active"])}>
|
||||
<Link
|
||||
href={props.path}
|
||||
className={classNames(classes["root"], isActive && classes["active"])}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}>
|
||||
<div className={classes["content"]}>
|
||||
<Typography typo={this.props.isActive ? ITypo.P_SB_18 : ITypo.NAV_HEADER_18}>{this.props.text}</Typography>
|
||||
<Typography typo={isActive || isHovered ? ITypo.P_SB_18 : ITypo.NAV_HEADER_18}>{props.text}</Typography>
|
||||
</div>
|
||||
{this.props.isActive && <div className={classes["underline"]} />}
|
||||
<div className={classes["underline"]} data-active={(isActive || isHovered).toString()} />
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className={classNames(classes["root"], classes["desactivated"])}>
|
||||
<div className={classes["content"]}>
|
||||
<Typography typo={ITypo.NAV_HEADER_18}>{this.props.text}</Typography>
|
||||
<Typography typo={ITypo.NAV_HEADER_18}>{props.text}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function HeaderLink(props: IProps) {
|
||||
const router = useRouter();
|
||||
const { pathname } = router;
|
||||
let isActive = props.path === pathname;
|
||||
if(props.routesActive){
|
||||
for (const routeActive of props.routesActive) {
|
||||
if (isActive) break;
|
||||
isActive = pathname.includes(routeActive);
|
||||
}
|
||||
}
|
||||
return <HeaderLinkClass {...props} isActive={isActive} />;
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import Typography, { ITypo } from "@Front/Components/DesignSystem/Typography";
|
||||
import useHoverable from "@Front/Hooks/useHoverable";
|
||||
|
||||
type IHeaderLinkProps = {
|
||||
text: string | JSX.Element;
|
||||
path: string;
|
||||
routesActive?: string[];
|
||||
};
|
||||
|
||||
export default function HeaderSubmenuLink(props: IHeaderLinkProps) {
|
||||
const router = useRouter();
|
||||
const { pathname } = router;
|
||||
const [isActive, setIsActive] = React.useState(props.path === pathname);
|
||||
const { handleMouseLeave, handleMouseEnter, isHovered } = useHoverable();
|
||||
|
||||
useEffect(() => {
|
||||
if (props.path === pathname) setIsActive(true);
|
||||
if (props.routesActive) {
|
||||
for (const routeActive of props.routesActive) {
|
||||
if (isActive) break;
|
||||
if (pathname.includes(routeActive)) setIsActive(true);
|
||||
}
|
||||
}
|
||||
}, [isActive, pathname, props.path, props.routesActive]);
|
||||
|
||||
return (
|
||||
<Link href={props.path} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
<Typography typo={isActive || isHovered ? ITypo.P_SB_18 : ITypo.NAV_HEADER_18}>{props.text}</Typography>
|
||||
</Link>
|
||||
);
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
height: 83px;
|
||||
padding: 10px 16px;
|
||||
.content {
|
||||
margin: auto;
|
||||
}
|
||||
.underline {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: $white;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
&[data-active="true"] {
|
||||
background-color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
&.desactivated {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.sub-menu {
|
||||
box-shadow: 0px 8px 10px 0px #00000012;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
gap: 24px;
|
||||
left: 0;
|
||||
transform: translateX(-25%);
|
||||
width: 300px;
|
||||
top: 112px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: white;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
import classNames from "classnames";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { IHeaderLinkProps } from "../HeaderLink";
|
||||
|
||||
import Typography, { ITypo } from "../../Typography";
|
||||
import classes from "./classes.module.scss";
|
||||
import useHoverable from "@Front/Hooks/useHoverable";
|
||||
import HeaderSubmenuLink from "./HeaderSubmenuLink";
|
||||
import { IAppRule } from "@Front/Api/Entities/rule";
|
||||
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
|
||||
|
||||
type IProps = {
|
||||
text: string | JSX.Element;
|
||||
links: (IHeaderLinkProps & {
|
||||
rules?: IAppRule[];
|
||||
})[];
|
||||
};
|
||||
|
||||
export default function HeaderSubmenu(props: IProps) {
|
||||
const router = useRouter();
|
||||
const { pathname } = router;
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const { handleMouseLeave, handleMouseEnter, isHovered } = useHoverable(100);
|
||||
|
||||
useEffect(() => {
|
||||
setIsActive(false);
|
||||
if (props.links.some((link) => link.path === pathname)) setIsActive(true);
|
||||
if (props.links.some((link) => link.routesActive?.some((routeActive) => pathname.includes(routeActive)))) setIsActive(true);
|
||||
}, [isActive, pathname, props.links]);
|
||||
|
||||
return (
|
||||
<Rules mode={RulesMode.OPTIONAL} rules={props.links.flatMap((link) => link.rules ?? [])}>
|
||||
<div className={classes["container"]} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
<div className={classNames(classes["root"], (isActive || isHovered) && classes["active"])}>
|
||||
<div className={classes["content"]}>
|
||||
<Typography typo={isActive || isHovered ? ITypo.P_SB_18 : ITypo.NAV_HEADER_18}>{props.text}</Typography>
|
||||
</div>
|
||||
<div className={classes["underline"]} data-active={(isActive || isHovered).toString()} />
|
||||
{isHovered && (
|
||||
<div className={classes["sub-menu"]}>
|
||||
{props.links.map((link) => (
|
||||
<Rules mode={RulesMode.NECESSARY} rules={link.rules ?? []} key={link.path}>
|
||||
<HeaderSubmenuLink {...link} />
|
||||
</Rules>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Rules>
|
||||
);
|
||||
}
|
@ -9,6 +9,7 @@ import { usePathname } from "next/navigation";
|
||||
import Notifications from "@Front/Api/LeCoffreApi/Notary/Notifications/Notifications";
|
||||
import Toasts from "@Front/Stores/Toasts";
|
||||
import OfficeFolderAnchors from "@Front/Api/LeCoffreApi/Notary/OfficeFolderAnchors/OfficeFolderAnchors";
|
||||
import HeaderSubmenu from "../HeaderSubmenu";
|
||||
export default function Navigation() {
|
||||
const pathname = usePathname();
|
||||
|
||||
@ -29,7 +30,7 @@ export default function Navigation() {
|
||||
await OfficeFolderAnchors.getInstance().getByUid(anchor.folder?.uid as string);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.error(e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@ -40,11 +41,11 @@ export default function Navigation() {
|
||||
read: false,
|
||||
},
|
||||
include: {
|
||||
notification: true
|
||||
notification: true,
|
||||
},
|
||||
orderBy: {
|
||||
notification: { created_at: "desc" },
|
||||
}
|
||||
},
|
||||
});
|
||||
notifications.forEach((notification) => {
|
||||
Toasts.getInstance().open({
|
||||
@ -83,10 +84,129 @@ export default function Navigation() {
|
||||
name: AppRuleNames.officeRoles,
|
||||
},
|
||||
]}>
|
||||
<HeaderLink
|
||||
text={"Collaborateurs"}
|
||||
path={Module.getInstance().get().modules.pages.Collaborators.props.path}
|
||||
routesActive={[Module.getInstance().get().modules.pages.Collaborators.props.path]}
|
||||
<HeaderSubmenu
|
||||
text={"Espace office"}
|
||||
links={[
|
||||
{
|
||||
text: "Collaborateurs",
|
||||
path: Module.getInstance().get().modules.pages.Collaborators.props.path,
|
||||
routesActive: [
|
||||
Module.getInstance().get().modules.pages.Collaborators.pages.CollaboratorInformations.props.path,
|
||||
Module.getInstance().get().modules.pages.Collaborators.props.path,
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
action: AppRuleActions.read,
|
||||
name: AppRuleNames.users,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Gestion des rôles",
|
||||
path: Module.getInstance().get().modules.pages.Roles.props.path,
|
||||
routesActive: [
|
||||
Module.getInstance().get().modules.pages.Roles.pages.Create.props.path,
|
||||
Module.getInstance().get().modules.pages.Roles.pages.RolesInformations.props.path,
|
||||
Module.getInstance().get().modules.pages.Roles.props.path,
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.officeRoles,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Paramétrage des listes de pièces",
|
||||
path: Module.getInstance().get().modules.pages.DeedTypes.props.path,
|
||||
routesActive: [
|
||||
Module.getInstance().get().modules.pages.DeedTypes.pages.Create.props.path,
|
||||
Module.getInstance().get().modules.pages.DeedTypes.pages.Edit.props.path,
|
||||
Module.getInstance().get().modules.pages.DeedTypes.props.path,
|
||||
Module.getInstance().get().modules.pages.DocumentTypes.props.path,
|
||||
Module.getInstance().get().modules.pages.DocumentTypes.pages.Create.props.path,
|
||||
Module.getInstance().get().modules.pages.DocumentTypes.pages.Edit.props.path,
|
||||
Module.getInstance().get().modules.pages.DocumentTypes.pages.DocumentTypesInformations.props.path,
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.deedTypes,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "RIB Office",
|
||||
path: Module.getInstance().get().modules.pages.OfficesRib.props.path,
|
||||
rules: [
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.rib,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Abonnement",
|
||||
path: Module.getInstance().get().modules.pages.Subscription.pages.Manage.props.path,
|
||||
routesActive: [
|
||||
Module.getInstance().get().modules.pages.Subscription.pages.Error.props.path,
|
||||
Module.getInstance().get().modules.pages.Subscription.pages.Success.props.path,
|
||||
Module.getInstance().get().modules.pages.Subscription.pages.Invite.props.path,
|
||||
Module.getInstance().get().modules.pages.Subscription.pages.Manage.props.path,
|
||||
Module.getInstance().get().modules.pages.Subscription.pages.ManageCollaborators.props.path,
|
||||
Module.getInstance().get().modules.pages.Subscription.pages.New.props.path,
|
||||
Module.getInstance().get().modules.pages.Subscription.pages.Subscribe.props.path,
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.subscriptions,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Rules>
|
||||
<Rules
|
||||
mode={RulesMode.NECESSARY}
|
||||
rules={[
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.officeRoles,
|
||||
},
|
||||
]}>
|
||||
<HeaderSubmenu
|
||||
text={"Espace super admin"}
|
||||
links={[
|
||||
{
|
||||
text: "Gestion des utilisateurs",
|
||||
path: Module.getInstance().get().modules.pages.Users.props.path,
|
||||
routesActive: [
|
||||
Module.getInstance().get().modules.pages.Users.pages.UsersInformations.props.path,
|
||||
Module.getInstance().get().modules.pages.Users.props.path,
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.offices,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Gestion des offices",
|
||||
path: Module.getInstance().get().modules.pages.Offices.props.path,
|
||||
routesActive: [
|
||||
Module.getInstance().get().modules.pages.Offices.pages.OfficesInformations.props.path,
|
||||
Module.getInstance().get().modules.pages.Offices.props.path,
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.offices,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Rules>
|
||||
</div>
|
||||
|
@ -4,8 +4,6 @@ import React from "react";
|
||||
|
||||
import NavigationLink from "../../NavigationLink";
|
||||
import classes from "./classes.module.scss";
|
||||
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
|
||||
import { AppRuleActions, AppRuleNames } from "@Front/Api/Entities/rule";
|
||||
|
||||
type IProps = {
|
||||
isOpen: boolean;
|
||||
@ -22,94 +20,6 @@ export default class ProfileModal extends React.Component<IProps, IState> {
|
||||
<div className={classes["background"]} onClick={this.props.closeModal} />
|
||||
<div className={classes["root"]}>
|
||||
<NavigationLink path={Module.getInstance().get().modules.pages.MyAccount.props.path} text="Mon compte" />
|
||||
<Rules
|
||||
mode={RulesMode.NECESSARY}
|
||||
rules={[
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.officeRoles,
|
||||
},
|
||||
]}>
|
||||
<NavigationLink
|
||||
path={Module.getInstance().get().modules.pages.Roles.props.path}
|
||||
text="Gestion des rôles"
|
||||
routesActive={[
|
||||
Module.getInstance().get().modules.pages.Roles.props.path,
|
||||
Module.getInstance().get().modules.pages.Roles.pages.RolesInformations.props.path,
|
||||
]}
|
||||
/>
|
||||
</Rules>
|
||||
<Rules
|
||||
mode={RulesMode.NECESSARY}
|
||||
rules={[
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.deedTypes,
|
||||
},
|
||||
]}>
|
||||
<NavigationLink
|
||||
path={Module.getInstance().get().modules.pages.DeedTypes.props.path}
|
||||
text="Paramétrage des listes de pièces"
|
||||
routesActive={[
|
||||
Module.getInstance().get().modules.pages.DeedTypes.props.path,
|
||||
Module.getInstance().get().modules.pages.DeedTypes.pages.Create.props.path,
|
||||
Module.getInstance().get().modules.pages.DeedTypes.pages.DeedTypesInformations.props.path,
|
||||
Module.getInstance().get().modules.pages.DeedTypes.pages.Edit.props.path,
|
||||
Module.getInstance().get().modules.pages.DocumentTypes.pages.Edit.props.path,
|
||||
Module.getInstance().get().modules.pages.DocumentTypes.pages.Create.props.path,
|
||||
Module.getInstance().get().modules.pages.DocumentTypes.pages.DocumentTypesInformations.props.path,
|
||||
Module.getInstance().get().modules.pages.DocumentTypes.props.path,
|
||||
]}
|
||||
/>
|
||||
</Rules>
|
||||
<Rules
|
||||
mode={RulesMode.NECESSARY}
|
||||
rules={[
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.offices,
|
||||
},
|
||||
]}>
|
||||
<NavigationLink
|
||||
path={Module.getInstance().get().modules.pages.Users.props.path}
|
||||
text="Gestion des utilisateurs"
|
||||
routesActive={[
|
||||
Module.getInstance().get().modules.pages.Users.props.path,
|
||||
Module.getInstance().get().modules.pages.Users.pages.UsersInformations.props.path,
|
||||
]}
|
||||
/>
|
||||
</Rules>
|
||||
<Rules
|
||||
mode={RulesMode.NECESSARY}
|
||||
rules={[
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.offices,
|
||||
},
|
||||
]}>
|
||||
<NavigationLink
|
||||
path={Module.getInstance().get().modules.pages.Offices.props.path}
|
||||
text="Gestion des offices"
|
||||
routesActive={[
|
||||
Module.getInstance().get().modules.pages.Offices.props.path,
|
||||
Module.getInstance().get().modules.pages.Offices.pages.OfficesInformations.props.path,
|
||||
]}
|
||||
/>
|
||||
</Rules>
|
||||
<Rules
|
||||
mode={RulesMode.NECESSARY}
|
||||
rules={[
|
||||
{
|
||||
action: AppRuleActions.update,
|
||||
name: AppRuleNames.rib,
|
||||
},
|
||||
]}>
|
||||
<NavigationLink
|
||||
path={Module.getInstance().get().modules.pages.OfficesRib.props.path}
|
||||
text="Gestion du RIB"
|
||||
routesActive={[Module.getInstance().get().modules.pages.OfficesRib.props.path]}
|
||||
/>
|
||||
</Rules>
|
||||
<NavigationLink target="_blank" path="/CGU_LeCoffre_io.pdf" text="CGU" />
|
||||
<div className={classes["separator"]} />
|
||||
<LogOutButton />
|
||||
|
@ -38,7 +38,6 @@ class ToastElementClass extends React.Component<IPropsClass, IState> {
|
||||
}
|
||||
|
||||
public override render(): JSX.Element {
|
||||
console.log(this.props);
|
||||
const toast = this.props.toast;
|
||||
const style = {
|
||||
"--data-duration": `${toast.time}ms`,
|
||||
|
@ -8,8 +8,13 @@
|
||||
&.H1-60 {
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-size: 50px;
|
||||
line-height: 61px;
|
||||
font-size: 56px;
|
||||
line-height: 67.2px;
|
||||
|
||||
@media (max-width: $screen-m) {
|
||||
font-size: 48px;
|
||||
line-height: 56.7px;
|
||||
}
|
||||
}
|
||||
|
||||
&.H1-bis-40 {
|
||||
@ -115,6 +120,14 @@
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&.Caption_14-semibold {
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&.re-hover {
|
||||
color: $re-hover;
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ export enum ITypo {
|
||||
P_ERR_16 = "Paragraphe-16-error",
|
||||
|
||||
CAPTION_14 = "Caption_14",
|
||||
CAPTION_14_SB = "Caption_14-semibold",
|
||||
}
|
||||
|
||||
export enum ITypoColor {
|
||||
|
31
src/front/Components/Elements/MessageBox/classes.module.scss
Normal file
31
src/front/Components/Elements/MessageBox/classes.module.scss
Normal file
@ -0,0 +1,31 @@
|
||||
.root {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
padding: 16px;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
&.info {
|
||||
border: 1px solid #005176;
|
||||
background: #c3eae64d;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background-color: var(--Warning-100);
|
||||
}
|
||||
|
||||
&.success {
|
||||
border: 1px solid var(--green-flash);
|
||||
background: #12bf4d0d;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border: 1px solid var(--red-soft);
|
||||
background: #f087711a;
|
||||
}
|
||||
}
|
37
src/front/Components/Elements/MessageBox/index.tsx
Normal file
37
src/front/Components/Elements/MessageBox/index.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import classes from "./classes.module.scss";
|
||||
import classNames from "classnames";
|
||||
import { InformationCircleIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||
import Typography, { ITypo } from "@Front/Components/DesignSystem/Typography";
|
||||
|
||||
export type IProps = {
|
||||
type: "info" | "warning" | "success" | "error";
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function MessageBox(props: IProps) {
|
||||
const { className, type, children } = props;
|
||||
return (
|
||||
<div className={classNames(className, classes["root"], classes[type])}>
|
||||
{getIcon(type)}
|
||||
<div className={classes["content"]}>
|
||||
<Typography className={classes["text"]} typo={ITypo.CAPTION_14}>
|
||||
{children}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function getIcon(type: IProps["type"]) {
|
||||
switch (type) {
|
||||
case "info":
|
||||
return <InformationCircleIcon />;
|
||||
case "warning":
|
||||
return <ExclamationTriangleIcon />;
|
||||
case "success":
|
||||
return <InformationCircleIcon />;
|
||||
case "error":
|
||||
return <InformationCircleIcon />;
|
||||
}
|
||||
}
|
||||
}
|
25
src/front/Components/Elements/NavTab/classes.module.scss
Normal file
25
src/front/Components/Elements/NavTab/classes.module.scss
Normal file
@ -0,0 +1,25 @@
|
||||
.root {
|
||||
nav {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
.link {
|
||||
cursor: pointer;
|
||||
padding: 16px;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: black;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
43
src/front/Components/Elements/NavTab/index.tsx
Normal file
43
src/front/Components/Elements/NavTab/index.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import classNames from "classnames";
|
||||
import classes from "./classes.module.scss";
|
||||
import Link from "next/link";
|
||||
import Typography, { ITypo } from "@Front/Components/DesignSystem/Typography";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
type ITabItem = {
|
||||
label: string;
|
||||
path: string;
|
||||
activePaths?: string[];
|
||||
};
|
||||
|
||||
type IProps = {
|
||||
items: ITabItem[];
|
||||
};
|
||||
|
||||
export default function NavTab(props: IProps) {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div className={classes["root"]}>
|
||||
<nav>
|
||||
{props.items.map((item, index) => {
|
||||
let isMatch = false;
|
||||
if (item.activePaths) {
|
||||
isMatch = item.activePaths.some((path) => router.pathname.includes(path));
|
||||
} else {
|
||||
isMatch = router.pathname.includes(item.path) ? true : false;
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
key={item.path.toString()}
|
||||
href={item.path}
|
||||
className={classNames(classes["link"], isMatch && classes["active"])}>
|
||||
<Typography key={index} typo={isMatch ? ITypo.P_SB_18 : ITypo.P_18}>
|
||||
{item.label}
|
||||
</Typography>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
.root {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
border: 1px solid #e7e7e7;
|
||||
padding: 24px;
|
||||
|
||||
.button {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
border: none;
|
||||
}
|
||||
}
|
46
src/front/Components/Elements/NumberPicker/index.tsx
Normal file
46
src/front/Components/Elements/NumberPicker/index.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { useState } from "react";
|
||||
import classes from "./classes.module.scss";
|
||||
import { MinusIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
type IProps = {
|
||||
defaultValue: number;
|
||||
onChange: (value: number) => void;
|
||||
min?: number;
|
||||
max?: number;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export default function NumberPicker(props: IProps) {
|
||||
const { defaultValue, onChange, min, max, disabled } = props;
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let value = parseInt(e.target.value);
|
||||
if (isNaN(value)) value = 1;
|
||||
if (min && value < min) value = min;
|
||||
if (max && value > max) value = max;
|
||||
|
||||
setValue(value);
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
const handleMinus = () => {
|
||||
handleChange({ target: { value: value - 1 } } as any);
|
||||
};
|
||||
|
||||
const handlePlus = () => {
|
||||
handleChange({ target: { value: value + 1 } } as any);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes["root"]}>
|
||||
<button onClick={handleMinus} disabled={min && value <= min ? true : false} className={classes["button"]}>
|
||||
<MinusIcon width="20" height="20" />
|
||||
</button>
|
||||
<input type="number" value={value} onChange={handleChange} disabled={disabled} className={classes["input"]} />
|
||||
<button onClick={handlePlus} disabled={max && value >= max ? true : false} className={classes["button"]}>
|
||||
<PlusIcon width="20" height="20" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -92,10 +92,18 @@ export default class DefaultCollaboratorDashboard extends React.Component<IProps
|
||||
if (!jwt) return;
|
||||
const query: IGetUsersparams = {
|
||||
where: { office_uid: jwt.office_Id },
|
||||
include: { contact: true },
|
||||
include: {
|
||||
contact: true,
|
||||
seats: {
|
||||
include: {
|
||||
subscription: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const collaborators = await Users.getInstance().get(query);
|
||||
console.log(collaborators);
|
||||
this.setState({ collaborators });
|
||||
}
|
||||
public override componentWillUnmount() {
|
||||
|
@ -33,7 +33,6 @@ export default function DocumentTypeListContainer(props: IProps) {
|
||||
const onSelectedBlock = useCallback(
|
||||
(block: IBlock) => {
|
||||
props.onCloseLeftSide && props.onCloseLeftSide();
|
||||
console.log("Block selected :", block);
|
||||
const redirectPath = Module.getInstance().get().modules.pages.DocumentTypes.pages.DocumentTypesInformations.props.path;
|
||||
router.push(redirectPath.replace("[uid]", block.id));
|
||||
},
|
||||
|
@ -4,7 +4,6 @@
|
||||
margin: var(--root-margin);
|
||||
max-width: var(--root-max-width);
|
||||
min-width: 100%;
|
||||
min-height: calc(100vh - 83px);
|
||||
|
||||
&.padding {
|
||||
padding: var(--root-padding);
|
||||
|
@ -4,6 +4,7 @@ import classNames from "classnames";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
import classes from "./classes.module.scss";
|
||||
import BackArrow from "@Front/Components/Elements/BackArrow";
|
||||
|
||||
type IProps = {
|
||||
title: string;
|
||||
@ -14,6 +15,8 @@ type IProps = {
|
||||
scrollTop: number | null;
|
||||
isPadding?: boolean;
|
||||
hasHeaderLinks: boolean;
|
||||
hasBackArrow?: boolean;
|
||||
backArrowUrl?: string;
|
||||
};
|
||||
type IState = {};
|
||||
|
||||
@ -28,7 +31,14 @@ export default class DefaultTemplate extends React.Component<IProps, IState> {
|
||||
return (
|
||||
<>
|
||||
<Header isUserConnected={this.props.hasHeaderLinks} />
|
||||
<div className={classNames(classes["root"], this.props.isPadding && classes["padding"])}>{this.props.children}</div>
|
||||
<div className={classNames(classes["root"], this.props.isPadding && classes["padding"])}>
|
||||
{this.props.hasBackArrow && (
|
||||
<div className={classes["back-arrow-desktop"]}>
|
||||
<BackArrow url={this.props.backArrowUrl ?? ""} />
|
||||
</div>
|
||||
)}
|
||||
{this.props.children}
|
||||
</div>
|
||||
<Version />
|
||||
</>
|
||||
);
|
||||
|
@ -72,7 +72,7 @@ export default function ClientDashboard(props: IProps) {
|
||||
setIsAddDocumentModalVisible(true);
|
||||
}, []);
|
||||
|
||||
async function downloadFile() {
|
||||
const downloadFile = useCallback(async () => {
|
||||
if (!folder?.office?.uid) return;
|
||||
const blob = await OfficeRib.getInstance().getRibStream(folder.office.uid);
|
||||
const ribUrl = URL.createObjectURL(blob);
|
||||
@ -84,7 +84,7 @@ export default function ClientDashboard(props: IProps) {
|
||||
a.download = "";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
}
|
||||
}, [folder]);
|
||||
|
||||
useEffect(() => {
|
||||
getDocuments();
|
||||
@ -126,7 +126,15 @@ export default function ClientDashboard(props: IProps) {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, [customer, folder?.folder_number, folder?.name, folder?.office?.name]);
|
||||
}, [
|
||||
customer?.contact?.first_name,
|
||||
customer?.contact?.last_name,
|
||||
downloadFile,
|
||||
folder?.folder_number,
|
||||
folder?.name,
|
||||
folder?.office?.name,
|
||||
folder?.office?.rib_name,
|
||||
]);
|
||||
|
||||
const renderBox = useCallback(() => {
|
||||
return (
|
||||
|
@ -36,7 +36,6 @@ export default function DeedTypesCreate(props: IProps) {
|
||||
try {
|
||||
await validateOrReject(deedType, { groups: ["createDeedType"], forbidUnknownValues: true });
|
||||
} catch (validationErrors: Array<ValidationError> | any) {
|
||||
console.log(validationErrors);
|
||||
setValidationError(validationErrors as ValidationError[]);
|
||||
return;
|
||||
}
|
||||
@ -57,12 +56,11 @@ export default function DeedTypesCreate(props: IProps) {
|
||||
.modules.pages.DeedTypes.pages.DeedTypesInformations.props.path.replace("[uid]", deedTypeCreated.uid!),
|
||||
);
|
||||
} catch (validationErrors: Array<ValidationError> | any) {
|
||||
console.log(validationErrors);
|
||||
setValidationError(validationErrors as ValidationError[]);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[router, validationError],
|
||||
[router],
|
||||
);
|
||||
|
||||
const closeConfirmModal = useCallback(() => {
|
||||
|
@ -76,7 +76,7 @@ export default function DeedTypesEdit() {
|
||||
return;
|
||||
}
|
||||
},
|
||||
[deedTypeUid, router, validationError],
|
||||
[deedTypeUid, router],
|
||||
);
|
||||
|
||||
const onFieldChange = useCallback((name: string, field: any) => {
|
||||
|
@ -42,7 +42,6 @@ export default function DocumentTypesEdit() {
|
||||
try {
|
||||
await validateOrReject(documentToUpdate, { groups: ["updateDocumentType"] });
|
||||
} catch (validationErrors: Array<ValidationError> | any) {
|
||||
console.log(validationErrors);
|
||||
if (!Array.isArray(validationErrors)) return;
|
||||
setValidationError(validationErrors as ValidationError[]);
|
||||
return;
|
||||
@ -63,7 +62,7 @@ export default function DocumentTypesEdit() {
|
||||
return;
|
||||
}
|
||||
},
|
||||
[documentTypeUid, router, validationError],
|
||||
[documentTypeUid, router],
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -242,7 +242,6 @@ class AddClientToFolderClass extends BasePage<IPropsClass, IState> {
|
||||
const contactToCreate = Contact.hydrate<Customer>(values);
|
||||
await contactToCreate.validateOrReject?.({ groups: ["createCustomer"], forbidUnknownValues: false });
|
||||
} catch (validationErrors) {
|
||||
console.log(validationErrors);
|
||||
this.setState({
|
||||
validationError: validationErrors as ValidationError[],
|
||||
});
|
||||
|
@ -200,7 +200,6 @@ class UpdateClientClass extends BasePage<IPropsClass, IState> {
|
||||
try {
|
||||
await contact.validateOrReject?.({ groups: ["createCustomer"], forbidUnknownValues: false });
|
||||
} catch (validationErrors) {
|
||||
console.log(validationErrors);
|
||||
this.setState({
|
||||
validationError: validationErrors as ValidationError[],
|
||||
});
|
||||
|
@ -200,7 +200,7 @@ class ViewDocumentsClass extends BasePage<IPropsClass, IState> {
|
||||
fileBlob,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,9 +3,6 @@
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 530px;
|
||||
margin: auto;
|
||||
margin-top: 220px;
|
||||
|
||||
.title {
|
||||
text-align: left;
|
@ -3,9 +3,6 @@
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 530px;
|
||||
margin: auto;
|
||||
margin-top: 220px;
|
||||
|
||||
.title {
|
||||
margin-bottom: 32px;
|
@ -31,16 +31,13 @@ export default function StepEmail(props: IProps) {
|
||||
|
||||
return (
|
||||
<div className={classes["root"]}>
|
||||
<Typography typo={ITypo.H1}>
|
||||
<div className={classes["title"]}>Identifiez-vous</div>
|
||||
</Typography>
|
||||
<Typography typo={ITypo.H2}>Connectez-vous en tant que client</Typography>
|
||||
{/* <Typography typo={ITypo.P_16}>Pour accéder à votre espace de dépôt des documents, veuillez vous identifier.</Typography>
|
||||
<Image alt="france-connect" src={franceConnectLogo} onClick={redirectCustomerOnConnection} className={classes["logo"]} />
|
||||
<div className={classes["what_is_france_connect"]}>Qu'est ce que FranceConnect ?</div>
|
||||
<Typography className={classes["or"]} typo={ITypo.P_16}>
|
||||
Ou
|
||||
</Typography> */}
|
||||
<Typography typo={ITypo.P_16}>Pour accéder à votre espace de dépôt des documents, veuillez vous identifier. </Typography>
|
||||
<Form className={classes["form"]} onSubmit={onSubmit}>
|
||||
<TextField
|
||||
placeholder="E-mail"
|
@ -3,9 +3,6 @@
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 530px;
|
||||
margin: auto;
|
||||
margin-top: 220px;
|
||||
|
||||
.title {
|
||||
text-align: left;
|
@ -3,9 +3,6 @@
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 530px;
|
||||
margin: auto;
|
||||
margin-top: 220px;
|
||||
|
||||
.title {
|
||||
text-align: left;
|
@ -3,8 +3,6 @@
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 530px;
|
||||
margin: 220px auto;
|
||||
|
||||
.title {
|
||||
text-align: left;
|
@ -1,25 +1,24 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
padding: 40px 40px 40px 64px;
|
||||
.notary-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-width: 530px;
|
||||
margin: auto;
|
||||
|
||||
.title {
|
||||
margin: 32px 0;
|
||||
text-align: center;
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
font-family: 48px;
|
||||
}
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.forget-password {
|
||||
margin-top: 32px;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: 48px 0;
|
||||
height: 1px;
|
||||
background-color: var(--grey-medium);
|
||||
}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
@ -1,24 +1,214 @@
|
||||
import CoffreIcon from "@Assets/Icons/coffre.svg";
|
||||
import idNoteLogo from "@Assets/Icons/id-note-logo.svg";
|
||||
import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button";
|
||||
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, 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";
|
||||
import StepEmail from "./StepEmail";
|
||||
import StepTotp from "./StepTotp";
|
||||
import Auth from "@Front/Api/Auth/Customer/Auth";
|
||||
import { ValidationError } from "class-validator";
|
||||
import StepPassword from "./StepPassword";
|
||||
import StepNewPassword from "./StepNewPassword";
|
||||
import CustomerStore from "@Front/Stores/CustomerStore";
|
||||
import Module from "@Front/Config/Module";
|
||||
import { TotpCodesReasons } from "le-coffre-resources/dist/Customer/TotpCodes";
|
||||
import PasswordForgotten from "./PasswordForgotten";
|
||||
import { FrontendVariables } from "@Front/Config/VariablesFront";
|
||||
import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button";
|
||||
import Link from "next/link";
|
||||
import idNoteLogo from "@Assets/Icons/id-note-logo.svg";
|
||||
|
||||
export enum LoginStep {
|
||||
EMAIL,
|
||||
TOTP,
|
||||
PASSWORD,
|
||||
NEW_PASSWORD,
|
||||
PASSWORD_FORGOTTEN,
|
||||
}
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
const error = router.query["error"];
|
||||
|
||||
const [isErrorModalOpen, setIsErrorModalOpen] = useState(0);
|
||||
|
||||
const [step, setStep] = useState<LoginStep>(LoginStep.EMAIL);
|
||||
const [totpCodeUid, setTotpCodeUid] = useState<string>("");
|
||||
const [totpCode, setTotpCode] = useState<string>("");
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [partialPhoneNumber, setPartialPhoneNumber] = useState<string>("");
|
||||
const [validationErrors, setValidationErrors] = useState<ValidationError[]>([]);
|
||||
|
||||
const openErrorModal = useCallback(() => {
|
||||
setIsErrorModalOpen(1);
|
||||
}, []);
|
||||
|
||||
const closeErrorModal = useCallback(() => {
|
||||
setIsErrorModalOpen(0);
|
||||
}, []);
|
||||
|
||||
const onEmailFormSubmit = useCallback(async (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => {
|
||||
try {
|
||||
if (!values["email"]) return;
|
||||
setEmail(values["email"]);
|
||||
const res = await Auth.getInstance().mailVerifySms({ email: values["email"] });
|
||||
setPartialPhoneNumber(res.partialPhoneNumber);
|
||||
setTotpCodeUid(res.totpCodeUid);
|
||||
setStep(LoginStep.TOTP);
|
||||
setValidationErrors([]);
|
||||
} catch (error: any) {
|
||||
setValidationErrors([
|
||||
{
|
||||
property: "email",
|
||||
constraints: {
|
||||
[error.http_status]: error.message,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onSmsCodeSubmit = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => {
|
||||
try {
|
||||
if (!values["totpCode"]) return;
|
||||
const res = await Auth.getInstance().verifyTotpCode({ totpCode: values["totpCode"], email });
|
||||
|
||||
// If the code is valid setting it in state
|
||||
if (res.validCode) setTotpCode(values["totpCode"]);
|
||||
|
||||
setValidationErrors([]);
|
||||
// If it's first connection, show the form for first connection
|
||||
if (res.reason === TotpCodesReasons.FIRST_LOGIN) setStep(LoginStep.NEW_PASSWORD);
|
||||
// If it's password forgotten, show the form for password forgotten
|
||||
else if (res.reason === TotpCodesReasons.RESET_PASSWORD) setStep(LoginStep.PASSWORD_FORGOTTEN);
|
||||
// Else just login normally
|
||||
else setStep(LoginStep.PASSWORD);
|
||||
} catch (error: any) {
|
||||
setValidationErrors([
|
||||
{
|
||||
property: "totpCode",
|
||||
constraints: {
|
||||
[error.http_status]: error.message,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[email, setStep],
|
||||
);
|
||||
|
||||
const onNewPasswordSubmit = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => {
|
||||
try {
|
||||
if (!values["password"] || !values["confirm_password"]) return;
|
||||
if (values["password"] !== values["confirm_password"]) {
|
||||
setValidationErrors([
|
||||
{
|
||||
property: "confirm_password",
|
||||
constraints: {
|
||||
"400": "Les mots de passe ne correspondent pas.",
|
||||
},
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordRegex = new RegExp(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d@$!%*?&]{8,}$/);
|
||||
if (!passwordRegex.test(values["password"])) {
|
||||
setValidationErrors([
|
||||
{
|
||||
property: "password",
|
||||
constraints: {
|
||||
"400": "Le mot de passe doit contenir au moins 8 caractères dont 1 majuscule, 1 minuscule et 1 chiffre.",
|
||||
},
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
const token = await Auth.getInstance().setPassword({ totpCode, email, password: values["password"] });
|
||||
CustomerStore.instance.connect(token.accessToken, token.refreshToken);
|
||||
setValidationErrors([]);
|
||||
router.push(Module.getInstance().get().modules.pages.Folder.pages.Select.props.path);
|
||||
// If set password worked, setting the token and redirecting
|
||||
} catch (error: any) {
|
||||
setValidationErrors([
|
||||
{
|
||||
property: "password",
|
||||
constraints: {
|
||||
[error.http_status]: error.message,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[totpCode, email, router],
|
||||
);
|
||||
|
||||
const onPasswordSubmit = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => {
|
||||
try {
|
||||
if (!values["password"]) return;
|
||||
const token = await Auth.getInstance().login({ totpCode, email, password: values["password"] });
|
||||
CustomerStore.instance.connect(token.accessToken, token.refreshToken);
|
||||
setValidationErrors([]);
|
||||
router.push(Module.getInstance().get().modules.pages.Folder.pages.Select.props.path);
|
||||
} catch (error: any) {
|
||||
setValidationErrors([
|
||||
{
|
||||
property: "password",
|
||||
constraints: {
|
||||
[error.http_status]: error.message,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[email, router, totpCode],
|
||||
);
|
||||
|
||||
const onPasswordForgotClicked = useCallback(async () => {
|
||||
try {
|
||||
const res = await Auth.getInstance().askNewPassword({ email });
|
||||
setPartialPhoneNumber(res.partialPhoneNumber);
|
||||
setValidationErrors([]);
|
||||
setStep(LoginStep.TOTP);
|
||||
} catch (error: any) {
|
||||
// If token already exists and is still valid redirect to the connect/register page
|
||||
if (error.http_status === 425) {
|
||||
setStep(LoginStep.TOTP);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}, [email]);
|
||||
|
||||
const onSendAnotherCode = useCallback(async () => {
|
||||
try {
|
||||
const res = await Auth.getInstance().sendAnotherCode({ email, totpCodeUid });
|
||||
|
||||
setValidationErrors([]);
|
||||
setPartialPhoneNumber(res.partialPhoneNumber);
|
||||
setTotpCodeUid(res.totpCodeUid);
|
||||
} catch (error: any) {
|
||||
setValidationErrors([
|
||||
{
|
||||
property: "totpCode",
|
||||
constraints: {
|
||||
[error.http_status]: error.message,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}, [email, totpCodeUid]);
|
||||
|
||||
const redirectUserOnConnection = useCallback(() => {
|
||||
const variables = FrontendVariables.getInstance();
|
||||
router.push(
|
||||
@ -28,44 +218,60 @@ export default function Login() {
|
||||
);
|
||||
}, [router]);
|
||||
|
||||
const openErrorModal = useCallback((index: number) => {
|
||||
setIsErrorModalOpen(index);
|
||||
}, []);
|
||||
|
||||
const closeErrorModal = useCallback(() => {
|
||||
setIsErrorModalOpen(0);
|
||||
}, []);
|
||||
|
||||
const closeNoEmailModal = useCallback(() => {
|
||||
setIsErrorModalOpen(0);
|
||||
router.push("https://connexion.idnot.fr/");
|
||||
}, [router]);
|
||||
|
||||
const closeContactAdminModal = useCallback(() => {
|
||||
const closeContactAdminModal = () => {
|
||||
setIsErrorModalOpen(0);
|
||||
window.open("https://www.lecoffre.io/contact", "_blank");
|
||||
}, [router]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
openErrorModal(parseInt(error as string));
|
||||
if (error === "1") openErrorModal();
|
||||
}, [error, openErrorModal]);
|
||||
|
||||
return (
|
||||
<DefaultDoubleSidePage title={"Login"} image={LandingImage}>
|
||||
<div className={classes["root"]}>
|
||||
<Image alt="coffre" src={CoffreIcon} />
|
||||
<Typography typo={ITypo.H1}>
|
||||
<div className={classes["title"]}>Connexion espace professionnel</div>
|
||||
{step === LoginStep.EMAIL && (
|
||||
<div className={classes["notary-container"]}>
|
||||
<Typography typo={ITypo.H2} className={classes["title"]}>
|
||||
Connectez vous en tant que notaire
|
||||
</Typography>
|
||||
<Button onClick={redirectUserOnConnection} icon={idNoteLogo} iconposition={"left"}>
|
||||
S'identifier avec ID.not
|
||||
</Button>
|
||||
<Typography typo={ITypo.P_18}>
|
||||
<div className={classes["forget-password"]}>Vous n'arrivez pas à vous connecter ?</div>
|
||||
<Typography typo={ITypo.P_18} className={classes["forget-password"]}>
|
||||
Vous n'arrivez pas à vous connecter ?
|
||||
</Typography>
|
||||
<Link href="mailto:g.texier@notaires.fr">
|
||||
<Button variant={EButtonVariant.LINE}>Contacter l'administrateur</Button>
|
||||
</Link>
|
||||
<div className={classes["separator"]} />
|
||||
</div>
|
||||
)}
|
||||
{step === LoginStep.EMAIL && <StepEmail onSubmit={onEmailFormSubmit} validationErrors={validationErrors} />}
|
||||
{step === LoginStep.TOTP && (
|
||||
<StepTotp
|
||||
onSubmit={onSmsCodeSubmit}
|
||||
validationErrors={validationErrors}
|
||||
partialPhoneNumber={partialPhoneNumber}
|
||||
onSendAnotherCode={onSendAnotherCode}
|
||||
/>
|
||||
)}
|
||||
{step === LoginStep.PASSWORD && (
|
||||
<StepPassword
|
||||
onSubmit={onPasswordSubmit}
|
||||
validationErrors={validationErrors}
|
||||
onPasswordForgotClicked={onPasswordForgotClicked}
|
||||
/>
|
||||
)}
|
||||
{step === LoginStep.NEW_PASSWORD && <StepNewPassword onSubmit={onNewPasswordSubmit} validationErrors={validationErrors} />}
|
||||
{step === LoginStep.PASSWORD_FORGOTTEN && (
|
||||
<PasswordForgotten onSubmit={onNewPasswordSubmit} validationErrors={validationErrors} />
|
||||
)}
|
||||
</div>
|
||||
<Confirm
|
||||
isOpen={isErrorModalOpen === 1}
|
||||
@ -77,7 +283,7 @@ export default function Login() {
|
||||
confirmText={"OK"}>
|
||||
<div className={classes["modal-content"]}>
|
||||
<Typography typo={ITypo.P_16} className={classes["text"]}>
|
||||
Une erreur est survenue lors de la connexion. Veuillez réessayer.
|
||||
Vous ne disposez pas d'un abonnement, veuillez contacter l'administrateur de votre office.
|
||||
</Typography>
|
||||
</div>
|
||||
</Confirm>
|
||||
|
@ -28,7 +28,6 @@ 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: any) {
|
||||
console.log("Log error : ", e);
|
||||
if (e.http_status === 401 && e.message === "Email not found") {
|
||||
return router.push(Module.getInstance().get().modules.pages.Login.props.path + "?error=3");
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ export default function LoginCallBackCustomer() {
|
||||
try {
|
||||
token = await Customers.getInstance().loginCallback(tokenid360);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.error(e);
|
||||
router.push(Module.getInstance().get().modules.pages.CustomersLogin.props.path + "?error=1");
|
||||
return;
|
||||
}
|
||||
|
@ -1,4 +0,0 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
}
|
@ -1,252 +0,0 @@
|
||||
import Typography, { ITypo } from "@Front/Components/DesignSystem/Typography";
|
||||
import DefaultDoubleSidePage from "@Front/Components/LayoutTemplates/DefaultDoubleSidePage";
|
||||
import { useRouter } from "next/router";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import classes from "./classes.module.scss";
|
||||
import LandingImage from "./landing-connect.jpeg";
|
||||
import Confirm from "@Front/Components/DesignSystem/Modal/Confirm";
|
||||
import StepEmail from "./StepEmail";
|
||||
import StepTotp from "./StepTotp";
|
||||
import Auth from "@Front/Api/Auth/Customer/Auth";
|
||||
import { ValidationError } from "class-validator";
|
||||
import StepPassword from "./StepPassword";
|
||||
import StepNewPassword from "./StepNewPassword";
|
||||
import CustomerStore from "@Front/Stores/CustomerStore";
|
||||
import Module from "@Front/Config/Module";
|
||||
import { TotpCodesReasons } from "le-coffre-resources/dist/Customer/TotpCodes";
|
||||
import PasswordForgotten from "./PasswordForgotten";
|
||||
|
||||
export enum LoginStep {
|
||||
EMAIL,
|
||||
TOTP,
|
||||
PASSWORD,
|
||||
NEW_PASSWORD,
|
||||
PASSWORD_FORGOTTEN,
|
||||
}
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
const error = router.query["error"];
|
||||
|
||||
const [isErrorModalOpen, setIsErrorModalOpen] = useState(false);
|
||||
|
||||
const [step, setStep] = useState<LoginStep>(LoginStep.EMAIL);
|
||||
const [totpCodeUid, setTotpCodeUid] = useState<string>("");
|
||||
const [totpCode, setTotpCode] = useState<string>("");
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [partialPhoneNumber, setPartialPhoneNumber] = useState<string>("");
|
||||
const [validationErrors, setValidationErrors] = useState<ValidationError[]>([]);
|
||||
|
||||
const openErrorModal = useCallback(() => {
|
||||
setIsErrorModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const closeErrorModal = useCallback(() => {
|
||||
setIsErrorModalOpen(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (error === "1") openErrorModal();
|
||||
}, [error, openErrorModal]);
|
||||
|
||||
const onEmailFormSubmit = useCallback(async (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => {
|
||||
try {
|
||||
if (!values["email"]) return;
|
||||
setEmail(values["email"]);
|
||||
const res = await Auth.getInstance().mailVerifySms({ email: values["email"] });
|
||||
setPartialPhoneNumber(res.partialPhoneNumber);
|
||||
setTotpCodeUid(res.totpCodeUid);
|
||||
setStep(LoginStep.TOTP);
|
||||
setValidationErrors([]);
|
||||
} catch (error: any) {
|
||||
setValidationErrors([
|
||||
{
|
||||
property: "email",
|
||||
constraints: {
|
||||
[error.http_status]: error.message,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onSmsCodeSubmit = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => {
|
||||
try {
|
||||
if (!values["totpCode"]) return;
|
||||
const res = await Auth.getInstance().verifyTotpCode({ totpCode: values["totpCode"], email });
|
||||
|
||||
// If the code is valid setting it in state
|
||||
if (res.validCode) setTotpCode(values["totpCode"]);
|
||||
|
||||
setValidationErrors([]);
|
||||
// If it's first connection, show the form for first connection
|
||||
if (res.reason === TotpCodesReasons.FIRST_LOGIN) setStep(LoginStep.NEW_PASSWORD);
|
||||
// If it's password forgotten, show the form for password forgotten
|
||||
else if (res.reason === TotpCodesReasons.RESET_PASSWORD) setStep(LoginStep.PASSWORD_FORGOTTEN);
|
||||
// Else just login normally
|
||||
else setStep(LoginStep.PASSWORD);
|
||||
} catch (error: any) {
|
||||
setValidationErrors([
|
||||
{
|
||||
property: "totpCode",
|
||||
constraints: {
|
||||
[error.http_status]: error.message,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[email, setStep],
|
||||
);
|
||||
|
||||
const onNewPasswordSubmit = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => {
|
||||
try {
|
||||
if (!values["password"] || !values["confirm_password"]) return;
|
||||
if (values["password"] !== values["confirm_password"]) {
|
||||
setValidationErrors([
|
||||
{
|
||||
property: "confirm_password",
|
||||
constraints: {
|
||||
"400": "Les mots de passe ne correspondent pas.",
|
||||
},
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordRegex = new RegExp(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d@$!%*?&]{8,}$/);
|
||||
if (!passwordRegex.test(values["password"])) {
|
||||
setValidationErrors([
|
||||
{
|
||||
property: "password",
|
||||
constraints: {
|
||||
"400": "Le mot de passe doit contenir au moins 8 caractères dont 1 majuscule, 1 minuscule et 1 chiffre.",
|
||||
},
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
const token = await Auth.getInstance().setPassword({ totpCode, email, password: values["password"] });
|
||||
CustomerStore.instance.connect(token.accessToken, token.refreshToken);
|
||||
setValidationErrors([]);
|
||||
router.push(Module.getInstance().get().modules.pages.Folder.pages.Select.props.path);
|
||||
// If set password worked, setting the token and redirecting
|
||||
} catch (error: any) {
|
||||
setValidationErrors([
|
||||
{
|
||||
property: "password",
|
||||
constraints: {
|
||||
[error.http_status]: error.message,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[totpCode, email, router],
|
||||
);
|
||||
|
||||
const onPasswordSubmit = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => {
|
||||
try {
|
||||
if (!values["password"]) return;
|
||||
const token = await Auth.getInstance().login({ totpCode, email, password: values["password"] });
|
||||
CustomerStore.instance.connect(token.accessToken, token.refreshToken);
|
||||
setValidationErrors([]);
|
||||
router.push(Module.getInstance().get().modules.pages.Folder.pages.Select.props.path);
|
||||
} catch (error: any) {
|
||||
setValidationErrors([
|
||||
{
|
||||
property: "password",
|
||||
constraints: {
|
||||
[error.http_status]: error.message,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[email, router, totpCode],
|
||||
);
|
||||
|
||||
const onPasswordForgotClicked = useCallback(async () => {
|
||||
try {
|
||||
const res = await Auth.getInstance().askNewPassword({ email });
|
||||
setPartialPhoneNumber(res.partialPhoneNumber);
|
||||
setValidationErrors([]);
|
||||
setStep(LoginStep.TOTP);
|
||||
} catch (error: any) {
|
||||
// If token already exists and is still valid redirect to the connect/register page
|
||||
if (error.http_status === 425) {
|
||||
setStep(LoginStep.TOTP);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}, [email]);
|
||||
|
||||
const onSendAnotherCode = useCallback(async () => {
|
||||
try {
|
||||
const res = await Auth.getInstance().sendAnotherCode({ email, totpCodeUid });
|
||||
|
||||
setValidationErrors([]);
|
||||
setPartialPhoneNumber(res.partialPhoneNumber);
|
||||
setTotpCodeUid(res.totpCodeUid);
|
||||
} catch (error: any) {
|
||||
setValidationErrors([
|
||||
{
|
||||
property: "totpCode",
|
||||
constraints: {
|
||||
[error.http_status]: error.message,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}, [email, totpCodeUid]);
|
||||
|
||||
return (
|
||||
<DefaultDoubleSidePage title={"Login"} image={LandingImage}>
|
||||
<div className={classes["root"]}>
|
||||
{step === LoginStep.EMAIL && <StepEmail onSubmit={onEmailFormSubmit} validationErrors={validationErrors} />}
|
||||
{step === LoginStep.TOTP && (
|
||||
<StepTotp
|
||||
onSubmit={onSmsCodeSubmit}
|
||||
validationErrors={validationErrors}
|
||||
partialPhoneNumber={partialPhoneNumber}
|
||||
onSendAnotherCode={onSendAnotherCode}
|
||||
/>
|
||||
)}
|
||||
{step === LoginStep.PASSWORD && (
|
||||
<StepPassword
|
||||
onSubmit={onPasswordSubmit}
|
||||
validationErrors={validationErrors}
|
||||
onPasswordForgotClicked={onPasswordForgotClicked}
|
||||
/>
|
||||
)}
|
||||
{step === LoginStep.NEW_PASSWORD && <StepNewPassword onSubmit={onNewPasswordSubmit} validationErrors={validationErrors} />}
|
||||
{step === LoginStep.PASSWORD_FORGOTTEN && (
|
||||
<PasswordForgotten onSubmit={onNewPasswordSubmit} validationErrors={validationErrors} />
|
||||
)}
|
||||
</div>
|
||||
<Confirm
|
||||
isOpen={isErrorModalOpen}
|
||||
onClose={closeErrorModal}
|
||||
showCancelButton={false}
|
||||
onAccept={closeErrorModal}
|
||||
closeBtn
|
||||
header={"Erreur"}
|
||||
confirmText={"OK"}>
|
||||
<div className={classes["modal-content"]}>
|
||||
<Typography typo={ITypo.P_16} className={classes["text"]}>
|
||||
Une erreur est survenue lors de la connexion. Veuillez réessayer.
|
||||
</Typography>
|
||||
</div>
|
||||
</Confirm>
|
||||
</DefaultDoubleSidePage>
|
||||
);
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 2.5 MiB |
@ -1,6 +1,10 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
@media (max-width: $screen-m) {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
@ -33,11 +33,11 @@ export default function Rib() {
|
||||
setFileName("");
|
||||
setKey("");
|
||||
}
|
||||
}, [officeUid]);
|
||||
}, [key]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [officeUid]);
|
||||
}, [fetchData, officeUid]);
|
||||
|
||||
function downloadFile() {
|
||||
if (!fileBlob) return;
|
||||
|
@ -35,7 +35,6 @@ export default function RolesCreate(props: IProps) {
|
||||
try {
|
||||
await officeRole.validateOrReject?.({ groups: ["createOfficeRole"], forbidUnknownValues: true });
|
||||
} catch (validationErrors: Array<ValidationError> | any) {
|
||||
console.log(validationErrors);
|
||||
if (!Array.isArray(validationErrors)) return;
|
||||
setValidationError(validationErrors as ValidationError[]);
|
||||
return;
|
||||
@ -52,13 +51,12 @@ export default function RolesCreate(props: IProps) {
|
||||
|
||||
router.push(Module.getInstance().get().modules.pages.Roles.pages.RolesInformations.props.path.replace("[uid]", role.uid!));
|
||||
} catch (validationErrors: Array<ValidationError> | any) {
|
||||
console.log(validationErrors);
|
||||
if (!Array.isArray(validationErrors)) return;
|
||||
setValidationError(validationErrors as ValidationError[]);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[router, validationError],
|
||||
[router],
|
||||
);
|
||||
|
||||
const closeConfirmModal = useCallback(() => {
|
||||
@ -96,7 +94,11 @@ export default function RolesCreate(props: IProps) {
|
||||
<Typography typo={ITypo.H1Bis}>Créer un rôle</Typography>
|
||||
</div>
|
||||
<Form onSubmit={onSubmitHandler} className={classes["form-container"]} onFieldChange={onFieldChange}>
|
||||
<TextField name="name" placeholder="Nom du rôle" validationError={validationError.find((error) => error.property === "name")}/>
|
||||
<TextField
|
||||
name="name"
|
||||
placeholder="Nom du rôle"
|
||||
validationError={validationError.find((error) => error.property === "name")}
|
||||
/>
|
||||
<div className={classes["buttons-container"]}>
|
||||
<Button variant={EButtonVariant.GHOST} onClick={onCancel}>
|
||||
Annuler
|
||||
|
@ -18,7 +18,6 @@ export default function SelectFolder() {
|
||||
async function getFolders() {
|
||||
const jwt = JwtService.getInstance().decodeCustomerJwt();
|
||||
if (!jwt) return;
|
||||
console.log(jwt)
|
||||
|
||||
const folders = await Folders.getInstance().get({
|
||||
q: {
|
||||
@ -37,8 +36,8 @@ export default function SelectFolder() {
|
||||
},
|
||||
],
|
||||
include: {
|
||||
customers: true
|
||||
}
|
||||
customers: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
setFolders(folders);
|
||||
|
@ -0,0 +1,67 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
width: 372px;
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
width: 100%;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
box-shadow: 0px 8px 10px 0px #00000012;
|
||||
padding: 22px 40px;
|
||||
border-radius: 16px;
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
box-shadow: none;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.forfeit-type {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container-frequency {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.container-line {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
.line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.line-sub-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.stroked-price {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-tight {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.payment-button {
|
||||
@media (max-width: $screen-s) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,202 @@
|
||||
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
|
||||
import { EForfeitType, collaboratorPrice, forfeitsPrices } from "../../SubscriptionFacturation";
|
||||
import classes from "./classes.module.scss";
|
||||
import { useEffect, useState } from "react";
|
||||
import RadioBox from "@Front/Components/DesignSystem/RadioBox";
|
||||
import Button from "@Front/Components/DesignSystem/Button";
|
||||
import classnames from "classnames";
|
||||
import { EType } from "le-coffre-resources/dist/Admin/Subscription";
|
||||
import Stripe from "@Front/Api/LeCoffreApi/Admin/Stripe/Stripe";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
type IProps = {
|
||||
forfeitType: EForfeitType;
|
||||
numberOfCollaborators: number;
|
||||
hasNavTab?: boolean;
|
||||
defaultFrequency?: EPaymentFrequency;
|
||||
disableInputs?: boolean;
|
||||
};
|
||||
|
||||
export enum EPaymentFrequency {
|
||||
monthly,
|
||||
yearly,
|
||||
}
|
||||
|
||||
export default function SubscribeCheckoutTicket(props: IProps) {
|
||||
const router = useRouter();
|
||||
const { forfeitType, numberOfCollaborators, hasNavTab = true } = props;
|
||||
const [paymentFrequency, setPaymentFrequency] = useState<EPaymentFrequency>(props.defaultFrequency ?? EPaymentFrequency.monthly);
|
||||
const [multiplier, setMultiplier] = useState<number>(1);
|
||||
const [totalPlan, setTotalPlan] = useState<number>(0);
|
||||
const [totalCollaborator, setTotalCollaborator] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
setMultiplier(paymentFrequency === EPaymentFrequency.monthly ? 1 : 12);
|
||||
}, [paymentFrequency]);
|
||||
|
||||
useEffect(() => {
|
||||
let multiplierToUse = paymentFrequency === EPaymentFrequency.yearly ? multiplier - 1 : multiplier;
|
||||
if (forfeitType === EForfeitType.unlimited) {
|
||||
setTotalPlan(forfeitsPrices[EForfeitType.unlimited] * multiplierToUse);
|
||||
setTotalCollaborator(0);
|
||||
} else {
|
||||
setTotalPlan(forfeitsPrices[EForfeitType.standard] * multiplierToUse);
|
||||
setTotalCollaborator(collaboratorPrice * numberOfCollaborators * multiplier);
|
||||
}
|
||||
}, [multiplier, forfeitType, numberOfCollaborators, paymentFrequency]);
|
||||
|
||||
const handleFrequencyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPaymentFrequency(parseInt(e.target.value) as EPaymentFrequency);
|
||||
};
|
||||
|
||||
const formatFloat = (value: number) => {
|
||||
return value.toFixed(2).replace(".", ",");
|
||||
};
|
||||
|
||||
const handleSubmitPayment = async () => {
|
||||
const stripeCheckout = {
|
||||
type: forfeitType === EForfeitType.standard ? EType.Standard : EType.Unlimited,
|
||||
nb_seats: forfeitType === EForfeitType.standard ? numberOfCollaborators : 0,
|
||||
};
|
||||
|
||||
try {
|
||||
const newStripeCheckout = await Stripe.getInstance().post(stripeCheckout);
|
||||
router.push(newStripeCheckout.url);
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes["root"]}>
|
||||
<div className={classes["container"]}>
|
||||
<div className={classes["forfeit-type"]}>
|
||||
<Typography typo={ITypo.P_18} color={ITypoColor.BLACK}>
|
||||
{forfeitType === EForfeitType.standard ? "Forfait standard" : "Forfait illimité"}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["separator"]} />
|
||||
<div className={classes["container-frequency"]}>
|
||||
<RadioBox
|
||||
name="paymentFrequency"
|
||||
value={EPaymentFrequency.yearly.toString()}
|
||||
onChange={handleFrequencyChange}
|
||||
defaultChecked={paymentFrequency === EPaymentFrequency.yearly}
|
||||
disabled={props.disableInputs}>
|
||||
<Typography typo={ITypo.P_ERR_18}>Annuel</Typography>
|
||||
</RadioBox>
|
||||
<RadioBox
|
||||
name="paymentFrequency"
|
||||
value={EPaymentFrequency.monthly.toString()}
|
||||
onChange={handleFrequencyChange}
|
||||
defaultChecked={paymentFrequency === EPaymentFrequency.monthly}
|
||||
disabled={props.disableInputs}>
|
||||
<Typography typo={ITypo.P_ERR_18}>Mensuel</Typography>
|
||||
</RadioBox>
|
||||
</div>
|
||||
<div className={classes["separator"]} />
|
||||
<div className={classes["container-line"]}>
|
||||
<div className={classes["line"]}>
|
||||
<div className={classes["line-sub-container"]}>
|
||||
<Typography typo={ITypo.P_18} color={ITypoColor.BLACK}>
|
||||
{forfeitType === EForfeitType.standard ? "Plan individuel" : "Plan illimité"}
|
||||
</Typography>
|
||||
{paymentFrequency === EPaymentFrequency.yearly && (
|
||||
<Typography typo={ITypo.CAPTION_14_SB} color={ITypoColor.BLACK}>
|
||||
{formatFloat(
|
||||
forfeitType === EForfeitType.standard
|
||||
? forfeitsPrices[EForfeitType.standard]
|
||||
: forfeitsPrices[EForfeitType.unlimited],
|
||||
)}
|
||||
€ x 11
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={classes["line-sub-container"]}>
|
||||
<Typography typo={ITypo.P_SB_18} color={ITypoColor.BLACK}>
|
||||
{totalPlan} €
|
||||
</Typography>
|
||||
{paymentFrequency === EPaymentFrequency.yearly && (
|
||||
<Typography typo={ITypo.P_16} color={ITypoColor.BLACK} className={classes["stroked-price"]}>
|
||||
{formatFloat(
|
||||
forfeitType === EForfeitType.standard
|
||||
? forfeitsPrices[EForfeitType.standard] * multiplier
|
||||
: forfeitsPrices[EForfeitType.unlimited] * multiplier,
|
||||
)}
|
||||
€
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{forfeitType === EForfeitType.standard && (
|
||||
<div className={classes["line"]}>
|
||||
<div className={classes["line-sub-container"]}>
|
||||
<Typography typo={ITypo.P_18} color={ITypoColor.BLACK}>
|
||||
{numberOfCollaborators} collaborateurs
|
||||
</Typography>
|
||||
<Typography typo={ITypo.CAPTION_14_SB} color={ITypoColor.BLACK}>
|
||||
{formatFloat(collaboratorPrice)} € x {numberOfCollaborators}{" "}
|
||||
{paymentFrequency === EPaymentFrequency.yearly && "x 12"}
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography typo={ITypo.P_SB_18} color={ITypoColor.BLACK}>
|
||||
{formatFloat(totalCollaborator)} €
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{forfeitType === EForfeitType.standard && (
|
||||
<>
|
||||
<div className={classes["separator"]} />
|
||||
<div className={classnames(classes["container-line"], classes["container-tight"])}>
|
||||
<div className={classes["line"]}>
|
||||
<Typography typo={ITypo.P_18} color={ITypoColor.BLACK}>
|
||||
Total HT
|
||||
</Typography>
|
||||
<Typography typo={ITypo.P_SB_18} color={ITypoColor.BLACK}>
|
||||
{formatFloat(totalCollaborator + totalPlan)}
|
||||
€
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["line"]}>
|
||||
<Typography typo={ITypo.P_18} color={ITypoColor.BLACK}>
|
||||
TVA 20%
|
||||
</Typography>
|
||||
<Typography typo={ITypo.P_SB_18} color={ITypoColor.BLACK}>
|
||||
{formatFloat((totalCollaborator + totalPlan) * 0.2)}
|
||||
€
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className={classes["separator"]} />
|
||||
<div className={classnames(classes["container-line"], classes["container-tight"])}>
|
||||
{forfeitType === EForfeitType.unlimited && (
|
||||
<div className={classes["line"]}>
|
||||
<Typography typo={ITypo.P_18} color={ITypoColor.BLACK}>
|
||||
TVA 20%
|
||||
</Typography>
|
||||
<Typography typo={ITypo.P_SB_18} color={ITypoColor.BLACK}>
|
||||
{formatFloat((totalCollaborator + totalPlan) * 0.2)} €
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
<div className={classes["line"]}>
|
||||
<Typography typo={ITypo.P_18} color={ITypoColor.BLACK}>
|
||||
Total TTC
|
||||
</Typography>
|
||||
<Typography typo={ITypo.P_SB_18} color={ITypoColor.BLACK}>
|
||||
{formatFloat((totalCollaborator + totalPlan) * 1.2)}
|
||||
€
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
{!props.disableInputs && (
|
||||
<Button onClick={handleSubmitPayment} fullwidth className={classes["payment-button"]}>
|
||||
{hasNavTab ? "Passer au paiement" : "Mettre à jour l'abonnement"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 104px;
|
||||
max-width: 1400px;
|
||||
margin: auto;
|
||||
@media (max-width: $screen-m) {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
gap: 72px;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
flex-direction: column;
|
||||
.infos-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
.line {
|
||||
svg {
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
}
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 372px;
|
||||
@media (max-width: $screen-m) {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
@media (max-width: $screen-s) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: none;
|
||||
position: sticky;
|
||||
bottom: 0px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
box-shadow: 0px 8px 10px 0px #00000012;
|
||||
padding: 22px 16px;
|
||||
border-radius: 16px 16px 0 0;
|
||||
background: white;
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
box-shadow: 0px 4px 24px 0px #00000026;
|
||||
|
||||
.forfeit-type {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container-frequency {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.container-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.container-tight {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
|
||||
import classes from "./classes.module.scss";
|
||||
import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate";
|
||||
import NavTab from "@Front/Components/Elements/NavTab";
|
||||
import SubscribeCheckoutTicket, { EPaymentFrequency } from "../SubscribeCheckoutTicket";
|
||||
import { EForfeitType, forfeitsPrices } from "../../SubscriptionFacturation";
|
||||
import { useEffect, useState } from "react";
|
||||
import Check from "@Front/Components/Elements/Icons/Check";
|
||||
import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button";
|
||||
import RadioBox from "@Front/Components/DesignSystem/RadioBox";
|
||||
import Confirm from "@Front/Components/DesignSystem/Modal/Confirm";
|
||||
import useOpenable from "@Front/Hooks/useOpenable";
|
||||
import { EType } from "le-coffre-resources/dist/Admin/Subscription";
|
||||
import Stripe from "@Front/Api/LeCoffreApi/Admin/Stripe/Stripe";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
type IProps = {
|
||||
hasNavTab?: boolean;
|
||||
};
|
||||
export default function SubscribeIllimityComponent({ hasNavTab = true }: IProps) {
|
||||
const { close, isOpen, open } = useOpenable();
|
||||
const router = useRouter();
|
||||
|
||||
const formatFloat = (value: number) => {
|
||||
return value.toFixed(2).replace(".", ",");
|
||||
};
|
||||
|
||||
const [paymentFrequency, setPaymentFrequency] = useState<EPaymentFrequency>(EPaymentFrequency.monthly);
|
||||
const [multiplier, setMultiplier] = useState<number>(1);
|
||||
|
||||
useEffect(() => {
|
||||
setMultiplier(paymentFrequency === EPaymentFrequency.monthly ? 1 : 12);
|
||||
}, [paymentFrequency]);
|
||||
|
||||
const handleFrequencyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPaymentFrequency(parseInt(e.target.value) as EPaymentFrequency);
|
||||
};
|
||||
|
||||
const handleSubmitPayment = async () => {
|
||||
const stripeCheckout = {
|
||||
type: EType.Unlimited,
|
||||
nb_seats: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
const newStripeCheckout = await Stripe.getInstance().post(stripeCheckout);
|
||||
router.push(newStripeCheckout.url);
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DefaultTemplate title="Nouvelle souscription" hasBackArrow>
|
||||
<div className={classes["root"]}>
|
||||
<div className={classes["left"]}>
|
||||
{hasNavTab && (
|
||||
<NavTab
|
||||
items={[
|
||||
{ label: "Forfait standard", path: "/subscription/subscribe/standard" },
|
||||
{ label: "Forfait illimité", path: "/subscription/subscribe/illimity" },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<Typography typo={ITypo.H2} color={ITypoColor.BLACK}>
|
||||
Nombre de collaborateurs illimité
|
||||
</Typography>
|
||||
<div className={classes["infos-container"]}>
|
||||
<div className={classes["line"]}>
|
||||
<Check color={ITypoColor.GREY} />
|
||||
<Typography typo={ITypo.P_16} color={ITypoColor.GREY}>
|
||||
Accompagnement facilité : profitez d'un onboarding individualisé, où nous vous guidons pour une prise en
|
||||
main optimale de l'application
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["line"]}>
|
||||
<Check color={ITypoColor.GREY} />
|
||||
<Typography typo={ITypo.P_16} color={ITypoColor.GREY}>
|
||||
Support technique : notre équipe support est disponible pour vous assister en cas d’incident
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["line"]}>
|
||||
<Check color={ITypoColor.GREY} />
|
||||
<Typography typo={ITypo.P_16} color={ITypoColor.GREY}>
|
||||
Mises à jour régulières : bénéficiez de mises à jour fréquentes pour profiter des dernières
|
||||
fonctionnalités, améliorations de sécurité et performances optimisées
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["line"]}>
|
||||
<Check color={ITypoColor.BLACK} />
|
||||
<Typography typo={ITypo.P_16} color={ITypoColor.BLACK}>
|
||||
Sans limite d'utilisateurs
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes["right"]}>
|
||||
<SubscribeCheckoutTicket forfeitType={EForfeitType.unlimited} numberOfCollaborators={1} hasNavTab={hasNavTab} />
|
||||
</div>
|
||||
</div>
|
||||
</DefaultTemplate>
|
||||
<Confirm isOpen={isOpen} onClose={close} showCancelButton={false} confirmText={"Passer au paiement"} closeBtn onAccept={close}>
|
||||
<SubscribeCheckoutTicket
|
||||
hasNavTab={hasNavTab}
|
||||
forfeitType={EForfeitType.unlimited}
|
||||
numberOfCollaborators={1}
|
||||
defaultFrequency={paymentFrequency}
|
||||
/>
|
||||
</Confirm>
|
||||
<div className={classes["bottom"]}>
|
||||
<div className={classes["container-frequency"]}>
|
||||
<RadioBox
|
||||
name="paymentFrequencyInSubscription"
|
||||
value={EPaymentFrequency.yearly.toString()}
|
||||
onChange={handleFrequencyChange}
|
||||
defaultChecked={paymentFrequency === EPaymentFrequency.yearly}>
|
||||
<Typography typo={ITypo.P_ERR_18}>Annuel</Typography>
|
||||
</RadioBox>
|
||||
<RadioBox
|
||||
name="paymentFrequencyInSubscription"
|
||||
value={EPaymentFrequency.monthly.toString()}
|
||||
onChange={handleFrequencyChange}
|
||||
defaultChecked={paymentFrequency === EPaymentFrequency.monthly}>
|
||||
<Typography typo={ITypo.P_ERR_18}>Mensuel</Typography>
|
||||
</RadioBox>
|
||||
</div>
|
||||
<div className={classes["separator"]} />
|
||||
<div className={classes["container-line"]}>
|
||||
<Typography typo={ITypo.P_18} color={ITypoColor.BLACK}>
|
||||
Total TTC
|
||||
</Typography>
|
||||
<Typography typo={ITypo.P_SB_18} color={ITypoColor.BLACK}>
|
||||
{formatFloat(forfeitsPrices[EForfeitType.unlimited] * 1.2 * multiplier)}
|
||||
€
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["voir-recap"]}>
|
||||
<Button fullwidth variant={EButtonVariant.LINE} onClick={open}>
|
||||
Voir le récapitulatif plus en détail
|
||||
</Button>
|
||||
</div>
|
||||
<div className={classes["payment-button"]} onClick={handleSubmitPayment}>
|
||||
<Button fullwidth>{hasNavTab ? "Passer au paiement" : "Mettre à jour l'abonnement"}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 104px;
|
||||
max-width: 1400px;
|
||||
margin: auto;
|
||||
@media (max-width: $screen-m) {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
gap: 72px;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
flex-direction: column;
|
||||
.infos-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
.line {
|
||||
svg {
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
}
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 372px;
|
||||
@media (max-width: $screen-m) {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
@media (max-width: $screen-s) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: none;
|
||||
position: sticky;
|
||||
bottom: 0px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
box-shadow: 0px 8px 10px 0px #00000012;
|
||||
padding: 22px 16px;
|
||||
border-radius: 16px 16px 0 0;
|
||||
background: white;
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
box-shadow: 0px 4px 24px 0px #00000026;
|
||||
|
||||
.forfeit-type {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container-frequency {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.container-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.container-tight {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
|
||||
import classes from "./classes.module.scss";
|
||||
import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate";
|
||||
import NavTab from "@Front/Components/Elements/NavTab";
|
||||
import NumberPicker from "@Front/Components/Elements/NumberPicker";
|
||||
import SubscribeCheckoutTicket, { EPaymentFrequency } from "../SubscribeCheckoutTicket";
|
||||
import { EForfeitType, collaboratorPrice, forfeitsPrices } from "../../SubscriptionFacturation";
|
||||
import { useEffect, useState } from "react";
|
||||
import Check from "@Front/Components/Elements/Icons/Check";
|
||||
import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button";
|
||||
import RadioBox from "@Front/Components/DesignSystem/RadioBox";
|
||||
import Confirm from "@Front/Components/DesignSystem/Modal/Confirm";
|
||||
import useOpenable from "@Front/Hooks/useOpenable";
|
||||
// import Stripe from "@Front/Api/LeCoffreApi/Admin/Stripe/Stripe";
|
||||
// import { EType } from "le-coffre-resources/dist/Admin/Subscription";
|
||||
|
||||
type IProps = {
|
||||
hasNavTab?: boolean;
|
||||
};
|
||||
export default function SubscribeStandardComponent({ hasNavTab = true }: IProps) {
|
||||
const [numberOfCollaborators, setNumberOfCollaborators] = useState(1);
|
||||
const { close, isOpen, open } = useOpenable();
|
||||
const handleCollaboratorsChange = (value: number) => {
|
||||
setNumberOfCollaborators(value);
|
||||
};
|
||||
|
||||
const formatFloat = (value: number) => {
|
||||
return value.toFixed(2).replace(".", ",");
|
||||
};
|
||||
|
||||
const [paymentFrequency, setPaymentFrequency] = useState<EPaymentFrequency>(EPaymentFrequency.monthly);
|
||||
const [multiplier, setMultiplier] = useState<number>(1);
|
||||
|
||||
useEffect(() => {
|
||||
setMultiplier(paymentFrequency === EPaymentFrequency.monthly ? 1 : 12);
|
||||
}, [paymentFrequency]);
|
||||
|
||||
const handleFrequencyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPaymentFrequency(parseInt(e.target.value) as EPaymentFrequency);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DefaultTemplate title="Nouvelle souscription" hasBackArrow>
|
||||
<div className={classes["root"]}>
|
||||
<div className={classes["left"]}>
|
||||
{hasNavTab && (
|
||||
<NavTab
|
||||
items={[
|
||||
{ label: "Forfait standard", path: "/subscription/subscribe/standard" },
|
||||
{ label: "Forfait illimité", path: "/subscription/subscribe/illimity" },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<Typography typo={ITypo.H2} color={ITypoColor.BLACK}>
|
||||
Choisissez le nombre de collaborateurs pour votre abonnement
|
||||
</Typography>
|
||||
<NumberPicker defaultValue={1} onChange={handleCollaboratorsChange} min={1} />
|
||||
<div className={classes["infos-container"]}>
|
||||
<div className={classes["line"]}>
|
||||
<Check color={ITypoColor.GREY} />
|
||||
<Typography typo={ITypo.P_16} color={ITypoColor.GREY}>
|
||||
Accompagnement facilité : profitez d'un onboarding individualisé, où nous vous guidons pour une prise en
|
||||
main optimale de l'application
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["line"]}>
|
||||
<Check color={ITypoColor.GREY} />
|
||||
<Typography typo={ITypo.P_16} color={ITypoColor.GREY}>
|
||||
Support technique : notre équipe support est disponible pour vous assister en cas d’incident
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["line"]}>
|
||||
<Check color={ITypoColor.GREY} />
|
||||
<Typography typo={ITypo.P_16} color={ITypoColor.GREY}>
|
||||
Mises à jour régulières : bénéficiez de mises à jour fréquentes pour profiter des dernières
|
||||
fonctionnalités, améliorations de sécurité et performances optimisées
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes["right"]}>
|
||||
<SubscribeCheckoutTicket
|
||||
forfeitType={EForfeitType.standard}
|
||||
numberOfCollaborators={numberOfCollaborators}
|
||||
hasNavTab={hasNavTab}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultTemplate>
|
||||
<Confirm isOpen={isOpen} onClose={close} showCancelButton={false} confirmText={"Passer au paiement"} closeBtn onAccept={close}>
|
||||
<SubscribeCheckoutTicket
|
||||
forfeitType={EForfeitType.standard}
|
||||
numberOfCollaborators={numberOfCollaborators}
|
||||
defaultFrequency={paymentFrequency}
|
||||
hasNavTab={hasNavTab}
|
||||
/>
|
||||
</Confirm>
|
||||
<div className={classes["bottom"]}>
|
||||
<div className={classes["container-frequency"]}>
|
||||
<RadioBox
|
||||
name="paymentFrequencyInSubscription"
|
||||
value={EPaymentFrequency.yearly.toString()}
|
||||
onChange={handleFrequencyChange}
|
||||
defaultChecked={paymentFrequency === EPaymentFrequency.yearly}>
|
||||
<Typography typo={ITypo.P_ERR_18}>Annuel</Typography>
|
||||
</RadioBox>
|
||||
<RadioBox
|
||||
name="paymentFrequencyInSubscription"
|
||||
value={EPaymentFrequency.monthly.toString()}
|
||||
onChange={handleFrequencyChange}
|
||||
defaultChecked={paymentFrequency === EPaymentFrequency.monthly}>
|
||||
<Typography typo={ITypo.P_ERR_18}>Mensuel</Typography>
|
||||
</RadioBox>
|
||||
</div>
|
||||
<div className={classes["separator"]} />
|
||||
<div className={classes["container-line"]}>
|
||||
<Typography typo={ITypo.P_18} color={ITypoColor.BLACK}>
|
||||
Total TTC
|
||||
</Typography>
|
||||
<Typography typo={ITypo.P_SB_18} color={ITypoColor.BLACK}>
|
||||
{formatFloat(
|
||||
(forfeitsPrices[EForfeitType.standard] + collaboratorPrice * numberOfCollaborators) * 1.2 * multiplier,
|
||||
)}
|
||||
€
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["voir-recap"]}>
|
||||
<Button fullwidth variant={EButtonVariant.LINE} onClick={open}>
|
||||
Voir le récapitulatif plus en détail
|
||||
</Button>
|
||||
</div>
|
||||
<div className={classes["payment-button"]}>
|
||||
<Button fullwidth>Passer au paiement</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
|
||||
import classes from "./classes.module.scss";
|
||||
import { IGetCustomerBySubscriptionIdParams } from "@Front/Api/LeCoffreApi/Admin/Stripe/Stripe";
|
||||
|
||||
type IProps = {
|
||||
customer: IGetCustomerBySubscriptionIdParams;
|
||||
};
|
||||
|
||||
export default function SubscriptionClientInfos(props: IProps) {
|
||||
const { customer } = props;
|
||||
return (
|
||||
<div className={classes["root"]}>
|
||||
<Typography typo={ITypo.P_SB_18} color={ITypoColor.BLACK}>
|
||||
Informations client
|
||||
</Typography>
|
||||
<Typography typo={ITypo.P_18} color={ITypoColor.BLACK}>
|
||||
{customer.email}
|
||||
</Typography>
|
||||
{/* <Typography typo={ITypo.P_SB_18} color={ITypoColor.BLACK}>
|
||||
Adresse de facturation
|
||||
</Typography> */}
|
||||
<Typography typo={ITypo.P_18} color={ITypoColor.BLACK}>
|
||||
{customer.name} <br />
|
||||
{/* 23 rue taitbout,
|
||||
<br />
|
||||
75009 Paris
|
||||
<br /> */}
|
||||
France
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
margin-top: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
.collaborators-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.buttons-container {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
max-width: 400px;
|
||||
@media (max-width: $screen-s) {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
|
||||
import classes from "./classes.module.scss";
|
||||
import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate";
|
||||
import Form from "@Front/Components/DesignSystem/Form";
|
||||
import CheckBox from "@Front/Components/DesignSystem/CheckBox";
|
||||
import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import User, { Subscription } from "le-coffre-resources/dist/Admin";
|
||||
import JwtService from "@Front/Services/JwtService/JwtService";
|
||||
import Subscriptions from "@Front/Api/LeCoffreApi/Admin/Subscriptions/Subscriptions";
|
||||
import Users from "@Front/Api/LeCoffreApi/Admin/Users/Users";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export default function SubscriptionManageCollaborators() {
|
||||
const router = useRouter();
|
||||
const [subscription, setSubscription] = useState<Subscription | null>(null);
|
||||
const [availableCollaborators, _setAvailableCollaborators] = useState<User[]>([]);
|
||||
const [selectedCollaborators, setSelectedCollaborators] = useState<string[]>([]);
|
||||
|
||||
const loadSubscription = useCallback(async () => {
|
||||
const jwt = JwtService.getInstance().decodeJwt();
|
||||
const subscription = await Subscriptions.getInstance().get({
|
||||
where: { office: { uid: jwt?.office_Id } },
|
||||
include: { seats: { include: { user: true } } },
|
||||
});
|
||||
if (!subscription[0]) return;
|
||||
subscription[0].seats?.forEach((seat) => setSelectedCollaborators((prev) => [...prev, seat.user.uid!]));
|
||||
setSubscription(subscription[0]);
|
||||
}, []);
|
||||
|
||||
const loadCollaborators = useCallback(async () => {
|
||||
const collaborators = await Users.getInstance().get({
|
||||
where: { office_membership: { uid: JwtService.getInstance().getUserJwtPayload()?.office_Id } },
|
||||
include: {
|
||||
contact: true,
|
||||
seats: true,
|
||||
},
|
||||
});
|
||||
_setAvailableCollaborators(collaborators);
|
||||
}, []);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!subscription) return;
|
||||
const value = event.target.value;
|
||||
if (selectedCollaborators.includes(value)) {
|
||||
setSelectedCollaborators((prev) => prev.filter((collaborator) => collaborator !== value));
|
||||
} else {
|
||||
if (selectedCollaborators.length < subscription.nb_seats!) {
|
||||
setSelectedCollaborators((prev) => [...prev, value]);
|
||||
}
|
||||
}
|
||||
},
|
||||
[selectedCollaborators, subscription],
|
||||
);
|
||||
|
||||
const cancelAll = () => {
|
||||
setSelectedCollaborators([]);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => {
|
||||
const subcriptionToUpdate = {
|
||||
seats: selectedCollaborators.map((collaborator) => ({ user: { uid: collaborator } })),
|
||||
};
|
||||
await Subscriptions.getInstance().put(subscription?.uid!, subcriptionToUpdate);
|
||||
router.push("/subscription/manage");
|
||||
if (!e) return;
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadSubscription();
|
||||
loadCollaborators();
|
||||
}, [loadCollaborators, loadSubscription]);
|
||||
|
||||
return (
|
||||
<DefaultTemplate title="Nouvelle souscription" hasBackArrow>
|
||||
{subscription && (
|
||||
<div className={classes["root"]}>
|
||||
<Typography typo={ITypo.H2} color={ITypoColor.BLACK}>
|
||||
Choisissez les collaborateurs pour votre abonnement
|
||||
</Typography>
|
||||
<Typography typo={ITypo.P_SB_18} color={ITypoColor.BLACK}>
|
||||
{subscription.nb_seats} sièges disponibles
|
||||
</Typography>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<div className={classes["collaborators-container"]}>
|
||||
{availableCollaborators.map((collaborator) => (
|
||||
<CheckBox
|
||||
key={collaborator.uid}
|
||||
option={{
|
||||
label: collaborator.contact?.first_name + " " + collaborator.contact?.last_name,
|
||||
value: collaborator.uid,
|
||||
}}
|
||||
checked={selectedCollaborators.includes(collaborator.uid!)}
|
||||
onChange={handleChange}
|
||||
disabled={
|
||||
selectedCollaborators.length >= subscription.nb_seats! &&
|
||||
!selectedCollaborators.includes(collaborator.uid!)
|
||||
}
|
||||
name="collaborators"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Typography typo={ITypo.CAPTION_14} color={ITypoColor.BLACK}>
|
||||
{selectedCollaborators.length} collaborateurs sélectionnés
|
||||
</Typography>
|
||||
<div className={classes["buttons-container"]}>
|
||||
<Button type="submit" variant={EButtonVariant.PRIMARY} fullwidth>
|
||||
Enregistrer
|
||||
</Button>
|
||||
<Button variant={EButtonVariant.GHOST} fullwidth onClick={cancelAll} type="button">
|
||||
Annuler
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
)}
|
||||
</DefaultTemplate>
|
||||
);
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import SubscribeIllimityComponent from "../../Components/SubscribeIllimityComponent";
|
||||
|
||||
export default function SubscribeManageIllimity() {
|
||||
return <SubscribeIllimityComponent hasNavTab={false} />;
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 104px;
|
||||
max-width: 1400px;
|
||||
margin: auto;
|
||||
@media (max-width: $screen-m) {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
gap: 72px;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
flex-direction: column;
|
||||
.infos-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
.line {
|
||||
svg {
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
}
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 372px;
|
||||
@media (max-width: $screen-m) {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
@media (max-width: $screen-s) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: none;
|
||||
position: sticky;
|
||||
bottom: 0px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
box-shadow: 0px 8px 10px 0px #00000012;
|
||||
padding: 22px 16px;
|
||||
border-radius: 16px 16px 0 0;
|
||||
background: white;
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
box-shadow: 0px 4px 24px 0px #00000026;
|
||||
|
||||
.forfeit-type {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container-frequency {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.container-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.container-tight {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import SubscribeStandardComponent from "../../Components/SubscribeStandardComponent";
|
||||
|
||||
export default function SubscribeManageStandard() {
|
||||
return <SubscribeStandardComponent hasNavTab={false} />;
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 104px;
|
||||
max-width: 1400px;
|
||||
margin: auto;
|
||||
@media (max-width: $screen-m) {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
gap: 72px;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
flex-direction: column;
|
||||
.infos-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
.line {
|
||||
svg {
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
}
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 372px;
|
||||
@media (max-width: $screen-m) {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
@media (max-width: $screen-s) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: none;
|
||||
position: sticky;
|
||||
bottom: 0px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
box-shadow: 0px 8px 10px 0px #00000012;
|
||||
padding: 22px 16px;
|
||||
border-radius: 16px 16px 0 0;
|
||||
background: white;
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
box-shadow: 0px 4px 24px 0px #00000026;
|
||||
|
||||
.forfeit-type {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container-frequency {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.container-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.container-tight {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import SubscribeIllimityComponent from "../../Components/SubscribeIllimityComponent";
|
||||
|
||||
export default function SubscribeIllimity() {
|
||||
return <SubscribeIllimityComponent />;
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 104px;
|
||||
max-width: 1400px;
|
||||
margin: auto;
|
||||
@media (max-width: $screen-m) {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
gap: 72px;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
flex-direction: column;
|
||||
.infos-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
.line {
|
||||
svg {
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
}
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 372px;
|
||||
@media (max-width: $screen-m) {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
@media (max-width: $screen-s) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: none;
|
||||
position: sticky;
|
||||
bottom: 0px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
box-shadow: 0px 8px 10px 0px #00000012;
|
||||
padding: 22px 16px;
|
||||
border-radius: 16px 16px 0 0;
|
||||
background: white;
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
box-shadow: 0px 4px 24px 0px #00000026;
|
||||
|
||||
.forfeit-type {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container-frequency {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.container-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.container-tight {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import SubscribeStandardComponent from "../../Components/SubscribeStandardComponent";
|
||||
|
||||
export default function SubscribeStandard() {
|
||||
return <SubscribeStandardComponent />;
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
.root {
|
||||
display: flex;
|
||||
gap: 104px;
|
||||
justify-content: center;
|
||||
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
|
||||
.left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
width: 548px;
|
||||
}
|
||||
.separator {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--grey-medium);
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate";
|
||||
import classes from "./classes.module.scss";
|
||||
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
|
||||
import MessageBox from "@Front/Components/Elements/MessageBox";
|
||||
import SubscriptionClientInfos from "../Components/SubscriptionClientInfos";
|
||||
import Button from "@Front/Components/DesignSystem/Button";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Subscription } from "le-coffre-resources/dist/Admin";
|
||||
import JwtService from "@Front/Services/JwtService/JwtService";
|
||||
import Subscriptions from "@Front/Api/LeCoffreApi/Admin/Subscriptions/Subscriptions";
|
||||
import SubscribeCheckoutTicket, { EPaymentFrequency } from "../Components/SubscribeCheckoutTicket";
|
||||
import { EForfeitType } from "../SubscriptionFacturation";
|
||||
import Stripe from "@Front/Api/LeCoffreApi/Admin/Stripe/Stripe";
|
||||
|
||||
export default function SubscriptionError() {
|
||||
const [subscription, setSubscription] = useState<Subscription | null>(null);
|
||||
const [customer, setCustomer] = useState<any | null>(null);
|
||||
|
||||
const loadSubscription = useCallback(async () => {
|
||||
const jwt = JwtService.getInstance().decodeJwt();
|
||||
const subscription = await Subscriptions.getInstance().get({ where: { office: { uid: jwt?.office_Id } } });
|
||||
if (!subscription[0]) return;
|
||||
setSubscription(subscription[0]);
|
||||
const customer = await Stripe.getInstance().getCustomerBySubscriptionId(subscription[0].stripe_subscription_id!);
|
||||
setCustomer(customer);
|
||||
}, []);
|
||||
|
||||
const getFrequency = useCallback(() => {
|
||||
if (!subscription) return;
|
||||
const start = new Date(subscription.start_date);
|
||||
const end = new Date(subscription.end_date);
|
||||
|
||||
const diffTime = Math.abs(end.getTime() - start.getTime());
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
return diffDays >= 365 ? EPaymentFrequency.yearly : EPaymentFrequency.monthly;
|
||||
}, [subscription]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSubscription();
|
||||
}, [loadSubscription]);
|
||||
|
||||
return (
|
||||
<DefaultTemplate title="Erreur à la souscription">
|
||||
{subscription && customer && (
|
||||
<div className={classes["root"]}>
|
||||
<div className={classes["left"]}>
|
||||
<div className={classes["title"]}>
|
||||
<Typography typo={ITypo.H1} color={ITypoColor.BLACK}>
|
||||
Paiement échoué
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["alert"]}>
|
||||
<MessageBox type={"error"}>
|
||||
Votre transaction n'a pas pu être complétée.
|
||||
<br />
|
||||
<br />
|
||||
Malheureusement, nous n'avons pas pu traiter votre paiement et votre abonnement n'a pas été activé. Veuillez
|
||||
vérifier vos informations de paiement et essayer à nouveau.
|
||||
</MessageBox>
|
||||
</div>
|
||||
<div className={classes["separator"]} />
|
||||
<div className={classes["client-infos"]}>
|
||||
<SubscriptionClientInfos customer={customer} />
|
||||
</div>
|
||||
<div className={classes["separator"]} />
|
||||
<Button>Réessayer le paiement</Button>
|
||||
</div>
|
||||
<div className={classes["right"]}>
|
||||
<SubscribeCheckoutTicket
|
||||
forfeitType={subscription.type === "STANDARD" ? EForfeitType.standard : EForfeitType.unlimited}
|
||||
numberOfCollaborators={subscription.nb_seats ?? 0}
|
||||
disableInputs
|
||||
defaultFrequency={getFrequency()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DefaultTemplate>
|
||||
);
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
justify-content: baseline;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 64px;
|
||||
max-width: 1000px;
|
||||
margin: auto;
|
||||
|
||||
@media (max-width: $screen-m) {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.top-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.forfeits-container {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.forfeit-block {
|
||||
flex: 1;
|
||||
padding: 32px;
|
||||
border: 1px solid black;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
height: fit-content;
|
||||
|
||||
&[data-inactive="true"] {
|
||||
border: 1px solid #e7e7e7;
|
||||
}
|
||||
|
||||
.forfeit-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.active-plan {
|
||||
@media (max-width: $screen-s) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
.price-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.price {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 48px;
|
||||
justify-content: flex-end;
|
||||
justify-self: flex-end;
|
||||
align-self: flex-end;
|
||||
margin-bottom: 64px;
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
flex-direction: column-reverse;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,272 @@
|
||||
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
|
||||
import classes from "./classes.module.scss";
|
||||
import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate";
|
||||
import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import Confirm from "@Front/Components/DesignSystem/Modal/Confirm";
|
||||
import useOpenable from "@Front/Hooks/useOpenable";
|
||||
import MessageBox from "@Front/Components/Elements/MessageBox";
|
||||
import Link from "next/link";
|
||||
import Module from "@Front/Config/Module";
|
||||
import Subscriptions from "@Front/Api/LeCoffreApi/Admin/Subscriptions/Subscriptions";
|
||||
import JwtService from "@Front/Services/JwtService/JwtService";
|
||||
import Stripe from "@Front/Api/LeCoffreApi/Admin/Stripe/Stripe";
|
||||
import { useRouter } from "next/router";
|
||||
import { Subscription } from "le-coffre-resources/dist/Admin";
|
||||
|
||||
export enum EForfeitType {
|
||||
"standard",
|
||||
"unlimited",
|
||||
}
|
||||
export const collaboratorPrice = 6.99;
|
||||
|
||||
export const forfeitsPrices: Record<EForfeitType, number> = {
|
||||
[EForfeitType.standard]: 99,
|
||||
[EForfeitType.unlimited]: 199,
|
||||
};
|
||||
export default function SubscriptionFacturation() {
|
||||
const router = useRouter();
|
||||
const [subscription, setSubscription] = useState<Subscription | null>(null);
|
||||
|
||||
const { close: closeCancelSubscription, isOpen: isCancelSubscriptionOpen } = useOpenable();
|
||||
const { close: closeConfirmation, isOpen: isConfirmationOpen } = useOpenable();
|
||||
|
||||
// const cancelSubscription = useCallback(() => {
|
||||
// closeCancelSubscription();
|
||||
// openConfirmation();
|
||||
// return;
|
||||
// }, [closeCancelSubscription, openConfirmation]);
|
||||
|
||||
const manageSubscription = async () => {
|
||||
try {
|
||||
const jwt = JwtService.getInstance().decodeJwt();
|
||||
const subscription = await Subscriptions.getInstance().get({ where: { office: { uid: jwt?.office_Id } } });
|
||||
if (!subscription[0]) return;
|
||||
const stripe_client_portal = await Stripe.getInstance().getClientPortalSession(subscription[0].stripe_subscription_id!);
|
||||
router.push(stripe_client_portal.url + "/subscriptions/" + subscription[0].stripe_subscription_id + "/update");
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
const cancelSubscription = async () => {
|
||||
try {
|
||||
const jwt = JwtService.getInstance().decodeJwt();
|
||||
const subscription = await Subscriptions.getInstance().get({ where: { office: { uid: jwt?.office_Id } } });
|
||||
if (!subscription[0]) return;
|
||||
const stripe_client_portal = await Stripe.getInstance().getClientPortalSession(subscription[0].stripe_subscription_id!);
|
||||
router.push(stripe_client_portal.url + "/subscriptions/" + subscription[0].stripe_subscription_id + "/cancel");
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
const manageBilling = async () => {
|
||||
try {
|
||||
const jwt = JwtService.getInstance().decodeJwt();
|
||||
const subscription = await Subscriptions.getInstance().get({ where: { office: { uid: jwt?.office_Id } } });
|
||||
if (!subscription[0]) return;
|
||||
const stripe_client_portal = await Stripe.getInstance().getClientPortalSession(subscription[0].stripe_subscription_id!);
|
||||
router.push(stripe_client_portal.url);
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
const loadSubscription = useCallback(async () => {
|
||||
const jwt = JwtService.getInstance().decodeJwt();
|
||||
const subscription = await Subscriptions.getInstance().get({ where: { office: { uid: jwt?.office_Id } } });
|
||||
if (!subscription[0]) {
|
||||
router.push(Module.getInstance().get().modules.pages.Subscription.pages.New.props.path);
|
||||
} else {
|
||||
setSubscription(subscription[0]);
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSubscription();
|
||||
}, [loadSubscription]);
|
||||
|
||||
console.log(forfeitsPrices[EForfeitType.unlimited].toString());
|
||||
return (
|
||||
<DefaultTemplate title="Nouvelle souscription">
|
||||
{subscription && (
|
||||
<div className={classes["root"]}>
|
||||
<div className={classes["top-container"]}>
|
||||
<div className={classes["top-container-title"]}>
|
||||
<Typography typo={ITypo.H1} color={ITypoColor.BLACK}>
|
||||
Abonnement
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["sub-title"]}>
|
||||
<Typography typo={ITypo.P_16} color={ITypoColor.BLACK}>
|
||||
Nos forfaits sont adaptés à la taille de votre office
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes["forfeits-container"]}>
|
||||
<div
|
||||
className={classes["forfeit-block"]}
|
||||
data-inactive={subscription.type === "STANDARD" ? EForfeitType.standard : EForfeitType.unlimited}>
|
||||
<div className={classes["forfeit-header"]}>
|
||||
<div className={classes["left"]}>
|
||||
<Typography typo={ITypo.P_SB_18} color={ITypoColor.BLACK}>
|
||||
Forfait standard
|
||||
</Typography>
|
||||
<Typography typo={ITypo.P_16} color={ITypoColor.PINK_FLASH}>
|
||||
Plan par utilisateur
|
||||
</Typography>
|
||||
</div>
|
||||
{subscription.type === "STANDARD" && (
|
||||
<div className={classes["active-plan"]}>
|
||||
<Typography typo={ITypo.P_18} color={ITypoColor.GREEN_FLASH}>
|
||||
Plan actif
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={classes["separator"]} />
|
||||
<div className={classes["price-container"]}>
|
||||
<Typography typo={ITypo.H1} color={ITypoColor.BLACK} className={classes["price"]}>
|
||||
{forfeitsPrices[EForfeitType.standard]}€
|
||||
<Typography typo={ITypo.H2} color={ITypoColor.BLACK}>
|
||||
HT
|
||||
</Typography>
|
||||
/ mois
|
||||
</Typography>
|
||||
<Typography typo={ITypo.P_18} color={ITypoColor.BLACK}>
|
||||
+ {collaboratorPrice}€ / collaborateur / mois
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["button-container"]}>
|
||||
{subscription.type === "UNLIMITED" && (
|
||||
// <Link
|
||||
// href={Module.getInstance().get().modules.pages.Subscription.pages.Manage.pages.Standard.props.path}>
|
||||
<Button onClick={manageSubscription} fullwidth variant={EButtonVariant.GHOST}>
|
||||
Rétrograder mon abonnement
|
||||
</Button>
|
||||
// </Link>
|
||||
)}
|
||||
{subscription.type === "STANDARD" && (
|
||||
<>
|
||||
{/* <Link
|
||||
href={
|
||||
Module.getInstance().get().modules.pages.Subscription.pages.Manage.pages.Standard.props.path
|
||||
}> */}
|
||||
<Button onClick={manageBilling} fullwidth variant={EButtonVariant.PRIMARY}>
|
||||
Changer de plan
|
||||
</Button>
|
||||
{/* </Link> */}
|
||||
<Link
|
||||
href={
|
||||
Module.getInstance().get().modules.pages.Subscription.pages.ManageCollaborators.props.path
|
||||
}>
|
||||
<Button fullwidth variant={EButtonVariant.GHOST}>
|
||||
Gérer mes attributions
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classes["forfeit-block"]}
|
||||
data-inactive={subscription.type === "STANDARD" ? EForfeitType.standard : EForfeitType.unlimited}>
|
||||
<div className={classes["forfeit-header"]}>
|
||||
<div className={classes["left"]}>
|
||||
<Typography typo={ITypo.P_SB_18} color={ITypoColor.BLACK}>
|
||||
Forfait illimité
|
||||
</Typography>
|
||||
<Typography typo={ITypo.P_16} color={ITypoColor.PINK_FLASH}>
|
||||
Plan par office
|
||||
</Typography>
|
||||
</div>
|
||||
{subscription.type === "UNLIMITED" && (
|
||||
<div className={classes["active-plan"]}>
|
||||
<Typography typo={ITypo.P_18} color={ITypoColor.GREEN_FLASH}>
|
||||
Plan actif
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={classes["separator"]} />
|
||||
<div className={classes["price-container"]}>
|
||||
<Typography typo={ITypo.H1} color={ITypoColor.BLACK} className={classes["price"]}>
|
||||
{forfeitsPrices[EForfeitType.unlimited]}€
|
||||
<Typography typo={ITypo.H2} color={ITypoColor.BLACK}>
|
||||
HT
|
||||
</Typography>
|
||||
/ mois
|
||||
</Typography>
|
||||
<Typography typo={ITypo.P_18} color={ITypoColor.BLACK}>
|
||||
Sans limite de collaborateurs
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["button-container"]}>
|
||||
{subscription.type === "UNLIMITED" && (
|
||||
<Button fullwidth variant={EButtonVariant.PRIMARY} disabled>
|
||||
Abonnement Max Activé
|
||||
</Button>
|
||||
)}
|
||||
{subscription.type === "STANDARD" && (
|
||||
// <Link
|
||||
// href={Module.getInstance().get().modules.pages.Subscription.pages.Manage.pages.Illimity.props.path}>
|
||||
<Button onClick={manageSubscription} fullwidth variant={EButtonVariant.GHOST}>
|
||||
Améliorer mon abonnement
|
||||
</Button>
|
||||
// </Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes["actions-container"]}>
|
||||
<Button variant={EButtonVariant.LINE} onClick={cancelSubscription}>
|
||||
<Typography typo={ITypo.P_18} color={ITypoColor.RED_FLASH}>
|
||||
Arrêter l'abonnement
|
||||
</Typography>
|
||||
</Button>
|
||||
<Button onClick={manageBilling}>Gérer la facturation</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Confirm
|
||||
isOpen={isCancelSubscriptionOpen}
|
||||
onClose={closeCancelSubscription}
|
||||
onAccept={cancelSubscription}
|
||||
closeBtn
|
||||
header={"Êtes-vous sûr de vouloir arrêter votre abonnement ?"}
|
||||
confirmText={"Confirmer"}
|
||||
cancelText={"Annuler"}>
|
||||
<div className={classes["modal-content"]}>
|
||||
<Typography typo={ITypo.P_16} className={classes["text"]}>
|
||||
Avant de confirmer, veuillez prendre note des conséquences <br />
|
||||
suivantes :
|
||||
<br />
|
||||
<ul>
|
||||
<li>
|
||||
Arrêt des fonctionnalités : Vous n'aurez plus accès aux outils de traitement et de mise à jour en temps
|
||||
réel.
|
||||
</li>
|
||||
<li>
|
||||
Accès limité : Vous pourrez uniquement télécharger vos documents existants, sans possibilité de les éditer
|
||||
ou de créer de nouveaux fichiers.
|
||||
</li>
|
||||
</ul>
|
||||
Votre abonnement se terminera le XX/XX/XXXX. Assurez-vous de sauvegarder tout ce dont vous avez besoin avant cette
|
||||
date.
|
||||
</Typography>
|
||||
</div>
|
||||
</Confirm>
|
||||
|
||||
<Confirm
|
||||
isOpen={isConfirmationOpen}
|
||||
onClose={closeConfirmation}
|
||||
onAccept={closeConfirmation}
|
||||
closeBtn
|
||||
header={"Abonnement résilié avec succès"}
|
||||
confirmText={"Retour à la plateforme"}
|
||||
showCancelButton={false}>
|
||||
<div className={classes["modal-content"]}>
|
||||
<MessageBox type="info">
|
||||
Votre abonnement se terminera le XX/XX/XXXX. Assurez-vous de sauvegarder tout ce dont vous avez besoin avant cette
|
||||
date.
|
||||
</MessageBox>
|
||||
</div>
|
||||
</Confirm>
|
||||
</DefaultTemplate>
|
||||
);
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
.root {
|
||||
max-width: 1400px;
|
||||
margin: auto;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
|
||||
.emails-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
> span {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.add-line-container {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
|
||||
import classes from "./classes.module.scss";
|
||||
import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate";
|
||||
import { useRouter } from "next/router";
|
||||
import TextField from "@Front/Components/DesignSystem/Form/TextField";
|
||||
import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button";
|
||||
import Form from "@Front/Components/DesignSystem/Form";
|
||||
import Subscriptions from "@Front/Api/LeCoffreApi/Admin/Subscriptions/Subscriptions";
|
||||
import { useCallback, useState } from "react";
|
||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||
import PlusIcon from "@Assets/Icons/plus.svg";
|
||||
import Module from "@Front/Config/Module";
|
||||
|
||||
type EmailLine = {
|
||||
element: JSX.Element;
|
||||
id: number;
|
||||
};
|
||||
|
||||
export default function SubscriptionInvite() {
|
||||
const router = useRouter();
|
||||
const nbOfCollaborators = parseInt(router.query["nbOfCollaborators"] as string);
|
||||
const [incrementalId, setIncrementalId] = useState(isNaN(nbOfCollaborators) ? 0 : nbOfCollaborators);
|
||||
|
||||
const getInitialLines = () => {
|
||||
const linesToGet = isNaN(nbOfCollaborators) ? 1 : nbOfCollaborators;
|
||||
const lines: EmailLine[] = [];
|
||||
for (let i = 0; i < linesToGet; i++) {
|
||||
lines.push({
|
||||
element: <TextField key={i} name={`email_${i}`} placeholder="Email" className={classes["input"]} required />,
|
||||
id: i,
|
||||
});
|
||||
}
|
||||
return lines;
|
||||
};
|
||||
|
||||
const [lines, setLines] = useState<EmailLine[]>(getInitialLines());
|
||||
|
||||
const sendInvitations = async (e: React.FormEvent<HTMLFormElement> | null) => {
|
||||
if (!e) return;
|
||||
e.preventDefault();
|
||||
const form = e.target as HTMLFormElement;
|
||||
const emails: string[] = [];
|
||||
Object.keys(form.elements).forEach((key) => {
|
||||
if (isNaN(parseInt(key))) return;
|
||||
const element = form.elements[key as any] as HTMLInputElement;
|
||||
if (element.name.includes("email_")) {
|
||||
emails.push(element.value);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await Subscriptions.getInstance().post({
|
||||
emails,
|
||||
});
|
||||
router.push(Module.getInstance().get().modules.pages.Subscription.pages.Manage.props.path);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const addLine = useCallback(() => {
|
||||
const newLine: EmailLine = {
|
||||
element: (
|
||||
<TextField key={lines.length} name={`email_${lines.length}`} placeholder="Email" className={classes["input"]} required />
|
||||
),
|
||||
id: incrementalId + 1,
|
||||
};
|
||||
setIncrementalId(incrementalId + 1);
|
||||
setLines((prev) => [...prev, newLine]);
|
||||
}, [incrementalId, lines]);
|
||||
|
||||
const deleteLine = (e: React.MouseEvent<SVGSVGElement>) => {
|
||||
const lineId = parseInt(e.currentTarget.getAttribute("data-line") as string);
|
||||
setLines((prev) => prev.filter((line) => line.id !== lineId));
|
||||
};
|
||||
|
||||
return (
|
||||
<DefaultTemplate title="Nouvelle souscription" hasBackArrow>
|
||||
<div className={classes["root"]}>
|
||||
<div className={classes["container"]}>
|
||||
<Typography typo={ITypo.H1} color={ITypoColor.BLACK}>
|
||||
Inviter vos collaborateurs
|
||||
</Typography>
|
||||
{!isNaN(nbOfCollaborators) && (
|
||||
<Typography typo={ITypo.P_SB_18} color={ITypoColor.BLACK}>
|
||||
{nbOfCollaborators} collaborateurs à inviter
|
||||
</Typography>
|
||||
)}
|
||||
{isNaN(nbOfCollaborators) && (
|
||||
<Typography typo={ITypo.P_SB_18} color={ITypoColor.BLACK}>
|
||||
Collaborateurs illimités
|
||||
</Typography>
|
||||
)}
|
||||
<Form className={classes["emails-form"]} onSubmit={sendInvitations}>
|
||||
{lines.map((line, index) => (
|
||||
<div key={line.id} className={classes["input-container"]}>
|
||||
{line.element}
|
||||
{!nbOfCollaborators && (
|
||||
<TrashIcon
|
||||
width="20"
|
||||
height="20"
|
||||
data-line={line.id}
|
||||
onClick={deleteLine}
|
||||
visibility={index === 0 ? "hidden" : "visible"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{isNaN(nbOfCollaborators) && (
|
||||
<div className={classes["add-line-container"]}>
|
||||
<Button onClick={addLine} variant={EButtonVariant.LINE} icon={PlusIcon}>
|
||||
Ajouter une adresse email
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className={classes["button-container"]}>
|
||||
<Button type="submit">Envoyer l'invitation</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultTemplate>
|
||||
);
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
@import "@Themes/constants.scss";
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
justify-content: baseline;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 64px;
|
||||
max-width: 1000px;
|
||||
margin: auto;
|
||||
|
||||
@media (max-width: $screen-m) {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.top-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.forfeits-container {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: $screen-s) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.forfeit-block {
|
||||
flex: 1;
|
||||
padding: 32px;
|
||||
border: 1px solid black;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
|
||||
.forfeit-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
.price-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.price {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.infos-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
.line {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
|
||||
import classes from "./classes.module.scss";
|
||||
import CheckIcon from "@Assets/Icons/check.svg";
|
||||
import Image from "next/image";
|
||||
import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate";
|
||||
import Button from "@Front/Components/DesignSystem/Button";
|
||||
import Link from "next/link";
|
||||
import { EForfeitType, collaboratorPrice, forfeitsPrices } from "../SubscriptionFacturation";
|
||||
|
||||
export default function SubscriptionNew() {
|
||||
return (
|
||||
<DefaultTemplate title="Nouvelle souscription">
|
||||
<div className={classes["root"]}>
|
||||
<div className={classes["top-container"]}>
|
||||
<div className={classes["top-container-title"]}>
|
||||
<Typography typo={ITypo.H1} color={ITypoColor.BLACK}>
|
||||
Tarifs
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["sub-title"]}>
|
||||
<Typography typo={ITypo.P_16} color={ITypoColor.BLACK}>
|
||||
Nos forfaits sont adaptés à la taille de votre office
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes["forfeits-container"]}>
|
||||
<div className={classes["forfeit-block"]}>
|
||||
<div className={classes["forfeit-header"]}>
|
||||
<Typography typo={ITypo.P_SB_18} color={ITypoColor.BLACK}>
|
||||
Forfait standard
|
||||
</Typography>
|
||||
<Typography typo={ITypo.P_16} color={ITypoColor.PINK_FLASH}>
|
||||
Plan par utilisateur
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["separator"]} />
|
||||
<div className={classes["price-container"]}>
|
||||
<Typography typo={ITypo.H1} color={ITypoColor.BLACK} className={classes["price"]}>
|
||||
99€
|
||||
<Typography typo={ITypo.H2} color={ITypoColor.BLACK}>
|
||||
HT
|
||||
</Typography>
|
||||
/ mois
|
||||
</Typography>
|
||||
<Typography typo={ITypo.P_18} color={ITypoColor.BLACK}>
|
||||
+ {collaboratorPrice}€ / collaborateur / mois
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["button-container"]}>
|
||||
<Link href={"/subscription/subscribe/standard"}>
|
||||
<Button fullwidth>S'abonner</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes["forfeit-block"]}>
|
||||
<div className={classes["forfeit-header"]}>
|
||||
<Typography typo={ITypo.P_SB_18} color={ITypoColor.BLACK}>
|
||||
Forfait illimité
|
||||
</Typography>
|
||||
<Typography typo={ITypo.P_16} color={ITypoColor.PINK_FLASH}>
|
||||
Plan par office
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["separator"]} />
|
||||
<div className={classes["price-container"]}>
|
||||
<Typography typo={ITypo.H1} color={ITypoColor.BLACK} className={classes["price"]}>
|
||||
{forfeitsPrices[EForfeitType.unlimited]}€
|
||||
<Typography typo={ITypo.H2} color={ITypoColor.BLACK}>
|
||||
HT
|
||||
</Typography>
|
||||
/ mois
|
||||
</Typography>
|
||||
<Typography typo={ITypo.P_18} color={ITypoColor.BLACK}>
|
||||
Sans limite de collaborateurs
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["button-container"]}>
|
||||
<Link href={"/subscription/subscribe/illimity"}>
|
||||
<Button fullwidth>S'abonner</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes["infos-container"]}>
|
||||
<div className={classes["line"]}>
|
||||
<Image src={CheckIcon} alt="Check icon" />
|
||||
<Typography typo={ITypo.P_16} color={ITypoColor.BLACK}>
|
||||
Accompagnement facilité : profitez d'un onboarding individualisé, où nous vous guidons pour une prise en main
|
||||
optimale de l'application
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["line"]}>
|
||||
<Image src={CheckIcon} alt="Check icon" />
|
||||
<Typography typo={ITypo.P_16} color={ITypoColor.BLACK}>
|
||||
Support technique : notre équipe support est disponible pour vous assister en cas d’incident
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["line"]}>
|
||||
<Image src={CheckIcon} alt="Check icon" />
|
||||
<Typography typo={ITypo.P_16} color={ITypoColor.BLACK}>
|
||||
Mises à jour régulières : bénéficiez de mises à jour fréquentes pour profiter des dernières fonctionnalités,
|
||||
améliorations de sécurité et performances optimisées
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultTemplate>
|
||||
);
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
.root {
|
||||
display: flex;
|
||||
gap: 104px;
|
||||
justify-content: center;
|
||||
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
|
||||
.left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
width: 548px;
|
||||
}
|
||||
.separator {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--grey-medium);
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate";
|
||||
import classes from "./classes.module.scss";
|
||||
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
|
||||
import MessageBox from "@Front/Components/Elements/MessageBox";
|
||||
import SubscriptionClientInfos from "../Components/SubscriptionClientInfos";
|
||||
import Button from "@Front/Components/DesignSystem/Button";
|
||||
import Link from "next/link";
|
||||
import Module from "@Front/Config/Module";
|
||||
import { EForfeitType } from "../SubscriptionFacturation";
|
||||
import SubscribeCheckoutTicket, { EPaymentFrequency } from "../Components/SubscribeCheckoutTicket";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import Subscriptions from "@Front/Api/LeCoffreApi/Admin/Subscriptions/Subscriptions";
|
||||
import JwtService from "@Front/Services/JwtService/JwtService";
|
||||
import { Subscription } from "le-coffre-resources/dist/Admin";
|
||||
import Stripe from "@Front/Api/LeCoffreApi/Admin/Stripe/Stripe";
|
||||
|
||||
export default function SubscriptionSuccess() {
|
||||
const [subscription, setSubscription] = useState<Subscription | null>(null);
|
||||
const [customer, setCustomer] = useState<any | null>(null);
|
||||
|
||||
const loadSubscription = useCallback(async () => {
|
||||
const jwt = JwtService.getInstance().decodeJwt();
|
||||
const subscription = await Subscriptions.getInstance().get({ where: { office: { uid: jwt?.office_Id } } });
|
||||
if (!subscription[0]) return;
|
||||
setSubscription(subscription[0]);
|
||||
const customer = await Stripe.getInstance().getCustomerBySubscriptionId(subscription[0].stripe_subscription_id!);
|
||||
setCustomer(customer);
|
||||
}, []);
|
||||
|
||||
const getFrequency = useCallback(() => {
|
||||
if (!subscription) return;
|
||||
const start = new Date(subscription.start_date);
|
||||
const end = new Date(subscription.end_date);
|
||||
|
||||
const diffTime = Math.abs(end.getTime() - start.getTime());
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
return diffDays >= 365 ? EPaymentFrequency.yearly : EPaymentFrequency.monthly;
|
||||
}, [subscription]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSubscription();
|
||||
}, [loadSubscription]);
|
||||
|
||||
return (
|
||||
<DefaultTemplate title="Abonnement réussi">
|
||||
{subscription && customer && (
|
||||
<div className={classes["root"]}>
|
||||
<div className={classes["left"]}>
|
||||
<div className={classes["title"]}>
|
||||
<Typography typo={ITypo.H1} color={ITypoColor.BLACK}>
|
||||
Abonnement réussi !
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes["alert"]}>
|
||||
<MessageBox type={"success"}>
|
||||
Votre transaction a été effectuée avec succès !
|
||||
<br />
|
||||
<br />
|
||||
Votre abonnement a été pris en compte et est désormais actif.
|
||||
</MessageBox>
|
||||
</div>
|
||||
<div className={classes["separator"]} />
|
||||
<div className={classes["client-infos"]}>
|
||||
<SubscriptionClientInfos customer={customer} />
|
||||
</div>
|
||||
<div className={classes["separator"]} />
|
||||
{subscription.type === "STANDARD" && (
|
||||
<Link
|
||||
href={
|
||||
Module.getInstance().get().modules.pages.Subscription.pages.Invite.props.path +
|
||||
`?nbOfCollaborators=${subscription.nb_seats ?? 0}`
|
||||
}>
|
||||
<Button>Inviter vos collaborateurs</Button>
|
||||
</Link>
|
||||
)}
|
||||
{subscription.type === "UNLIMITED" && (
|
||||
<Link href={Module.getInstance().get().modules.pages.Subscription.pages.Invite.props.path}>
|
||||
<Button>Inviter vos collaborateurs</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className={classes["right"]}>
|
||||
<SubscribeCheckoutTicket
|
||||
forfeitType={subscription.type === "STANDARD" ? EForfeitType.standard : EForfeitType.unlimited}
|
||||
numberOfCollaborators={subscription.nb_seats ?? 0}
|
||||
disableInputs
|
||||
defaultFrequency={getFrequency()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DefaultTemplate>
|
||||
);
|
||||
}
|
@ -270,6 +270,96 @@
|
||||
"path": "/404",
|
||||
"labelKey": "not_found"
|
||||
}
|
||||
},
|
||||
"Subscription": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription",
|
||||
"labelKey": "subscription"
|
||||
},
|
||||
"pages": {
|
||||
"Invite": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/invite",
|
||||
"labelKey": "invite"
|
||||
}
|
||||
},
|
||||
"ManageCollaborators": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/manage-collaborators",
|
||||
"labelKey": "manage_collaborators"
|
||||
}
|
||||
},
|
||||
"Manage": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/manage",
|
||||
"labelKey": "manage"
|
||||
},
|
||||
"pages": {
|
||||
"Standard": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/manage/standard",
|
||||
"labelKey": "standard"
|
||||
}
|
||||
},
|
||||
"Illimity": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/manage/illimity",
|
||||
"labelKey": "illimity"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"New": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/new",
|
||||
"labelKey": "subscribe"
|
||||
}
|
||||
},
|
||||
"Error": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/error",
|
||||
"labelKey": "error"
|
||||
}
|
||||
},
|
||||
"Success": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/success",
|
||||
"labelKey": "success"
|
||||
}
|
||||
},
|
||||
"Subscribe": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/subscribe",
|
||||
"labelKey": "subscribe"
|
||||
},
|
||||
"pages": {
|
||||
"Standard": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/subscribe/standard",
|
||||
"labelKey": "standard"
|
||||
}
|
||||
},
|
||||
"Illimity": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/subscribe/illimity",
|
||||
"labelKey": "illimity"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -270,6 +270,96 @@
|
||||
"path": "/404",
|
||||
"labelKey": "not_found"
|
||||
}
|
||||
},
|
||||
"Subscription": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription",
|
||||
"labelKey": "subscription"
|
||||
},
|
||||
"pages": {
|
||||
"Invite": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/invite",
|
||||
"labelKey": "invite"
|
||||
}
|
||||
},
|
||||
"ManageCollaborators": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/manage-collaborators",
|
||||
"labelKey": "manage_collaborators"
|
||||
}
|
||||
},
|
||||
"Manage": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/manage",
|
||||
"labelKey": "manage"
|
||||
},
|
||||
"pages": {
|
||||
"Standard": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/manage/standard",
|
||||
"labelKey": "standard"
|
||||
}
|
||||
},
|
||||
"Illimity": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/manage/illimity",
|
||||
"labelKey": "illimity"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"New": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/new",
|
||||
"labelKey": "subscribe"
|
||||
}
|
||||
},
|
||||
"Error": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/error",
|
||||
"labelKey": "error"
|
||||
}
|
||||
},
|
||||
"Success": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/success",
|
||||
"labelKey": "success"
|
||||
}
|
||||
},
|
||||
"Subscribe": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/subscribe",
|
||||
"labelKey": "subscribe"
|
||||
},
|
||||
"pages": {
|
||||
"Standard": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/subscribe/standard",
|
||||
"labelKey": "standard"
|
||||
}
|
||||
},
|
||||
"Illimity": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/subscribe/illimity",
|
||||
"labelKey": "illimity"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -270,6 +270,96 @@
|
||||
"path": "/404",
|
||||
"labelKey": "not_found"
|
||||
}
|
||||
},
|
||||
"Subscription": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription",
|
||||
"labelKey": "subscription"
|
||||
},
|
||||
"pages": {
|
||||
"Invite": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/invite",
|
||||
"labelKey": "invite"
|
||||
}
|
||||
},
|
||||
"ManageCollaborators": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/manage-collaborators",
|
||||
"labelKey": "manage_collaborators"
|
||||
}
|
||||
},
|
||||
"Manage": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/manage",
|
||||
"labelKey": "manage"
|
||||
},
|
||||
"pages": {
|
||||
"Standard": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/manage/standard",
|
||||
"labelKey": "standard"
|
||||
}
|
||||
},
|
||||
"Illimity": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/manage/illimity",
|
||||
"labelKey": "illimity"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"New": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/new",
|
||||
"labelKey": "subscribe"
|
||||
}
|
||||
},
|
||||
"Error": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/error",
|
||||
"labelKey": "error"
|
||||
}
|
||||
},
|
||||
"Success": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/success",
|
||||
"labelKey": "success"
|
||||
}
|
||||
},
|
||||
"Subscribe": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/subscribe",
|
||||
"labelKey": "subscribe"
|
||||
},
|
||||
"pages": {
|
||||
"Standard": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/subscribe/standard",
|
||||
"labelKey": "standard"
|
||||
}
|
||||
},
|
||||
"Illimity": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/subscribe/illimity",
|
||||
"labelKey": "illimity"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -270,6 +270,96 @@
|
||||
"path": "/404",
|
||||
"labelKey": "not_found"
|
||||
}
|
||||
},
|
||||
"Subscription": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription",
|
||||
"labelKey": "subscription"
|
||||
},
|
||||
"pages": {
|
||||
"Invite": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/invite",
|
||||
"labelKey": "invite"
|
||||
}
|
||||
},
|
||||
"ManageCollaborators": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/manage-collaborators",
|
||||
"labelKey": "manage_collaborators"
|
||||
}
|
||||
},
|
||||
"Manage": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/manage",
|
||||
"labelKey": "manage"
|
||||
},
|
||||
"pages": {
|
||||
"Standard": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/manage/standard",
|
||||
"labelKey": "standard"
|
||||
}
|
||||
},
|
||||
"Illimity": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/manage/illimity",
|
||||
"labelKey": "illimity"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"New": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/new",
|
||||
"labelKey": "subscribe"
|
||||
}
|
||||
},
|
||||
"Error": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/error",
|
||||
"labelKey": "error"
|
||||
}
|
||||
},
|
||||
"Success": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/success",
|
||||
"labelKey": "success"
|
||||
}
|
||||
},
|
||||
"Subscribe": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/subscribe",
|
||||
"labelKey": "subscribe"
|
||||
},
|
||||
"pages": {
|
||||
"Standard": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/subscribe/standard",
|
||||
"labelKey": "standard"
|
||||
}
|
||||
},
|
||||
"Illimity": {
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"path": "/subscription/subscribe/illimity",
|
||||
"labelKey": "illimity"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
26
src/front/Hooks/useHoverable.ts
Normal file
26
src/front/Hooks/useHoverable.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export default function useHoverable(delay: number = 0) {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const [stateTimeout, setStateTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (stateTimeout) clearTimeout(stateTimeout);
|
||||
setIsHovered(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setStateTimeout(
|
||||
setTimeout(() => {
|
||||
setIsHovered(false);
|
||||
}, delay),
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
isHovered,
|
||||
handleMouseEnter,
|
||||
handleMouseLeave,
|
||||
};
|
||||
}
|
26
src/front/Hooks/useOpenable.ts
Normal file
26
src/front/Hooks/useOpenable.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
function useOpenable() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const open = useCallback(() => {
|
||||
setIsOpen(true);
|
||||
}, []);
|
||||
|
||||
const close = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
}, []);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setIsOpen((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
open,
|
||||
close,
|
||||
toggle,
|
||||
};
|
||||
}
|
||||
|
||||
export default useOpenable;
|
24
src/front/Hooks/useToggle.ts
Normal file
24
src/front/Hooks/useToggle.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export default function useToggle(defaultValue: boolean = false) {
|
||||
const [active, setActive] = useState(defaultValue);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setActive((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const activate = useCallback(() => {
|
||||
setActive(true);
|
||||
}, []);
|
||||
|
||||
const deactivate = useCallback(() => {
|
||||
setActive(false);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
active,
|
||||
toggle,
|
||||
activate,
|
||||
deactivate,
|
||||
};
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user