This commit is contained in:
VincentAlamelle 2024-04-24 12:25:53 +02:00 committed by GitHub
commit 360bae022a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
120 changed files with 4845 additions and 882 deletions

View File

@ -8,6 +8,11 @@ env:
PROJECT_ID: c0ed1e9e-d945-461f-920c-98c844ef1ad4
NAMESPACE_ID: a052faf9-a712-41d7-bbfa-8293ee948e70
CONTAINER_REGISTRY_ENDPOINT: rg.fr-par.scw.cloud/funcscwlecoffreppdw9e10llz
PROJECT_ID_LECOFFRE: 72d08499-37c2-412b-877e-f8af0471654a
NAMESPACE_ID_LECOFFRE: e975f056-967e-43fe-b237-84bfa8032e64
CONTAINER_REGISTRY_ENDPOINT_LECOFFRE: rg.fr-par.scw.cloud/funcscwlecoffreppdmp73pool
IMAGE_NAME: front
CONTAINER_NAME: front
@ -36,6 +41,30 @@ jobs:
run: docker build . -t ${{ env.CONTAINER_REGISTRY_ENDPOINT }}/${{ env.IMAGE_NAME }}
- name: Push the Docker Image to Scaleway Container Registry
run: docker push ${{ env.CONTAINER_REGISTRY_ENDPOINT }}/${{ env.IMAGE_NAME }}
build-and-push-image-lecoffre:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Copy SSH
run: cp ~/.ssh/id_rsa id_rsa
- name: Login to Scaleway Container Registry
uses: docker/login-action@v3
with:
username: nologin
password: ${{ secrets.SCW_SECRET_KEY_LECOFFRE }}
registry: ${{ env.CONTAINER_REGISTRY_ENDPOINT_LECOFFRE }}
- name: Build the Docker Image
run: docker build . -t ${{ env.CONTAINER_REGISTRY_ENDPOINT_LECOFFRE }}/${{ env.IMAGE_NAME }}
- name: Push the Docker Image to Scaleway Container Registry
run: docker push ${{ env.CONTAINER_REGISTRY_ENDPOINT_LECOFFRE }}/${{ env.IMAGE_NAME }}
deploy-to-scaleway:
needs: build-and-push-image
runs-on: ubuntu-latest
@ -71,3 +100,39 @@ jobs:
SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY }}
SCW_DEFAULT_PROJECT_ID: ${{ env.PROJECT_ID }}
SCW_DEFAULT_ORGANIZATION_ID: ${{ secrets.SCW_ORGANIZATION_ID }}
deploy-to-scaleway-lecoffre:
needs: build-and-push-image-lecoffre
runs-on: ubuntu-latest
environment: preprod
steps:
- name: Install CLI
uses: scaleway/action-scw@v0
- name: Get container ID
run: |
echo "CONTAINER_ID=$(scw container container list namespace-id=${{env.NAMESPACE_ID_LECOFFRE}} -o json | jq -r '.[] | select(.name == "${{ env.CONTAINER_NAME }}") | .id')" >> $GITHUB_ENV
env:
SCW_ACCESS_KEY: ${{ secrets.SCW_ACCESS_KEY_LECOFFRE }}
SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY_LECOFFRE }}
SCW_DEFAULT_PROJECT_ID: ${{ env.PROJECT_ID_LECOFFRE }}
SCW_DEFAULT_ORGANIZATION_ID: ${{ secrets.SCW_ORGANIZATION_ID_LECOFFRE }}
- name: Deploy the container based on the new image
run: |
env_string=""
while IFS= read -r line; do
if [[ "$line" == *"="* ]]; then
key=$(echo "$line" | cut -d '=' -f 1)
value=$(echo "$line" | cut -d '=' -f 2-)
if [[ -n "$key" ]]; then
env_string+="environment-variables.$key=$value "
fi
fi
done <<< "$ENV_VARS"
env_string=$(echo $env_string | sed 's/ $//')
scw container container update ${{ env.CONTAINER_ID }} $env_string
env:
ENV_VARS: ${{ secrets.ENV }}
SCW_ACCESS_KEY: ${{ secrets.SCW_ACCESS_KEY_LECOFFRE }}
SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY_LECOFFRE }}
SCW_DEFAULT_PROJECT_ID: ${{ env.PROJECT_ID_LECOFFRE }}
SCW_DEFAULT_ORGANIZATION_ID: ${{ secrets.SCW_ORGANIZATION_ID_LECOFFRE }}

View File

@ -8,6 +8,11 @@ env:
PROJECT_ID: c0ed1e9e-d945-461f-920c-98c844ef1ad4
NAMESPACE_ID: 17374437-5428-468c-9f41-d89787ffce0e
CONTAINER_REGISTRY_ENDPOINT: rg.fr-par.scw.cloud/funcscwlecoffreprdg7h5bbub
PROJECT_ID_LECOFFRE: 72d08499-37c2-412b-877e-f8af0471654a
NAMESPACE_ID_LECOFFRE: 8fbbce9d-31d1-4368-94c4-445e79f10834
CONTAINER_REGISTRY_ENDPOINT_LECOFFRE: rg.fr-par.scw.cloud/funcscwlecoffreprdjulp9mam
IMAGE_NAME: front
CONTAINER_NAME: front
@ -36,10 +41,34 @@ jobs:
run: docker build . -t ${{ env.CONTAINER_REGISTRY_ENDPOINT }}/${{ env.IMAGE_NAME }}
- name: Push the Docker Image to Scaleway Container Registry
run: docker push ${{ env.CONTAINER_REGISTRY_ENDPOINT }}/${{ env.IMAGE_NAME }}
build-and-push-image-lecoffre:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Copy SSH
run: cp ~/.ssh/id_rsa id_rsa
- name: Login to Scaleway Container Registry
uses: docker/login-action@v3
with:
username: nologin
password: ${{ secrets.SCW_SECRET_KEY_LECOFFRE }}
registry: ${{ env.CONTAINER_REGISTRY_ENDPOINT_LECOFFRE }}
- name: Build the Docker Image
run: docker build . -t ${{ env.CONTAINER_REGISTRY_ENDPOINT_LECOFFRE }}/${{ env.IMAGE_NAME }}
- name: Push the Docker Image to Scaleway Container Registry
run: docker push ${{ env.CONTAINER_REGISTRY_ENDPOINT_LECOFFRE }}/${{ env.IMAGE_NAME }}
deploy-to-scaleway:
needs: build-and-push-image
runs-on: ubuntu-latest
environment: prod
environment: preprod
steps:
- name: Install CLI
uses: scaleway/action-scw@v0
@ -71,3 +100,39 @@ jobs:
SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY }}
SCW_DEFAULT_PROJECT_ID: ${{ env.PROJECT_ID }}
SCW_DEFAULT_ORGANIZATION_ID: ${{ secrets.SCW_ORGANIZATION_ID }}
deploy-to-scaleway-lecoffre:
needs: build-and-push-image-lecoffre
runs-on: ubuntu-latest
environment: preprod
steps:
- name: Install CLI
uses: scaleway/action-scw@v0
- name: Get container ID
run: |
echo "CONTAINER_ID=$(scw container container list namespace-id=${{env.NAMESPACE_ID_LECOFFRE}} -o json | jq -r '.[] | select(.name == "${{ env.CONTAINER_NAME }}") | .id')" >> $GITHUB_ENV
env:
SCW_ACCESS_KEY: ${{ secrets.SCW_ACCESS_KEY_LECOFFRE }}
SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY_LECOFFRE }}
SCW_DEFAULT_PROJECT_ID: ${{ env.PROJECT_ID_LECOFFRE }}
SCW_DEFAULT_ORGANIZATION_ID: ${{ secrets.SCW_ORGANIZATION_ID_LECOFFRE }}
- name: Deploy the container based on the new image
run: |
env_string=""
while IFS= read -r line; do
if [[ "$line" == *"="* ]]; then
key=$(echo "$line" | cut -d '=' -f 1)
value=$(echo "$line" | cut -d '=' -f 2-)
if [[ -n "$key" ]]; then
env_string+="environment-variables.$key=$value "
fi
fi
done <<< "$ENV_VARS"
env_string=$(echo $env_string | sed 's/ $//')
scw container container update ${{ env.CONTAINER_ID }} $env_string
env:
ENV_VARS: ${{ secrets.ENV }}
SCW_ACCESS_KEY: ${{ secrets.SCW_ACCESS_KEY_LECOFFRE }}
SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY_LECOFFRE }}
SCW_DEFAULT_PROJECT_ID: ${{ env.PROJECT_ID_LECOFFRE }}
SCW_DEFAULT_ORGANIZATION_ID: ${{ secrets.SCW_ORGANIZATION_ID_LECOFFRE }}

View File

@ -8,6 +8,11 @@ env:
PROJECT_ID: c0ed1e9e-d945-461f-920c-98c844ef1ad4
NAMESPACE_ID: 9f949ff2-97bc-4979-ade2-1994dcaabde0
CONTAINER_REGISTRY_ENDPOINT: rg.fr-par.scw.cloud/funcscwlecoffrestgqhhn4ixh
PROJECT_ID_LECOFFRE: 72d08499-37c2-412b-877e-f8af0471654a
NAMESPACE_ID_LECOFFRE: f8137e85-47ad-46a5-9e2e-18af5de829c5
CONTAINER_REGISTRY_ENDPOINT_LECOFFRE: rg.fr-par.scw.cloud/funcscwlecoffrestgbqbfhtv6
IMAGE_NAME: front
CONTAINER_NAME: front
@ -36,6 +41,30 @@ jobs:
run: docker build . -t ${{ env.CONTAINER_REGISTRY_ENDPOINT }}/${{ env.IMAGE_NAME }}
- name: Push the Docker Image to Scaleway Container Registry
run: docker push ${{ env.CONTAINER_REGISTRY_ENDPOINT }}/${{ env.IMAGE_NAME }}
build-and-push-image-lecoffre:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Copy SSH
run: cp ~/.ssh/id_rsa id_rsa
- name: Login to Scaleway Container Registry
uses: docker/login-action@v3
with:
username: nologin
password: ${{ secrets.SCW_SECRET_KEY_LECOFFRE }}
registry: ${{ env.CONTAINER_REGISTRY_ENDPOINT_LECOFFRE }}
- name: Build the Docker Image
run: docker build . -t ${{ env.CONTAINER_REGISTRY_ENDPOINT_LECOFFRE }}/${{ env.IMAGE_NAME }}
- name: Push the Docker Image to Scaleway Container Registry
run: docker push ${{ env.CONTAINER_REGISTRY_ENDPOINT_LECOFFRE }}/${{ env.IMAGE_NAME }}
deploy-to-scaleway:
needs: build-and-push-image
runs-on: ubuntu-latest
@ -71,3 +100,38 @@ jobs:
SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY }}
SCW_DEFAULT_PROJECT_ID: ${{ env.PROJECT_ID }}
SCW_DEFAULT_ORGANIZATION_ID: ${{ secrets.SCW_ORGANIZATION_ID }}
deploy-to-scaleway-lecoffre:
needs: build-and-push-image-lecoffre
runs-on: ubuntu-latest
environment: staging
steps:
- name: Install CLI
uses: scaleway/action-scw@v0
- name: Get container ID
run: |
echo "CONTAINER_ID=$(scw container container list namespace-id=${{env.NAMESPACE_ID_LECOFFRE}} -o json | jq -r '.[] | select(.name == "${{ env.CONTAINER_NAME }}") | .id')" >> $GITHUB_ENV
env:
SCW_ACCESS_KEY: ${{ secrets.SCW_ACCESS_KEY_LECOFFRE }}
SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY_LECOFFRE }}
SCW_DEFAULT_PROJECT_ID: ${{ env.PROJECT_ID_LECOFFRE }}
SCW_DEFAULT_ORGANIZATION_ID: ${{ secrets.SCW_ORGANIZATION_ID_LECOFFRE }}
- name: Deploy the container based on the new image
run: |
env_string=""
while IFS= read -r line; do
if [[ "$line" == *"="* ]]; then
key=$(echo "$line" | cut -d '=' -f 1)
value=$(echo "$line" | cut -d '=' -f 2-)
if [[ -n "$key" ]]; then
env_string+="environment-variables.$key=$value "
fi
fi
done <<< "$ENV_VARS"
env_string=$(echo $env_string | sed 's/ $//')
scw container container update ${{ env.CONTAINER_ID }} $env_string
env:
ENV_VARS: ${{ secrets.ENV }}
SCW_ACCESS_KEY: ${{ secrets.SCW_ACCESS_KEY_LECOFFRE }}
SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY_LECOFFRE }}
SCW_DEFAULT_PROJECT_ID: ${{ env.PROJECT_ID_LECOFFRE }}
SCW_DEFAULT_ORGANIZATION_ID: ${{ secrets.SCW_ORGANIZATION_ID_LECOFFRE }}

View File

@ -18,10 +18,10 @@ lecoffreFront:
limits:
memory: 2Gi
ingress:
host: app.ppd.lecoffre.smart-chain.fr
host: app-tp.ppd.lecoffre.smart-chain.fr
tls:
hosts:
- app.ppd.lecoffre.smart-chain.fr
- app-tp.ppd.lecoffre.smart-chain.fr
secretName: front-tls
annotations:
kubernetes.io/ingress.class: nginx

962
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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,16 +25,20 @@
"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.132",
"next": "13.2.4",
"prettier": "^2.8.7",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-gtm-module": "^2.0.11",
"react-select": "^5.7.2",
"react-toastify": "^9.1.3",
"sass": "^1.59.2",
"sharp": "^0.32.1",
"typescript": "4.9.5",
"uuidv4": "^6.2.13"
},
"devDependencies": {
"@types/react-gtm-module": "^2.0.3"
}
}

View File

@ -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,25 +27,28 @@ 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) {
this.onError(err);
return Promise.reject(err);
}
}
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);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
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);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -135,9 +135,8 @@ export default abstract class BaseApiService {
private async checkJwtToken() {
const accessToken = CookieService.getInstance().getCookie("leCoffreAccessToken");
if (!accessToken) {
return;
}
if (!accessToken) return;
const userDecodedToken = jwt_decode(accessToken) as IUserJwtPayload;
const customerDecodedToken = jwt_decode(accessToken) as ICustomerJwtPayload;

View File

@ -18,4 +18,6 @@ export enum AppRuleNames {
offices = "offices",
documents = "documents",
rib = "rib",
subscriptions = "subscriptions",
stripe = "stripe",
}

View File

@ -0,0 +1,49 @@
import { RulesGroup } from "le-coffre-resources/dist/Admin";
import BaseAdmin from "../BaseAdmin";
export type IGetRulesGroupsParams = {
where?: {};
include?: {};
select?: {};
};
export default class RulesGroups extends BaseAdmin {
private static instance: RulesGroups;
private readonly baseURl = this.namespaceUrl.concat("/rules-groups");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new RulesGroups();
} else {
return this.instance;
}
}
public async get(q: IGetRulesGroupsParams): Promise<RulesGroup[]> {
const url = new URL(this.baseURl);
const query = { q };
Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
return await this.getRequest<RulesGroup[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async getByUid(uid: string, q?: any): Promise<RulesGroup> {
const url = new URL(this.baseURl.concat(`/${uid}`));
if (q) Object.entries(q).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
return await this.getRequest<RulesGroup>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,78 @@
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;
cancel_at?: number;
};
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 getStripeSubscriptionByUid(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 getClientPortalSession(stripe_subscription_id: string) {
const url = new URL(this.baseURl.concat(`/${stripe_subscription_id}/client-portal`));
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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -15,7 +15,7 @@ export interface IPostFilesParams {}
export default class OfficeRib extends BaseNotary {
private static instance: OfficeRib;
private readonly baseURl = this.namespaceUrl.concat("/office/rib");
private readonly baseURl = this.namespaceUrl.concat("/rib");
private constructor() {
super();

View File

@ -0,0 +1,3 @@
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.364 4.13604L12.8284 7.67157M12.8284 13.3284L16.364 16.864M7.17157 7.67157L3.63604 4.13604M7.17157 13.3284L3.63604 16.864M19 10.5C19 15.4706 14.9706 19.5 10 19.5C5.02944 19.5 1 15.4706 1 10.5C1 5.52944 5.02944 1.5 10 1.5C14.9706 1.5 19 5.52944 19 10.5ZM14 10.5C14 12.7091 12.2091 14.5 10 14.5C7.79086 14.5 6 12.7091 6 10.5C6 8.29086 7.79086 6.5 10 6.5C12.2091 6.5 14 8.29086 14 10.5Z" stroke="black" stroke-width="2" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 558 B

View File

@ -1,38 +1,34 @@
<svg width="174" height="39" viewBox="0 0 174 39" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3896_98053)">
<path d="M25.5282 5.84779C25.541 5.51073 25.347 5.16635 24.9299 4.98683L14.1642 0.368742C13.2988 -0.00312242 12.2797 -0.00312242 11.4124 0.368742L0.707113 4.96118C0.282634 5.14253 0.0832014 5.49058 0.0941793 5.83497V26.9287L2.50017 28.0626L3.93828 27.3738L10.7483 30.1857V31.6164L12.8341 32.5982L14.94 31.5907V30.2004L21.739 27.3921L23.1698 28.0626L25.5337 26.9305V5.84779H25.5282Z" fill="#C5B2D4"/>
<path d="M12.8314 32.7007L10.6541 31.6767V30.2497L3.93741 27.4763L2.49748 28.1651L0 26.9872L0.00365931 5.7433C0.0311041 5.37143 0.279937 5.04536 0.669653 4.87683L11.3768 0.284394C12.2605 -0.094798 13.3162 -0.094798 14.1999 0.284394L24.9656 4.90248C25.3517 5.06734 25.5932 5.38975 25.6188 5.75612H25.6206V26.989L23.1671 28.1633L21.7344 27.4928L15.0306 30.2626V31.6493L12.8332 32.7007H12.8314ZM10.837 31.5595L12.8314 32.4974L14.8458 31.5339V30.1398L14.9025 30.116L21.7381 27.2931L23.1652 27.9618L25.4377 26.8736V5.9393H25.434V5.84405C25.4486 5.51065 25.2456 5.22305 24.8924 5.07101L14.1267 0.452924C13.2888 0.0938821 12.2861 0.0938821 11.4481 0.452924L0.741009 5.04536C0.382398 5.19924 0.173817 5.49233 0.182965 5.83122V5.92465V26.87L2.49748 27.9599L3.93192 27.273L3.97035 27.2876L10.837 30.1233V31.5558V31.5595Z" fill="#4E1480"/>
<path d="M10.5185 28.7041C10.1984 28.7041 9.8745 28.64 9.56895 28.5099L2.57602 25.5295C1.68132 25.1485 1.10315 24.2729 1.10315 23.3002L1.09766 10.186C1.09766 9.36715 1.50567 8.61059 2.18813 8.15996C2.87059 7.70933 3.72687 7.63422 4.47886 7.96029L11.4773 10.9847C12.3647 11.3675 12.9392 12.2413 12.9392 13.2104V26.2806C12.9392 27.0958 12.533 27.8523 11.8542 28.3029C11.4517 28.5704 10.9869 28.7059 10.5204 28.7059L10.5185 28.7041ZM9.78485 28.0043C10.3648 28.2516 11.0254 28.1912 11.5505 27.8431C12.0756 27.4951 12.3903 26.9107 12.3903 26.2787V13.2085C12.3903 12.4593 11.9475 11.7852 11.2596 11.4884L4.26113 8.46405C3.68113 8.21308 3.01879 8.2717 2.49002 8.61975C1.96125 8.9678 1.64655 9.554 1.64655 10.186L1.65204 23.3002C1.65204 24.0531 2.09848 24.729 2.79009 25.0239L9.78302 28.0043H9.78485Z" fill="white"/>
<path d="M13.052 15.5426V14.8557C13.052 14.8374 13.041 14.8227 13.0246 14.8154L12.1774 14.449C12.1482 14.4362 12.1152 14.4582 12.1152 14.4893V15.1763C12.1152 15.1946 12.1262 15.2093 12.1427 15.2166L12.9898 15.5829C13.0191 15.5958 13.052 15.5738 13.052 15.5426Z" fill="white"/>
<path d="M13.0073 15.861C12.9634 15.861 12.9213 15.8519 12.8792 15.8336L12.0339 15.4672C11.9168 15.4177 11.8418 15.3023 11.8418 15.1741V14.4871C11.8418 14.3791 11.8949 14.2802 11.9845 14.2197C12.0742 14.1611 12.1876 14.1519 12.2864 14.1941L13.1335 14.5604C13.2506 14.6099 13.3256 14.7253 13.3256 14.8535V15.5405C13.3256 15.6485 13.2726 15.7475 13.1811 15.8079C13.128 15.8427 13.0677 15.861 13.0073 15.861ZM12.3907 15.0239L12.7767 15.1906V15.0074L12.3907 14.8407V15.0239Z" fill="white"/>
<path d="M13.0833 26.2946V25.6077C13.0833 25.5893 13.0723 25.5747 13.0558 25.5674L12.2087 25.201C12.1794 25.1882 12.1465 25.2101 12.1465 25.2413V25.9282C12.1465 25.9465 12.1575 25.9612 12.1739 25.9685L13.0211 26.3349C13.0503 26.3477 13.0833 26.3257 13.0833 26.2946Z" fill="white"/>
<path d="M13.0385 26.6156C12.9965 26.6156 12.9544 26.6065 12.9123 26.59L12.0652 26.2236C11.9481 26.1723 11.873 26.0569 11.873 25.9305V25.2436C11.873 25.1355 11.9261 25.0366 12.0158 24.9761C12.1054 24.9157 12.2189 24.9083 12.3177 24.9505L13.1648 25.3168C13.2819 25.3663 13.3569 25.4817 13.3569 25.6099V26.2969C13.3569 26.405 13.3038 26.5039 13.2142 26.5625C13.1611 26.5973 13.1007 26.6156 13.0385 26.6156ZM12.4219 25.7785L12.808 25.9452V25.7601L12.4219 25.5934V25.7785Z" fill="white"/>
<path d="M4.94763 14.1684C3.52599 14.7217 3.1875 17.2606 4.18832 19.8362C5.18914 22.4136 7.15419 24.0531 8.57583 23.498C9.99747 22.943 10.336 20.4059 9.33514 17.8303C8.33432 15.2547 6.36927 13.6134 4.94763 14.1684ZM7.34631 18.8854C7.32984 18.825 7.30971 18.7645 7.28593 18.7041C7.23836 18.5795 7.17798 18.466 7.11211 18.3652L7.45243 15.8189C7.99949 16.3538 8.50082 17.1177 8.85394 18.0245C9.19608 18.9038 9.34246 19.7776 9.31318 20.5268L7.34448 18.8836L7.34631 18.8854ZM5.31539 15.0715C5.79293 14.8847 6.34731 15.0074 6.89072 15.3628L6.54675 17.9237C6.48637 17.9164 6.42782 17.9219 6.37293 17.942C6.34 17.9549 6.30889 17.9732 6.28145 17.997L4.34385 16.3795C4.5012 15.7365 4.8287 15.2602 5.31356 15.0715H5.31539ZM4.70246 19.6457C4.35482 18.7499 4.20845 17.8614 4.24504 17.1031L6.10397 18.6546C6.11861 18.803 6.1552 18.9624 6.21741 19.1218C6.25583 19.2207 6.30157 19.3123 6.35097 19.3965L6.03261 21.7761C5.51299 21.2449 5.04094 20.5103 4.70429 19.6438L4.70246 19.6457ZM6.59249 22.2597L6.90902 19.8948C6.98586 19.9131 7.06088 19.9131 7.12858 19.8856C7.2164 19.8508 7.2841 19.7794 7.33167 19.6823L9.2217 21.2595C9.06801 21.919 8.73684 22.4099 8.24284 22.6023C7.74151 22.7964 7.15785 22.6554 6.59249 22.2597Z" fill="white"/>
<path d="M6.96684 18.6397C7.22665 19.0592 7.22482 19.53 6.97782 19.6802C6.73081 19.8304 6.30999 19.6106 6.07946 19.1746C5.83428 18.7112 5.82879 18.2935 6.0575 18.147C6.32463 17.9784 6.69971 18.2111 6.96684 18.6397Z" fill="#3FA79E"/>
<path d="M92.327 11.9149C92.4917 11.2921 92.7661 10.7023 93.1504 10.1436C93.5346 9.58484 94.0012 9.09573 94.5501 8.67258C95.099 8.25125 95.7174 7.92152 96.4017 7.68338C97.0878 7.44524 97.816 7.32617 98.5826 7.32617H102.59L102.041 9.60682H100.284C99.3694 9.60682 98.6101 9.83214 98.0063 10.2791C97.4025 10.7279 97.0274 11.2738 96.881 11.9131L96.3047 14.1938H99.7902L99.2138 16.4744H95.7284L92.9015 27.9326H88.3457L92.3252 11.9131L92.327 11.9149Z" fill="#4E1480"/>
<path d="M102.839 11.9149C103.003 11.2921 103.278 10.7023 103.662 10.1436C104.046 9.58484 104.513 9.09573 105.062 8.67258C105.611 8.25125 106.229 7.92152 106.913 7.68338C107.6 7.44524 108.328 7.32617 109.094 7.32617H113.101L112.552 9.60682H110.796C109.881 9.60682 109.122 9.83214 108.518 10.2791C107.914 10.7279 107.539 11.2738 107.393 11.9131L106.816 14.1938H110.302L109.726 16.4744H106.24L103.413 27.9326H98.8574L102.837 11.9131L102.839 11.9149Z" fill="#4E1480"/>
<path d="M122.873 11.9158L122.296 14.1964H120.567C119.652 14.1964 118.893 14.4217 118.289 14.8687C117.686 15.3175 117.301 15.8634 117.137 16.5027L114.31 27.9334H109.727L112.553 16.5302C112.7 15.9257 112.96 15.345 113.336 14.7844C113.712 14.2257 114.174 13.7311 114.723 13.3006C115.272 12.8702 115.894 12.5313 116.59 12.284C117.285 12.0367 118.035 11.9121 118.84 11.9121H122.874L122.873 11.9158Z" fill="#4E1480"/>
<path d="M126.332 27.9336C124.978 27.9336 123.948 27.6038 123.243 26.9444C122.539 26.2849 122.186 25.4514 122.186 24.4439C122.186 24.1325 122.231 23.7661 122.323 23.3448L124.024 16.4754C124.189 15.8525 124.463 15.2627 124.848 14.704C125.232 14.1453 125.698 13.6598 126.247 13.2476C126.796 12.8355 127.418 12.5112 128.114 12.2713C128.809 12.0331 129.541 11.9141 130.309 11.9141H133.767C135.121 11.9141 136.146 12.2401 136.841 12.8904C137.536 13.5407 137.884 14.3706 137.884 15.3781C137.884 15.7078 137.838 16.0833 137.747 16.5047L136.622 21.066H127.482L126.906 23.3741C126.741 24.0152 126.846 24.5556 127.223 24.9953C127.598 25.4349 128.242 25.6547 129.158 25.6547H133.769L133.22 27.9354H126.332V27.9336ZM128.061 18.7835H132.644L133.193 16.49C133.339 15.8525 133.248 15.3103 132.918 14.8652C132.589 14.4182 132.104 14.1965 131.464 14.1965C131.153 14.1965 130.842 14.2552 130.531 14.3742C130.22 14.4933 129.936 14.6563 129.68 14.8652C129.424 15.074 129.2 15.3195 129.008 15.6016C128.816 15.8837 128.683 16.1804 128.611 16.49L128.062 18.7835H128.061Z" fill="#4E1480"/>
<path d="M140.686 27.9345V23.3457H145.269V27.9345H140.686Z" fill="#4E1480"/>
<path d="M152.101 11.9149H156.684L152.704 27.9344H148.121L152.101 11.9149ZM153.226 7.32617H157.809L157.26 9.60682H152.677L153.226 7.32617Z" fill="#3FA79E"/>
<path d="M162.447 27.9336C161.093 27.9336 160.063 27.6038 159.358 26.9444C158.654 26.2849 158.301 25.4514 158.301 24.4439C158.301 24.1325 158.347 23.7661 158.438 23.3448L160.14 16.4754C160.304 15.8525 160.579 15.2627 160.963 14.704C161.347 14.1453 161.814 13.6598 162.363 13.2476C162.912 12.8355 163.534 12.5112 164.229 12.2713C164.924 12.0331 165.656 11.9141 166.424 11.9141H169.882C171.236 11.9141 172.261 12.2438 172.956 12.9033C173.652 13.5627 173.999 14.3871 173.999 15.3763C173.999 15.6877 173.953 16.054 173.862 16.4754L172.16 23.3448C171.996 23.9859 171.721 24.5813 171.337 25.1308C170.953 25.6804 170.486 26.1658 169.937 26.5871C169.388 27.0085 168.77 27.3382 168.086 27.5763C167.4 27.8145 166.671 27.9336 165.903 27.9336H162.445H162.447ZM169.281 16.5028C169.445 15.8617 169.363 15.3176 169.034 14.8688C168.704 14.42 168.219 14.1965 167.579 14.1965C166.939 14.1965 166.344 14.4219 165.795 14.8688C165.246 15.3176 164.889 15.8635 164.725 16.5028L163.023 23.3448C162.858 23.9859 162.941 24.5318 163.27 24.9788C163.599 25.4276 164.084 25.6511 164.725 25.6511C165.036 25.6511 165.347 25.5925 165.658 25.4734C165.969 25.3543 166.252 25.1895 166.509 24.9788C166.765 24.7681 166.984 24.5263 167.167 24.2515C167.35 23.9768 167.487 23.6745 167.579 23.3448L169.281 16.5028Z" fill="#3FA79E"/>
<path d="M83.5743 13.3998C80.0815 12.504 76.4387 14.9495 75.4378 18.8605C74.437 22.7715 76.457 26.6696 79.9516 27.5654C83.4444 28.4612 87.0873 26.0157 88.0881 22.1047C89.0889 18.1937 87.069 14.2955 83.5743 13.3998ZM82.8754 21.1594C82.912 21.0752 82.9431 20.9873 82.9669 20.8957C83.0144 20.707 83.0291 20.5183 83.0126 20.3369L86.6664 17.8841C87.1385 19.0565 87.2501 20.4322 86.8988 21.8098C86.5567 23.1452 85.8339 24.2663 84.899 25.0649L82.8736 21.1594H82.8754ZM83.2578 14.7865C84.4324 15.0869 85.4003 15.8105 86.0718 16.7795L82.396 19.247C82.2844 19.1737 82.1618 19.1188 82.0264 19.084C81.9441 19.062 81.8618 19.051 81.7794 19.0473L79.7869 15.2023C80.8518 14.6582 82.0648 14.4805 83.2578 14.7865ZM76.6912 19.1939C77.0388 17.8346 77.7816 16.6971 78.7404 15.8966L80.6524 19.5841C80.5078 19.7618 80.3962 19.9779 80.334 20.2197C80.2956 20.3699 80.2791 20.5201 80.2809 20.6667L76.8631 22.9602C76.4423 21.8226 76.3545 20.5091 76.6912 19.1939ZM77.4267 24.0886L80.8225 21.8098C80.9543 21.9123 81.1061 21.9911 81.2744 22.0332C81.4922 22.0882 81.7117 22.0809 81.9185 22.0241L83.8616 25.7739C82.7839 26.3417 81.5471 26.5304 80.334 26.219C79.1045 25.9039 78.1 25.1272 77.4267 24.0886Z" fill="#3FA79E"/>
<path d="M81.4103 27.8402C80.9182 27.8402 80.4223 27.7798 79.9301 27.6534C78.2157 27.2137 76.7941 26.0524 75.9305 24.3854C75.0687 22.7221 74.862 20.751 75.3523 18.8367C75.8409 16.9224 76.9679 15.2939 78.5231 14.2498C80.082 13.2038 81.8842 12.8704 83.6004 13.31C87.1371 14.2168 89.19 18.1718 88.18 22.1267C87.3091 25.5303 84.4475 27.8402 81.4122 27.8402H81.4103ZM82.1074 13.3045C80.9053 13.3045 79.7051 13.6782 78.6256 14.4036C77.107 15.4222 76.0074 17.014 75.5298 18.8843C75.0523 20.7547 75.2517 22.6781 76.0934 24.3029C76.9332 25.9241 78.3127 27.0507 79.9759 27.4775C83.4138 28.3605 87.0146 25.9388 88.0007 22.0827C88.9869 18.2267 86.9926 14.3707 83.5528 13.4896C83.0753 13.3668 82.5905 13.3064 82.1056 13.3064L82.1074 13.3045ZM81.5073 26.4572C81.1048 26.4572 80.7041 26.4077 80.3125 26.307C79.1105 25.9992 78.0584 25.228 77.3503 24.1381L77.3009 24.063L80.8267 21.6962L80.8797 21.7365C81.0078 21.8354 81.1469 21.9051 81.2987 21.9435C81.4945 21.993 81.6939 21.9912 81.8952 21.9344L81.9665 21.9142L83.9865 25.8105L83.906 25.8527C83.1467 26.252 82.3252 26.4554 81.5073 26.4554V26.4572ZM77.5534 24.1143C78.2359 25.1254 79.2276 25.8399 80.3583 26.1311C81.4853 26.4206 82.6527 26.2832 83.7395 25.7354L81.8714 22.1322C81.6628 22.178 81.4561 22.1762 81.253 22.1249C81.0993 22.0864 80.9547 22.0186 80.8212 21.9234L77.5552 24.1161L77.5534 24.1143ZM84.8739 25.2097L82.7752 21.1632L82.7935 21.1229C82.8301 21.0386 82.8594 20.9562 82.8814 20.8737C82.9253 20.7015 82.9399 20.5238 82.9253 20.3462L82.9198 20.293L86.7127 17.7468L86.7547 17.8512C87.2487 19.0803 87.3329 20.4945 86.9908 21.8336C86.6541 23.147 85.9534 24.2901 84.9617 25.1346L84.8739 25.2097ZM82.9783 21.1558L84.9306 24.9203C85.8472 24.1069 86.4968 23.0261 86.8133 21.7878C87.1371 20.5238 87.0694 19.1921 86.6267 18.0234L83.1101 20.3828C83.1192 20.5641 83.1028 20.7437 83.057 20.9177C83.0369 20.9965 83.0113 21.0752 82.9783 21.154V21.1558ZM76.8179 23.1012L76.7776 22.9932C76.333 21.7878 76.2727 20.4671 76.602 19.1719C76.9387 17.853 77.6778 16.6641 78.6805 15.827L78.7683 15.7537L80.7608 19.5951L80.7224 19.6409C80.5797 19.8167 80.479 20.0183 80.4223 20.2417C80.3875 20.381 80.3693 20.522 80.3729 20.6631V20.7125L76.8197 23.0994L76.8179 23.1012ZM76.6917 19.1939L76.7795 19.2159C76.4684 20.4359 76.5123 21.6779 76.9094 22.8191L80.1881 20.6191C80.1881 20.478 80.2082 20.337 80.243 20.1996C80.3016 19.9706 80.4022 19.7618 80.5412 19.5768L78.7098 16.0431C77.7803 16.851 77.096 17.9739 76.7776 19.2177L76.6898 19.1958L76.6917 19.1939ZM82.3983 19.357L82.3471 19.324C82.241 19.2544 82.1276 19.2031 82.005 19.1719C81.9336 19.1536 81.8586 19.1426 81.7763 19.139H81.7232L79.663 15.1639L79.7453 15.1217C80.8797 14.5429 82.1019 14.3963 83.2802 14.6986C84.4421 14.9953 85.4319 15.6988 86.1473 16.7283L86.2004 16.8052L82.3965 19.3588L82.3983 19.357ZM81.8385 18.9594C81.9135 18.9649 81.983 18.9778 82.0507 18.9943C82.1715 19.0254 82.2867 19.073 82.3947 19.1371L85.9424 16.7557C85.2581 15.8032 84.3231 15.1547 83.2345 14.8744C82.1276 14.5905 80.9804 14.7187 79.91 15.2426L81.8366 18.9594H81.8385Z" fill="#3FA79E"/>
<path d="M34.8332 7.37695H39.4165L34.8607 25.7046H41.7219L41.173 27.9852H29.7285L34.8332 7.37695Z" fill="#4E1480"/>
<path d="M49.1575 7.37695H60.6019L60.053 9.6576H53.1919L51.4628 16.5545H57.7202L57.1713 18.8351H50.9139L49.1849 25.7046H56.0461L55.4972 27.9852H44.0527L49.1575 7.37695Z" fill="#4E1480"/>
<path d="M65.7887 23.3974C65.6241 24.0385 65.7064 24.5844 66.0357 25.0314C66.3651 25.4802 66.8499 25.7037 67.4903 25.7037C68.826 25.7037 69.7865 24.9343 70.372 23.3956H72.6499C72.1742 24.9343 71.4424 26.0847 70.4544 26.8449C69.4663 27.6051 68.1124 27.9843 66.3925 27.9843H65.2124C63.8585 27.9843 62.8284 27.6546 62.1239 26.9951C61.4195 26.3357 61.0664 25.5022 61.0664 24.4947C61.0664 24.1833 61.1121 23.8169 61.2036 23.3956L62.9052 16.5261C63.0699 15.9033 63.3443 15.3135 63.7285 14.7547C64.1128 14.196 64.5793 13.7106 65.1282 13.2984C65.6771 12.8863 66.2992 12.562 66.9945 12.3221C67.6897 12.0839 68.4216 11.9648 69.1901 11.9648H70.3702C71.8522 11.9648 72.9134 12.2268 73.5538 12.7489C74.1942 13.2709 74.5144 14.0531 74.5144 15.0991C74.5144 15.5388 74.4595 16.0151 74.3497 16.528H72.0718C72.273 15.0075 71.6967 14.2473 70.3427 14.2473C69.7024 14.2473 69.1077 14.4726 68.5588 14.9196C68.0099 15.3684 67.6532 15.9143 67.4885 16.5536L65.7869 23.3956L65.7887 23.3974Z" fill="#4E1480"/>
<path d="M134.897 37.6796C134.613 37.6796 134.251 37.5881 134.081 37.3829V37.5734H133.6V32.6934H134.094V34.6113C134.331 34.2559 134.666 33.9848 135.135 33.9848C135.925 33.9848 136.425 34.7487 136.425 35.758C136.425 36.7674 135.905 37.676 134.897 37.676V37.6796ZM134.97 34.4556C134.621 34.4556 134.397 34.6461 134.245 34.8439C134.121 35.0088 134.094 35.0821 134.094 35.2598V36.7765C134.094 37.0605 134.344 37.2785 134.818 37.2785C135.543 37.2785 135.91 36.6447 135.91 35.8679C135.91 35.0912 135.601 34.4574 134.968 34.4574L134.97 34.4556Z" fill="#320756"/>
<path d="M138.482 37.8194C138.284 38.4056 137.955 38.9331 137.086 38.9991L137.046 38.5576C137.585 38.5503 137.922 38.2077 138.067 37.6875L136.795 34.0879H137.335L138.034 36.211C138.165 36.6067 138.277 36.9822 138.317 37.1086H138.33C138.376 36.9236 138.482 36.581 138.601 36.1927L139.247 34.0897H139.715L138.484 37.8212L138.482 37.8194Z" fill="#320756"/>
<path d="M144.456 37.5768V33.664C144.456 32.8506 143.984 32.378 143.139 32.378C142.601 32.378 142.129 32.6198 141.799 33.0485C141.591 33.3013 141.558 33.4222 141.558 33.7519V37.5768H140.734V31.7625H141.503V32.6858C141.942 32.0373 142.557 31.5977 143.402 31.5977C144.511 31.5977 145.279 32.2791 145.279 33.5321V37.5768H144.456Z" fill="#3FA79E"/>
<path d="M149.219 37.7854C147.408 37.7854 146.65 36.2247 146.65 34.6529C146.65 33.0812 147.485 31.5645 149.263 31.5645C151.042 31.5645 151.832 33.0812 151.832 34.6639C151.832 36.2467 150.998 37.7854 149.219 37.7854ZM149.263 32.2789C147.957 32.2789 147.507 33.5868 147.507 34.6529C147.507 35.7191 147.891 37.06 149.219 37.06C150.548 37.06 150.976 35.6971 150.976 34.6639C150.976 33.6308 150.591 32.2789 149.263 32.2789Z" fill="#3FA79E"/>
<path d="M154.873 37.7637C153.841 37.7637 153.391 37.0493 153.391 36.181V32.5649H152.414V31.8725H153.391V30.5975L154.214 30.3008V31.8725H155.795V32.5649H154.214V36.0821C154.214 36.7196 154.533 37.0493 155.137 37.0493C155.389 37.0493 155.609 36.9724 155.773 36.8844L155.927 37.5659C155.664 37.6758 155.257 37.7637 154.873 37.7637Z" fill="#3FA79E"/>
<path d="M157.661 37.6416C157.32 37.6416 157.09 37.3998 157.09 37.0591C157.09 36.7184 157.309 36.4766 157.65 36.4766C157.99 36.4766 158.221 36.7184 158.221 37.0591C158.221 37.3998 158.001 37.6416 157.661 37.6416Z" fill="#4E1480"/>
<path d="M159.418 37.5761V29.8164H160.318V37.5761H159.418Z" fill="#4E1480"/>
<path d="M164.127 30.6407V37.5761H163.216V30.6407H160.658V29.8164H166.718V30.6407H164.127Z" fill="#4E1480"/>
</g>
<defs>
<clipPath id="clip0_3896_98053">
<rect width="174" height="39" fill="white"/>
</clipPath>
</defs>
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Calque_2" data-name="Calque 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1245.64 178.38">
<defs>
<style>
.cls-1 {
fill: #41a89f;
}
.cls-1, .cls-2 {
stroke-width: 0px;
}
.cls-2 {
fill: #4a2779;
}
</style>
</defs>
<g id="Calque_1-2" data-name="Calque 1">
<g>
<path class="cls-2" d="M540.47,39.62c1.42-5.38,3.79-10.47,7.11-15.29,3.32-4.82,7.35-9.05,12.09-12.7,4.74-3.64,10.08-6.48,15.99-8.54,5.92-2.06,12.21-3.08,18.83-3.08h34.6l-4.74,19.69h-15.17c-7.9,0-14.45,1.95-19.67,5.8-5.21,3.87-8.45,8.59-9.72,14.11l-4.98,19.69h30.09l-4.98,19.69h-30.09l-24.41,98.93h-39.34l34.36-138.31.02.02Z"/>
<path class="cls-2" d="M631.24,39.62c1.42-5.38,3.79-10.47,7.11-15.29,3.32-4.82,7.35-9.05,12.09-12.7,4.74-3.64,10.08-6.48,15.98-8.54,5.93-2.06,12.22-3.08,18.83-3.08h34.6l-4.74,19.69h-15.16c-7.9,0-14.45,1.95-19.67,5.8-5.21,3.87-8.45,8.59-9.71,14.11l-4.98,19.69h30.1l-4.97,19.69h-30.1l-24.41,98.93h-39.33l34.36-138.31.02.02Z"/>
<path class="cls-2" d="M804.21,39.62l-4.98,19.69h-14.93c-7.9,0-14.45,1.95-19.67,5.8-5.21,3.87-8.53,8.59-9.95,14.11l-24.41,98.69h-39.57l24.4-98.46c1.27-5.22,3.51-10.23,6.76-15.07,3.25-4.82,7.24-9.09,11.98-12.81,4.74-3.72,10.11-6.64,16.12-8.78,6-2.14,12.48-3.21,19.43-3.21h34.83v.03Z"/>
<path class="cls-2" d="M834.08,177.93c-11.69,0-20.58-2.85-26.67-8.54-6.08-5.69-9.13-12.89-9.13-21.59,0-2.69.39-5.85,1.18-9.49l14.69-59.31c1.42-5.38,3.79-10.47,7.11-15.29,3.32-4.82,7.35-9.01,12.08-12.57,4.74-3.56,10.11-6.36,16.12-8.43,6-2.06,12.32-3.08,18.95-3.08h29.86c11.69,0,20.54,2.82,26.54,8.43,6,5.61,9.01,12.78,9.01,21.48,0,2.85-.4,6.09-1.18,9.73l-9.71,39.38h-78.92l-4.97,19.93c-1.42,5.54-.52,10.2,2.74,14,3.24,3.8,8.8,5.69,16.71,5.69h39.81l-4.74,19.69h-59.47v-.02ZM849.01,98.93h39.57l4.74-19.8c1.26-5.5.47-10.19-2.37-14.03-2.84-3.86-7.03-5.77-12.55-5.77-2.69,0-5.37.51-8.06,1.53-2.69,1.03-5.14,2.44-7.35,4.24-2.21,1.8-4.14,3.92-5.8,6.36-1.66,2.44-2.81,5-3.43,7.67l-4.74,19.8h0Z"/>
<path class="cls-2" d="M958,177.93v-39.62h39.57v39.62h-39.57Z"/>
<path class="cls-1" d="M1056.56,39.62h39.57l-34.36,138.31h-39.57l34.36-138.31ZM1066.28,0h39.57l-4.74,19.69h-39.57l4.74-19.69Z"/>
<path class="cls-1" d="M1145.9,177.93c-11.69,0-20.58-2.85-26.67-8.54-6.08-5.69-9.13-12.89-9.13-21.59,0-2.69.4-5.85,1.18-9.49l14.7-59.31c1.42-5.38,3.79-10.47,7.11-15.29,3.32-4.82,7.35-9.01,12.09-12.57s10.11-6.36,16.11-8.43c6-2.06,12.32-3.08,18.95-3.08h29.87c11.68,0,20.53,2.85,26.53,8.54,6.01,5.69,9.01,12.81,9.01,21.35,0,2.69-.4,5.85-1.18,9.49l-14.7,59.31c-1.42,5.54-3.79,10.68-7.11,15.42-3.32,4.74-7.35,8.94-12.09,12.57-4.74,3.64-10.08,6.49-15.98,8.54-5.92,2.06-12.22,3.08-18.85,3.08h-29.86.02ZM1204.9,79.24c1.42-5.54.71-10.23-2.13-14.11-2.85-3.87-7.04-5.8-12.56-5.8s-10.66,1.95-15.4,5.8c-4.74,3.87-7.82,8.59-9.24,14.11l-14.7,59.07c-1.42,5.54-.71,10.25,2.13,14.11,2.84,3.88,7.03,5.8,12.56,5.8,2.69,0,5.37-.51,8.06-1.53,2.69-1.03,5.13-2.45,7.35-4.27,2.21-1.82,4.1-3.91,5.68-6.28,1.58-2.37,2.76-4.98,3.56-7.83l14.7-59.07Z"/>
<path class="cls-1" d="M464.92,52.45c-30.16-7.73-61.61,13.38-70.25,47.15-8.64,33.77,8.8,67.42,38.97,75.16,30.16,7.73,61.61-13.38,70.25-47.15,8.64-33.77-8.8-67.43-38.97-75.16ZM458.89,119.45c.32-.73.58-1.49.79-2.28.41-1.63.54-3.26.4-4.82l31.55-21.18c4.08,10.12,5.04,22,2.01,33.89-2.95,11.53-9.19,21.21-17.27,28.1l-17.49-33.72h.02ZM462.19,64.42c10.14,2.59,18.5,8.84,24.3,17.21l-31.74,21.3c-.96-.63-2.02-1.11-3.19-1.41-.71-.19-1.42-.28-2.13-.32l-17.2-33.2c9.19-4.7,19.67-6.23,29.97-3.59ZM405.49,102.47c3-11.74,9.42-21.56,17.69-28.47l16.51,31.84c-1.25,1.53-2.21,3.4-2.75,5.49-.33,1.3-.47,2.59-.46,3.86l-29.51,19.8c-3.63-9.82-4.39-21.16-1.49-32.52ZM411.84,144.74l29.32-19.68c1.14.89,2.45,1.57,3.9,1.93,1.88.47,3.78.41,5.56-.08l16.78,32.38c-9.3,4.9-19.98,6.53-30.46,3.84-10.62-2.72-19.29-9.43-25.1-18.39Z"/>
<path class="cls-1" d="M446.23,177.13c-4.25,0-8.53-.52-12.78-1.61-14.8-3.8-27.08-13.82-34.53-28.22-7.44-14.36-9.23-31.38-4.99-47.91,4.22-16.53,13.95-30.59,27.38-39.6,13.46-9.03,29.02-11.91,43.84-8.11,30.54,7.83,48.26,41.98,39.54,76.12-7.52,29.39-32.23,49.33-58.43,49.33h-.02ZM452.25,51.62c-10.38,0-20.74,3.23-30.06,9.49-13.11,8.79-22.61,22.54-26.73,38.69-4.12,16.15-2.4,32.76,4.87,46.78,7.25,14,19.16,23.72,33.52,27.41,29.68,7.62,60.77-13.29,69.29-46.58,8.51-33.29-8.7-66.59-38.4-74.19-4.12-1.06-8.31-1.58-12.5-1.58l.02-.02ZM447.06,165.19c-3.48,0-6.93-.43-10.32-1.3-10.38-2.66-19.46-9.32-25.58-18.73l-.43-.65,30.44-20.43.46.35c1.11.85,2.31,1.45,3.62,1.79,1.69.43,3.41.41,5.15-.08l.62-.17,17.44,33.64-.7.36c-6.56,3.45-13.65,5.2-20.71,5.2v.02ZM412.93,144.96c5.89,8.73,14.46,14.9,24.22,17.41,9.73,2.5,19.81,1.31,29.19-3.42l-16.13-31.11c-1.8.4-3.59.38-5.34-.06-1.33-.33-2.58-.92-3.73-1.74l-28.2,18.93-.02-.02ZM476.13,154.42l-18.12-34.94.16-.35c.32-.73.57-1.44.76-2.15.38-1.49.51-3.02.38-4.56l-.05-.46,32.75-21.99.36.9c4.27,10.61,4.99,22.82,2.04,34.39-2.91,11.34-8.96,21.21-17.52,28.5l-.76.65ZM459.77,119.41l16.86,32.5c7.91-7.02,13.52-16.35,16.26-27.05,2.8-10.91,2.21-22.41-1.61-32.5l-30.36,20.37c.08,1.57-.06,3.12-.46,4.62-.17.68-.39,1.36-.68,2.04v.02ZM406.58,136.21l-.35-.93c-3.84-10.41-4.36-21.81-1.52-32.99,2.91-11.39,9.29-21.65,17.95-28.88l.76-.63,17.2,33.17-.33.4c-1.23,1.52-2.1,3.26-2.59,5.19-.3,1.2-.46,2.42-.43,3.64v.43l-30.68,20.61-.02.02ZM405.49,102.47l.76.19c-2.69,10.53-2.31,21.26,1.12,31.11l28.31-19c0-1.22.17-2.44.47-3.62.51-1.98,1.37-3.78,2.57-5.38l-15.81-30.51c-8.03,6.98-13.93,16.67-16.68,27.41l-.76-.19.02-.02ZM454.76,103.88l-.44-.28c-.92-.6-1.9-1.04-2.95-1.31-.62-.16-1.26-.25-1.97-.28h-.46l-17.79-34.32.71-.36c9.79-5,20.35-6.26,30.52-3.65,10.03,2.56,18.58,8.64,24.75,17.52l.46.66-32.84,22.05.02-.02ZM449.92,100.45c.65.05,1.25.16,1.83.3,1.04.27,2.04.68,2.97,1.23l30.63-20.56c-5.91-8.22-13.98-13.82-23.38-16.24-9.56-2.45-19.46-1.34-28.7,3.18l16.63,32.09h.02Z"/>
<path class="cls-2" d="M44.07.44h39.57l-39.34,158.24h59.24l-4.74,19.69H0L44.07.44Z"/>
<path class="cls-2" d="M167.75.44h98.81l-4.74,19.69h-59.24l-14.93,59.55h54.03l-4.74,19.69h-54.03l-14.93,59.31h59.24l-4.74,19.69h-98.81L167.75.44Z"/>
<path class="cls-2" d="M311.35,138.76c-1.42,5.54-.71,10.25,2.13,14.11,2.84,3.87,7.03,5.8,12.56,5.8,11.53,0,19.83-6.64,24.88-19.93h19.67c-4.11,13.29-10.43,23.22-18.96,29.78-8.53,6.56-20.22,9.84-35.07,9.84h-10.19c-11.69,0-20.58-2.85-26.67-8.54-6.08-5.69-9.13-12.89-9.13-21.59,0-2.69.39-5.85,1.18-9.49l14.69-59.31c1.42-5.38,3.79-10.47,7.11-15.29,3.32-4.82,7.35-9.02,12.09-12.57,4.74-3.56,10.11-6.36,16.11-8.43,6-2.06,12.32-3.08,18.96-3.08h10.19c12.8,0,21.96,2.26,27.49,6.77,5.53,4.51,8.29,11.26,8.29,20.29,0,3.8-.47,7.91-1.42,12.34h-19.67c1.74-13.13-3.24-19.69-14.93-19.69-5.53,0-10.66,1.95-15.4,5.8-4.74,3.87-7.82,8.59-9.24,14.11l-14.69,59.07.02.02Z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -9,6 +9,7 @@ export type IBlock = {
name: string;
id: string;
selected: boolean;
rightIcon?: JSX.Element;
hasFlag?: boolean;
};
@ -23,6 +24,7 @@ export default function BlockList({ blocks, onSelectedBlock }: IProps) {
},
[blocks, onSelectedBlock],
);
return (
<div>
{blocks.map((folder) => {
@ -34,6 +36,7 @@ export default function BlockList({ blocks, onSelectedBlock }: IProps) {
</div>
<div className={classes["right-side"]}>
{folder.hasFlag && <WarningBadge />}
{folder.rightIcon}
<Image alt="chevron" src={ChevronIcon} />
</div>
</div>

View File

@ -96,6 +96,10 @@
flex: 1;
}
&[fullwidthattr="false"] {
width: fit-content;
}
&[touppercase="false"] {
text-transform: inherit;
}

View File

@ -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 {

View File

@ -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} />}

View File

@ -421,7 +421,6 @@ export default class DepositDocument extends React.Component<IProps, IState> {
}
} catch (e) {
this.setState({ loading: false });
console.log(e);
}
}

View File

@ -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: [] });
// };

View File

@ -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;
}
}

View File

@ -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>
);
}

View File

@ -11,6 +11,8 @@
width: 100%;
left: 0;
text-align: center;
max-height: calc(100vh - 83px);
overflow: auto;
> *:not(:last-child) {
margin-bottom: 24px;
}

View File

@ -4,8 +4,9 @@ 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";
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
type IProps = {
isOpen: boolean;
@ -21,124 +22,144 @@ export default class BurgerModal extends React.Component<IProps, IState> {
<>
<div className={classes["background"]} onClick={this.props.closeModal} />
<div className={classes["root"]}>
<NavigationLink
path={Module.getInstance().get().modules.pages.Folder.props.path}
text="Dossiers en cours"
routesActive={[
Module.getInstance().get().modules.pages.Folder.pages.FolderInformation.props.path,
Module.getInstance().get().modules.pages.Folder.pages.CreateFolder.props.path,
<Rules
mode={RulesMode.OPTIONAL}
rules={[
{
action: AppRuleActions.read,
name: AppRuleNames.officeFolders,
},
]}>
<>
<NavigationLink
path={Module.getInstance().get().modules.pages.Folder.props.path}
text="Dossiers en cours"
routesActive={[
Module.getInstance().get().modules.pages.Folder.pages.FolderInformation.props.path,
Module.getInstance().get().modules.pages.Folder.pages.CreateFolder.props.path,
]}
/>
<NavigationLink
path={Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path}
text="Dossiers archivés"
routesActive={[Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path]}
/>
<div className={classes["separator"]} />
</>
</Rules>
<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,
},
],
},
]}
/>
<NavigationLink
path={Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path}
text="Dossiers archivés"
routesActive={[Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path]}
<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.update,
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
mode={RulesMode.NECESSARY}
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={[
{
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={[
{
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>
<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>
</>

View File

@ -14,13 +14,17 @@
.underline {
width: 100%;
height: 3px;
background-color: $black;
background-color: $white;
position: absolute;
bottom: 0;
left: 0;
&[data-active="true"] {
background-color: $black;
}
}
&.desactivated{
&.desactivated {
cursor: not-allowed;
}
}

View File

@ -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;
type IStateClass = {};
class HeaderLinkClass extends React.Component<IPropsClass, IStateClass> {
public override render(): JSX.Element {
if (this.props.path !== "" && this.props.path !== undefined) {
return (
<Link href={this.props.path} className={classNames(classes["root"], this.props.isActive && classes["active"])}>
<div className={classes["content"]}>
<Typography typo={this.props.isActive ? ITypo.P_SB_18 : ITypo.NAV_HEADER_18}>{this.props.text}</Typography>
</div>
{this.props.isActive && <div className={classes["underline"]} />}
</Link>
);
} else {
return (
<div className={classNames(classes["root"], classes["desactivated"])}>
<div className={classes["content"]}>
<Typography typo={ITypo.NAV_HEADER_18}>{this.props.text}</Typography>
</div>
</div>
);
}
}
}
export default function HeaderLink(props: IProps) {
export default function HeaderLink(props: IHeaderLinkProps) {
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);
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]);
if (props.path !== "" && props.path !== undefined) {
return (
<Link
href={props.path}
className={classNames(classes["root"], isActive && classes["active"])}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}>
<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()} />
</Link>
);
} else {
return (
<div className={classNames(classes["root"], classes["desactivated"])}>
<div className={classes["content"]}>
<Typography typo={ITypo.NAV_HEADER_18}>{props.text}</Typography>
</div>
</div>
);
}
return <HeaderLinkClass {...props} isActive={isActive} />;
}

View File

@ -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>
);
}

View File

@ -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;
}
}

View File

@ -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>
);
}

View File

@ -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"},
}
notification: { created_at: "desc" },
},
});
notifications.forEach((notification) => {
Toasts.getInstance().open({
@ -62,19 +63,113 @@ export default function Navigation() {
return (
<div className={classes["root"]}>
<HeaderLink
text={"Dossiers en cours"}
path={Module.getInstance().get().modules.pages.Folder.props.path}
routesActive={[
Module.getInstance().get().modules.pages.Folder.pages.FolderInformation.props.path,
Module.getInstance().get().modules.pages.Folder.pages.CreateFolder.props.path,
<Rules
mode={RulesMode.OPTIONAL}
rules={[
{
action: AppRuleActions.read,
name: AppRuleNames.officeFolders,
},
]}>
<>
<HeaderLink
text={"Dossiers en cours"}
path={Module.getInstance().get().modules.pages.Folder.props.path}
routesActive={[
Module.getInstance().get().modules.pages.Folder.pages.FolderInformation.props.path,
Module.getInstance().get().modules.pages.Folder.pages.CreateFolder.props.path,
]}
/>
<HeaderLink
text={"Dossiers archivés"}
path={Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path}
routesActive={[Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path]}
/>
</>
</Rules>
<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.update,
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,
},
],
},
]}
/>
<HeaderLink
text={"Dossiers archivés"}
path={Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path}
routesActive={[Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path]}
/>
{/* </Rules> */}
<Rules
mode={RulesMode.NECESSARY}
rules={[
@ -83,10 +178,38 @@ 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 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>

View File

@ -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 />

View File

@ -46,6 +46,14 @@
margin-right: 32px;
}
}
.help-section {
display: inline-flex;
cursor: pointer;
> :first-child {
margin-right: 32px;
}
}
}
.burger-menu {
@ -61,3 +69,14 @@
}
}
}
.subscription-line {
position: sticky;
top: 83px;
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
background-color: var(--grey-soft);
padding: 14px 0;
}

View File

@ -12,6 +12,12 @@ import Module from "@Front/Config/Module";
import Head from "next/head";
import { useRouter } from "next/router";
import LogoCielNatureIcon from "./logo-ciel-notaires.jpeg";
import LifeBuoy from "@Assets/Icons/life_buoy.svg";
import Stripe from "@Front/Api/LeCoffreApi/Admin/Stripe/Stripe";
import JwtService from "@Front/Services/JwtService/JwtService";
import Subscriptions from "@Front/Api/LeCoffreApi/Admin/Subscriptions/Subscriptions";
import Typography, { ITypo, ITypoColor } from "../Typography";
import { InformationCircleIcon } from "@heroicons/react/24/outline";
enum EHeaderOpeningState {
OPEN = "open",
@ -32,6 +38,7 @@ type IState = {
isBurgerMenuOpen: boolean;
isNotificationMenuOpen: boolean;
isProfileMenuOpen: boolean;
cancelAt: Date | null;
};
class HeaderClass extends React.Component<IPropsClass, IState> {
@ -46,6 +53,7 @@ class HeaderClass extends React.Component<IPropsClass, IState> {
isBurgerMenuOpen: false,
isNotificationMenuOpen: false,
isProfileMenuOpen: false,
cancelAt: null,
};
this.openBurgerMenu = this.openBurgerMenu.bind(this);
this.closeBurgerMenu = this.closeBurgerMenu.bind(this);
@ -53,59 +61,90 @@ class HeaderClass extends React.Component<IPropsClass, IState> {
this.closeNotificationMenu = this.closeNotificationMenu.bind(this);
this.closeProfileMenu = this.closeProfileMenu.bind(this);
this.openProfileMenu = this.openProfileMenu.bind(this);
this.loadSubscription = this.loadSubscription.bind(this);
}
public override render(): JSX.Element {
return (
<div className={classes["root"]} data-open={this.state.open}>
<Head>
<link rel="shortcut icon" href={"/favicon.svg"} />
</Head>
<div className={classes["logo-container"]}>
<Link href={"#"}>
<Image src={LogoIcon} alt="logo" className={classes["logo"]} />
</Link>
<>
<div className={classes["root"]} data-open={this.state.open}>
<Head>
<link rel="shortcut icon" href={"/favicon.svg"} />
</Head>
<div className={classes["logo-container"]}>
<Link href={"#"}>
<Image src={LogoIcon} alt="logo" className={classes["logo"]} />
</Link>
</div>
{this.props.isUserConnected && (
<>
<Navigation />
<div className={classes["right-section"]}>
<div className={classes["help-section"]}>
<Link href="https://tally.so/r/mBGaNY" target="blank">
<Image src={LifeBuoy} alt="help" />
</Link>
</div>
<div className={classes["notification-section"]}>
<Notifications
isModalOpen={this.state.isNotificationMenuOpen}
openNotificationModal={this.openNotificationMenu}
closeNotificationModal={this.closeNotificationMenu}
/>
</div>
<div className={classes["profile-section"]}>
<Profile
isModalOpen={this.state.isProfileMenuOpen}
closeProfileModal={this.closeProfileMenu}
openProfileModal={this.openProfileMenu}
/>
</div>
<div className={classes["burger-menu"]}>
<BurgerMenu
isModalOpen={this.state.isBurgerMenuOpen}
closeBurgerMenu={this.closeBurgerMenu}
openBurgerMenu={this.openBurgerMenu}
/>
</div>
</div>
</>
)}
{this.props.isOnCustomerLoginPage && <Image width={70} height={70} alt="ciel-nature" src={LogoCielNatureIcon}></Image>}
</div>
{this.props.isUserConnected && (
<>
<Navigation />
<div className={classes["right-section"]}>
<div className={classes["notification-section"]}>
<Notifications
isModalOpen={this.state.isNotificationMenuOpen}
openNotificationModal={this.openNotificationMenu}
closeNotificationModal={this.closeNotificationMenu}
/>
</div>
<div className={classes["profile-section"]}>
<Profile
isModalOpen={this.state.isProfileMenuOpen}
closeProfileModal={this.closeProfileMenu}
openProfileModal={this.openProfileMenu}
/>
</div>
<div className={classes["burger-menu"]}>
<BurgerMenu
isModalOpen={this.state.isBurgerMenuOpen}
closeBurgerMenu={this.closeBurgerMenu}
openBurgerMenu={this.openBurgerMenu}
/>
</div>
</div>
</>
{this.state.cancelAt && (
<div className={classes["subscription-line"]}>
<InformationCircleIcon height="24" />;
<Typography typo={ITypo.P_16} color={ITypoColor.BLACK}>
Assurez vous de sauvegarder tout ce dont vous avez besoin avant la fin de votre abonnement le{" "}
{this.state.cancelAt.toLocaleDateString()}.
</Typography>
</div>
)}
{this.props.isOnCustomerLoginPage && <Image width={70} height={70} alt="ciel-nature" src={LogoCielNatureIcon}></Image>}
</div>
</>
);
}
public override componentDidMount() {
this.onWindowResize = WindowStore.getInstance().onResize((window) => this.onResize(window));
this.loadSubscription();
}
public override componentWillUnmount() {
this.onWindowResize();
}
public async loadSubscription() {
const jwt = JwtService.getInstance().decodeJwt();
const subscription = await Subscriptions.getInstance().get({ where: { office: { uid: jwt?.office_Id } } });
if (subscription[0]) {
const stripeSubscription = await Stripe.getInstance().getStripeSubscriptionByUid(subscription[0].stripe_subscription_id!);
if (stripeSubscription.cancel_at !== null)
this.setState({
...this.state,
cancelAt: new Date(stripeSubscription.cancel_at! * 1000),
});
}
}
private onResize(window: Window) {
if (window.innerWidth > this.headerBreakpoint && this.state.isBurgerMenuOpen) this.setState({ isBurgerMenuOpen: false });
if (window.innerWidth < this.headerBreakpoint && this.state.isProfileMenuOpen) this.setState({ isProfileMenuOpen: false });

View File

@ -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`,

View File

@ -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;
}

View File

@ -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 {

View 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;
}
}

View 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 />;
}
}
}

View 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;
}
}

View 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>
);
}

View File

@ -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;
}
}

View 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>
);
}

View File

@ -53,6 +53,18 @@ export default function CollaboratorListContainer(props: IProps) {
name: user.contact?.first_name + " " + user.contact?.last_name,
id: user.uid!,
selected: user.uid === collaboratorUid,
rightIcon: user.seats?.some((seat) => new Date(seat.subscription!.end_date) >= new Date()) ? (
<div
style={{
height: "12px",
width: "12px",
borderRadius: "100px",
backgroundColor: "var(--green-flash)",
}}
/>
) : (
<></>
),
};
})}
onSelectedBlock={onSelectedBlock}

View File

@ -92,7 +92,14 @@ 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);

View File

@ -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));
},

View File

@ -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);

View File

@ -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 />
</>
);

View File

@ -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 (

View File

@ -1,6 +1,22 @@
@import "@Themes/constants.scss";
.root {
.folder-header {
display: flex;
justify-content: space-between;
align-items: center;
.subscription-active {
display: flex;
align-items: center;
gap: 8px;
.subscription-active-dot {
width: 12px;
height: 12px;
background-color: var(--green-flash);
border-radius: 100px;
}
}
}
.user-infos {
background-color: var(--grey-soft);
display: flex;

View File

@ -123,6 +123,11 @@ export default function CollaboratorInformations(props: IProps) {
contact: true,
office_role: true,
role: true,
seats: {
include: {
subscription: true,
},
},
},
});
if (!user) return;
@ -145,6 +150,14 @@ export default function CollaboratorInformations(props: IProps) {
<div className={classes["root"]}>
<div className={classes["folder-header"]}>
<Typography typo={ITypo.H1Bis}>{userSelected?.contact?.first_name + " " + userSelected?.contact?.last_name}</Typography>
{userSelected && userSelected.seats?.some((seat) => new Date(seat.subscription!.end_date) >= new Date()) && (
<div className={classes["subscription-active"]}>
<div className={classes["subscription-active-dot"]} />
<Typography typo={ITypo.P_18} color={ITypoColor.GREEN_FLASH}>
Abonnement actif
</Typography>
</div>
)}
</div>
<div className={classes["user-infos"]}>
<div className={classes["user-infos-row"]}>

View File

@ -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(() => {

View File

@ -76,7 +76,7 @@ export default function DeedTypesEdit() {
return;
}
},
[deedTypeUid, router, validationError],
[deedTypeUid, router],
);
const onFieldChange = useCallback((name: string, field: any) => {

View File

@ -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 (

View File

@ -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[],
});

View File

@ -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[],
});

View File

@ -200,7 +200,7 @@ class ViewDocumentsClass extends BasePage<IPropsClass, IState> {
fileBlob,
});
} catch (e) {
console.log(e);
console.error(e);
}
}

View File

@ -41,10 +41,10 @@ export default function Login() {
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));
@ -77,7 +77,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>

View File

@ -26,9 +26,13 @@ export default function LoginCallBack() {
const token = await Auth.getInstance().getIdnotJwt(code as string);
if (!token) return router.push(Module.getInstance().get().modules.pages.Login.props.path);
await UserStore.instance.connect(token.accessToken, token.refreshToken);
const jwt = JwtService.getInstance().decodeJwt();
if (!jwt) return router.push(Module.getInstance().get().modules.pages.Login.props.path + "?error=1");
if (!jwt.rules.includes("GET folders")) {
return router.push(Module.getInstance().get().modules.pages.Subscription.pages.New.props.path);
}
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");
}
@ -41,6 +45,11 @@ export default function LoginCallBack() {
const refreshToken = CookieService.getInstance().getCookie("leCoffreRefreshToken");
if (!refreshToken) return router.push(Module.getInstance().get().modules.pages.Login.props.path + "?error=1");
const isTokenRefreshed = await JwtService.getInstance().refreshToken(refreshToken);
const jwt = JwtService.getInstance().decodeJwt();
if (!jwt) return router.push(Module.getInstance().get().modules.pages.Login.props.path + "?error=1");
if (!jwt.rules.includes("GET folders")) {
return router.push(Module.getInstance().get().modules.pages.Subscription.pages.New.props.path);
}
if (isTokenRefreshed) {
return router.push(Module.getInstance().get().modules.pages.Folder.props.path);
}

View File

@ -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;
}
@ -34,7 +34,7 @@ export default function LoginCallBackCustomer() {
router.push(Module.getInstance().get().modules.pages.Folder.pages.Select.props.path);
}
const refreshToken = CookieService.getInstance().getCookie("leCoffreRefreshToken");
if(!refreshToken) return router.push(Module.getInstance().get().modules.pages.Login.props.path + "?error=1");
if (!refreshToken) return router.push(Module.getInstance().get().modules.pages.Login.props.path + "?error=1");
const isTokenRefreshed = await JwtService.getInstance().refreshToken(refreshToken);
if (isTokenRefreshed) {
return router.push(Module.getInstance().get().modules.pages.Folder.pages.Select.props.path);

View File

@ -0,0 +1,23 @@
.root {
padding: 64px;
.content {
display: flex;
gap: 48px;
flex-direction: column;
margin-top: 48px;
.section {
display: flex;
flex-direction: column;
gap: 24px;
}
.separator {
height: 1px;
background-color: var(--grey-medium);
}
}
.bottom {
margin-top: 48px;
}
}

View File

@ -0,0 +1,47 @@
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
import DefaultDoubleSidePage from "@Front/Components/LayoutTemplates/DefaultDoubleSidePage";
import classes from "./classes.module.scss";
import LandingImage from "../Login/landing-connect.jpeg";
import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button";
import Link from "next/link";
import Module from "@Front/Config/Module";
export default function LoginHome() {
return (
<DefaultDoubleSidePage title={"Login"} image={LandingImage}>
<div className={classes["root"]}>
<Typography typo={ITypo.H1}>
<div className={classes["title"]}>Connectez-vous à votre plateforme LEcoffre.io</div>
</Typography>
<div className={classes["content"]}>
<div className={classes["section"]}>
<Typography typo={ITypo.P_18} color={ITypoColor.BLACK}>
Je suis un notaire
</Typography>
<Link href={Module.getInstance().get().modules.pages.Login.props.path}>
<Button>Se connecter</Button>
</Link>
</div>
<div className={classes["separator"]} />
<div className={classes["section"]}>
<Typography typo={ITypo.P_18} color={ITypoColor.BLACK}>
Je suis un client
</Typography>
<Link href={Module.getInstance().get().modules.pages.CustomersLogin.props.path}>
<Button>Se connecter</Button>
</Link>
</div>
</div>
<div className={classes["bottom"]}>
<Typography typo={ITypo.P_18}>
<div className={classes["forget-password"]}>Vous n'arrivez pas à vous connecter ?</div>
</Typography>
<Link href="mailto:g.texier@notaires.fr">
<Button variant={EButtonVariant.LINE}>Contacter l'administrateur</Button>
</Link>
</div>
</div>
</DefaultDoubleSidePage>
);
}

View File

@ -1,6 +1,10 @@
@import "@Themes/constants.scss";
.root {
@media (max-width: $screen-m) {
margin-top: 24px;
}
.title {
margin-top: 24px;
}

View File

@ -33,11 +33,11 @@ export default function Rib() {
setFileName("");
setKey("");
}
}, [officeUid]);
}, [key]);
useEffect(() => {
fetchData();
}, [officeUid]);
}, [fetchData, officeUid]);
function downloadFile() {
if (!fileBlob) return;

View File

@ -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

View File

@ -1,28 +1,28 @@
import OfficeRoles from "@Front/Api/LeCoffreApi/Admin/OfficeRoles/OfficeRoles";
import Rules from "@Front/Api/LeCoffreApi/Admin/Rules/Rules";
import Button from "@Front/Components/DesignSystem/Button";
import CheckBox from "@Front/Components/DesignSystem/CheckBox";
import Form from "@Front/Components/DesignSystem/Form";
import Confirm from "@Front/Components/DesignSystem/Modal/Confirm";
import Typography, { ITypo } from "@Front/Components/DesignSystem/Typography";
import DefaultRoleDashboard from "@Front/Components/LayoutTemplates/DefaultRoleDashboard";
import { OfficeRole, Rule } from "le-coffre-resources/dist/Admin";
import { OfficeRole, Rule, RulesGroup } from "le-coffre-resources/dist/Admin";
import { useRouter } from "next/router";
import { useCallback, useEffect, useState } from "react";
import React from "react";
import classes from "./classes.module.scss";
import RulesGroups from "@Front/Api/LeCoffreApi/Admin/RulesGroups/RulesGroups";
type IProps = {};
type RuleCheckbox = Rule & {
type RuleGroupsCheckbox = RulesGroup & {
checked: boolean;
};
export default function RolesInformations(props: IProps) {
export default function RolesInformations() {
const router = useRouter();
let { roleUid } = router.query;
const [roleSelected, setRoleSelected] = useState<OfficeRole | null>(null);
const [rulesCheckboxes, setRulesCheckboxes] = useState<RuleCheckbox[]>([]);
const [rulesGroupsCheckboxes, setRulesGroupsCheckboxes] = useState<RuleGroupsCheckbox[]>([]);
const [selectAll, setSelectAll] = useState<boolean>(false);
const [isConfirmModalOpened, setIsConfirmModalOpened] = useState<boolean>(false);
@ -45,35 +45,28 @@ export default function RolesInformations(props: IProps) {
},
});
const rules = await Rules.getInstance().get({
where: {
OR: [
{
namespace: "notary",
},
{
namespace: "collaborator",
},
],
const rulesGroups = await RulesGroups.getInstance().get({
include: {
rules: true,
},
});
if (!role) return;
setRoleSelected(role);
if (!role.rules) return;
const rulesCheckboxes = rules
.map((rule) => {
if (role.rules?.find((r) => r.uid === rule.uid)) {
return { ...rule, checked: true };
const rulesCheckboxes = rulesGroups
.map((ruleGroup) => {
if (ruleGroup.rules?.every((rule) => role.rules?.find((r) => r.uid === rule.uid))) {
return { ...ruleGroup, checked: true };
}
return { ...rule, checked: false };
return { ...ruleGroup, checked: false };
})
.sort((ruleA, ruleB) => (ruleA.label < ruleB.label ? 1 : -1))
.sort((ruleA, ruleB) => (ruleA.name! < ruleB.name! ? 1 : -1))
.sort((rule) => (rule.checked ? -1 : 1));
const selectAll = rulesCheckboxes.every((rule) => rule.checked);
setSelectAll(selectAll);
setRulesCheckboxes(rulesCheckboxes);
setRulesGroupsCheckboxes(rulesCheckboxes);
}
getUser();
@ -83,18 +76,25 @@ export default function RolesInformations(props: IProps) {
(e: React.ChangeEvent<HTMLInputElement>) => {
setSelectAll(e.target.checked);
const checked = e.target.checked;
rulesCheckboxes.forEach((rule) => (rule.checked = checked));
setRulesCheckboxes([...rulesCheckboxes]);
rulesGroupsCheckboxes.forEach((rule) => (rule.checked = checked));
setRulesGroupsCheckboxes([...rulesGroupsCheckboxes]);
},
[rulesCheckboxes],
[rulesGroupsCheckboxes],
);
const modifyRules = useCallback(async () => {
if (!roleSelected || !roleSelected.uid) return;
const rules = rulesCheckboxes.filter((rule) => rule.checked)?.map((rule) => Rule.hydrate<Rule>(rule));
const rulesGroupsChecked = rulesGroupsCheckboxes.filter((rule) => rule.checked);
let newRules: Rule[] = [];
for (let ruleGroup of rulesGroupsChecked) {
if (!ruleGroup.rules) continue;
newRules = [...newRules, ...ruleGroup.rules];
}
await OfficeRoles.getInstance().put(roleSelected.uid, {
uid: roleSelected.uid,
rules,
rules: newRules,
});
const roleUpdated = await OfficeRoles.getInstance().getByUid(roleSelected.uid, {
@ -104,17 +104,17 @@ export default function RolesInformations(props: IProps) {
});
setRoleSelected(roleUpdated);
closeConfirmModal();
}, [closeConfirmModal, roleSelected, rulesCheckboxes]);
}, [closeConfirmModal, roleSelected, rulesGroupsCheckboxes]);
const handleRuleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const ruleUid = e.target.value;
const rule = rulesCheckboxes.find((rule) => rule.uid === ruleUid);
const rule = rulesGroupsCheckboxes.find((rule) => rule.uid === ruleUid);
if (!rule) return;
rule.checked = e.target.checked;
setRulesCheckboxes([...rulesCheckboxes]);
setRulesGroupsCheckboxes([...rulesGroupsCheckboxes]);
},
[rulesCheckboxes],
[rulesGroupsCheckboxes],
);
return (
@ -142,11 +142,11 @@ export default function RolesInformations(props: IProps) {
</div>
<Form>
<div className={classes["rights"]}>
{rulesCheckboxes.map((rule) => (
<div className={classes["right"]} key={rule.uid}>
{rulesGroupsCheckboxes.map((ruleGroup) => (
<div className={classes["right"]} key={ruleGroup.uid}>
<CheckBox
option={{ label: rule.label, value: rule.uid }}
checked={rule.checked}
option={{ label: ruleGroup.name!, value: ruleGroup.uid }}
checked={ruleGroup.checked}
onChange={handleRuleChange}
/>
</div>

View File

@ -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);

View File

@ -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;
}
}
}
}

View File

@ -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],
)}
&nbsp; x 11
</Typography>
)}
</div>
<div className={classes["line-sub-container"]}>
<Typography typo={ITypo.P_SB_18} color={ITypoColor.BLACK}>
{totalPlan}&nbsp;
</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,
)}
&nbsp;
</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)}&nbsp; x {numberOfCollaborators}{" "}
{paymentFrequency === EPaymentFrequency.yearly && "x 12"}
</Typography>
</div>
<Typography typo={ITypo.P_SB_18} color={ITypoColor.BLACK}>
{formatFloat(totalCollaborator)}&nbsp;
</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)}
&nbsp;
</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)}
&nbsp;
</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)}&nbsp;
</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)}
&nbsp;
</Typography>
</div>
</div>
{!props.disableInputs && (
<Button onClick={handleSubmitPayment} fullwidth className={classes["payment-button"]}>
{hasNavTab ? "Passer au paiement" : "Mettre à jour l'abonnement"}
</Button>
)}
</div>
</div>
);
}

View File

@ -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;
}
}

View File

@ -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é, 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 dincident
</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)}
&nbsp;
</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>
</>
);
}

View File

@ -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;
}
}

View File

@ -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é, 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 dincident
</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,
)}
&nbsp;
</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>
</>
);
}

View File

@ -0,0 +1,5 @@
.root {
display: flex;
flex-direction: column;
gap: 32px;
}

View File

@ -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>
);
}

View File

@ -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;
}
}
}
}

View File

@ -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();
}, [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>
);
}

View File

@ -0,0 +1,5 @@
import SubscribeIllimityComponent from "../../Components/SubscribeIllimityComponent";
export default function SubscribeManageIllimity() {
return <SubscribeIllimityComponent hasNavTab={false} />;
}

View File

@ -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;
}
}

View File

@ -0,0 +1,5 @@
import SubscribeStandardComponent from "../../Components/SubscribeStandardComponent";
export default function SubscribeManageStandard() {
return <SubscribeStandardComponent hasNavTab={false} />;
}

View File

@ -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;
}
}

View File

@ -0,0 +1,5 @@
import SubscribeIllimityComponent from "../../Components/SubscribeIllimityComponent";
export default function SubscribeIllimity() {
return <SubscribeIllimityComponent />;
}

View File

@ -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;
}
}

View File

@ -0,0 +1,5 @@
import SubscribeStandardComponent from "../../Components/SubscribeStandardComponent";
export default function SubscribeStandard() {
return <SubscribeStandardComponent />;
}

View File

@ -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);
}
}

View File

@ -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 é 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>
);
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,314 @@
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 [cancelAt, setCancelAt] = useState<Date | null>(null);
const { close: closeCancelSubscription, isOpen: isCancelSubscriptionOpen } = useOpenable();
const { close: closeConfirmation, isOpen: isConfirmationOpen } = useOpenable();
const { close: closeConfirmationRemoveSeats, isOpen: isConfirmationRemoveSeatsOpen, open: openConfirmationRemoveSeats } = 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 cancelOrReactivateSubscription = 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!);
if (!cancelAt) {
router.push(stripe_client_portal.url + "/subscriptions/" + subscription[0].stripe_subscription_id + "/cancel");
} else {
router.push(stripe_client_portal.url + "/subscriptions/" + subscription[0].stripe_subscription_id + "/reactivate");
}
} 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 {
const stripeSubscription = await Stripe.getInstance().getStripeSubscriptionByUid(subscription[0].stripe_subscription_id!);
if (stripeSubscription.cancel_at !== null) setCancelAt(new Date(stripeSubscription.cancel_at! * 1000));
setSubscription(subscription[0]);
}
}, [router]);
useEffect(() => {
loadSubscription();
}, [loadSubscription]);
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" && !cancelAt && (
<div className={classes["active-plan"]}>
<Typography typo={ITypo.P_18} color={ITypoColor.GREEN_FLASH}>
Plan actif
</Typography>
</div>
)}
{subscription.type === "STANDARD" && cancelAt && (
<div className={classes["active-plan"]}>
<Typography typo={ITypo.P_18} color={ITypoColor.GREEN_FLASH}>
Plan actif jusqu'au {cancelAt.toLocaleDateString()}
</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}>
&nbsp;HT&nbsp;
</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={openConfirmationRemoveSeats} 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" && !cancelAt && (
<div className={classes["active-plan"]}>
<Typography typo={ITypo.P_18} color={ITypoColor.GREEN_FLASH}>
Plan actif
</Typography>
</div>
)}
{subscription.type === "UNLIMITED" && cancelAt && (
<div className={classes["active-plan"]}>
<Typography typo={ITypo.P_18} color={ITypoColor.GREEN_FLASH}>
Plan actif jusqu'au {cancelAt.toLocaleDateString()}
</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}>
&nbsp;HT&nbsp;
</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={cancelOrReactivateSubscription}>
{!cancelAt && (
<Typography typo={ITypo.P_18} color={ITypoColor.RED_FLASH}>
Arrêter l'abonnement
</Typography>
)}
{cancelAt && (
<Typography typo={ITypo.P_18} color={ITypoColor.RED_FLASH}>
Renouveler l'abonnement
</Typography>
)}
</Button>
<Button onClick={manageBilling}>Gérer la facturation</Button>
</div>
</div>
)}
<Confirm
isOpen={isCancelSubscriptionOpen}
onClose={closeCancelSubscription}
onAccept={cancelOrReactivateSubscription}
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>
<Confirm
isOpen={isConfirmationRemoveSeatsOpen}
onClose={closeConfirmationRemoveSeats}
onAccept={manageBilling}
closeBtn
confirmText={"Continuer"}
cancelText={"Annuler"}>
<div className={classes["modal-content"]}>
<Typography typo={ITypo.P_16} color={ITypoColor.BLACK}>
En cas de suppression de sièges, les derniers comptes ajoutés seront concernés. Vous pouvez réattribuer les sièges
que vous avez dans votre page sur la page "Gérer mes attributions.
</Typography>
</div>
</Confirm>
</DefaultTemplate>
);
}

View File

@ -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: center;
align-items: center;
> span {
width: 100%;
flex: 1;
}
}
.add-line-container {
display: flex;
justify-content: center;
}
.button-container {
display: flex;
justify-content: center;
}
}
}
}

View File

@ -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>
);
}

View File

@ -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;
}
}
}

View File

@ -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}>
&nbsp;HT&nbsp;
</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}>
&nbsp;HT&nbsp;
</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é, 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 dincident
</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>
);
}

View File

@ -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);
}
}

View File

@ -0,0 +1,103 @@
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";
import CookieService from "@Front/Services/CookieService/CookieService";
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 refreshToken = useCallback(async () => {
CookieService.getInstance().deleteCookie("leCoffreAccessToken");
const refreshToken = CookieService.getInstance().getCookie("leCoffreRefreshToken");
if (!refreshToken) return;
await JwtService.getInstance().forceRefreshToken(refreshToken);
await loadSubscription();
}, [loadSubscription]);
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(() => {
refreshToken();
}, [refreshToken]);
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 é effectuée avec succès !
<br />
<br />
Votre abonnement a é 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>
);
}

View File

@ -269,7 +269,7 @@ export default function UserInformations(props: IProps) {
)}
<Switch
label="Super-admin LeCoffre.io"
label="Super-admin LEcoffre.io"
checked={isSuperAdminChecked}
disabled={userHasVoted()}
onChange={handleSuperAdminChanged}

View File

@ -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"
}
}
}
}
}
}
}
}

View File

@ -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"
}
}
}
}
}
}
}
}

View File

@ -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"
}
}
}
}
}
}
}
}

View File

@ -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"
}
}
}
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More