Merge branch 'staging' into preprod

This commit is contained in:
Maxime Lalo 2023-09-15 16:53:35 +02:00
commit d2f125e8e0
294 changed files with 19779 additions and 3305 deletions

View File

@ -23,71 +23,48 @@ jobs:
- run: docker build --tag rg.fr-par.scw.cloud/lecoffre/front:$TAG .
- run: docker push rg.fr-par.scw.cloud/lecoffre/front:$TAG
deploy-docker-image:
parameters:
env:
type: string
default: ""
docker:
- image: cimg/base:stable
environment:
TAG: << pipeline.git.tag >>
steps:
- checkout
- kubernetes/install-kubeconfig:
kubeconfig: KUBECONFIG_DATA
- helm/install-helm-client
- run:
name: Deploy
command: >
helm upgrade
lecoffre-front devops/ -i -f devops/<<parameters.env>>.values.yaml
-n lecoffre
--create-namespace
--set lecoffreFront.image.repository='rg.fr-par.scw.cloud/lecoffre/front'
--set lecoffreFront.image.tag=$TAG
build-push-docker-image:
docker:
- image: cimg/base:stable
environment:
TAG: << pipeline.git.tag >>
steps:
- checkout
- add_ssh_keys:
fingerprints:
- "39:25:57:64:62:43:1f:98:b1:5e:75:53:87:d8:e7:71"
- run: cp $HOME/.ssh/id_rsa_3925576462431f98b15e755387d8e771 id_rsa
- setup_remote_docker:
version: 20.10.12
docker_layer_caching: true
- run: docker login rg.fr-par.scw.cloud/lecoffre -u nologin -p $SCW_SECRET_KEY
- run: docker build --tag rg.fr-par.scw.cloud/lecoffre/front:${CIRCLE_SHA1:0:7} .
- run: docker push rg.fr-par.scw.cloud/lecoffre/front:${CIRCLE_SHA1:0:7}
workflows:
version: 2
build-and-deploy-stg:
jobs:
- build-push-docker-image:
filters:
tags:
only: /^v.*/
branches:
ignore: /.*/
- deploy-docker-image:
env: stg
requires:
- build-push-docker-image
context:
- staging
filters:
tags:
only: /^v.*/
branches:
ignore: /.*/
build-and-deploy-ppd:
jobs:
- build-push-docker-image:
filters:
tags:
only: /^v.*/
branches:
ignore: /.*/
- deploy-docker-image:
env: ppd
requires:
- build-push-docker-image
context:
- production
filters:
tags:
only: /^v.*/
branches:
ignore: /.*/
deploy-docker-image:
docker:
- image: cimg/base:stable
environment:
TAG: << pipeline.git.tag >>
parameters:
env:
type: string
default: stg
steps:
- checkout
- kubernetes/install-kubeconfig:
kubeconfig: KUBECONFIG_DATA
- helm/install-helm-client
- run:
name: Deploy
command: >
helm upgrade
lecoffre-front devops/ -i -f devops/<<parameters.env>>.values.yaml
-n lecoffre-<<parameters.env>>
--create-namespace
--set lecoffreFront.image.repository='rg.fr-par.scw.cloud/lecoffre/front'
--set lecoffreFront.image.tag=${CIRCLE_SHA1:0:7}
# build-and-deploy-prod:
# jobs:
@ -107,3 +84,60 @@ workflows:
# only: /^v.*/
# branches:
# ignore: /.*/
workflows:
version: 2
build-and-register-stg:
jobs:
- build-push-docker-image:
context:
- sc-shared-prd
filters:
branches:
only: staging
- deploy-docker-image:
env: stg
requires:
- build-push-docker-image
context:
- sc-shared-prd
filters:
branches:
only: staging
build-and-register-ppd:
jobs:
- build-push-docker-image:
context:
- sc-shared-prd
filters:
branches:
only: preprod
- deploy-docker-image:
env: ppd
requires:
- build-push-docker-image
context:
- sc-shared-prd
filters:
branches:
only: preprod
build-and-register-prd:
jobs:
- build-push-docker-image:
context:
- sc-shared-prd
filters:
branches:
only: main
- deploy-docker-image:
env: prd
requires:
- build-push-docker-image
context:
- sc-shared-prd
filters:
branches:
only: main

2
.gitignore vendored
View File

@ -4,6 +4,7 @@
/node_modules
/.pnp
.pnp.js
dist/
# testing
/coverage
@ -11,6 +12,7 @@
# next.js
/.next/
/out/
dist/
# production
/build

1
.ssh/id_rsa.pub Normal file
View File

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDOfOFPvQNw5XguFuX1YNvED8ulP+tIA/5xw7LHcA0gRj3hwILCAEAjuDs+d13zCsnEb0yd+laT3PU9iRIKa28Tynu+sTjGDyfx8MX/HjJtbEzyd6jLn87uTvw/6lzg2y5ZDEa6PEqrPIv0KEhuq6HuU8qAA0nBpsTIAUTK3XR8qm3I6J9Rs1JyBjvIP5UeICApvoLmgHuz6mKdvoQ8qKDWamsL4pSc4Hr7HlQ8ITNhnyS8XMgQInU/I2TzT/I4Dxx5IeFUQ5KOfJJNgK1d+PByLSWUrn+eRXki8m1hjMiwGIehVAriFW1C309SEHxLHjQKUPXHSv4kH7zqjO+p3kY5gwp/lvsBRSnihj8s1lADsJlMqjnSLeIQ+sY2CNkmXXI8ABkzhuJKTGTl+8pzGGhIHzeU7e7lpSn3gLn4p217kIppHNAr6dZH9UaYbgnwVonwr5cLbatRPFyI1NfXKDyZtSYlGQxLQUt9KDrNvVTZzaTt3YwM/YCsRIADAagKosM= gisele-smartchain@MacBook-Pro-6.local

View File

@ -6,4 +6,10 @@
],
"description": "media queries"
},
"Default div": {
"prefix": "<div",
"body": [
"<div className={classes[\"$1\"]}>$2</div>"
]
}
}

View File

@ -1,6 +1,5 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},

44
Dockerfile.front Normal file
View File

@ -0,0 +1,44 @@
# Install dependencies only when needed
FROM node:19-alpine AS deps
WORKDIR leCoffre-front
COPY package.json ./
RUN apk update && apk add openssh-client git
COPY id_rsa /root/.ssh/id_rsa
RUN chmod 600 ~/.ssh/id_rsa
RUN eval "$(ssh-agent -s)" && ssh-add /root/.ssh/id_rsa
RUN ssh-keyscan github.com smart-chain-fr/leCoffre-resources.git >> /root/.ssh/known_hosts
RUN npm install --frozen-lockfile
# Rebuild the source code only when needed
FROM node:19-alpine AS builder
WORKDIR leCoffre-front
COPY --from=deps leCoffre-front/node_modules ./node_modules
COPY --from=deps leCoffre-front/package.json package.json
COPY tsconfig.json tsconfig.json
COPY src src
RUN npm run build
# Production image, copy all the files and run next
FROM node:19-alpine AS production
WORKDIR leCoffre-front
RUN adduser -D lecoffreuser --uid 10000 && chown -R lecoffreuser .
COPY public ./public
COPY --from=builder --chown=lecoffreuser leCoffre-front/node_modules ./node_modules
COPY --from=builder --chown=lecoffreuser leCoffre-front/.next ./.next
COPY --from=builder --chown=lecoffreuser leCoffre-front/package.json ./package.json
USER lecoffreuser
CMD ["npm", "run", "start"]
EXPOSE 3000

View File

@ -1,5 +1,5 @@
apiVersion: v2
name: leCoffre-front
name: leCoffre-back
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
@ -21,4 +21,5 @@ version: 0.0.1
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: 0.4.15
appVersion: 0.5.6

View File

@ -1,24 +1,11 @@
dockerPullSecret: secret/data/lecoffre-front-ppd/config/dockerpullsecret
dockerPullSecret: docker-pull-secret
namespace: lecoffre
scwSecretKey: AgCgjF5QEzxT3GYTS5B6cmQ0e+0/qFWzKaUDSi+Vjc7RoameuvaIJvTXMBkS3he1oy1ulbB34v6vpZI2kxnGNqERA/U5BaYDAyfKSBwMAy4br7HVKhhuwkoF5qoG5JzJXseSmqB1U9vncVIGOZWzJc1Y4/eGlWcvLcLyfw2z/WEpyeNiWJfEhTYpJOB7gv0XnRb2U/JM3jRy1QgEUIk1WR6kgBalF+xaczPQ6uKh+PR2pqkbZa3WaKUrddmzNsgEz4d8PZMWt8IBwR2JOQEHUqCd34p/pJNyLdUgcdDhg02DKwn1oRoAxKTbAio/a7WrMbodjCb3TNWIYGal5mFmItZ7Ok/EBmUf4E85eOkTR+j8ynuuiexld3Q5Kw3o8LsHjgzVL9uP+T2rYaKkjtVt+YQRX1U8l9CrsdUEz0/wEBA0jwCWMfnh1qhD5pM/xwwjsEEAcK4rYV+Q7iAgGZZvZBCQ5aEHzrtn5D95tr1GZCV2hmrW6Seu+LKKLVBS1JmsuEsOuhudYsEK9m2RYVcxbjuS5eokKEjNrGobf2oB8rhBByavfw1JTBixR5JrI8lcYlnCa+oEhxXKJY+4Fx5SAB4YaLCMSo5vw6zsFQ3WKQzlEmCFt+EnapS+a+MGrdlwq07OHTDpvgk/1z39hopoCuhhKckGGfErLXsTYQvDOkFu+EPzgY7m7qDw/d9pSiht5tuSOkAqeOgm7tpNkUufZhaXmP+1aT7i+H5gq1JILGAmXzTI5Wc=
lecoffreFront:
serviceAccountName: lecoffre-front-sa
command: "'sh', '-c', '. /vault/secrets/envs && npm run start'"
vault:
role : custom_lecoffre-front_injector_rol
server: https://vault-ppd.smart-chain.fr
annotations:
vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-inject-secret-envs: secret/data/lecoffre-front-ppd/config/envs
vault.hashicorp.com/role: custom_lecoffre-front_injector_rol
vault.hashicorp.com/agent-inject-template-envs: |
{{ with secret "secret/data/lecoffre-front-ppd/config/envs" }}
{{ range $k, $v := .Data.data }}
export {{ $k }}="{{ $v }}"
{{ end }}
{{ end }}
command: "'sh', '-c', 'export $(xargs </etc/env/.env) && npm run cron'"
envSecrets: ppd-env
imagePullSecrets:
- name: docker-pull-secret
image:
@ -35,10 +22,13 @@ lecoffreFront:
tls:
hosts:
- app.ppd.lecoffre.smart-chain.fr
secretName: app-tls
secretName: front-tls
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/from-to-www-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
env:
- key: .env
scwID: "id:a131edea-84e0-49d6-b4a8-20ab417220c9"

34
devops/prd.values.yaml Normal file
View File

@ -0,0 +1,34 @@
dockerPullSecret: docker-pull-secret
scwSecretKey: AgBG2y7uQuap+2akNPGFxpCR+l0INO6Wxez5qljtY6t71GFGhJLYN9ZfefflKcFzD2Nv7DQMXXhpnCCaFti+9JMCMDuN324dDgtMMLTot+Pkxk/bAm+L8t3HfRharFdLz/vvzg77bvypi28TEoNYR/AM0e8VMYxBEgEp2TmP5uXcxZOgPzXMrfQoSdNRyzGTJ5tXZwe3PP7XvXyTNsZzHBtoQQM+nul9nL+VFA7CBRaaOpCmKOXjAlt7TyNXo4X5eYBNlxr+NuQw4dh4E/1zqdU/dDCE1+vx88BDbdydBA1qJaTOUSGTFquSK4kb9qAVAexBAIUqRwpfEW6Li945AXtnxLN42gEGPRsA9tSXL2c20k6thuRCqxwEOZljq2E03qtLAkxdP6WFBcb77o4PIEMZ8AmzPASnI+eW5z2mCoP3L+HZQrTLliDjmF4AMtOfZxRi0CCTrsSabOrimJC6v3y3ve0VcSsjA3rd5vvJ3Va4mZK4JAtYwEUx4PCHCGkUxc0w6jRwKB5tL/auZVT4SV/0z/WgW4Kq4AdvxsU6yGOqflt6e3ePIIuvCgjw+1yOYRpUiSGj36oOqNPMA4smxIB7p7Gi3csqt2TrQoW3TaLv/s7gbCcxHWSor+WT71WGg8AVmLm+FzUINmNop+c2RNo3O/Gj7h1uybX/pj+tRLNOuBQCqa+GQkY2bT2NcT9ifnAZB6K+2zAWXl+tdbMlDGV89P2yMYuRMdHGhuOoyuIUPWeA5i0=
lecoffreFront:
serviceAccountName: lecoffre-front-sa
command: "'sh', '-c', 'export $(xargs </etc/env/.env) && npm run cron'"
envSecrets: prd-env
imagePullSecrets:
- name: docker-pull-secret
image:
pullPolicy: Always
repository: "rg.fr-par.scw.cloud/lecoffre/front"
resources:
requests:
cpu: 200m
memory: 1Gi
limits:
memory: 2Gi
ingress:
host: app.lecoffre.smart-chain.fr
tls:
hosts:
- app.lecoffre.smart-chain.fr
secretName: front-tls
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/from-to-www-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
env:
- key: .env
scwID: "id:a131edea-84e0-49d6-b4a8-20ab417220c9"

View File

@ -1,24 +1,11 @@
dockerPullSecret: secret/data/lecoffre-front-stg/config/dockerpullsecret
dockerPullSecret: docker-pull-secret
namespace: lecoffre
scwSecretKey: AgChoEnPitXp4Ny/rVMEcevaWKNVpyj2cJYAcq+yFqKwVwnLB+ffDvwqz9XBHu+6d4Nyyjkf37zUAMoaM21lEDWA7x3zfG2/D/j+rvX1qxzZgLD0mjBk7fGElVm332I6JA83oInes8AMMYEDPLElzHnpKRb9KtkIP4NzgOcCeW0ijft3N7Vroez6LEHsBPCA1I9XjKSkGEDvrO0MhWX3iJOlfz+SPMfJAV7rPawOs0ZmohTHrPW8qIvGDn8HCzKyU8zRBoMt+Ogpf5pH4U3JryEFuqD61KAQgablAM8edPIvsgNno9HAEuC2QtRLYA9aUhuKdaKuS58c9P2E80PHWXIlbpFCg6EugQTgNfnYp+3qDUNz8edeCfapYLvF4s9eCMGyMsGnpDR8EDNOyuGy7Y3l7okX8Xqu464gMp9E+hX7bHkcD6a4xfyIgJcWxsku0tm1TH1dpn4M1UXRuyZZif8P08nuE6MTUL67sAR9J1lpn4lVEL4kflk0pP2tZ5ncgPQFafJrRz05krMb0eU5tb2H4gs7ao/LL6idWo8MM9K1yr8lIuT5x2WW5CX+RjA+i50ex114V6vX3PNP5oVyt+DynTUB9QmXzVm3oLfDc3Cae1uqh7X0CFd+xiztJBtg0VtJaD/xUJcuWfY4cV2lERo9fRrykltzlJqiXHO4nowt8OtN0BcViVV8NJhPhYFzyb4ympxpOlTjm3GETuT2TYhUqdgS9nzleEAbOmOHZdIO2COunPE=
lecoffreFront:
serviceAccountName: lecoffre-front-sa
command: "'sh', '-c', '. /vault/secrets/envs && npm run start'"
vault:
role : custom_lecoffre-front_injector_rol
server: https://vault-stg.smart-chain.fr
annotations:
vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-inject-secret-envs: secret/data/lecoffre-front-stg/config/envs
vault.hashicorp.com/role: custom_lecoffre-front_injector_rol
vault.hashicorp.com/agent-inject-template-envs: |
{{ with secret "secret/data/lecoffre-front-stg/config/envs" }}
{{ range $k, $v := .Data.data }}
export {{ $k }}="{{ $v }}"
{{ end }}
{{ end }}
envSecrets: stg-env
command: "'sh', '-c', 'export $(xargs </etc/env/.env) && npm run start'"
imagePullSecrets:
- name: docker-pull-secret
image:
@ -35,10 +22,15 @@ lecoffreFront:
tls:
hosts:
- app.stg.lecoffre.smart-chain.fr
secretName: app-tls
secretName: front-tls
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/from-to-www-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
# key is name of the environment variable, scwID is the secret ID in SCW with "id:" in front
env:
- key: .env
scwID: "id:a131edea-84e0-49d6-b4a8-20ab417220c9"

View File

@ -0,0 +1 @@
### USE SECRET FROM BACK

View File

@ -3,7 +3,6 @@ apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: lecoffre-front
namespace: {{ .Values.namespace }}
{{if .Values.lecoffreFront.ingress.annotations}}
annotations:
{{toYaml .Values.lecoffreFront.ingress.annotations | indent 4 }}
@ -28,7 +27,6 @@ apiVersion: v1
kind: Service
metadata:
name: lecoffre-front-svc
namespace: {{ .Values.namespace }}
labels:
spec:
ports:
@ -42,7 +40,6 @@ apiVersion: apps/v1
kind: Deployment
metadata:
name: lecoffre-front
namespace: {{ .Values.namespace }}
labels:
app: lecoffre-front
spec:
@ -53,7 +50,6 @@ spec:
template:
metadata:
annotations:
{{toYaml .Values.lecoffreFront.vault.annotations | indent 8 }}
labels:
app: lecoffre-front
spec:
@ -62,10 +58,17 @@ spec:
- name: docker-pull-secret
containers:
- name: lecoffre-front
image: "{{ .Values.lecoffreFront.image.repository }}:v{{ .Chart.AppVersion }}"
image: "{{ .Values.lecoffreFront.image.repository }}:{{ .Values.lecoffreFront.image.tag }}"
{{if .Values.lecoffreFront.resources}}
resources:
{{toYaml .Values.lecoffreFront.resources | indent 10}}
{{end}}
imagePullPolicy: {{ .Values.lecoffreFront.image.pullPolicy }}
command: [{{ .Values.lecoffreFront.command }}]
command: [{{ .Values.lecoffreFront.command }}]
volumeMounts:
- name: secret-volume
mountPath: /etc/env
volumes:
- name: secret-volume
secret:
secretName: {{ .Values.lecoffreFront.envSecrets }}

View File

@ -0,0 +1 @@
## USE SEALED SECRET FROM BACK

View File

@ -0,0 +1 @@
## USE SECRET STORE FROM BACK

View File

@ -0,0 +1 @@
## same secret as back

View File

@ -1,29 +1,17 @@
dockerPullSecret: secret/data/lecoffre-front-stg/config/dockerpullsecret
dockerPullSecret: docker-pull-secret
namespace: lecoffre
scwSecretKey: ss
lecoffreFront:
serviceAccountName: lecoffre-front-sa
command: "'sh', '-c', '. /vault/secrets/envs && npm run start'"
vault:
role : custom_lecoffre-front_injector_rol
server: https://vault-stg.smart-chain.fr
annotations:
vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-inject-secret-envs: secret/data/lecoffre-front-stg/config/envs
vault.hashicorp.com/role: custom_lecoffre-front_injector_rol
vault.hashicorp.com/agent-inject-template-envs: |
{{ with secret "secret/data/lecoffre-front-stg/config/envs" }}
{{ range $k, $v := .Data.data }}
export {{ $k }}="{{ $v }}"
{{ end }}
{{ end }}
command: "npm run api:start"
envSecrets: env-env
imagePullSecrets:
- name: docker-pull-secret
image:
pullPolicy: Always
repository: "rg.fr-par.scw.cloud/lecoffre/front"
tag:
resources:
requests:
cpu: 200m
@ -31,14 +19,18 @@ lecoffreFront:
limits:
memory: 2Gi
ingress:
host: app.stg.lecoffre.smart-chain.fr
host: api.stg.lecoffre.smart-chain.fr
tls:
hosts:
- app.stg.lecoffre.smart-chain.fr
secretName: app-tls
- api.stg.lecoffre.smart-chain.fr
secretName: api-tls
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/from-to-www-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
env:
- key: a
scwID: b

View File

@ -4,16 +4,25 @@ const nextConfig = {
reactStrictMode: false,
publicRuntimeConfig: {
// Will be available on both server and client
BACK_API_PROTOCOL: process.env.BACK_API_PROTOCOL,
BACK_API_HOST: process.env.BACK_API_HOST,
BACK_API_PORT: process.env.BACK_API_PORT,
BACK_API_ROOT_URL: process.env.BACK_API_ROOT_URL,
BACK_API_VERSION: process.env.BACK_API_VERSION,
FRONT_APP_HOST: process.env.FRONT_APP_HOST,
FRONT_APP_PORT: process.env.FRONT_APP_PORT,
IDNOT_AUTHORIZE_ENDPOINT: process.env.IDNOT_AUTHORIZE_ENDPOINT,
IDNOT_CLIENT_ID: process.env.IDNOT_CLIENT_ID,
NEXT_PUBLIC_BACK_API_PROTOCOL: process.env.NEXT_PUBLIC_BACK_API_PROTOCOL,
NEXT_PUBLIC_BACK_API_HOST: process.env.NEXT_PUBLIC_BACK_API_HOST,
NEXT_PUBLIC_BACK_API_ROOT_URL: process.env.NEXT_PUBLIC_BACK_API_ROOT_URL,
NEXT_PUBLIC_BACK_API_VERSION: process.env.NEXT_PUBLIC_BACK_API_VERSION,
NEXT_PUBLIC_FRONT_APP_HOST: process.env.NEXT_PUBLIC_FRONT_APP_HOST,
NEXT_PUBLIC_FRONT_APP_PORT: process.env.NEXT_PUBLIC_FRONT_APP_PORT,
NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT: process.env.NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT,
NEXT_PUBLIC_IDNOT_CLIENT_ID: process.env.NEXT_PUBLIC_IDNOT_CLIENT_ID,
},
// webpack: config => {
// config.node = {
// fs: 'empty',
// child_process: 'empty',
// net: 'empty',
// dns: 'empty',
// tls: 'empty',
// };
// return config;
// },
};
module.exports = nextConfig;

5531
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,20 +18,23 @@
"@types/react-dom": "18.0.11",
"class-validator": "^0.14.0",
"classnames": "^2.3.2",
"crypto-random-string": "^5.0.0",
"dotenv": "^16.0.3",
"eslint": "8.36.0",
"eslint-config-next": "13.2.4",
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.18",
"form-data": "^4.0.0",
"jwt-decode": "^3.1.2",
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.73",
"next": "13.2.4",
"prettier": "^2.8.7",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-select": "^5.7.2",
"reflect-metadata": "^0.1.13",
"sass": "^1.59.2",
"typedi": "^0.10.0",
"sharp": "^0.32.1",
"typescript": "4.9.5"
}
},
"devDependencies": {}
}

View File

@ -0,0 +1,49 @@
import BaseApiService from "@Front/Api/BaseApiService";
export default class User extends BaseApiService {
private static instance: User;
private readonly baseURl = `${this.getBaseUrl()}/idnot/user`;
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new User();
} else {
return this.instance;
}
}
public async login(uid: string) {
const url = new URL(`${this.baseURl}/login/${uid}`);
try {
return await this.postRequest(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async verifyJwt(jwt: string) {
console.log(this.baseURl);
const url = new URL(`${this.baseURl}/verify-token/${jwt}`);
try {
return await this.postRequest(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async refreshToken(refreshToken: string): Promise<{ accessToken: string }> {
const url = new URL(`${this.baseURl}/refresh-token`);
try {
return await this.postRequest(url, {}, refreshToken);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -1,8 +1,6 @@
import { Service } from "typedi";
import BaseApiService from "@Front/Api/BaseApiService";
import { FrontendVariables } from "@Front/Config/VariablesFront";
@Service()
export default class Auth extends BaseApiService {
private static instance: Auth;
@ -16,8 +14,7 @@ export default class Auth extends BaseApiService {
public async getIdnotJwt(autorizationCode: string | string[]): Promise<any> {
const variables = FrontendVariables.getInstance();
const baseBackUrl =
variables.BACK_API_PROTOCOL + variables.BACK_API_HOST + (variables.BACK_API_PORT ? ":" + variables.BACK_API_PORT : "");
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<any>(url);

View File

@ -0,0 +1,38 @@
import BaseApiService from "@Front/Api/BaseApiService";
export default class Customer extends BaseApiService {
private static instance: Customer;
private readonly baseURl = this.getBaseUrl().concat("/france-connect/customer");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new Customer();
} else {
return this.instance;
}
}
public async login(email: string) {
const url = new URL(this.baseURl.concat("/login/").concat(email));
try {
return await this.postRequest(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async refreshToken(refreshToken: string): Promise<{ accessToken: string }> {
const url = new URL(this.baseURl.concat("/refresh-token"));
try {
return await this.postRequest(url, {}, refreshToken);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -1,4 +1,5 @@
import { FrontendVariables } from "@Front/Config/VariablesFront";
import CookieService from "@Front/Services/CookieService/CookieService";
export enum ContentType {
JSON = "application/json",
@ -10,23 +11,25 @@ export default abstract class BaseApiService {
protected constructor() {
BaseApiService.baseUrl ??=
FrontendVariables.getInstance().BACK_API_PROTOCOL +
FrontendVariables.getInstance().BACK_API_HOST +
(FrontendVariables.getInstance().BACK_API_PORT ? ":" + FrontendVariables.getInstance().BACK_API_PORT : "") +
FrontendVariables.getInstance().BACK_API_ROOT_URL +
FrontendVariables.getInstance().BACK_API_VERSION;
this.variables.BACK_API_PROTOCOL +
this.variables.BACK_API_HOST +
this.variables.BACK_API_ROOT_URL +
this.variables.BACK_API_VERSION;
}
protected getBaseUrl() {
protected getBaseUrl(): string {
return BaseApiService.baseUrl;
}
protected buildHeaders(contentType: ContentType) {
const token = CookieService.getInstance().getCookie("leCoffreAccessToken");
const headers = new Headers();
if (contentType === ContentType.JSON) {
headers.set("Content-Type", contentType);
}
headers.set("Authorization", `Bearer ${token}`);
return headers;
}
@ -34,7 +37,7 @@ export default abstract class BaseApiService {
return JSON.stringify(body);
}
protected async getRequest<T>(url: URL) {
protected async getRequest<T>(url: URL, token?: string) {
const request = async () =>
await fetch(url, {
method: "GET",
@ -43,7 +46,7 @@ export default abstract class BaseApiService {
return this.sendRequest<T>(request);
}
protected async postRequest<T>(url: URL, body: { [key: string]: unknown } = {}) {
protected async postRequest<T>(url: URL, body: { [key: string]: unknown } = {}, token?: string) {
return this.sendRequest<T>(
async () =>
await fetch(url, {
@ -54,7 +57,18 @@ export default abstract class BaseApiService {
);
}
protected async putRequest<T>(url: URL, body: { [key: string]: unknown } = {}) {
protected async postRequestFormData<T>(url: URL, body: FormData) {
return this.sendRequest<T>(
async () =>
await fetch(url, {
method: "POST",
headers: this.buildHeaders(ContentType.FORM_DATA),
body,
}),
);
}
protected async putRequest<T>(url: URL, body: { [key: string]: unknown } = {}, token?: string) {
const request = async () =>
await fetch(url, {
method: "PUT",
@ -76,7 +90,7 @@ export default abstract class BaseApiService {
return this.sendRequest<T>(request);
}
protected async deleteRequest<T>(url: URL, body: { [key: string]: unknown } = {}) {
protected async deleteRequest<T>(url: URL, body: { [key: string]: unknown } = {}, token?: string) {
const request = async () =>
await fetch(url, {
method: "DELETE",
@ -87,7 +101,7 @@ export default abstract class BaseApiService {
return this.sendRequest<T>(request);
}
protected async putFormDataRequest<T>(url: URL, body: FormData) {
protected async putFormDataRequest<T>(url: URL, body: FormData, token?: string) {
const request = async () =>
await fetch(url, {
method: "PUT",
@ -109,17 +123,18 @@ export default abstract class BaseApiService {
responseJson = await response.json();
} catch (err: unknown) {
responseJson = null;
return Promise.reject(err);
}
if (!response.ok) {
return Promise.reject(response);
return Promise.reject(responseJson);
}
return responseJson as T;
}
protected onError(error: unknown) {
console.error(error);
//console.error(error);
}
}

View File

@ -0,0 +1,16 @@
export interface IAppRule {
name: AppRuleNames;
action: AppRuleActions;
}
export enum AppRuleActions {
read = "GET",
create = "POST",
update = "PUT",
delete = "DELETE",
}
export enum AppRuleNames {
users = "users",
officeFolders = "folders",
}

View File

@ -0,0 +1,5 @@
import BaseApiService from "@Front/Api/BaseApiService";
export default abstract class BaseAdmin extends BaseApiService {
protected readonly namespaceUrl = this.getBaseUrl().concat("/admin");
}

View File

@ -0,0 +1,94 @@
import { DeedType } from "le-coffre-resources/dist/Admin";
import BaseAdmin from "../BaseAdmin";
export type IPutDeedTypesParams = {
uid?: DeedType["uid"];
name?: DeedType["name"];
description?: DeedType["description"];
deed?: DeedType["deed"];
office?: DeedType["office"];
archived_at?: DeedType["archived_at"];
document_types?: DeedType["document_types"];
};
export type IPostDeedTypesParams = {
name?: DeedType["name"];
description?: DeedType["description"];
};
export type IGetDeedTypesParams = {
where?: {};
include?: {};
select?: {};
};
export default class DeedTypes extends BaseAdmin {
private static instance: DeedTypes;
private readonly baseURl = this.namespaceUrl.concat("/deed-types");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new DeedTypes();
} else {
return this.instance;
}
}
public async get(q: IGetDeedTypesParams): Promise<DeedType[]> {
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<DeedType[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async getByUid(uid: string, q?: any): Promise<DeedType> {
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<DeedType>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async put(uid: string, body: IPutDeedTypesParams) {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest<DeedType>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async post(body: IPostDeedTypesParams) {
const url = new URL(this.baseURl);
try {
return await this.postRequest<DeedType>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async delete(uid: string) {
const url = new URL(this.baseURl);
try {
return await this.deleteRequest<DeedType>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,49 @@
import { Deed } from "le-coffre-resources/dist/Admin";
import BaseAdmin from "../BaseAdmin";
export type IGetDeedsParams = {
where?: {};
include?: {};
select?: {};
};
export default class Deeds extends BaseAdmin {
private static instance: Deeds;
private readonly baseURl = this.namespaceUrl.concat("/deeds");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new Deeds();
} else {
return this.instance;
}
}
public async get(q: IGetDeedsParams): Promise<Deed[]> {
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<Deed[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async getByUid(uid: string, q?: any): Promise<Deed> {
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<Deed>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,86 @@
import { DocumentType } from "le-coffre-resources/dist/Admin";
import BaseAdmin from "../BaseAdmin";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetDocumentTypesparams {
where?: {};
include?: {};
}
// TODO Type getbyuid query params
export type IPutDocumentTypesParams = {};
export interface IPostDocumentTypesParams {
name: string;
public_description: string;
private_description: string;
office: {
uid: string;
};
}
export default class DocumentTypes extends BaseAdmin {
private static instance: DocumentTypes;
private readonly baseURl = this.namespaceUrl.concat("/document-types");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
public async get(q: IGetDocumentTypesparams): Promise<DocumentType[]> {
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<DocumentType[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Create a Document
*/
public async post(body: DocumentType): Promise<DocumentType> {
const url = new URL(this.baseURl);
try {
return await this.postRequest<DocumentType>(url, body as any);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async getByUid(uid: string, q?: any): Promise<DocumentType> {
const url = new URL(this.baseURl.concat(`/${uid}`));
const query = { q };
if (q) Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
return await this.getRequest<DocumentType>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async put(uid: string, body: IPutDocumentTypesParams): Promise<DocumentType> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest<DocumentType>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,93 @@
import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document";
import { Document } from "le-coffre-resources/dist/SuperAdmin";
import BaseAdmin from "../BaseAdmin";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetDocumentsparams {
where?: {};
include?: {};
}
// TODO Type getbyuid query params
export type IPutDocumentsParams = {
document_status?: EDocumentStatus;
refused_reason?: string;
};
export interface IPostDocumentsParams {}
export default class Documents extends BaseAdmin {
private static instance: Documents;
private readonly baseURl = this.namespaceUrl.concat("/documents");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
public async get(q: IGetDocumentsparams): Promise<Document[]> {
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<Document[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Create a Document
*/
public async post(body: any): Promise<Document> {
const url = new URL(this.baseURl);
try {
return await this.postRequest<Document>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async getByUid(uid: string, q?: any): Promise<Document> {
const url = new URL(this.baseURl.concat(`/${uid}`));
const query = { q };
if (q) Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
return await this.getRequest<Document>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async put(uid: string, body: IPutDocumentsParams): Promise<Document> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest<Document>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async delete(uid: string): Promise<Document> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.deleteRequest<Document>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

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

View File

@ -0,0 +1,83 @@
import { Role } from "le-coffre-resources/dist/Admin";
import BaseAdmin from "../BaseAdmin";
export type IGetRolesParams = {
where?: {};
include?: {};
select?: {};
};
export type IPutRoleParams = {
uid: Role["uid"];
rules: Role["rules"];
};
export default class Roles extends BaseAdmin {
private static instance: Roles;
private readonly baseURl = this.namespaceUrl.concat("/roles");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new Roles();
} else {
return this.instance;
}
}
public async get(q?: IGetRolesParams): Promise<Role[]> {
const url = new URL(this.baseURl);
if (q) {
const query = { q };
Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
}
try {
return await this.getRequest<Role[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async getOne(q?: IGetRolesParams): Promise<Role | null> {
const url = new URL(this.baseURl);
if (q) {
const query = { q };
Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
}
try {
const res = await this.getRequest<Role[]>(url);
if (!res) return null;
if (res.length > 1) throw new Error("More than one role found");
return res[0] ? res[0] : null;
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async getByUid(uid: string, q?: any): Promise<Role> {
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<Role>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async put(uid: string, body: IPutRoleParams): Promise<Role> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest<Role>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,49 @@
import { Rule } from "le-coffre-resources/dist/Admin";
import BaseAdmin from "../BaseAdmin";
export type IGetRulesParams = {
where?: {};
include?: {};
select?: {};
};
export default class Rules extends BaseAdmin {
private static instance: Rules;
private readonly baseURl = this.namespaceUrl.concat("/rules");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new Rules();
} else {
return this.instance;
}
}
public async get(q: IGetRulesParams): Promise<Rule[]> {
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<Rule[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async getByUid(uid: string, q?: any): Promise<Rule> {
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<Rule>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,91 @@
import User from "le-coffre-resources/dist/SuperAdmin";
import BaseAdmin from "../BaseAdmin";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetUsersparams {
where?: {};
include?: {};
select?: {};
}
// TODO Type getbyuid query params
export type IPutUsersParams = {
uid?: User["uid"];
idNot?: User["idNot"];
contact?: User["contact"];
office_membership?: User["office_membership"];
documents?: User["documents"];
};
export default class Users extends BaseAdmin {
private static instance: Users;
private readonly baseURl = this.namespaceUrl.concat("/users");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
/**
* @description : Get all Users
*/
public async get(q: IGetUsersparams): Promise<User[]> {
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<User[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Get a folder by uid
*/
public async getByUid(uid: string, q?: any): Promise<User> {
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<User>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Create a User
*/
// public async post(body: IPostDeedsParams): Promise<OfficeFolder> {
// const url = new URL(this.baseURl);
// try {
// return await this.postRequest<OfficeFolder>(url, body);
// } catch (err) {
// this.onError(err);
// return Promise.reject(err);
// }
// }
/**
* @description : Update the folder description
*/
public async put(uid: string, body: IPutUsersParams): Promise<User> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest<User>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -1,5 +1,5 @@
import BaseApiService from "@Front/Api/BaseApiService";
export default abstract class BaseNotary extends BaseApiService {
protected readonly namespaceUrl = this.getBaseUrl().concat("/customers");
protected readonly namespaceUrl = this.getBaseUrl().concat("/customer");
}

View File

@ -0,0 +1,67 @@
import Customer, { Contact } from "le-coffre-resources/dist/Customer";
import BaseCustomer from "../BaseCustomer";
import { ECivility } from "le-coffre-resources/dist/Customer/Contact";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetCustomersparams {
where?: {};
include?: {};
}
// TODO Type getbyuid query params
export type IPutCustomersParams = {
uid?: Customer["uid"];
contact?: Customer["contact"];
};
export interface IPostCustomersParams {
first_name: string;
last_name: string;
email: string;
cell_phone_number: string;
civility: ECivility;
address?: Contact["address"];
}
export default class Customers extends BaseCustomer {
private static instance: Customers;
private readonly baseURl = this.namespaceUrl.concat("/customers");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
public async get(q: IGetCustomersparams): Promise<Customer[]> {
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<Customer[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async getByUid(uid: string, q?: any): Promise<Customer> {
const url = new URL(this.baseURl.concat(`/${uid}`));
const query = { q };
if (q) Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
return await this.getRequest<Customer>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,93 @@
import { Document } from "le-coffre-resources/dist/Customer";
import BaseCustomer from "../BaseCustomer";
import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetDocumentsparams {
where?: {};
include?: {};
}
// TODO Type getbyuid query params
export type IPutDocumentsParams = {
document_status?: EDocumentStatus;
refused_reason?: string;
};
export interface IPostDocumentsParams {}
export default class Documents extends BaseCustomer {
private static instance: Documents;
private readonly baseURl = this.namespaceUrl.concat("/documents");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
public async get(q: IGetDocumentsparams): Promise<Document[]> {
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<Document[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Create a Document
*/
public async post(body: any): Promise<Document> {
const url = new URL(this.baseURl);
try {
return await this.postRequest<Document>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async getByUid(uid: string, q?: any): Promise<Document> {
const url = new URL(this.baseURl.concat(`/${uid}`));
const query = { q };
if (q) Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
return await this.getRequest<Document>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async put(uid: string, body: IPutDocumentsParams): Promise<Document> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest<Document>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async delete(uid: string): Promise<Document> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.deleteRequest<Document>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,93 @@
import { File } from "le-coffre-resources/dist/Customer";
import BaseCustomer from "../BaseCustomer";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetFilesparams {
where?: {};
include?: {};
}
// TODO Type getbyuid query params
export type IPutFilesParams = {};
export interface IPostFilesParams {}
export default class Files extends BaseCustomer {
private static instance: Files;
private readonly baseURl = this.namespaceUrl.concat("/files");
private constructor() {
super();
}
public static getInstance() {
return (this.instance ??= new this());
}
public async get(q: IGetFilesparams): Promise<File[]> {
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 {
const files = await this.getRequest<File[]>(url);
return files;
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Create a File
*/
public async post(body: any): Promise<File> {
const url = new URL(this.baseURl);
try {
return await this.postRequestFormData<File>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public getUploadLink(uid: string): string {
return this.baseURl.concat(`/download/${uid}`);
}
public async getByUid(uid: string, q?: any): Promise<File> {
const url = new URL(this.baseURl.concat(`/${uid}`));
const query = { q };
if (q) Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
const file = await this.getRequest<File>(url);
return file;
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async put(uid: string, body: IPutFilesParams): Promise<File> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest<File>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Delete a folder only if the folder don't contains customers
*/
public async delete(uid: string): Promise<File> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.deleteRequest<File>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,57 @@
import { type OfficeFolder } from "le-coffre-resources/dist/Customer";
import BaseCustomer from "../BaseCustomer";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetFoldersParams {
q?: {
select?: {};
where?: {};
include?: {};
};
}
export default class Folders extends BaseCustomer {
private static instance: Folders;
private readonly baseURl = this.namespaceUrl.concat("/folders");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
/**
* @description : Get all folders
*/
public async get(q: IGetFoldersParams): Promise<OfficeFolder[]> {
const url = new URL(this.baseURl);
Object.entries(q).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
return await this.getRequest<OfficeFolder[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Get a folder by uid
*/
public async getByUid(uid: string, q?: any): Promise<OfficeFolder> {
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<OfficeFolder>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -1,8 +1,6 @@
import { Service } from "typedi";
import BaseNotary from "../BaseCustomer";
import User from "le-coffre-resources/dist/Notary";
@Service()
export default class Users extends BaseNotary {
private static instance: Users;
private readonly baseURl = this.namespaceUrl.concat("/Users");

View File

@ -0,0 +1,90 @@
import { Contact, Customer } from "le-coffre-resources/dist/Notary";
import BaseNotary from "../BaseNotary";
import { ECivility } from "le-coffre-resources/dist/Customer/Contact";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetCustomersparams {
where?: {};
include?: {};
}
// TODO Type getbyuid query params
export type IPutCustomersParams = {
uid?: Customer["uid"];
contact?: Customer["contact"];
};
export interface IPostCustomersParams {
first_name: string;
last_name: string;
email: string;
cell_phone_number: string;
civility: ECivility;
address?: Contact["address"];
}
export default class Customers extends BaseNotary {
private static instance: Customers;
private readonly baseURl = this.namespaceUrl.concat("/customers");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
public async get(q: IGetCustomersparams): Promise<Customer[]> {
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<Customer[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Create a Customer
*/
public async post(body: any): Promise<Customer> {
const url = new URL(this.baseURl);
try {
return await this.postRequest<Customer>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async getByUid(uid: string, q?: any): Promise<Customer> {
const url = new URL(this.baseURl.concat(`/${uid}`));
const query = { q };
if (q) Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
return await this.getRequest<Customer>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async put(uid: string, body: IPutCustomersParams): Promise<Customer> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest<Customer>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,84 @@
import { DeedType } from "le-coffre-resources/dist/Notary";
import BaseNotary from "../BaseNotary";
export type IPutDeedTypesParams = {
uid?: DeedType["uid"];
name?: DeedType["name"];
description?: DeedType["description"];
deed?: DeedType["deed"];
office?: DeedType["office"];
archived_at?: DeedType["archived_at"];
document_types?: DeedType["document_types"];
};
export type IPostDeedTypesParams = {
name?: DeedType["name"];
description?: DeedType["description"];
};
export type IGetDeedTypesParams = {
where?: {};
include?: {};
select?: {};
};
export default class DeedTypes extends BaseNotary {
private static instance: DeedTypes;
private readonly baseURl = this.namespaceUrl.concat("/deed-types");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new DeedTypes();
} else {
return this.instance;
}
}
public async get(q?: IGetDeedTypesParams): Promise<DeedType[]> {
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<DeedType[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async getByUid(uid: string, q?: any): Promise<DeedType> {
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<DeedType>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async put(uid: string, body: IPutDeedTypesParams) {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest<DeedType>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async post(body: IPostDeedTypesParams) {
const url = new URL(this.baseURl);
try {
return await this.postRequest<DeedType>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,72 @@
import { Deed, OfficeFolder } from "le-coffre-resources/dist/Notary";
import BaseAdmin from "../BaseNotary";
export type IGetDeedsParams = {
where?: {};
include?: {};
select?: {};
};
export type IPutDeedsParams = {
uid?: OfficeFolder["uid"];
folder_number?: OfficeFolder["folder_number"];
name?: OfficeFolder["name"];
description?: OfficeFolder["description"];
archived_description?: OfficeFolder["archived_description"];
status?: OfficeFolder["status"];
document_types?: Deed["document_types"];
};
export default class Deeds extends BaseAdmin {
private static instance: Deeds;
private readonly baseURl = this.namespaceUrl.concat("/deeds");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new Deeds();
} else {
return this.instance;
}
}
public async get(q: IGetDeedsParams): Promise<Deed[]> {
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<Deed[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async getByUid(uid: string, q?: any): Promise<Deed> {
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<Deed>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Update the folder description
*/
public async put(uid: string, body: IPutDeedsParams): Promise<Deed> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest<Deed>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,86 @@
import { DocumentType } from "le-coffre-resources/dist/Notary";
import BaseNotary from "../BaseNotary";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetDocumentTypesparams {
where?: {};
include?: {};
}
// TODO Type getbyuid query params
export type IPutDocumentTypesParams = {};
export interface IPostDocumentTypesParams {
name: string;
public_description: string;
private_description: string | null;
office?: {
uid?: string;
};
}
export default class DocumentTypes extends BaseNotary {
private static instance: DocumentTypes;
private readonly baseURl = this.namespaceUrl.concat("/document-types");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
public async get(q: IGetDocumentTypesparams): Promise<DocumentType[]> {
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<DocumentType[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Create a Document
*/
public async post(body: IPostDocumentTypesParams): Promise<DocumentType> {
const url = new URL(this.baseURl);
try {
return await this.postRequest<DocumentType>(url, body as any);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async getByUid(uid: string, q?: any): Promise<DocumentType> {
const url = new URL(this.baseURl.concat(`/${uid}`));
const query = { q };
if (q) Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
return await this.getRequest<DocumentType>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async put(uid: string, body: IPutDocumentTypesParams): Promise<DocumentType> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest<DocumentType>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,93 @@
import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document";
import { Document } from "le-coffre-resources/dist/Notary";
import BaseNotary from "../BaseNotary";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetDocumentsparams {
where?: {};
include?: {};
}
// TODO Type getbyuid query params
export type IPutDocumentsParams = {
document_status?: EDocumentStatus;
refused_reason?: string;
};
export interface IPostDocumentsParams {}
export default class Documents extends BaseNotary {
private static instance: Documents;
private readonly baseURl = this.namespaceUrl.concat("/documents");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
public async get(q: IGetDocumentsparams): Promise<Document[]> {
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<Document[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Create a Document
*/
public async post(body: any): Promise<Document> {
const url = new URL(this.baseURl);
try {
return await this.postRequest<Document>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async getByUid(uid: string, q?: any): Promise<Document> {
const url = new URL(this.baseURl.concat(`/${uid}`));
const query = { q };
if (q) Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
return await this.getRequest<Document>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async put(uid: string, body: IPutDocumentsParams): Promise<Document> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest<Document>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async delete(uid: string): Promise<Document> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.deleteRequest<Document>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,119 @@
import { type OfficeFolder } from "le-coffre-resources/dist/Notary";
import BaseNotary from "../BaseNotary";
import EFolderStatus from "le-coffre-resources/dist/Customer/EFolderStatus";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetFoldersParams {
q?: {
select?: {};
where?: {};
include?: {};
};
}
export default class Folders extends BaseNotary {
private static instance: Folders;
private readonly baseURl = this.namespaceUrl.concat("/folders");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
/**
* @description : Get all folders
*/
public async get(q: IGetFoldersParams): Promise<OfficeFolder[]> {
const url = new URL(this.baseURl);
Object.entries(q).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
return await this.getRequest<OfficeFolder[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Get a folder by uid
*/
public async getByUid(uid: string, q?: any): Promise<OfficeFolder> {
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<OfficeFolder>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Create a folder
*/
public async post(officeFolder: Partial<OfficeFolder>): Promise<OfficeFolder> {
const url = new URL(this.baseURl);
try {
return await this.postRequest(url, officeFolder);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Update the folder description
*/
public async put(uid: string, body: Partial<OfficeFolder>): Promise<OfficeFolder> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Delete a folder only if the folder don't contains customers
*/
public async delete(uid: string): Promise<OfficeFolder> {
const url = new URL(this.baseURl.concat(`/${uid}`));
const targetedFolder = await this.getByUid(uid);
if (targetedFolder.customers) return Promise.reject(`The folder ${uid} contains customers`);
try {
return await this.deleteRequest(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async archive(uid: string, body: Partial<OfficeFolder>): Promise<OfficeFolder> {
body.status = EFolderStatus.ARCHIVED;
try {
return await this.put(uid, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async restore(uid: string, body: Partial<OfficeFolder>): Promise<OfficeFolder> {
body.status = EFolderStatus.LIVE;
try {
return await this.put(uid, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -1,11 +1,15 @@
import { Service } from "typedi";
import BaseNotary from "../BaseNotary";
import User from "le-coffre-resources/dist/Notary";
@Service()
export type IGetUsersParams = {
where?: {};
include?: {};
select?: {};
};
export default class Users extends BaseNotary {
private static instance: Users;
private readonly baseURl = this.namespaceUrl.concat("/Users");
private readonly baseURl = this.namespaceUrl.concat("/users");
private constructor() {
super();
@ -19,8 +23,10 @@ export default class Users extends BaseNotary {
}
}
public async get(): Promise<User[]> {
public async get(q?: IGetUsersParams): Promise<User[]> {
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<User[]>(url);
} catch (err) {
@ -29,23 +35,9 @@ export default class Users extends BaseNotary {
}
}
public async getAuthorizationCode(): Promise<any> {
try {
const url = new URL(`https://qual-connexion.idnot.fr/IdPOAuth2/authorize/idnot_idp_v1?
client_id=4501646203F3EF67
&redirect_uri=https://app.stg.lecoffre.smart-chain.fr/
&scope=openid,profile,offline_access
&response_type=code`);
// const url = new URL("https://jsonplaceholder.typicode.com/todos/1");
await this.getRequest(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async getByUid(uid: string): Promise<User> {
public async getByUid(uid: string, q?: any): Promise<User> {
const url = new URL(this.baseURl.concat("/").concat(uid));
if (q) Object.entries(q).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
return await this.getRequest<User>(url);
} catch (err) {
@ -53,14 +45,4 @@ export default class Users extends BaseNotary {
return Promise.reject(err);
}
}
// public async post(params: User): Promise<User> {
// const url = new URL(this.baseURl);
// try {
// return await this.postRequest<User>(url, params);
// } catch (err) {
// this.onError(err);
// return Promise.reject(err);
// }
// }
}

View File

@ -0,0 +1,90 @@
import { Contact, Customer } from "le-coffre-resources/dist/SuperAdmin";
import BaseSuperAdmin from "../BaseSuperAdmin";
import { ECivility } from "le-coffre-resources/dist/Customer/Contact";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetCustomersparams {
where?: {};
include?: {};
}
// TODO Type getbyuid query params
export type IPutCustomersParams = {
uid?: Customer["uid"];
contact?: Customer["contact"];
};
export interface IPostCustomersParams {
first_name: string;
last_name: string;
email: string;
cell_phone_number: string;
civility: ECivility;
address?: Contact["address"];
}
export default class Customers extends BaseSuperAdmin {
private static instance: Customers;
private readonly baseURl = this.namespaceUrl.concat("/customers");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
public async get(q: IGetCustomersparams): Promise<Customer[]> {
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<Customer[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Create a Customer
*/
public async post(body: any): Promise<Customer> {
const url = new URL(this.baseURl);
try {
return await this.postRequest<Customer>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async getByUid(uid: string, q?: any): Promise<Customer> {
const url = new URL(this.baseURl.concat(`/${uid}`));
const query = { q };
if (q) Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
return await this.getRequest<Customer>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async put(uid: string, body: IPutCustomersParams): Promise<Customer> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest<Customer>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,92 @@
import { DeedType } from "le-coffre-resources/dist/Notary";
import BaseSuperAdmin from "../BaseSuperAdmin";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetDeedTypesParams {
q?: {};
}
// TODO Type getbyuid query params
export type IPutDeedTypesParams = {
uid?: DeedType["uid"];
name?: DeedType["name"];
description?: DeedType["description"];
deed?: DeedType["deed"];
office?: DeedType["office"];
archived_at?: DeedType["archived_at"];
document_types?: DeedType["document_types"];
};
export default class DeedTypes extends BaseSuperAdmin {
private static instance: DeedTypes;
private readonly baseURl = this.namespaceUrl.concat("/deed-types");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
/**
* @description : Get all DeedTypes
*/
public async get(q?: IGetDeedTypesParams): Promise<DeedType[]> {
const url = new URL(this.baseURl);
if(q){
Object.entries(q).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
}
try {
return await this.getRequest<DeedType[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Get a folder by uid
*/
public async getByUid(uid: string, q?: any): Promise<DeedType> {
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<DeedType>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Create a deed
*/
// public async post(body: IPostDeedTypesParams): Promise<OfficeFolder> {
// const url = new URL(this.baseURl);
// try {
// return await this.postRequest<OfficeFolder>(url, body);
// } catch (err) {
// this.onError(err);
// return Promise.reject(err);
// }
// }
/**
* @description : Update the folder description
*/
public async put(uid: string, body: IPutDeedTypesParams): Promise<DeedType> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest<DeedType>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,90 @@
import { Deed, OfficeFolder } from "le-coffre-resources/dist/Notary";
import BaseSuperAdmin from "../BaseSuperAdmin";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetDeedsParams {
q?: {};
}
// TODO Type getbyuid query params
export type IPutDeedsParams = {
uid?: OfficeFolder["uid"];
folder_number?: OfficeFolder["folder_number"];
name?: OfficeFolder["name"];
description?: OfficeFolder["description"];
archived_description?: OfficeFolder["archived_description"];
status?: OfficeFolder["status"];
document_types?: Deed["document_types"];
};
export default class Deeds extends BaseSuperAdmin {
private static instance: Deeds;
private readonly baseURl = this.namespaceUrl.concat("/deeds");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
/**
* @description : Get all deeds
*/
public async get(q: IGetDeedsParams): Promise<Deed[]> {
const url = new URL(this.baseURl);
Object.entries(q).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
return await this.getRequest<Deed[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Get a folder by uid
*/
public async getByUid(uid: string, q?: any): Promise<Deed> {
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<Deed>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Create a deed
*/
// public async post(body: IPostDeedsParams): Promise<OfficeFolder> {
// const url = new URL(this.baseURl);
// try {
// return await this.postRequest<OfficeFolder>(url, body);
// } catch (err) {
// this.onError(err);
// return Promise.reject(err);
// }
// }
/**
* @description : Update the folder description
*/
public async put(uid: string, body: IPutDeedsParams): Promise<Deed> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest<Deed>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,88 @@
import { DocumentType } from "le-coffre-resources/dist/SuperAdmin";
import BaseSuperAdmin from "../BaseSuperAdmin";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetDocumentTypesparams {
where?: {};
include?: {};
}
// TODO Type getbyuid query params
export type IPutDocumentTypesParams = {};
export interface IPostDocumentTypesParams {
name: string;
public_description: string;
private_description: string;
office: {
uid: string;
};
}
export default class DocumentTypes extends BaseSuperAdmin {
private static instance: DocumentTypes;
private readonly baseURl = this.namespaceUrl.concat("/document-types");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
public async get(q?: IGetDocumentTypesparams): Promise<DocumentType[]> {
const url = new URL(this.baseURl);
if (q) {
const query = { q };
if (q) Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
}
try {
return await this.getRequest<DocumentType[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Create a Document
*/
public async post(body: IPostDocumentTypesParams): Promise<DocumentType> {
const url = new URL(this.baseURl);
try {
return await this.postRequest<DocumentType>(url, body as any);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async getByUid(uid: string, q?: any): Promise<DocumentType> {
const url = new URL(this.baseURl.concat(`/${uid}`));
const query = { q };
if (q) Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
return await this.getRequest<DocumentType>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async put(uid: string, body: IPutDocumentTypesParams): Promise<DocumentType> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest<DocumentType>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,93 @@
import { Document } from "le-coffre-resources/dist/SuperAdmin";
import BaseSuperAdmin from "../BaseSuperAdmin";
import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetDocumentsparams {
where?: {};
include?: {};
}
// TODO Type getbyuid query params
export type IPutDocumentsParams = {
document_status?: EDocumentStatus;
refused_reason?: string;
};
export interface IPostDocumentsParams {}
export default class Documents extends BaseSuperAdmin {
private static instance: Documents;
private readonly baseURl = this.namespaceUrl.concat("/documents");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
public async get(q: IGetDocumentsparams): Promise<Document[]> {
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<Document[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Create a Document
*/
public async post(body: any): Promise<Document> {
const url = new URL(this.baseURl);
try {
return await this.postRequest<Document>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async getByUid(uid: string, q?: any): Promise<Document> {
const url = new URL(this.baseURl.concat(`/${uid}`));
const query = { q };
if (q) Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
return await this.getRequest<Document>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async put(uid: string, body: IPutDocumentsParams): Promise<Document> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest<Document>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async delete(uid: string): Promise<Document> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.deleteRequest<Document>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,93 @@
import { File } from "le-coffre-resources/dist/SuperAdmin";
import BaseSuperAdmin from "../BaseSuperAdmin";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetFilesparams {
where?: {};
include?: {};
}
// TODO Type getbyuid query params
export type IPutFilesParams = {};
export interface IPostFilesParams {}
export default class Files extends BaseSuperAdmin {
private static instance: Files;
private readonly baseURl = this.namespaceUrl.concat("/files");
private constructor() {
super();
}
public static getInstance() {
return (this.instance ??= new this());
}
public async get(q: IGetFilesparams): Promise<File[]> {
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 {
const files = await this.getRequest<File[]>(url);
return files;
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Create a File
*/
public async post(body: any): Promise<File> {
const url = new URL(this.baseURl);
try {
return await this.postRequestFormData<File>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public getUploadLink(uid: string): string {
return this.baseURl.concat(`/download/${uid}`);
}
public async getByUid(uid: string, q?: any): Promise<File> {
const url = new URL(this.baseURl.concat(`/${uid}`));
const query = { q };
if (q) Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
const file = await this.getRequest<File>(url);
return file;
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async put(uid: string, body: IPutFilesParams): Promise<File> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest<File>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Delete a folder only if the folder don't contains customers
*/
public async delete(uid: string): Promise<File> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.deleteRequest<File>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,119 @@
import { type OfficeFolder } from "le-coffre-resources/dist/Notary";
import BaseSuperAdmin from "../BaseSuperAdmin";
import EFolderStatus from "le-coffre-resources/dist/Customer/EFolderStatus";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetFoldersParams {
q?: {
select?: {};
where?: {};
include?: {};
};
}
export default class Folders extends BaseSuperAdmin {
private static instance: Folders;
private readonly baseURl = this.namespaceUrl.concat("/folders");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
/**
* @description : Get all folders
*/
public async get(q: IGetFoldersParams): Promise<OfficeFolder[]> {
const url = new URL(this.baseURl);
Object.entries(q).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
try {
return await this.getRequest<OfficeFolder[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Get a folder by uid
*/
public async getByUid(uid: string, q?: any): Promise<OfficeFolder> {
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<OfficeFolder>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Create a folder
*/
public async post(officeFolder: Partial<OfficeFolder>): Promise<OfficeFolder> {
const url = new URL(this.baseURl);
try {
return await this.postRequest(url, officeFolder);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Update the folder description
*/
public async put(uid: string, body: Partial<OfficeFolder>): Promise<OfficeFolder> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Delete a folder only if the folder don't contains customers
*/
public async delete(uid: string): Promise<OfficeFolder> {
const url = new URL(this.baseURl.concat(`/${uid}`));
const targetedFolder = await this.getByUid(uid);
if (targetedFolder.customers) return Promise.reject(`The folder ${uid} contains customers`);
try {
return await this.deleteRequest(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async archive(uid: string, body: Partial<OfficeFolder>): Promise<OfficeFolder> {
body.status = EFolderStatus.ARCHIVED;
try {
return await this.put(uid, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async restore(uid: string, body: Partial<OfficeFolder>): Promise<OfficeFolder> {
body.status = EFolderStatus.LIVE;
try {
return await this.put(uid, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,49 @@
import { Appointment } from "le-coffre-resources/dist/SuperAdmin";
import BaseSuperAdmin from "../BaseSuperAdmin";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetLiveVotessparams {
where?: {};
include?: {};
select?: {};
}
export type IPostLiveVotesParams = {
appointment: Appointment;
};
export type LiveVote = {
uid: string;
appointment: Appointment;
};
export default class LiveVotes extends BaseSuperAdmin {
private static instance: LiveVotes;
private readonly baseURl = this.namespaceUrl.concat("/live-votes");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
/**
* @description : Create a LiveVotes
*/
public async post(body: IPostLiveVotesParams): Promise<LiveVote> {
const url = new URL(this.baseURl);
try {
return await this.postRequest<LiveVote>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,53 @@
import { Office } from "le-coffre-resources/dist/SuperAdmin";
import BaseSuperAdmin from "../BaseSuperAdmin";
export interface IGetOfficesparams {
where?: {};
include?: {};
select?: {};
}
export default class Offices extends BaseSuperAdmin {
private static instance: Offices;
private readonly baseURl = this.namespaceUrl.concat("/offices");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
public async get(q?: IGetOfficesparams): Promise<Office[]> {
const url = new URL(this.baseURl);
if (q) {
const query = { q };
Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
}
try {
return await this.getRequest<Office[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
/**
* @description : Get a folder by uid
*/
public async getByUid(uid: string, q?: any): Promise<Office> {
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<Office>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -1,11 +1,26 @@
import { Service } from "typedi";
import User from "le-coffre-resources/dist/Notary";
import User from "le-coffre-resources/dist/SuperAdmin";
import BaseSuperAdmin from "../BaseSuperAdmin";
@Service()
// TODO Type get query params -> Where + inclue + orderby
export interface IGetUsersparams {
where?:{},
include?:{},
select?:{},
}
// TODO Type getbyuid query params
export type IPutUsersParams = {
uid?: User["uid"];
idNot?: User["idNot"];
contact?: User["contact"];
office_membership?: User["office_membership"];
documents?: User["documents"];
};
export default class Users extends BaseSuperAdmin {
private static instance: Users;
private readonly baseURl = this.namespaceUrl.concat("/Users");
private readonly baseURl = this.namespaceUrl.concat("/users");
private constructor() {
super();
@ -19,8 +34,13 @@ export default class Users extends BaseSuperAdmin {
}
}
public async get(): Promise<User[]> {
/**
* @description : Get all Users
*/
public async get(q: IGetUsersparams): Promise<User[]> {
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<User[]>(url);
} catch (err) {
@ -29,8 +49,12 @@ export default class Users extends BaseSuperAdmin {
}
}
public async getByUid(uid: string): Promise<User> {
const url = new URL(this.baseURl.concat("/").concat(uid));
/**
* @description : Get a folder by uid
*/
public async getByUid(uid: string, q?: any): Promise<User> {
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<User>(url);
} catch (err) {
@ -38,4 +62,30 @@ export default class Users extends BaseSuperAdmin {
return Promise.reject(err);
}
}
/**
* @description : Create a User
*/
// public async post(body: IPostDeedsParams): Promise<OfficeFolder> {
// const url = new URL(this.baseURl);
// try {
// return await this.postRequest<OfficeFolder>(url, body);
// } catch (err) {
// this.onError(err);
// return Promise.reject(err);
// }
// }
/**
* @description : Update the folder description
*/
public async put(uid: string, body: IPutUsersParams): Promise<User> {
const url = new URL(this.baseURl.concat(`/${uid}`));
try {
return await this.putRequest<User>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,44 @@
import { Vote } from "le-coffre-resources/dist/SuperAdmin";
import BaseSuperAdmin from "../BaseSuperAdmin";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetVotessparams {
where?: {};
include?: {};
select?: {};
}
export type IDeleteVotesParams = {
uid: Vote["uid"];
};
export default class Votes extends BaseSuperAdmin {
private static instance: Votes;
private readonly baseURl = this.namespaceUrl.concat("/votes");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
/**
* @description : Create a Votes
*/
public async delete(body: IDeleteVotesParams): Promise<Vote> {
const url = new URL(this.baseURl + "/" + body.uid);
try {
return await this.deleteRequest<Vote>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 9H11C9.89543 9 9 9.89543 9 11V20C9 21.1046 9.89543 22 11 22H20C21.1046 22 22 21.1046 22 20V11C22 9.89543 21.1046 9 20 9Z" stroke="#939393" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 15H4C3.46957 15 2.96086 14.7893 2.58579 14.4142C2.21071 14.0391 2 13.5304 2 13V4C2 3.46957 2.21071 2.96086 2.58579 2.58579C2.96086 2.21071 3.46957 2 4 2H13C13.5304 2 14.0391 2.21071 14.4142 2.58579C14.7893 2.96086 15 3.46957 15 4V5" stroke="#939393" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 649 B

View File

@ -0,0 +1,6 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.5 2.5H7.5C6.83696 2.5 6.20107 2.76339 5.73223 3.23223C5.26339 3.70107 5 4.33696 5 5V25C5 25.663 5.26339 26.2989 5.73223 26.7678C6.20107 27.2366 6.83696 27.5 7.5 27.5H22.5C23.163 27.5 23.7989 27.2366 24.2678 26.7678C24.7366 26.2989 25 25.663 25 25V10L17.5 2.5Z" stroke="#320756" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15 22.5V15" stroke="#C5B2D4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.25 18.75H18.75" stroke="#C5B2D4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.5 2.5V10H25" stroke="#320756" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 786 B

View File

@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32 16C32 24.8366 24.8366 32 16 32C7.16344 32 0 24.8366 0 16C0 7.16344 7.16344 0 16 0C24.8366 0 32 7.16344 32 16ZM4 16C4 22.6274 9.37258 28 16 28C22.6274 28 28 22.6274 28 16C28 9.37258 22.6274 4 16 4C9.37258 4 4 9.37258 4 16Z" fill="#3FA79E"/>
<path d="M22 12L13.75 20.5714L10 16.6753" stroke="#3FA79E" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 483 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.66667 4V5.33333H3.33333V12.6667H10.6667V9.33333H12V13.3333C12 13.7015 11.7015 14 11.3333 14H2.66667C2.29848 14 2 13.7015 2 13.3333V4.66667C2 4.29848 2.29848 4 2.66667 4H6.66667ZM14 2V7.33333H12.6667V4.27533L7.47133 9.47133L6.52867 8.52867L11.7233 3.33333H8.66667V2H14Z" fill="#000091"/>
</svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L8.375 16L4 11.4545" stroke="#12BF4D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 221 B

View File

@ -0,0 +1,10 @@
<svg width="38" height="34" viewBox="0 0 38 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.2604 14.8265C16.2604 13.6968 17.0829 12.7432 18.211 12.4341C17.7974 12.1931 17.5221 11.7716 17.5221 11.2907C17.5221 10.5411 18.1883 9.93368 19.0101 9.93368C19.8319 9.93368 20.498 10.5411 20.498 11.2907C20.498 11.7738 20.2202 12.197 19.803 12.4374C20.9241 12.7506 21.7393 13.7013 21.7393 14.8265C21.7393 16.2065 16.2604 16.2065 16.2604 14.8265ZM18.9992 1.28762C21.0096 1.28762 22.6765 2.80752 22.6765 4.64138V5.67479C22.1825 5.52664 21.6553 5.44538 21.1071 5.44538C18.9568 5.44538 17.1281 6.68091 16.4147 8.41682C16.1197 8.12162 15.7459 7.89281 15.3226 7.76244V4.64138C15.3226 2.80752 16.9882 1.28762 18.9992 1.28762ZM26.0998 10.4523C26.117 10.3105 26.1232 10.1652 26.1232 10.0205C26.1232 8.51198 25.3232 7.17483 24.0895 6.3416V4.64138C24.0895 2.08198 21.8057 -0.000185013 18.9994 -0.000185013C16.1936 -0.000185013 13.9103 2.08198 13.9103 4.64138V7.70774C12.7737 7.95494 11.9277 8.88443 11.9277 9.99764C11.9277 10.1516 11.9453 10.3006 11.9775 10.4465C10.0735 10.7329 8.61719 12.2297 8.61719 14.0451C8.61719 16.0685 10.4129 17.7051 12.6299 17.7051H25.3702C27.5873 17.7051 29.3828 16.0685 29.3828 14.0451C29.3836 12.2521 27.9664 10.767 26.0998 10.4523Z" fill="white"/>
<path d="M29.3828 22.3568C29.3828 22.6621 24.7343 22.9102 18.9998 22.9102C13.2662 22.9102 8.61719 22.6621 8.61719 22.3568C8.61719 22.051 13.2662 21.8036 18.9998 21.8036C24.7343 21.8036 29.3828 22.051 29.3828 22.3568Z" fill="white"/>
<path d="M1.02349 27.963C0.886712 27.963 0.775312 27.9655 0.689023 27.9704C0.602334 27.9754 0.539421 27.9812 0.499883 27.9876C0.330512 28.0335 0.246094 28.1535 0.246094 28.347V33.5719C0.246094 33.7656 0.332516 33.8871 0.505226 33.9362C0.555717 33.9428 0.623973 33.9488 0.710529 33.9536C0.796817 33.9585 0.901138 33.9609 1.02349 33.9609C1.30426 33.9609 1.44478 33.8313 1.44478 33.5719V28.347C1.44478 28.0909 1.30426 27.963 1.02349 27.963Z" fill="white"/>
<path d="M9.28891 32.3359C9.28891 32.6053 9.0963 32.8022 8.71121 32.9268C8.44139 33.0153 8.12081 33.0598 7.75028 33.0598H3.4798V28.8543H7.77192C8.11734 28.8543 8.42883 28.8937 8.70586 28.9726C9.09456 29.0873 9.28891 29.2615 9.28891 29.4946V32.3359ZM7.80411 27.9629H2.70227C2.4215 27.9629 2.28125 28.091 2.28125 28.347V33.5719C2.28125 33.8312 2.4215 33.9609 2.70227 33.9609H7.77726C8.47198 33.9609 9.06037 33.8493 9.5427 33.6261C10.1654 33.3339 10.4768 32.9055 10.4768 32.3408V29.4896C10.4768 28.4716 9.58584 27.9629 7.80411 27.9629Z" fill="white"/>
<path d="M12.3938 33.0194C12.2462 32.9341 12.0771 32.8915 11.8862 32.8915C11.6991 32.8915 11.5316 32.9341 11.3841 33.0194C11.2256 33.1147 11.1465 33.2475 11.1465 33.4181C11.1465 33.5923 11.2256 33.7286 11.3841 33.8269C11.5209 33.9123 11.69 33.9551 11.8917 33.9551C12.086 33.9551 12.2534 33.9106 12.3938 33.8222C12.5556 33.7204 12.6366 33.5857 12.6366 33.4181C12.6366 33.2506 12.5556 33.1178 12.3938 33.0194Z" fill="white"/>
<path d="M21.0548 27.9626C20.9329 27.9626 20.8288 27.9651 20.7431 27.97C20.6569 27.975 20.5889 27.9808 20.5386 27.9872C20.3668 28.0364 20.281 28.1562 20.281 28.3466V32.5225L14.7535 28.2335C14.4797 28.0234 14.2131 27.9182 13.9538 27.9182C13.5431 27.9182 13.3379 28.1348 13.3379 28.5683V33.5715C13.3379 33.8309 13.4783 33.9605 13.759 33.9605C13.8814 33.9605 13.9857 33.9581 14.0721 33.9532C14.1584 33.9483 14.227 33.9424 14.2773 33.9358C14.45 33.8867 14.5364 33.7652 14.5364 33.5715V29.371L20.135 33.7093C20.3906 33.9031 20.6191 34 20.8209 34C21.2599 34 21.4796 33.7719 21.4796 33.3155V28.3466C21.4796 28.0905 21.3375 27.9626 21.0548 27.9626Z" fill="white"/>
<path d="M29.0875 32.326C29.0875 32.8152 28.4823 33.0598 27.2718 33.0598H25.364C24.139 33.0598 23.5267 32.8152 23.5267 32.326V29.6126C23.5267 29.107 24.139 28.8543 25.364 28.8543H27.2663C28.4805 28.8543 29.0875 29.107 29.0875 29.6126V32.326ZM29.2387 28.1895C28.85 28.0385 28.1821 27.9629 27.2357 27.9629H25.3676C23.3413 27.9629 22.3281 28.5047 22.3281 29.588V32.3555C22.3281 32.9659 22.6916 33.406 23.4189 33.6754C23.8939 33.8658 24.5432 33.9609 25.3676 33.9609H27.2357C29.2693 33.9609 30.2862 33.4241 30.2862 32.3506V29.588C30.2862 28.902 29.9367 28.4357 29.2387 28.1895Z" fill="white"/>
<path d="M37.331 27.963H30.8309C30.6471 27.963 30.5266 28.009 30.4691 28.101C30.4259 28.1664 30.4043 28.288 30.4043 28.4653C30.4043 28.7248 30.5463 28.8543 30.8309 28.8543H33.4709V33.5719C33.4709 33.7656 33.5557 33.8855 33.7257 33.9315C33.7944 33.9511 33.9698 33.9609 34.2519 33.9609C34.5302 33.9609 34.6693 33.8313 34.6693 33.5719V28.8543H37.331C37.6118 28.8543 37.7522 28.7248 37.7522 28.4653C37.7522 28.1304 37.6118 27.963 37.331 27.963Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 6L9 12L15 18" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 211 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 18L15 12L9 6" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,38 +1,38 @@
<svg width="174" height="39" viewBox="0 0 174 39" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2800_16209)">
<path d="M25.5273 5.84731C25.5401 5.51025 25.3462 5.16586 24.929 4.98634L14.1634 0.368254C13.2979 -0.0036107 12.2788 -0.0036107 11.4116 0.368254L0.706259 4.96069C0.281779 5.14204 0.0823469 5.4901 0.0933248 5.83448V26.9282L2.49932 28.0621L3.93743 27.3733L10.7474 30.1852V31.6159L12.8332 32.5978L14.9391 31.5902V30.1999L21.7381 27.3916L23.1689 28.0621L25.5328 26.93V5.84731H25.5273Z" fill="#C5B2D4"/>
<path d="M12.8314 32.7L10.6541 31.676V30.249L3.93741 27.4756L2.49748 28.1644L0 26.9865L0.00365931 5.74256C0.0311041 5.3707 0.279937 5.04463 0.669653 4.8761L11.3768 0.283662C12.2605 -0.0955304 13.3162 -0.0955304 14.1999 0.283662L24.9656 4.90175C25.3517 5.06661 25.5932 5.38902 25.6188 5.75539H25.6206V26.9883L23.1671 28.1625L21.7344 27.4921L15.0306 30.2618V31.6485L12.8332 32.7H12.8314ZM10.837 31.5588L12.8314 32.4967L14.8458 31.5331V30.1391L14.9025 30.1153L21.7381 27.2924L23.1652 27.961L25.4377 26.8729V5.93857H25.434V5.84331C25.4486 5.50992 25.2456 5.22232 24.8924 5.07028L14.1267 0.452192C13.2888 0.0931497 12.2861 0.0931497 11.4481 0.452192L0.741009 5.04463C0.382398 5.1985 0.173817 5.4916 0.182965 5.83049V5.92392V26.8692L2.49748 27.9592L3.93192 27.2723L3.97035 27.2869L10.837 30.1226V31.5551V31.5588Z" fill="#4E1480"/>
<path d="M10.5187 28.7051C10.1985 28.7051 9.87462 28.641 9.56907 28.5109L2.57614 25.5305C1.68144 25.1495 1.10327 24.2739 1.10327 23.3011L1.09778 10.187C1.09778 9.36812 1.50579 8.61157 2.18825 8.16094C2.87071 7.7103 3.72699 7.6352 4.47898 7.96127L11.4774 10.9856C12.3648 11.3685 12.9393 12.2423 12.9393 13.2113V26.2816C12.9393 27.0967 12.5331 27.8533 11.8543 28.3039C11.4518 28.5714 10.9871 28.7069 10.5205 28.7069L10.5187 28.7051ZM9.78497 28.0053C10.365 28.2526 11.0255 28.1922 11.5506 27.8441C12.0757 27.4961 12.3904 26.9117 12.3904 26.2797V13.2095C12.3904 12.4603 11.9476 11.7862 11.2597 11.4894L4.26125 8.46502C3.68125 8.21406 3.01891 8.27268 2.49014 8.62073C1.96137 8.96878 1.64667 9.55497 1.64667 10.187L1.65216 23.3011C1.65216 24.054 2.0986 24.73 2.79021 25.0249L9.78314 28.0053H9.78497Z" fill="white"/>
<path d="M13.0509 15.5434V14.8564C13.0509 14.8381 13.0399 14.8235 13.0235 14.8161L12.1763 14.4498C12.1471 14.4369 12.1141 14.4589 12.1141 14.4901V15.177C12.1141 15.1953 12.1251 15.21 12.1416 15.2173L12.9887 15.5837C13.018 15.5965 13.0509 15.5745 13.0509 15.5434Z" fill="white"/>
<path d="M13.007 15.862C12.9631 15.862 12.921 15.8529 12.879 15.8345L12.0337 15.4682C11.9166 15.4187 11.8416 15.3033 11.8416 15.1751V14.4881C11.8416 14.38 11.8946 14.2811 11.9843 14.2207C12.0739 14.1621 12.1874 14.1529 12.2862 14.195L13.1333 14.5614C13.2504 14.6109 13.3254 14.7263 13.3254 14.8545V15.5414C13.3254 15.6495 13.2723 15.7484 13.1809 15.8089C13.1278 15.8437 13.0674 15.862 13.007 15.862ZM12.3904 15.0249L12.7765 15.1916V15.0084L12.3904 14.8417V15.0249Z" fill="white"/>
<path d="M13.0819 26.2944V25.6074C13.0819 25.5891 13.0709 25.5744 13.0545 25.5671L12.2074 25.2007C12.1781 25.1879 12.1451 25.2099 12.1451 25.241V25.928C12.1451 25.9463 12.1561 25.961 12.1726 25.9683L13.0197 26.3347C13.049 26.3475 13.0819 26.3255 13.0819 26.2944Z" fill="white"/>
<path d="M13.038 26.6149C12.996 26.6149 12.9539 26.6057 12.9118 26.5892L12.0647 26.2229C11.9476 26.1716 11.8726 26.0562 11.8726 25.9298V25.2428C11.8726 25.1348 11.9256 25.0358 12.0153 24.9754C12.1049 24.9149 12.2184 24.9076 12.3172 24.9497L13.1643 25.3161C13.2814 25.3656 13.3564 25.481 13.3564 25.6092V26.2961C13.3564 26.4042 13.3033 26.5031 13.2137 26.5618C13.1606 26.5966 13.1003 26.6149 13.038 26.6149ZM12.4215 25.7777L12.8075 25.9444V25.7594L12.4215 25.5927V25.7777Z" fill="white"/>
<path d="M4.94739 14.1675C3.52575 14.7207 3.18726 17.2596 4.18808 19.8352C5.1889 22.4126 7.15395 24.0521 8.57559 23.4971C9.99723 22.942 10.3357 20.4049 9.33489 17.8293C8.33407 15.2538 6.36903 13.6124 4.94739 14.1675ZM7.34606 18.8845C7.32959 18.824 7.30947 18.7636 7.28568 18.7031C7.23811 18.5786 7.17773 18.465 7.11187 18.3642L7.45218 15.818C7.99925 16.3529 8.50057 17.1167 8.8537 18.0235C9.19584 18.9028 9.34221 19.7766 9.31294 20.5258L7.34423 18.8826L7.34606 18.8845ZM5.31515 15.0706C5.79269 14.8837 6.34707 15.0065 6.89048 15.3618L6.5465 17.9228C6.48612 17.9154 6.42758 17.9209 6.37269 17.9411C6.33975 17.9539 6.30865 17.9722 6.2812 17.996L4.3436 16.3785C4.50095 15.7355 4.82846 15.2592 5.31332 15.0706H5.31515ZM4.70221 19.6447C4.35458 18.7489 4.20821 17.8605 4.2448 17.1021L6.10373 18.6537C6.11836 18.802 6.15496 18.9614 6.21717 19.1208C6.25559 19.2197 6.30133 19.3113 6.35073 19.3956L6.03237 21.7751C5.51275 21.2439 5.0407 20.5093 4.70404 19.6429L4.70221 19.6447ZM6.59224 22.2587L6.90877 19.8938C6.98562 19.9121 7.06064 19.9121 7.12833 19.8847C7.21616 19.8499 7.28385 19.7784 7.33142 19.6813L9.22146 21.2585C9.06776 21.918 8.7366 22.4089 8.24259 22.6013C7.74127 22.7955 7.15761 22.6544 6.59224 22.2587Z" fill="white"/>
<path d="M6.96733 18.639C7.22714 19.0585 7.22531 19.5293 6.9783 19.6795C6.7313 19.8297 6.31048 19.6099 6.07995 19.1739C5.83477 18.7104 5.82928 18.2928 6.05799 18.1462C6.32512 17.9777 6.7002 18.2104 6.96733 18.639Z" fill="#3FA79E"/>
<path d="M92.3262 11.9142C92.4908 11.2914 92.7653 10.7015 93.1495 10.1428C93.5337 9.58411 94.0003 9.095 94.5492 8.67185C95.0981 8.25052 95.7165 7.92079 96.4008 7.68265C97.0869 7.44451 97.8151 7.32544 98.5818 7.32544H102.589L102.04 9.60609H100.283C99.3685 9.60609 98.6092 9.8314 98.0054 10.2784C97.4016 10.7272 97.0266 11.2731 96.8802 11.9124L96.3038 14.193H99.7893L99.213 16.4737H95.7275L92.9007 27.9319H88.3448L92.3243 11.9124L92.3262 11.9142Z" fill="#4E1480"/>
<path d="M102.838 11.9142C103.002 11.2914 103.277 10.7015 103.661 10.1428C104.045 9.58411 104.512 9.095 105.061 8.67185C105.609 8.25052 106.228 7.92079 106.912 7.68265C107.598 7.44451 108.326 7.32544 109.093 7.32544H113.1L112.551 9.60609H110.795C109.88 9.60609 109.121 9.8314 108.517 10.2784C107.913 10.7272 107.538 11.2731 107.392 11.9124L106.815 14.193H110.301L109.724 16.4737H106.239L103.412 27.9319H98.8562L102.836 11.9124L102.838 11.9142Z" fill="#4E1480"/>
<path d="M122.872 11.9143L122.296 14.195H120.567C119.652 14.195 118.893 14.4203 118.289 14.8672C117.685 15.316 117.301 15.8619 117.136 16.5013L114.309 27.932H109.726L112.553 16.5287C112.699 15.9242 112.959 15.3435 113.336 14.783C113.711 14.2243 114.174 13.7297 114.723 13.2992C115.272 12.8687 115.894 12.5298 116.589 12.2825C117.284 12.0352 118.035 11.9106 118.84 11.9106H122.874L122.872 11.9143Z" fill="#4E1480"/>
<path d="M126.33 27.9338C124.976 27.9338 123.946 27.6041 123.242 26.9446C122.537 26.2851 122.184 25.4516 122.184 24.4441C122.184 24.1327 122.23 23.7663 122.322 23.345L124.023 16.4756C124.188 15.8528 124.462 15.2629 124.846 14.7042C125.231 14.1455 125.697 13.6601 126.246 13.2479C126.795 12.8357 127.417 12.5115 128.112 12.2715C128.808 12.0334 129.54 11.9143 130.308 11.9143H133.766C135.12 11.9143 136.145 12.2404 136.84 12.8907C137.535 13.541 137.883 14.3708 137.883 15.3783C137.883 15.7081 137.837 16.0836 137.746 16.5049L136.62 21.0662H127.481L126.905 23.3743C126.74 24.0155 126.844 24.5559 127.221 24.9955C127.596 25.4352 128.24 25.655 129.157 25.655H133.768L133.219 27.9356H126.33V27.9338ZM128.059 18.7837H132.643L133.192 16.4903C133.338 15.8528 133.246 15.3105 132.917 14.8654C132.588 14.4184 132.103 14.1968 131.463 14.1968C131.151 14.1968 130.84 14.2554 130.529 14.3745C130.218 14.4935 129.935 14.6566 129.679 14.8654C129.422 15.0742 129.199 15.3197 129.007 15.6018C128.815 15.8839 128.681 16.1807 128.61 16.4903L128.061 18.7837H128.059Z" fill="#4E1480"/>
<path d="M140.684 27.9337V23.345H145.267V27.9337H140.684Z" fill="#4E1480"/>
<path d="M152.099 11.9142H156.682L152.703 27.9337H148.12L152.099 11.9142ZM153.224 7.32544H157.808L157.259 9.60609H152.675L153.224 7.32544Z" fill="#3FA79E"/>
<path d="M162.446 27.9338C161.092 27.9338 160.062 27.6041 159.357 26.9446C158.653 26.2851 158.3 25.4516 158.3 24.4441C158.3 24.1327 158.346 23.7664 158.437 23.345L160.139 16.4756C160.303 15.8528 160.578 15.2629 160.962 14.7042C161.346 14.1455 161.813 13.6601 162.362 13.2479C162.911 12.8357 163.533 12.5115 164.228 12.2715C164.923 12.0334 165.655 11.9143 166.423 11.9143H169.882C171.235 11.9143 172.26 12.244 172.955 12.9035C173.651 13.563 173.998 14.3873 173.998 15.3765C173.998 15.6879 173.952 16.0543 173.861 16.4756L172.159 23.345C171.995 23.9862 171.72 24.5815 171.336 25.1311C170.952 25.6806 170.485 26.1661 169.936 26.5874C169.387 27.0087 168.769 27.3384 168.085 27.5766C167.399 27.8147 166.67 27.9338 165.902 27.9338H162.444H162.446ZM169.28 16.5031C169.444 15.8619 169.362 15.3179 169.033 14.8691C168.703 14.4203 168.218 14.1968 167.578 14.1968C166.938 14.1968 166.343 14.4221 165.794 14.8691C165.245 15.3179 164.888 15.8638 164.724 16.5031L163.022 23.345C162.857 23.9862 162.94 24.5321 163.269 24.979C163.598 25.4278 164.083 25.6513 164.724 25.6513C165.035 25.6513 165.346 25.5927 165.657 25.4736C165.968 25.3546 166.251 25.1897 166.508 24.979C166.764 24.7684 166.983 24.5266 167.166 24.2518C167.349 23.977 167.486 23.6748 167.578 23.345L169.28 16.5031Z" fill="#3FA79E"/>
<path d="M83.5749 13.4C80.0821 12.5042 76.4393 14.9497 75.4385 18.8607C74.4376 22.7717 76.4576 26.6699 79.9522 27.5657C83.445 28.4614 87.0879 26.0159 88.0887 22.1049C89.0895 18.1939 87.0696 14.2958 83.5749 13.4ZM82.876 21.1597C82.9126 21.0754 82.9437 20.9875 82.9675 20.8959C83.015 20.7072 83.0297 20.5185 83.0132 20.3372L86.667 17.8844C87.1391 19.0567 87.2507 20.4325 86.8994 21.81C86.5573 23.1454 85.8345 24.2665 84.8996 25.0652L82.8742 21.1597H82.876ZM83.2584 14.7867C84.433 15.0871 85.4009 15.8107 86.0724 16.7797L82.3966 19.2472C82.285 19.174 82.1624 19.119 82.027 19.0842C81.9447 19.0622 81.8624 19.0512 81.78 19.0476L79.7875 15.2025C80.8524 14.6585 82.0655 14.4808 83.2584 14.7867ZM76.6918 19.1941C77.0394 17.8349 77.7822 16.6973 78.741 15.8968L80.653 19.5843C80.5084 19.762 80.3968 19.9782 80.3346 20.22C80.2962 20.3702 80.2797 20.5204 80.2815 20.6669L76.8638 22.9604C76.4429 21.8228 76.3551 20.5094 76.6918 19.1941ZM77.4273 24.0888L80.8231 21.81C80.9549 21.9126 81.1067 21.9914 81.2751 22.0335C81.4928 22.0884 81.7123 22.0811 81.9191 22.0243L83.8622 25.7741C82.7845 26.342 81.5477 26.5307 80.3346 26.2193C79.1051 25.9042 78.1006 25.1275 77.4273 24.0888Z" fill="#3FA79E"/>
<path d="M81.4104 27.8405C80.9183 27.8405 80.4224 27.78 79.9303 27.6536C78.2159 27.214 76.7942 26.0526 75.9306 24.3856C75.0689 22.7223 74.8621 20.7512 75.3525 18.837C75.841 16.9227 76.9681 15.2942 78.5233 14.25C80.0821 13.204 81.8843 12.8706 83.6005 13.3103C87.1373 14.217 89.1901 18.172 88.1802 22.127C87.3093 25.5305 84.4477 27.8405 81.4123 27.8405H81.4104ZM82.1075 13.3048C80.9055 13.3048 79.7052 13.6785 78.6257 14.4039C77.1071 15.4224 76.0075 17.0143 75.5299 18.8846C75.0524 20.7549 75.2518 22.6783 76.0935 24.3032C76.9333 25.9244 78.3128 27.051 79.976 27.4778C83.4139 28.3607 87.0147 25.939 88.0009 22.083C88.987 18.227 86.9927 14.3709 83.553 13.4898C83.0754 13.3671 82.5906 13.3066 82.1057 13.3066L82.1075 13.3048ZM81.5074 26.4574C81.1049 26.4574 80.7042 26.408 80.3127 26.3072C79.1106 25.9995 78.0585 25.2283 77.3504 24.1383L77.301 24.0632L80.8268 21.6965L80.8798 21.7368C81.0079 21.8357 81.147 21.9053 81.2988 21.9438C81.4946 21.9932 81.694 21.9914 81.8953 21.9346L81.9667 21.9145L83.9866 25.8108L83.9061 25.8529C83.1468 26.2523 82.3253 26.4556 81.5074 26.4556V26.4574ZM77.5535 24.1145C78.236 25.1257 79.2277 25.8401 80.3584 26.1314C81.4855 26.4208 82.6528 26.2834 83.7396 25.7357L81.8715 22.1324C81.6629 22.1782 81.4562 22.1764 81.2531 22.1251C81.0994 22.0867 80.9549 22.0189 80.8213 21.9236L77.5554 24.1163L77.5535 24.1145ZM84.874 25.2099L82.7754 21.1634L82.7937 21.1231C82.8303 21.0388 82.8595 20.9564 82.8815 20.874C82.9254 20.7018 82.94 20.5241 82.9254 20.3464L82.9199 20.2933L86.7128 17.747L86.7549 17.8514C87.2489 19.0806 87.333 20.4948 86.9909 21.8339C86.6542 23.1473 85.9535 24.2904 84.9618 25.1348L84.874 25.2099ZM82.9785 21.1561L84.9307 24.9205C85.8474 24.1072 86.4969 23.0264 86.8134 21.7881C87.1373 20.5241 87.0696 19.1923 86.6268 18.0236L83.1102 20.383C83.1193 20.5644 83.1029 20.7439 83.0571 20.9179C83.037 20.9967 83.0114 21.0755 82.9785 21.1542V21.1561ZM76.818 23.1015L76.7778 22.9934C76.3332 21.7881 76.2728 20.4673 76.6021 19.1722C76.9388 17.8533 77.678 16.6644 78.6806 15.8272L78.7684 15.754L80.7609 19.5953L80.7225 19.6411C80.5798 19.817 80.4792 20.0185 80.4224 20.242C80.3877 20.3812 80.3694 20.5223 80.373 20.6633V20.7128L76.8198 23.0997L76.818 23.1015ZM76.6918 19.1942L76.7796 19.2161C76.4686 20.4362 76.5125 21.6781 76.9095 22.8194L80.1882 20.6193C80.1882 20.4783 80.2084 20.3372 80.2431 20.1998C80.3017 19.9709 80.4023 19.762 80.5414 19.577L78.7099 16.0434C77.7804 16.8512 77.0961 17.9742 76.7778 19.218L76.6899 19.196L76.6918 19.1942ZM82.3985 19.3572L82.3472 19.3242C82.2411 19.2546 82.1277 19.2033 82.0051 19.1722C81.9337 19.1539 81.8587 19.1429 81.7764 19.1392H81.7233L79.6631 15.1641L79.7455 15.122C80.8798 14.5431 82.1021 14.3966 83.2804 14.6988C84.4422 14.9956 85.432 15.699 86.1474 16.7285L86.2005 16.8054L82.3966 19.359L82.3985 19.3572ZM81.8386 18.9597C81.9136 18.9652 81.9831 18.978 82.0508 18.9945C82.1716 19.0256 82.2868 19.0733 82.3948 19.1374L85.9425 16.756C85.2582 15.8034 84.3233 15.1549 83.2346 14.8747C82.1277 14.5907 80.9805 14.719 79.9101 15.2429L81.8368 18.9597H81.8386Z" fill="#3FA79E"/>
<path d="M34.8329 7.37695H39.4162L34.8603 25.7046H41.7215L41.1726 27.9852H29.7281L34.8329 7.37695Z" fill="#4E1480"/>
<path d="M49.1572 7.37695H60.6017L60.0528 9.6576H53.1916L51.4626 16.5545H57.72L57.1711 18.8351H50.9137L49.1847 25.7046H56.0459L55.497 27.9852H44.0525L49.1572 7.37695Z" fill="#4E1480"/>
<path d="M65.7887 23.3964C65.6241 24.0376 65.7064 24.5835 66.0357 25.0304C66.3651 25.4792 66.8499 25.7027 67.4903 25.7027C68.826 25.7027 69.7865 24.9333 70.372 23.3946H72.6499C72.1742 24.9333 71.4424 26.0837 70.4544 26.844C69.4663 27.6042 68.1124 27.9834 66.3925 27.9834H65.2124C63.8585 27.9834 62.8284 27.6536 62.1239 26.9942C61.4195 26.3347 61.0664 25.5012 61.0664 24.4937C61.0664 24.1823 61.1121 23.8159 61.2036 23.3946L62.9052 16.5252C63.0699 15.9023 63.3443 15.3125 63.7285 14.7538C64.1128 14.1951 64.5793 13.7096 65.1282 13.2975C65.6771 12.8853 66.2992 12.561 66.9945 12.3211C67.6897 12.0829 68.4216 11.9639 69.1901 11.9639H70.3702C71.8522 11.9639 72.9134 12.2258 73.5538 12.7479C74.1942 13.27 74.5144 14.0522 74.5144 15.0982C74.5144 15.5378 74.4595 16.0141 74.3497 16.527H72.0718C72.273 15.0066 71.6967 14.2463 70.3427 14.2463C69.7024 14.2463 69.1077 14.4717 68.5588 14.9186C68.0099 15.3674 67.6532 15.9133 67.4885 16.5526L65.7869 23.3946L65.7887 23.3964Z" fill="#4E1480"/>
<path d="M134.897 37.6811C134.613 37.6811 134.251 37.5895 134.081 37.3844V37.5749H133.599V32.6948H134.093V34.6128C134.331 34.2574 134.666 33.9863 135.134 33.9863C135.925 33.9863 136.424 34.7502 136.424 35.7595C136.424 36.7689 135.905 37.6774 134.897 37.6774V37.6811ZM134.97 34.4571C134.62 34.4571 134.397 34.6476 134.245 34.8454C134.121 35.0103 134.093 35.0836 134.093 35.2612V36.778C134.093 37.0619 134.344 37.2799 134.818 37.2799C135.542 37.2799 135.91 36.6461 135.91 35.8694C135.91 35.0927 135.601 34.4589 134.968 34.4589L134.97 34.4571Z" fill="#320756"/>
<path d="M138.481 37.8203C138.283 38.4065 137.954 38.9341 137.085 39L137.045 38.5586C137.584 38.5512 137.921 38.2087 138.066 37.6884L136.794 34.0889H137.334L138.033 36.212C138.164 36.6077 138.276 36.9832 138.316 37.1096H138.329C138.375 36.9246 138.481 36.582 138.6 36.1937L139.246 34.0907H139.714L138.483 37.8222L138.481 37.8203Z" fill="#320756"/>
<path d="M144.455 37.5766V33.6637C144.455 32.8504 143.983 32.3778 143.137 32.3778C142.599 32.3778 142.127 32.6196 141.798 33.0482C141.589 33.301 141.556 33.4219 141.556 33.7517V37.5766H140.733V31.7623H141.502V32.6855C141.941 32.0371 142.555 31.5974 143.401 31.5974C144.51 31.5974 145.278 32.2789 145.278 33.5318V37.5766H144.455Z" fill="#059C8C"/>
<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.041 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.547 37.06 150.976 35.6971 150.976 34.6639C150.976 33.6308 150.591 32.2789 149.263 32.2789Z" fill="#059C8C"/>
<path d="M154.873 37.7635C153.841 37.7635 153.391 37.0491 153.391 36.1808V32.5647H152.414V31.8723H153.391V30.5973L154.214 30.3005V31.8723H155.795V32.5647H154.214V36.0818C154.214 36.7193 154.532 37.0491 155.136 37.0491C155.389 37.0491 155.608 36.9721 155.773 36.8842L155.927 37.5656C155.663 37.6755 155.257 37.7635 154.873 37.7635Z" fill="#059C8C"/>
<path d="M157.661 37.6426C157.321 37.6426 157.09 37.4008 157.09 37.0601C157.09 36.7193 157.31 36.4775 157.65 36.4775C157.991 36.4775 158.221 36.7193 158.221 37.0601C158.221 37.4008 158.001 37.6426 157.661 37.6426Z" fill="#25124B"/>
<path d="M159.418 37.5766V29.8169H160.318V37.5766H159.418Z" fill="#25124B"/>
<path d="M164.127 30.6412V37.5766H163.216V30.6412H160.658V29.8169H166.718V30.6412H164.127Z" fill="#25124B"/>
<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_2800_16209">
<clipPath id="clip0_3896_98053">
<rect width="174" height="39" fill="white"/>
</clipPath>
</defs>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,29 @@
@import "@Themes/constants.scss";
.root {
display: inline-flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 24px;
border: 1px solid $grey-medium;
cursor: pointer;
&:hover {
background-color: $grey-medium;
}
&[data-selected="true"] {
background-color: $grey-medium;
}
.left-side {
display: inline-flex;
justify-content: space-between;
align-items: center;
.warning {
margin-left: 32px;
}
}
}

View File

@ -0,0 +1,40 @@
import React, { useCallback } from "react";
import classes from "./classes.module.scss";
import Typography, { ITypo } from "../Typography";
import ChevronIcon from "@Assets/Icons/chevron.svg";
import Image from "next/image";
export type IBlock = {
name: string;
id: string;
selected: boolean;
};
type IProps = {
blocks: IBlock[];
onSelectedBlock: (block: IBlock) => void;
};
export default function BlockList({ blocks, onSelectedBlock }: IProps) {
const selectBlock = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
onSelectedBlock && onSelectedBlock(blocks.find((folder) => folder.id === e.currentTarget.id)!);
},
[blocks, onSelectedBlock],
);
return (
<div>
{blocks.map((folder) => {
return (
<div onClick={selectBlock} key={folder.id} id={folder.id}>
<div className={classes["root"]} data-selected={folder.selected.toString()}>
<div className={classes["left-side"]}>
<Typography typo={ITypo.P_16}>{folder.name}</Typography>
</div>
<Image alt="chevron" src={ChevronIcon} />
</div>
</div>
);
})}
</div>
);
}

View File

@ -15,6 +15,7 @@
white-space: nowrap;
user-select: none;
cursor: pointer;
font-family: var(--font-primary);
svg {
width: 18px;
@ -109,4 +110,4 @@
line-height: 22px;
text-decoration-line: underline;
}
}
}

View File

@ -1,6 +1,6 @@
import React from "react";
import { IOption } from "../Select";
import { IOption } from "../Form/SelectField";
import Tooltip from "../ToolTip";
import Typography, { ITypo, ITypoColor } from "../Typography";
import classes from "./classes.module.scss";
@ -9,13 +9,29 @@ type IProps = {
name?: string;
option: IOption;
toolTip?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
checked: boolean;
};
export default class CheckBox extends React.Component<IProps> {
type IState = {
checked: boolean;
};
export default class CheckBox extends React.Component<IProps, IState> {
static defaultProps = {
toolTip: "",
checked: false,
};
constructor(props: IProps) {
super(props);
this.state = {
checked: this.props.checked ?? false,
};
this.onChange = this.onChange.bind(this);
}
public override render(): JSX.Element {
return (
<Typography typo={ITypo.P_ERR_16} color={ITypoColor.BLACK}>
@ -24,6 +40,8 @@ export default class CheckBox extends React.Component<IProps> {
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}
/>
{this.props.option.label}
{this.props.toolTip && <Tooltip className={classes["tooltip"]} text={this.props.toolTip} />}
@ -31,4 +49,20 @@ export default class CheckBox extends React.Component<IProps> {
</Typography>
);
}
public override componentDidUpdate(prevProps: Readonly<IProps>): void {
if (prevProps.checked !== this.props.checked) {
this.setState({
checked: this.props.checked,
});
}
}
private onChange(e: React.ChangeEvent<HTMLInputElement>) {
this.setState({
checked: !this.state.checked,
});
this.props.onChange && this.props.onChange(e);
}
}

View File

@ -0,0 +1,108 @@
.container {
.root {
padding: 24px;
background-color: var(--white);
border: 1px dashed #e7e7e7;
height: fit-content;
&[data-drag-over="true"] {
border: 1px dashed var(--grey);
}
&.validated {
border: 1px dashed var(--green-flash);
}
.top-container {
display: flex;
align-items: center;
.left {
margin-right: 28px;
}
.separator {
background-color: #939393;
width: 1px;
align-self: stretch;
}
.right {
margin-left: 18px;
.validated {
color: var(--green-flash);
}
.refused-button {
font-size: 14px;
color: var(--re-hover);
margin-left: 8px;
}
.title {
display: flex;
align-items: center;
gap: 8px;
}
}
}
.documents-container {
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 16px;
.file-container {
display: flex;
align-items: center;
justify-content: space-between;
.left-part {
display: flex;
align-items: center;
gap: 8px;
.loader {
width: 32px;
height: 32px;
}
}
.cross {
cursor: pointer;
}
}
}
.bottom-container {
margin-top: 16px;
.add-button {
.add-document {
display: flex;
align-items: center;
gap: 14px;
}
}
}
.text {
margin-bottom: 12px;
}
}
.modal-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.error-message {
color: var(--red-flash);
margin-top: 8px;
}
}

View File

@ -0,0 +1,409 @@
import DepositDocumentIcon from "@Assets/Icons/deposit-document.svg";
import PlusIcon from "@Assets/Icons/plus.svg";
import CrossIcon from "@Assets/Icons/cross.svg";
import DocumentCheckIcon from "@Assets/Icons/document-check.svg";
import Image from "next/image";
import React from "react";
import Button, { EButtonVariant } from "../Button";
import Tooltip from "../ToolTip";
import Typography, { ITypo, ITypoColor } from "../Typography";
import classes from "./classes.module.scss";
import { Document, DocumentHistory, File as FileCustomer } from "le-coffre-resources/dist/Customer";
import Files from "@Front/Api/LeCoffreApi/Customer/Files/Files";
import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document";
import classNames from "classnames";
import Confirm from "../Modal/Confirm";
import GreenCheckIcon from "@Assets/Icons/green-check.svg";
import Loader from "../Loader";
import TextAreaField from "../Form/TextareaField";
type IProps = {
defaultFiles?: FileCustomer[];
onChange?: (files: File[]) => void;
document: Document;
};
type IFile = {
index: number;
file: File;
uid: string;
archived: Date | null;
fileName: string;
};
type IState = {
files: IFile[];
isDragOver: boolean;
currentFiles?: FileCustomer[];
refusedReason?: string;
isShowRefusedReasonModalVisible: boolean;
loading: boolean;
};
type fileAccepted = {
extension: string;
size: number;
};
const filesAccepted: { [key: string]: fileAccepted } = {
"application/pdf": {
extension: "pdf",
size: 41943040,
},
"image/jpeg": {
extension: "jpeg",
size: 41943040,
},
"image/png": {
extension: "png",
size: 41943040,
},
"image/jpg": {
extension: "jpg",
size: 41943040,
},
};
export default class DepositDocument extends React.Component<IProps, IState> {
private inputRef = React.createRef<HTMLInputElement>();
private index = 0;
public constructor(props: IProps) {
super(props);
this.state = {
files: [],
isDragOver: false,
currentFiles: this.props.defaultFiles,
refusedReason: "",
isShowRefusedReasonModalVisible: false,
loading: false,
};
this.addDocument = this.addDocument.bind(this);
this.onFileChange = this.onFileChange.bind(this);
this.removeFile = this.removeFile.bind(this);
this.onDragOver = this.onDragOver.bind(this);
this.onDragDrop = this.onDragDrop.bind(this);
this.onDragLeave = this.onDragLeave.bind(this);
this.onCloseModalShowRefusedReason = this.onCloseModalShowRefusedReason.bind(this);
this.onOpenModalShowRefusedReason = this.onOpenModalShowRefusedReason.bind(this);
this.showRefusedReason = this.showRefusedReason.bind(this);
}
public override render(): JSX.Element {
return (
<div className={classes["container"]}>
<div
className={classNames(
classes["root"],
this.props.document.document_status === EDocumentStatus.VALIDATED && classes["validated"],
)}
onDragOver={this.onDragOver}
onDrop={this.onDragDrop}
onDragLeave={this.onDragLeave}
data-drag-over={this.state.isDragOver.toString()}>
<input
type="file"
ref={this.inputRef}
hidden
onChange={this.onFileChange}
accept={Object.keys(filesAccepted).join(",")}
/>
<div className={classes["top-container"]}>
<div className={classes["left"]}>
<Image src={DepositDocumentIcon} alt="Deposit document" />
</div>
<div className={classes["separator"]} />
<div className={classes["right"]}>
<Typography typo={ITypo.P_SB_16} color={ITypoColor.BLACK} className={classes["title"]}>
<div
className={
this.props.document.document_status === EDocumentStatus.VALIDATED ? classes["validated"] : ""
}>
{this.props.document.document_type?.name}
</div>
{this.props.document.document_type?.public_description !== "" &&
this.props.document.document_status !== EDocumentStatus.VALIDATED && (
<Tooltip text={this.props.document.document_type?.public_description} />
)}
{this.props.document.document_status === EDocumentStatus.VALIDATED && (
<Image src={GreenCheckIcon} alt="Document check" />
)}
</Typography>
{this.props.document.document_status !== EDocumentStatus.VALIDATED && (
<Typography color={ITypoColor.GREY} typo={ITypo.CAPTION_14}>
Sélectionnez des documents .jpg, .pdf ou .png
</Typography>
)}
{this.props.document.document_history?.map((history) => (
<div key={history.uid}>{this.renderDocumentHistory(history)}</div>
))}
</div>
</div>
{this.props.document.document_status !== EDocumentStatus.VALIDATED && this.state.files.length > 0 && (
<div className={classes["documents-container"]}>
{this.state.files.map((file) => {
const fileObj = file.file;
if (file.archived) return;
return (
<div className={classes["file-container"]} key={fileObj.name + file.index}>
<div className={classes["left-part"]}>
<Image src={DocumentCheckIcon} alt="Document check" />
<Typography typo={ITypo.P_16} color={ITypoColor.GREY} title={file.fileName ?? fileObj.name}>
{this.shortName(file.fileName || fileObj.name)}
</Typography>
</div>
<Image
src={CrossIcon}
alt="Cross icon"
className={classes["cross"]}
onClick={this.removeFile}
data-file={file.index}
/>
</div>
);
})}
{this.state.loading && (
<div className={classes["file-container"]}>
<div className={classes["left-part"]}>
<div className={classes["loader"]}>
<Loader />
</div>
<Typography typo={ITypo.P_16} color={ITypoColor.GREY}>
Chargement...
</Typography>
</div>
<div />
</div>
)}
</div>
)}
{this.props.document.document_status !== EDocumentStatus.VALIDATED && (
<div className={classes["bottom-container"]}>
<Button variant={EButtonVariant.LINE} className={classes["add-button"]} onClick={this.addDocument}>
<Typography typo={ITypo.P_SB_16} color={ITypoColor.PINK_FLASH} className={classes["add-document"]}>
Ajouter un document <Image src={PlusIcon} alt="Plus icon" />
</Typography>
</Button>
</div>
)}
<Confirm
isOpen={this.state.isShowRefusedReasonModalVisible}
onClose={this.onCloseModalShowRefusedReason}
showCancelButton={false}
onAccept={this.onCloseModalShowRefusedReason}
closeBtn
header={"Motif du refus"}
confirmText={"J'ai compris"}>
<div className={classes["modal-content"]}>
<Typography typo={ITypo.P_16} className={classes["text"]}>
Votre document a é refusé pour la raison suivante :
</Typography>
<TextAreaField placeholder="Description" defaultValue={this.state.refusedReason} readonly />
</div>
</Confirm>
</div>
{this.props.document.document_status === EDocumentStatus.REFUSED && (
<Typography typo={ITypo.CAPTION_14} className={classes["error-message"]}>
Ce document nest pas conforme. Veuillez le déposer à nouveau.
</Typography>
)}
</div>
);
}
public override componentDidMount(): void {
if (this.props.defaultFiles) {
this.setState({
files: this.props.defaultFiles.map((file) => ({
index: this.index++,
file: new File([""], file.file_path ?? "", {}),
uid: file.uid!,
fileName: file.file_name,
archived: file.archived_at ? new Date(file.archived_at) : null,
})),
});
}
}
private onCloseModalShowRefusedReason() {
this.setState({
isShowRefusedReasonModalVisible: false,
});
}
private onOpenModalShowRefusedReason() {
this.setState({
isShowRefusedReasonModalVisible: true,
});
}
private renderDocumentHistory(history: DocumentHistory): JSX.Element | null {
switch (history.document_status) {
case EDocumentStatus.ASKED:
return (
<Typography color={ITypoColor.GREY} typo={ITypo.CAPTION_14}>
Demandé par votre notaire le {this.formatDate(history.created_at!)}
</Typography>
);
case EDocumentStatus.VALIDATED:
return (
<Typography color={ITypoColor.GREY} typo={ITypo.CAPTION_14}>
Validé par votre notaire le {this.formatDate(history.created_at!)}
</Typography>
);
case EDocumentStatus.DEPOSITED:
return (
<Typography color={ITypoColor.GREY} typo={ITypo.CAPTION_14}>
Déposé le {this.formatDate(history.created_at!)}
</Typography>
);
case EDocumentStatus.REFUSED:
return (
<Typography typo={ITypo.CAPTION_14} color={ITypoColor.RE_HOVER}>
Document non conforme
{history.refused_reason && history.refused_reason.length > 0 && (
<Button
variant={EButtonVariant.LINE}
className={classes["refused-button"]}
onClick={() => this.showRefusedReason(history.refused_reason ?? "")}>
Voir le motif de refus
</Button>
)}
</Typography>
);
}
return null;
}
private shortName(name: string): string {
const maxLength = 20;
if (name.length > maxLength) {
return name.substring(0, maxLength / 2) + "..." + name.substring(name.length - maxLength / 2, name.length);
}
return name;
}
private onDragOver(event: React.DragEvent<HTMLDivElement>) {
if (!this.state.isDragOver) {
this.setState({
isDragOver: true,
});
}
event.preventDefault();
}
private showRefusedReason(refusedReason: string) {
this.setState({
refusedReason,
});
this.onOpenModalShowRefusedReason();
}
private onDragLeave(event: React.DragEvent<HTMLDivElement>) {
this.setState({
isDragOver: false,
});
event.preventDefault();
}
private async onDragDrop(event: React.DragEvent<HTMLDivElement>) {
event.preventDefault();
this.setState({
isDragOver: false,
});
const file = event.dataTransfer.files[0];
if (file) this.addFile(file);
}
private async addFile(file: File) {
const fileAccepted = filesAccepted[file.type];
if (!fileAccepted) {
alert("Ce type de fichier n'est pas accepté");
return false;
}
if (file.size > fileAccepted.size) {
alert("Ce fichier est trop volumineux");
return false;
}
this.setState({
loading: true,
});
const formData = new FormData();
formData.append("file", file, file.name);
const query = JSON.stringify({ document: { uid: this.props.document.uid } });
formData.append("q", query);
const newFile = await Files.getInstance().post(formData);
const files = this.state.currentFiles ? [...this.state.currentFiles, newFile] : [newFile];
const newFileList = [
...this.state.files,
{
index: this.index++,
file: file,
uid: newFile.uid!,
archived: null,
fileName: newFile?.file_name ?? "",
},
];
this.setState(
{
currentFiles: files,
loading: false,
files: newFileList,
},
() => {
if (this.props.onChange) this.props.onChange(newFileList.map((file) => file.file));
},
);
return true;
}
private async removeFile(e: any) {
const image = e.target as HTMLElement;
const indexToRemove = image.getAttribute("data-file");
if (!indexToRemove) return;
const file = this.state.files.find((file) => file.index === parseInt(indexToRemove));
if (!file) return;
this.setState({
files: this.state.files.filter((file) => file.index !== parseInt(indexToRemove)),
});
if (this.props.onChange) this.props.onChange(this.state.files.map((file) => file.file));
await Files.getInstance().delete(file.uid);
}
private async onFileChange() {
if (!this.inputRef.current) return;
const files = this.inputRef.current.files;
if (!files) {
this.setState({ loading: false });
return;
}
const file = files[0];
try {
if (file) this.addFile(file);
} catch (e) {
console.log(e);
}
}
private addDocument() {
if (!this.inputRef.current) return;
this.inputRef.current.value = "";
this.inputRef.current.click();
}
private formatDate(date: Date) {
const dateToConvert = new Date(date);
return dateToConvert.toLocaleDateString("fr-FR");
}
}

View File

@ -0,0 +1,106 @@
.container {
.root {
padding: 24px;
background-color: var(--white);
border: 1px dashed #e7e7e7;
height: fit-content;
&[data-drag-over="true"] {
border: 1px dashed var(--grey);
}
&.validated {
border: 1px dashed var(--green-flash);
}
.top-container {
display: flex;
align-items: center;
.left {
margin-right: 28px;
}
.separator {
background-color: #939393;
width: 1px;
align-self: stretch;
}
.right {
margin-left: 18px;
.validated {
color: var(--green-flash);
}
.refused-button {
font-size: 14px;
color: var(--re-hover);
margin-left: 8px;
}
.title {
display: flex;
align-items: center;
gap: 8px;
}
}
}
.documents-container {
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 16px;
.file-container {
display: flex;
align-items: center;
justify-content: space-between;
.left-part {
display: flex;
align-items: center;
gap: 8px;
.loader {
width: 32px;
height: 32px;
}
}
.cross {
cursor: pointer;
}
}
}
.bottom-container {
margin-top: 16px;
.add-button {
.add-document {
display: flex;
align-items: center;
gap: 14px;
}
}
}
.text {
margin-bottom: 12px;
}
}
.modal-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.error-message {
color: var(--red-flash);
margin-top: 8px;
}
}

View File

@ -0,0 +1,303 @@
import DepositDocumentIcon from "@Assets/Icons/deposit-document.svg";
import CrossIcon from "@Assets/Icons/cross.svg";
import DocumentCheckIcon from "@Assets/Icons/document-check.svg";
import PlusIcon from "@Assets/Icons/plus.svg";
import Image from "next/image";
import React from "react";
import Typography, { ITypo, ITypoColor } from "../Typography";
import classes from "./classes.module.scss";
import { Document } from "le-coffre-resources/dist/Customer";
import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document";
import classNames from "classnames";
import Button, { EButtonVariant } from "../Button";
import Confirm from "../Modal/Confirm";
import Documents from "@Front/Api/LeCoffreApi/Customer/Documents/Documents";
import Files from "@Front/Api/LeCoffreApi/Customer/Files/Files";
type IProps = {
onChange?: (files: File[]) => void;
open: boolean;
onClose?: () => void;
document: Document;
customer_uid: string;
folder_uid: string | string[];
};
type IFile = {
index: number;
file: File;
fileName: string;
};
type IState = {
files: IFile[];
isDragOver: boolean;
currentFiles?: IFile[];
refusedReason?: string;
isShowRefusedReasonModalVisible: boolean;
isAddDocumentModalVisible: boolean;
};
export default class DepositOtherDocument extends React.Component<IProps, IState> {
private inputRef = React.createRef<HTMLInputElement>();
private index = 0;
public constructor(props: IProps) {
super(props);
this.state = {
isAddDocumentModalVisible: this.props.open,
files: [],
isDragOver: false,
refusedReason: "",
isShowRefusedReasonModalVisible: false,
};
this.addDocument = this.addDocument.bind(this);
this.onFileChange = this.onFileChange.bind(this);
this.removeFile = this.removeFile.bind(this);
this.onDragOver = this.onDragOver.bind(this);
this.onDragDrop = this.onDragDrop.bind(this);
this.onDragLeave = this.onDragLeave.bind(this);
this.onAccept = this.onAccept.bind(this);
}
private async onAccept() {
const filesArray = this.state.currentFiles;
if (!filesArray) return;
const documentCreated = await Documents.getInstance().post({
folder: {
uid: this.props.folder_uid,
},
depositor: {
uid: this.props.customer_uid,
},
});
console.log(documentCreated);
filesArray.forEach(async (file) => {
const formData = new FormData();
formData.append("file", file.file, file.fileName);
const query = JSON.stringify({ document: { uid: documentCreated.uid } });
formData.append("q", query);
const newFile = await Files.getInstance().post(formData);
console.log(newFile);
});
this.props.onClose!();
}
public override render(): JSX.Element {
return (
<Confirm
isOpen={this.state.isAddDocumentModalVisible}
onClose={this.props.onClose!}
onAccept={this.onAccept}
closeBtn
header={"Ajouter un document"}
cancelText={"Annuler"}
confirmText={"Déposer le document"}>
<div className={classes["modal-content"]}>
<div className={classes["container"]}>
<Typography typo={ITypo.P_16} className={classes["text"]}>
Vous souhaitez envoyer un autre document à votre notaire ?
</Typography>
<Typography typo={ITypo.P_16} className={classes["text"]}>
Glissez / Déposez votre document dans la zone prévue à cet effet ou cliquez sur la zone puis sélectionnez le
document correspondant.
</Typography>
<div
className={classNames(
classes["root"],
this.props.document.document_status === EDocumentStatus.VALIDATED && classes["validated"],
)}
onDragOver={this.onDragOver}
onDrop={this.onDragDrop}
onDragLeave={this.onDragLeave}
data-drag-over={this.state.isDragOver.toString()}>
<input type="file" ref={this.inputRef} hidden onChange={this.onFileChange} />
<div className={classes["top-container"]}>
<div className={classes["left"]}>
<Image src={DepositDocumentIcon} alt="Deposit document" />
</div>
<div className={classes["separator"]} />
<div className={classes["right"]}>
<Typography typo={ITypo.P_SB_16} color={ITypoColor.BLACK} className={classes["title"]}>
<div
className={
this.props.document.document_status === EDocumentStatus.VALIDATED
? classes["validated"]
: ""
}>
{this.props.document.document_type?.name}
</div>
</Typography>
<Typography color={ITypoColor.GREY} typo={ITypo.CAPTION_14}>
Sélectionnez des TEST documents .jpg, .pdf ou .png
</Typography>
</div>
</div>
{this.state.currentFiles && this.state.currentFiles.length > 0 && (
<div className={classes["documents-container"]}>
{this.state.currentFiles.map((file) => {
console.log(file);
const fileObj = file.file;
return (
<div className={classes["file-container"]} key={fileObj.name}>
<div className={classes["left-part"]}>
<Image src={DocumentCheckIcon} alt="Document check" />
<Typography typo={ITypo.P_16} color={ITypoColor.GREY}>
{this.shortName(fileObj.name)}
</Typography>
</div>
<Image
src={CrossIcon}
alt="Cross icon"
className={classes["cross"]}
onClick={this.removeFile}
data-file={file.index}
/>
</div>
);
})}
</div>
)}
<div className={classes["bottom-container"]}>
<Button variant={EButtonVariant.LINE} className={classes["add-button"]} onClick={this.addDocument}>
<Typography typo={ITypo.P_SB_16} color={ITypoColor.PINK_FLASH} className={classes["add-document"]}>
Ajouter un document <Image src={PlusIcon} alt="Plus icon" />
</Typography>
</Button>
</div>
</div>
</div>
</div>
</Confirm>
);
}
public override componentDidMount(): void {}
private shortName(name: string): string {
const maxLength = 20;
if (name.length > maxLength) {
return name.substring(0, maxLength / 2) + "..." + name.substring(name.length - maxLength / 2, name.length);
}
return name;
}
private onDragOver(event: React.DragEvent<HTMLDivElement>) {
if (!this.state.isDragOver) {
this.setState({
isDragOver: true,
});
}
event.preventDefault();
}
private onDragLeave(event: React.DragEvent<HTMLDivElement>) {
this.setState({
isDragOver: false,
});
event.preventDefault();
}
private async onDragDrop(event: React.DragEvent<HTMLDivElement>) {
event.preventDefault();
this.setState({
isDragOver: false,
});
const file = event.dataTransfer.files[0];
if (file) this.addFile(file);
}
private async addFile(file: File) {
const iFile: IFile = {
file: file,
fileName: file.name,
index: this.index++,
};
const tmpArray: IFile[] = this.state.currentFiles || [];
tmpArray.push(iFile);
this.setState({
currentFiles: tmpArray,
});
console.log(this.state.currentFiles);
// const formData = new FormData();
// formData.append("file", file, file.name);
// const query = JSON.stringify({ document: { uid: this.props.document.uid } });
// formData.append("q", query);
// const newFile = await Files.getInstance().post(formData);
// const newFile: FileCustomer = {
// file_name: file.name,
// }
// const files = this.state.currentFiles ? [...this.state.currentFiles, newFile] : [newFile];
// this.setState({
// currentFiles: files,
// loading: false,
// files: [
// ...this.state.files,
// {
// index: this.index++,
// file: file,
// uid: newFile.uid!,
// archived: null,
// fileName: newFile?.file_name ?? "",
// },
// ],
// });
// if (this.props.onChange) this.props.onChange(this.state.files.map((file) => file.file));
}
private async removeFile(e: any) {
const image = e.target as HTMLElement;
const indexToRemove = image.getAttribute("data-file");
console.log(indexToRemove);
if (!indexToRemove) return;
const file = this.state.currentFiles!.find((file) => file.index === parseInt(indexToRemove));
if (!file) return;
this.setState({
currentFiles: this.state.currentFiles!.filter((file) => file.index !== parseInt(indexToRemove)),
});
if (this.props.onChange) this.props.onChange(this.state.currentFiles!.map((file) => file.file));
}
private async onFileChange() {
if (!this.inputRef.current) return;
const files = this.inputRef.current.files;
if (!files) {
return;
}
const file = files[0];
if (file) this.addFile(file);
}
private addDocument() {
if (!this.inputRef.current) return;
this.inputRef.current.click();
}
// private formatDate(date: Date) {
// const dateToConvert = new Date(date);
// return dateToConvert.toLocaleDateString("fr-FR");
// }
}

View File

@ -6,12 +6,23 @@
display: flex;
justify-content: space-between;
align-items: center;
&.PENDING {
border-color: $orange-soft;
&.DEPOSITED {
cursor: pointer;
border: 1px solid $orange-soft;
&:hover {
border: 1px solid $orange-soft;
outline: 1px solid $orange-soft;
}
}
&.VALIDATED {
border-color: $green-soft;
cursor: pointer;
border: 1px solid $green-soft;
&:hover {
border: 1px solid $green-soft;
outline: 1px solid $green-soft;
}
}
.valid-radius {
background-color: $green-flash;
padding: 6px;

View File

@ -1,40 +1,85 @@
import React from "react";
import classes from "./classes.module.scss";
import { Document } from "le-coffre-resources/dist/Customer";
import Typography, { ITypo } from "../../Typography";
import Image from "next/image";
import TrashIcon from "@Assets/Icons/trash.svg";
import ValidIcon from "@Assets/Icons/check-valid.svg";
import TrashIcon from "@Assets/Icons/trash.svg";
import classNames from "classnames";
import { Document } from "le-coffre-resources/dist/Customer";
import Image from "next/image";
import { NextRouter, useRouter } from "next/router";
import React from "react";
import Typography, { ITypo } from "../../Typography";
import WarningBadge from "../../WarningBadge";
import classes from "./classes.module.scss";
import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document";
type IProps = {
folderUid: string;
document: {
uid: Document["uid"];
document_type: Document["document_type"];
uid?: Document["uid"];
document_type?: Document["document_type"];
document_status: Document["document_status"];
folder?: Document["folder"];
files?: Document["files"];
};
openDeletionModal?: (uid: Document["uid"]) => void;
};
type IPropsClass = IProps & {
router: NextRouter;
};
type IState = {};
export default class DocumentNotary extends React.Component<IProps, IState> {
public constructor(props: IProps) {
class DocumentNotaryClass extends React.Component<IPropsClass, IState> {
public constructor(props: IPropsClass) {
super(props);
this.onOpenDeletionModal = this.onOpenDeletionModal.bind(this);
this.onClick = this.onClick.bind(this);
}
public override render(): JSX.Element {
return (
<div className={classNames(classes["root"], classes[this.props.document.document_status])}>
<div className={classNames(classes["root"], classes[this.props.document.document_status])} onClick={this.onClick}>
<div>
<Typography typo={ITypo.P_SB_16}>{this.props.document?.document_type?.name}</Typography>
<Typography typo={ITypo.CAPTION_14}>Aucun document déposé</Typography>
<Typography typo={ITypo.CAPTION_14}>{this.getDocumentsTitle()}</Typography>
</div>
{this.renderIcon()}
</div>
);
}
private getDocumentsTitle() {
const documentFiles = this.props.document.files;
if (documentFiles) {
if (documentFiles.length === 1) {
const fileName = documentFiles[0]?.file_path?.split("/").pop();
if (fileName && fileName.length > 20) {
return `${fileName.substring(0, 7)}...${fileName.substring(fileName.length - 7, fileName.length)}`;
} else {
return fileName;
}
} else {
const archivedFilesLength = documentFiles.filter((file) => file.archived_at).length;
const documentFileLength = documentFiles.length - archivedFilesLength;
if(this.props.document.document_status === EDocumentStatus.ASKED || this.props.document.document_status === EDocumentStatus.REFUSED){
return 'Aucun document déposé';
}
return `${documentFileLength} documents déposés`;
}
} else {
return "Aucun document déposé";
}
}
private onClick() {
if (
this.props.document.document_status !== EDocumentStatus.VALIDATED &&
this.props.document.document_status !== EDocumentStatus.DEPOSITED
)
return;
this.props.router.push(`/folders/${this.props.folderUid}/documents/${this.props.document.uid}`);
}
private renderIcon(): JSX.Element {
switch (this.props.document.document_status) {
case "VALIDATED":
@ -43,7 +88,7 @@ export default class DocumentNotary extends React.Component<IProps, IState> {
<Image src={ValidIcon} alt="valid icon" />
</div>
);
case "PENDING":
case EDocumentStatus.DEPOSITED:
return <WarningBadge />;
default:
return <Image src={TrashIcon} alt="trash icon" className={classes["trash"]} onClick={this.onOpenDeletionModal} />;
@ -52,6 +97,11 @@ export default class DocumentNotary extends React.Component<IProps, IState> {
private onOpenDeletionModal(): void {
if (!this.props.openDeletionModal) return;
this.props.openDeletionModal(this.props.document.uid);
this.props.openDeletionModal(this.props.document.uid ?? "");
}
}
export default function DocumentNotary(props: IProps): JSX.Element {
const router = useRouter();
return <DocumentNotaryClass {...props} router={router} />;
}

View File

@ -0,0 +1,34 @@
.root {
height: inherit;
min-height: inherit;
position: relative;
.file-container {
height: inherit;
min-height: inherit;
.image {
width: 100%;
height: inherit;
min-height: inherit;
object-fit: contain;
}
.pdf {
width: 100%;
min-height: inherit;
height: inherit;
}
}
.loader {
width: 48px;
height: 48px;
z-index: -1;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}

View File

@ -0,0 +1,37 @@
import React from "react";
import Typography, { ITypo, ITypoColor } from "../Typography";
import classes from "./classes.module.scss";
import Loader from "../Loader";
type IProps = {
href: string;
fileName?: string;
};
type IState = {};
export default class FilePreview extends React.Component<IProps, IState> {
override render() {
let type = this.props.href.split(".").pop();
if (this.props.fileName) type = this.props.fileName.split(".").pop();
return (
<div className={classes["root"]}>
<div className={classes["loader"]}>
<Loader />
</div>
{!type && (
<Typography typo={ITypo.H1} color={ITypoColor.BLACK}>
Erreur lors du chargement du fichier
</Typography>
)}
<div className={classes["file-container"]}>
{type?.toLowerCase() === "pdf" && (
<embed src={this.props.href} width="100%" height="100%" type="application/pdf" className={classes["pdf"]} />
)}
{type?.toLowerCase() !== "pdf" && <img src={this.props.href} alt="File preview" className={classes["image"]} />}
</div>
</div>
);
}
}

View File

@ -8,6 +8,12 @@
align-items: center;
justify-content: space-between;
&.single-information {
.content {
grid-template-columns: 1fr;
}
}
.content {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
@ -17,7 +23,6 @@
.text-container {
display: flex;
flex-direction: column;
justify-content: space-between;
> :first-child {
margin-bottom: 12px;
@ -33,9 +38,7 @@
grid-template-columns: 1fr;
}
&.isSignleDescription {
grid-template-columns: 1fr;
}
}
.edit-icon-container {

View File

@ -1,63 +1,91 @@
import React from "react";
import classes from "./classes.module.scss";
import classNames from "classnames";
import Image from "next/image";
import PenICon from "@Assets/Icons/pen.svg";
import { IDashBoardFolder } from "@Front/Components/LayoutTemplates/DefaultNotaryDashboard";
import Typography, { ITypo } from "../Typography";
import Module from "@Front/Config/Module";
import classNames from "classnames";
import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import Image from "next/image";
import Link from "next/link";
import React from "react";
import Typography, { ITypo } from "../Typography";
import classes from "./classes.module.scss";
type IProps = {
folder: IDashBoardFolder;
isDescription?: boolean;
folder: OfficeFolder;
type: EFolderBoxInformationType;
isArchived?: boolean;
};
export default function FolderBoxInformation(props: IProps) {
const { isDescription = false } = props;
const path = isDescription
? "/folder/".concat(props.folder.uid).concat("/update/description")
: "/folder/".concat(props.folder.uid).concat("/update/metadata");
export enum EFolderBoxInformationType {
INFORMATIONS = "informations",
DESCRIPTION = "description",
ARCHIVED_DESCRIPTION = "archivedDescription",
}
export default function FolderBoxInformation(props: IProps) {
const { isArchived = false, type } = props;
const editDescriptionPath = Module.getInstance()
.get()
.modules.pages.Folder.pages.EditDescription.props.path.replace("[folderUid]", props.folder.uid ?? "");
const editInformationsPath = Module.getInstance()
.get()
.modules.pages.Folder.pages.EditInformations.props.path.replace("[folderUid]", props.folder.uid ?? "");
const path = type === EFolderBoxInformationType.DESCRIPTION ? editDescriptionPath : editInformationsPath;
return (
<div className={classNames(classes["root"], isDescription && classes["isSignleDescription"])}>
<div className={classes["content"]}>
{isDescription ? (
<>
<div className={classes["text-container"]}>
<Typography typo={ITypo.NAV_INPUT_16}>Note dossier</Typography>
<Typography typo={ITypo.P_18}>{props.folder.description ?? "..."}</Typography>
</div>
</>
) : (
<div className={classNames(classes["root"], type !== EFolderBoxInformationType.INFORMATIONS && classes["single-information"])}>
<div className={classes["content"]}>{renderContentByType(props.folder, type)}</div>
{!isArchived && (
<Link href={path} className={classes["edit-icon-container"]}>
<Image src={PenICon} alt="edit informations" />
</Link>
)}
</div>
);
function renderContentByType(folder: OfficeFolder, type: EFolderBoxInformationType) {
switch (type) {
case EFolderBoxInformationType.DESCRIPTION:
return (
<div className={classes["text-container"]}>
<Typography typo={ITypo.NAV_INPUT_16}>Note dossier</Typography>
<Typography typo={ITypo.P_18}>{folder.description ?? ""}</Typography>
</div>
);
case EFolderBoxInformationType.ARCHIVED_DESCRIPTION:
return (
<div className={classes["text-container"]}>
<Typography typo={ITypo.NAV_INPUT_16}>Note archive</Typography>
<Typography typo={ITypo.P_18}>{folder.archived_description ?? ""}</Typography>
</div>
);
case EFolderBoxInformationType.INFORMATIONS:
return (
<>
<div className={classes["text-container"]}>
<Typography typo={ITypo.NAV_INPUT_16}>Intitulé du dossier</Typography>
<Typography typo={ITypo.P_18}>{props.folder.name ?? "..."}</Typography>
<Typography typo={ITypo.P_18}>{folder.name ?? ""}</Typography>
</div>
<div className={classes["text-container"]}>
<Typography typo={ITypo.NAV_INPUT_16}>Numéro de dossier</Typography>
<Typography typo={ITypo.P_18}>{props.folder.folder_number ?? "..."}</Typography>
<Typography typo={ITypo.P_18}>{folder.folder_number ?? ""}</Typography>
</div>
<div className={classes["text-container"]}>
<Typography typo={ITypo.NAV_INPUT_16}>Type dacte</Typography>
<Typography typo={ITypo.P_18}>{props.folder.deed.deed_type.name ?? "..."}</Typography>
<Typography typo={ITypo.NAV_INPUT_16}>Type d'acte</Typography>
<Typography typo={ITypo.P_18}>{folder.deed?.deed_type?.name ?? ""}</Typography>
</div>
<div className={classes["text-container"]}>
<Typography typo={ITypo.NAV_INPUT_16}>Ouverture du dossier</Typography>
<Typography typo={ITypo.P_18}>{formatDate(props.folder.created_at)}</Typography>
<Typography typo={ITypo.P_18}>{formatDate(folder.created_at)}</Typography>
</div>
</>
)}
</div>
<Link href={path} className={classes["edit-icon-container"]}>
<Image src={PenICon} alt="edit informations" />
</Link>
</div>
);
);
}
}
}
function formatDate(date: Date | null): string {
if (!date) return "...";
if (!(date instanceof Date)) date = new Date(date);
return date.toLocaleDateString("fr-FR", {
year: "numeric",
month: "long",

View File

@ -1,5 +1,6 @@
import ChevronIcon from "@Assets/Icons/chevron.svg";
import { IDashBoardFolder } from "@Front/Components/LayoutTemplates/DefaultNotaryDashboard";
import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import { EDocumentStatus } from "le-coffre-resources/dist/Notary/Document";
import Image from "next/image";
import React from "react";
@ -8,8 +9,8 @@ import WarningBadge from "../WarningBadge";
import classes from "./classes.module.scss";
type IProps = {
folder: IDashBoardFolder;
onSelectedFolder?: (folder: IDashBoardFolder) => void;
folder: OfficeFolder;
onSelectedFolder?: (folder: OfficeFolder) => void;
};
type IState = {};
@ -39,7 +40,7 @@ export default class FolderContainer extends React.Component<IProps, IState> {
private countPendingDocuments(): number {
if (!this.props.folder.documents) return 0;
return this.props.folder.documents?.filter((document) => document.document_status === "PENDING").length ?? 0;
return this.props.folder.documents?.filter((document) => document.document_status === EDocumentStatus.DEPOSITED).length ?? 0;
}
private onSelectedFolder(): void {

View File

@ -1,7 +1,14 @@
@import "@Themes/constants.scss";
.root {
height: calc(100vh - 290px);
overflow-y: scroll;
&.archived{
height: calc(100vh - 220px);
}
.active {
background-color: var(--grey-medium);
}
}
}

View File

@ -1,4 +1,7 @@
import { IDashBoardFolder } from "@Front/Components/LayoutTemplates/DefaultNotaryDashboard";
import Module from "@Front/Config/Module";
import classNames from "classnames";
import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import { EDocumentStatus } from "le-coffre-resources/dist/Notary/Document";
import Link from "next/link";
import { useRouter } from "next/router";
import React from "react";
@ -7,8 +10,9 @@ import FolderContainer from "../FolderContainer";
import classes from "./classes.module.scss";
type IProps = {
folders: IDashBoardFolder[];
onSelectedFolder?: (folder: IDashBoardFolder) => void;
folders: OfficeFolder[];
isArchived: boolean;
onSelectedFolder?: (folder: OfficeFolder) => void;
onCloseLeftSide?: () => void;
};
@ -19,24 +23,48 @@ type IPropsClass = IProps & {
type IState = {};
class FolderListClass extends React.Component<IPropsClass, IState> {
private redirectPath: string = this.props.isArchived
? Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.pages.FolderInformation.props.path
: Module.getInstance().get().modules.pages.Folder.pages.FolderInformation.props.path;
public override render(): JSX.Element {
return (
<div className={classes["root"]}>
{this.props.folders.map((folder) => {
return (
<div
onClick={this.props.onCloseLeftSide}
key={folder.uid}
className={folder.uid === this.props.selectedFolder ? classes["active"] : ""}>
<Link href={"/folder/".concat(folder.uid)}>
<FolderContainer folder={folder} onSelectedFolder={this.props.onSelectedFolder} />;
</Link>
</div>
);
})}
;
</div>
);
return <div className={classNames(classes["root"], this.props.isArchived ? classes["archived"] : "")}>{this.renderFolders()}</div>;
}
private renderFolders(): JSX.Element[] {
const pendingFolders = this.props.folders
.filter((folder) => {
const pendingDocuments = (folder.documents ?? []).filter(
(document) => document.document_status === EDocumentStatus.DEPOSITED,
);
return pendingDocuments.length >= 1;
})
.sort((folder1, folder2) => {
return folder1.created_at! > folder2.created_at! ? -1 : 1;
});
const otherFolders = this.props.folders
.filter((folder) => {
const pendingDocuments = (folder.documents ?? []).filter(
(document) => document.document_status === EDocumentStatus.DEPOSITED,
);
return pendingDocuments.length === 0;
})
.sort((folder1, folder2) => {
return folder1.created_at! > folder2.created_at! ? -1 : 1;
});
return [...pendingFolders, ...otherFolders].map((folder) => {
return (
<div
onClick={this.props.onCloseLeftSide}
key={folder.uid}
className={folder.uid === this.props.selectedFolder ? classes["active"] : ""}>
<Link href={this.redirectPath.replace("[folderUid]", folder.uid ?? "")}>
<FolderContainer folder={folder} onSelectedFolder={this.props.onSelectedFolder} />;
</Link>
</div>
);
});
}
}

View File

@ -1,13 +1,23 @@
@import "@Themes/constants.scss";
.root {
min-height: 100%;
width: 100%;
width: calc(100vh - 83px);
display: flex;
flex-direction: column;
justify-content: space-between;
.header {
flex: 1;
}
.searchbar {
padding: 40px 24px 24px 24px;
}
.folderlist-container {
max-height: calc(100vh - 290px);
height: calc(100vh - 290px);
overflow: auto;
border-right: 1px solid var(--grey-medium);
}
}

View File

@ -1,55 +1,139 @@
import { IDashBoardFolder } from "@Front/Components/LayoutTemplates/DefaultNotaryDashboard";
import Module from "@Front/Config/Module";
import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import { EDocumentStatus } from "le-coffre-resources/dist/Notary/Document";
import Link from "next/link";
import { NextRouter, useRouter } from "next/router";
import React from "react";
import BlockList, { IBlock } from "../BlockList";
import Button from "../Button";
import FolderList from "../FolderList";
import SearchBar from "../SearchBar";
import classes from "./classes.module.scss";
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
import { AppRuleActions, AppRuleNames } from "@Front/Api/Entities/rule";
type IProps = {
folders: IDashBoardFolder[];
onSelectedFolder?: (folder: IDashBoardFolder) => void;
folders: OfficeFolder[];
isArchived: boolean;
onSelectedFolder?: (folder: OfficeFolder) => void;
onCloseLeftSide?: () => void;
};
type IState = {
filteredFolders: IDashBoardFolder[];
type IPropsClass = IProps & {
router: NextRouter;
selectedFolder: string;
};
export default class FolderListContainer extends React.Component<IProps, IState> {
public constructor(props: IProps) {
type IState = {
filteredFolders: OfficeFolder[];
};
class FolderListContainerClass extends React.Component<IPropsClass, IState> {
private redirectPath: string = this.props.isArchived
? Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.pages.FolderInformation.props.path
: Module.getInstance().get().modules.pages.Folder.pages.FolderInformation.props.path;
public constructor(props: IPropsClass) {
super(props);
this.state = {
filteredFolders: this.props.folders,
};
this.filterFolders = this.filterFolders.bind(this);
this.onSelectedFolder = this.onSelectedFolder.bind(this);
}
public override render(): JSX.Element {
const navigatePath = "/folder/create";
const navigatePath = Module.getInstance().get().modules.pages.Folder.pages.CreateFolder.props.path;
return (
<div className={classes["root"]}>
<div>
<div className={classes["header"]}>
<div className={classes["searchbar"]}>
<SearchBar folders={this.props.folders} onChange={this.filterFolders} placeholder="Chercher un dossier" />
<SearchBar onChange={this.filterFolders} placeholder="Chercher un dossier" />
</div>
<div className={classes["folderlist-container"]}>
<BlockList blocks={this.getBlocks()} onSelectedBlock={this.onSelectedFolder} />
</div>
<FolderList
folders={this.state.filteredFolders}
onSelectedFolder={this.props.onSelectedFolder && this.props.onSelectedFolder}
onCloseLeftSide={this.props.onCloseLeftSide}
/>
</div>
<div>
<Link href={navigatePath}>
<Button fullwidth={true}>Créer un dossier</Button>
</Link>
</div>
{!this.props.isArchived && (
<div>
<Rules
mode={RulesMode.NECESSARY}
rules={[
{
action: AppRuleActions.create,
name: AppRuleNames.officeFolders,
},
]}>
<Link href={navigatePath}>
<Button fullwidth={true}>Créer un dossier</Button>
</Link>
</Rules>
</div>
)}
</div>
);
}
private filterFolders(folders: IDashBoardFolder[]): IDashBoardFolder[] {
this.setState({ filteredFolders: folders });
return folders;
private getBlocks(): IBlock[] {
const pendingFolders = this.props.folders
.filter((folder) => {
const pendingDocuments = (folder.documents ?? []).filter(
(document) => document.document_status === EDocumentStatus.DEPOSITED,
);
return pendingDocuments.length >= 1;
})
.sort((folder1, folder2) => {
return folder1.created_at! > folder2.created_at! ? -1 : 1;
});
const otherFolders = this.props.folders
.filter((folder) => {
const pendingDocuments = (folder.documents ?? []).filter(
(document) => document.document_status === EDocumentStatus.DEPOSITED,
);
return pendingDocuments.length === 0;
})
.sort((folder1, folder2) => {
return folder1.created_at! > folder2.created_at! ? -1 : 1;
});
return [...pendingFolders, ...otherFolders].map((folder) => {
return {
id: folder.uid!,
name: folder.folder_number! + " - " + folder.name!,
selected: this.props.selectedFolder === folder.uid,
};
});
}
private onSelectedFolder(block: IBlock) {
const folder = this.props.folders.find((folder) => folder.uid === block.id);
if (!folder) return;
this.props.onSelectedFolder && this.props.onSelectedFolder(folder);
const path = this.redirectPath.replace("[folderUid]", folder.uid ?? "");
this.props.router.push(path);
}
private filterFolders(value: string): void {
const filteredFolders: OfficeFolder[] = this.props.folders.filter((folder) => {
const name = folder.name.toLowerCase();
const number = folder.folder_number.toLowerCase();
if (folder.customers) {
const customerNames = folder.customers
.map((customer) => {
return `${customer.contact?.first_name.toLowerCase()} ${customer.contact?.last_name.toLowerCase()}`;
})
.join(", ");
return name.includes(value) || number.includes(value) || customerNames.includes(value);
}
return name.includes(value) || number.includes(value);
});
this.setState({ filteredFolders });
}
}
export default function FolderListContainer(props: IProps) {
const router = useRouter();
const { folderUid } = router.query;
return <FolderListContainerClass {...props} router={router} selectedFolder={folderUid as string} />;
}

View File

@ -0,0 +1,117 @@
import React from "react";
import { FormContext, IFormContext } from ".";
import { ValidationError } from "class-validator";
import Typography, { ITypo, ITypoColor } from "../Typography";
export type IProps = {
value?: string;
onChange?: (event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => void;
name: string;
required?: boolean;
placeholder?: string;
readonly?: boolean;
className?: string;
defaultValue?: string;
disableValidation?: boolean;
validationError?: ValidationError;
disabled?: boolean;
};
type IState = {
value: string;
validationError: ValidationError | null;
};
export default abstract class BaseField<P extends IProps, S extends IState = IState> extends React.Component<P, S> {
public static override contextType = FormContext;
public override context: IFormContext | null = null;
public fieldRef: React.RefObject<any> = React.createRef();
static defaultProps: Partial<IProps> = {
disableValidation: false,
required: true,
};
constructor(props: P) {
super(props);
this.onChange = this.onChange.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onBlur = this.onBlur.bind(this);
this.hasError = this.hasError.bind(this);
this.renderErrors = this.renderErrors.bind(this);
}
public override componentDidMount() {
this.context?.setField(this.props.name, this);
this.setState({
value: this.props.defaultValue ?? "",
});
}
public override componentDidUpdate(prevProps: IProps) {
if (this.props.value !== prevProps.value) {
this.setState({
value: this.props.value ?? "",
});
}
if (this.props.defaultValue !== prevProps.defaultValue) {
this.setState({
value: this.props.defaultValue ?? "",
});
}
if (this.props.validationError !== prevProps.validationError) {
this.setState({
validationError: this.props.validationError ?? null,
});
}
}
public override componentWillUnmount() {
this.context?.unSetField(this.props.name);
}
protected getDefaultState(): IState {
return {
value: this.props.value ?? "",
validationError: this.props.validationError ?? null,
};
}
protected onFocus(event: React.FocusEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) {
this.context?.onFieldFocusChange(this.props.name, this, true);
}
protected onBlur(event: React.FocusEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) {
this.context?.onFieldFocusChange(this.props.name, this, false);
}
protected onChange(event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) {
this.context?.onFieldChange(this.props.name, this);
this.setState({ value: event.currentTarget.value, validationError: null });
if (this.props.onChange) {
this.props.onChange(event);
}
}
protected hasError(): boolean {
return this.state.validationError !== null;
// if(!this.context) return false;
// if(!this.context.hasOneFocusedInput() && this.state.validationError !== null) return true;
// return this.state.validationError !== null && this.context.isInputFocused(this.props.name);
}
protected renderErrors(): JSX.Element[] | null {
if (!this.state.validationError || !this.state.validationError.constraints) return null;
let errors: JSX.Element[] = [];
Object.entries(this.state.validationError.constraints).forEach(([key, value]) => {
errors.push(
<Typography key={key} typo={ITypo.CAPTION_14} color={ITypoColor.RED_FLASH}>
{value}
</Typography>,
);
});
return errors;
}
}

View File

@ -1,154 +0,0 @@
import { ChangeEvent, Component, createRef } from "react";
import { FormContext, IFormContext } from "..";
// elements
import Validators, { IValidationTypes } from "../Validators/Validators";
export type IError = {
message: string;
validator: string;
value: string | number | readonly string[];
args: any[];
isErrored?: (hasError: boolean) => void;
};
export type INewBasefieldProps = {
onChange?: (event: ChangeEvent<HTMLInputElement & HTMLSelectElement & HTMLTextAreaElement>) => void;
name: string;
regex?: RegExp;
onCancel?: () => void;
disableValidation?: boolean;
onErrors?: (errors: IError[]) => void;
fieldRef?: React.RefObject<any>;
};
export type IProps = IValidationTypes & React.InputHTMLAttributes<HTMLInputElement> & INewBasefieldProps;
type IState = {
value?: string | number | readonly string[];
errors: IError[];
};
export default abstract class BaseField<P extends IProps> extends Component<P, IState> {
public static override contextType = FormContext;
public override context: IFormContext | null = null;
public fieldRef: React.RefObject<any> = createRef();
static defaultProps: Partial<IProps> = {
disableValidation: false,
};
constructor(props: P) {
super(props);
this.onChange = this.onChange.bind(this);
this.validate = this.validate.bind(this);
this.state = {
value: this.props.value ?? this.props.defaultValue ?? "",
errors: [],
};
}
public override componentDidMount() {
this.context?.setField(this.props.name, this);
}
public override componentDidUpdate(prevProps: P) {
if (prevProps.value !== this.props.value || prevProps.defaultValue !== this.props.defaultValue) {
this.setState({ value: this.props.value ?? this.props.defaultValue ?? "" });
}
}
public override componentWillUnmount() {
this.context?.unSetField(this.props.name);
}
public async onBlur(event: React.FocusEvent<HTMLInputElement, Element>) {
// this.validate();
// if (this.props.onBlur) {
// this.props.onBlur(event);
// }
}
public async validate(isOnSubmit?: boolean) {
if (this.props.disableValidation) return;
if (this.props.readOnly) return;
const errorArray: IError[] = [];
const props: { [key: string]: any } = this.props;
const validators = Object.entries(Validators).filter(([key]) => props[key]);
const isValidable = isOnSubmit
? this.props.required || (this.state.value && this.state.value !== "")
: this.state.value && this.state.value !== "";
if (isValidable) {
const validations = await Promise.all(
validators.map(async ([key, validator]) => {
const validation = await (validator.validate as any)(this.state.value, ...(props[key].args ?? []));
if (props[key].isErrored) {
props[key].isErrored(!validation);
}
return [key, validator, validation];
}),
);
const unValidateds = validations.filter(([key, validator, validation]) => !validation);
const errors: IError[] = unValidateds.map(([key, unValidated]) => {
let message = unValidated.message;
if (typeof props[key] === "object" && props[key].message) message = props[key].message;
return { message, validator: key, value: this.state.value!, args: props[key].args ?? [] };
});
errorArray.push(...errors);
} else {
validators.forEach(async ([key]) => {
if (props[key].isErrored) {
props[key].isErrored(false);
}
});
}
this.setState({ errors: errorArray });
this.onErrors(errorArray);
return errorArray;
}
public setErrors(errors: IError[]) {
this.setState({ ...this.state, errors });
}
/**
* It is automatically called by the parent form when the user cancelled the
* form and all of its changes.
*
* Override the method for custom cancelling logic, or pass a custom onCancel
* callback.
*/
public cancel() {
if (this.props.onCancel) {
this.props.onCancel();
}
}
public onErrors(errors: IError[]) {
if (this.props.onErrors) {
this.props.onErrors(errors);
}
}
protected onChange(event: ChangeEvent<HTMLInputElement & HTMLSelectElement & HTMLTextAreaElement>) {
if (this.props.regex) {
if (!this.props.regex.test(event.currentTarget.value)) {
event.currentTarget.value = event.currentTarget.value.substring(0, event.currentTarget.value.length - 1);
}
}
this.setState({ value: event.currentTarget.value }, () => {
this.validate();
this.context?.onFieldChange(this.props.name, this);
});
if (this.props.onChange) {
this.props.onChange(event);
}
}
}

View File

@ -1,94 +0,0 @@
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
import { ReactNode } from "react";
import Validators from "../../Validators/Validators";
import BaseField, { IProps as IBaseFieldProps } from "../BaseField";
import classes from "./classes.module.scss";
export type IProps = IBaseFieldProps & {
fakeplaceholder: string;
textarea?: boolean;
};
// @ts-ignore TODO: typing error on IProps (validator class?? cf Massi 22/02/23)
export default class InputField extends BaseField<IProps> {
public override render(): ReactNode {
let pattern;
if (this.props.type === "number") {
pattern = "(^[0-9]*)(\\.{0,1})([0-9]*)$";
}
if (this.props.pattern) {
pattern = this.props.pattern;
}
if (this.props.fieldRef) {
this.fieldRef = this.props.fieldRef;
}
// we always need to control the input so we need to set the value as "" by default
const value = this.state.value ?? "";
if (this.props.textarea === true) {
return (
<Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}>
<div className={classes["root"]}>
<textarea
maxLength={this.props.maxLength}
name={this.props.name}
required={this.props.required}
ref={this.props.fieldRef}
rows={4}
data-value={value}
onChange={this.onChange}
data-has-validation-errors={this.state.errors.length > 0}
className={
this.props.className ? [classes["textarea"], classes[this.props.className]].join(" ") : classes["textarea"]
}
/>
<div className={classes["fake-placeholder"]}>{this.props.fakeplaceholder}</div>
</div>
</Typography>
);
} else {
return (
<Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}>
<div className={classes["root"]}>
<input
{...this.getHtmlAttributes()}
ref={this.props.fieldRef}
pattern={pattern}
onChange={this.onChange}
onBlur={this.onBlur}
data-value={value}
data-has-validation-errors={this.state.errors.length > 0}
className={
this.props.className ? [classes["input"], classes[this.props.className]].join(" ") : classes["input"]
}
/>
<div className={classes["fake-placeholder"]}>{this.props.fakeplaceholder}</div>
</div>
</Typography>
);
}
}
// We filter the props we'll pass to the primitive input as they're useless for it
// It also avoids the console warning because of passing useless props to a primitive DOM element
private getHtmlAttributes() {
const htmlAttributes = { ...this.props };
delete htmlAttributes.disableValidation;
delete htmlAttributes.onErrors;
delete htmlAttributes.fieldRef;
delete htmlAttributes.className;
delete htmlAttributes.defaultValue;
for (const validator in Validators) {
delete (htmlAttributes as { [key: string]: any })[validator];
}
return htmlAttributes;
}
}

View File

@ -1,77 +0,0 @@
import { ReactNode } from "react";
import Validators from "../Validators/Validators";
//import { IProps as IBaseFieldProps } from "../.";
import classes from "./classes.module.scss";
import BaseField, { IProps as IBaseFieldProps } from "../Elements/BaseField";
export type IProps = IBaseFieldProps & {
large?: boolean;
};
export default class InputField extends BaseField<IProps> {
public override render(): ReactNode {
let pattern;
if (this.props.type === "number") {
pattern = "(^[0-9]*)(\\.{0,1})([0-9]*)$";
}
if (this.props.pattern) {
pattern = this.props.pattern;
}
if (this.props.fieldRef) {
this.fieldRef = this.props.fieldRef;
}
// we always need to control the input so we need to set the value as "" by default
const value = this.state.value ?? "";
if (this.props.large === true) {
return (
<textarea
maxLength={this.props.maxLength}
name={this.props.name}
required={this.props.required}
ref={this.props.fieldRef}
rows={4}
value={value}
onChange={this.onChange}
data-has-validation-errors={this.state.errors.length > 0}
className={this.props.className ? [classes["textarea"], classes[this.props.className]].join(" ") : classes["textarea"]}
/>
);
} else {
return (
<input
{...this.getHtmlAttributes()}
ref={this.props.fieldRef}
pattern={pattern}
onChange={this.onChange}
value={value}
data-has-validation-errors={this.state.errors.length > 0}
className={this.props.className ? [classes["input"], classes[this.props.className]].join(" ") : classes["input"]}
/>
);
}
}
// We filter the props we'll pass to the primitive input as they're useless for it
// It also avoids the console warning because of passing useless props to a primitive DOM element
private getHtmlAttributes() {
const htmlAttributes = { ...this.props };
delete htmlAttributes.disableValidation;
delete htmlAttributes.onErrors;
delete htmlAttributes.fieldRef;
delete htmlAttributes.className;
delete htmlAttributes.defaultValue;
for (const validator in Validators) {
delete (htmlAttributes as { [key: string]: any })[validator];
}
return htmlAttributes;
}
}

View File

@ -5,7 +5,18 @@
position: relative;
flex-direction: column;
width: 100%;
border: 1px solid $grey-medium;
border: 1px solid var(--grey-medium);
&[data-errored="true"]{
border: 1px solid var(--red-flash);
}
&[data-disabled="true"]{
.container-label{
cursor: not-allowed;
}
opacity: 0.6;
}
.container-label {
display: flex;
@ -17,6 +28,8 @@
padding: 24px;
z-index: 1;
&[data-border-right-collapsed="true"] {
border-radius: 8px 0 0 8px;
}

View File

@ -0,0 +1,194 @@
import ChevronIcon from "@Assets/Icons/chevron.svg";
import WindowStore from "@Front/Stores/WindowStore";
import { ValidationError } from "class-validator";
import classNames from "classnames";
import Image from "next/image";
import React, { FormEvent, ReactNode } from "react";
import Typography, { ITypo, ITypoColor } from "../../Typography";
import classes from "./classes.module.scss";
type IProps = {
selectedOption?: IOption;
onChange?: (selectedOption: IOption) => void;
options: IOption[];
hasBorderRightCollapsed?: boolean;
placeholder?: string;
className?: string;
name: string;
disabled: boolean;
errors?: ValidationError;
};
export type IOption = {
value: unknown;
label: string;
icon?: ReactNode;
description?: string;
};
type IState = {
isOpen: boolean;
listWidth: number;
listHeight: number;
selectedOption: IOption | null;
errors: ValidationError | null;
};
export default class SelectField extends React.Component<IProps, IState> {
private contentRef = React.createRef<HTMLUListElement>();
private rootRef = React.createRef<HTMLDivElement>();
private removeOnresize = () => {};
static defaultProps = {
disabled: false,
};
constructor(props: IProps) {
super(props);
this.state = {
isOpen: false,
listHeight: 0,
listWidth: 0,
selectedOption: null,
errors: this.props.errors ?? null,
};
this.toggle = this.toggle.bind(this);
this.onSelect = this.onSelect.bind(this);
}
public override render(): JSX.Element {
const selectedOption = this.state.selectedOption ?? this.props.selectedOption;
return (
<div className={classes["container"]}>
<div
className={classNames(classes["root"], this.props.className)}
ref={this.rootRef}
data-disabled={this.props.disabled.toString()}
data-errored={(this.state.errors !== null).toString()}>
{selectedOption && <input type="text" defaultValue={selectedOption.value as string} name={this.props.name} hidden />}
<label
className={classNames(classes["container-label"])}
data-open={this.state.isOpen}
onClick={this.toggle}
data-border-right-collapsed={this.props.hasBorderRightCollapsed}>
<div className={classNames(classes["container-input"])}>
{selectedOption && (
<>
<span className={classNames(classes["icon"], classes["token-icon"])}>{selectedOption?.icon}</span>
<Typography typo={ITypo.P_18}>
<span className={classes["text"]}>{selectedOption?.label}</span>
</Typography>
</>
)}
{!selectedOption && (
<div className={classes["placeholder"]} data-open={(selectedOption ? true : false).toString()}>
<Typography typo={ITypo.NAV_INPUT_16}>
<span className={classes["text"]}>{this.props.placeholder ?? ""}</span>
</Typography>
</div>
)}
</div>
<Image className={classes["chevron-icon"]} data-open={this.state.isOpen} src={ChevronIcon} alt="chevron icon" />
</label>
<ul
className={classes["container-ul"]}
data-open={this.state.isOpen}
ref={this.contentRef}
style={{
height: this.state.listHeight + "px",
}}>
{this.props.options.map((option, index) => (
<li
key={`${index}-${option.value}`}
className={classes["container-li"]}
onClick={(e) => this.onSelect(option, e)}>
<div className={classes["token-icon"]}>{option.icon}</div>
<Typography typo={ITypo.P_18}>{option.label}</Typography>
</li>
))}
</ul>
{this.state.isOpen && <div className={classes["backdrop"]} onClick={this.toggle} />}
</div>
{this.state.errors !== null && <div className={classes["errors-container"]}>{this.renderErrors()}</div>}
</div>
);
}
public override componentDidUpdate(prevProps: IProps) {
if (this.props.errors !== prevProps.errors) {
this.setState({
errors: this.props.errors ?? null,
});
}
if (this.props.selectedOption !== prevProps.selectedOption) {
this.setState({
selectedOption: this.props.selectedOption ?? null,
});
}
}
static getDerivedStateFromProps(props: IProps, state: IState) {
if (props.selectedOption?.value) {
return {
value: props.selectedOption?.value,
};
}
return null;
}
public override componentDidMount(): void {
this.onResize();
this.removeOnresize = WindowStore.getInstance().onResize(() => this.onResize());
}
public override componentWillUnmount() {
this.removeOnresize();
}
private onResize() {
let listHeight = 0;
let listWidth = 0;
listWidth = this.rootRef.current?.scrollWidth ?? 0;
if (this.state.listHeight) {
listHeight = this.contentRef.current?.scrollHeight ?? 0;
}
this.setState({ listHeight, listWidth });
}
private toggle(e: FormEvent) {
if (this.props.disabled) return;
e.preventDefault();
let listHeight = 0;
let listWidth = this.rootRef.current?.scrollWidth ?? 0;
if (!this.state.listHeight) {
listHeight = this.contentRef.current?.scrollHeight ?? 0;
}
this.setState((state) => {
return { isOpen: !state.isOpen, listHeight, listWidth };
});
}
private onSelect(option: IOption, e: React.MouseEvent<HTMLLIElement, MouseEvent>) {
if (this.props.disabled) return;
this.props.onChange && this.props.onChange(option);
this.setState({
selectedOption: option,
});
this.toggle(e);
}
private renderErrors(): JSX.Element | null {
if (!this.state.errors) return null;
return (
<Typography typo={ITypo.CAPTION_14} color={ITypoColor.RED_FLASH}>
{this.props.placeholder} est requis
</Typography>
);
}
}

View File

@ -3,17 +3,6 @@
.root {
position: relative;
textarea {
resize: none;
height: auto;
box-sizing: border-box;
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-size: 18px;
line-height: 22px;
}
.input {
z-index: 1;
display: flex;
@ -23,7 +12,11 @@
gap: 10px;
width: 100%;
height: 70px;
border: 1px solid $grey-medium;
border: 1px solid var(--grey-medium);
&:disabled {
cursor: not-allowed;
}
&:focus {
~ .fake-placeholder {
@ -61,7 +54,6 @@
&::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
-moz-appearance: textfield;
}
/* For IE 10+ */
@ -88,41 +80,23 @@
transition: transform 0.3s ease-in-out;
}
}
}
.textarea {
z-index: 1;
display: flex;
flex-direction: row;
align-items: center;
padding: 24px;
gap: 10px;
&[data-is-errored="true"] {
.input {
border: 1px solid var(--red-flash);
~ .fake-placeholder {
color: var(--red-flash);
}
}
}
width: 100%;
height: 70px;
border: 1px solid $grey-medium;
~ .fake-placeholder {
z-index: 2;
top: -12px;
margin-left: 8px;
padding: 0 16px;
pointer-events: none;
.copy-icon {
cursor: pointer;
height: 24px;
width: 24px;
position: absolute;
background: $white;
transform: translateY(35px);
transition: transform 0.3s ease-in-out;
}
&:focus {
~ .fake-placeholder {
transform: translateY(0px);
}
}
&:not([data-value=""]) {
~ .fake-placeholder {
transform: translateY(0px);
}
top: 50%;
right: 24px;
transform: translate(0, -50%);
}
}

View File

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

View File

@ -0,0 +1,63 @@
@import "@Themes/constants.scss";
.root {
position: relative;
.textarea {
resize: none;
height: auto;
box-sizing: border-box;
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-size: 18px;
line-height: 22px;
&:read-only{
cursor: not-allowed;
}
z-index: 1;
display: flex;
flex-direction: row;
align-items: center;
padding: 24px;
gap: 10px;
width: 100%;
height: 100px;
border: 1px solid var(--grey-medium);
~ .fake-placeholder {
z-index: 2;
top: -12px;
margin-left: 8px;
padding: 0 16px;
pointer-events: none;
position: absolute;
background: $white;
transform: translateY(35px);
transition: transform 0.3s ease-in-out;
}
&:focus {
~ .fake-placeholder {
transform: translateY(0px);
}
}
&:not([data-value=""]) {
~ .fake-placeholder {
transform: translateY(0px);
}
}
}
&[data-is-errored="true"] {
.textarea {
border: 1px solid var(--red-flash);
~ .fake-placeholder {
color: var(--red-flash);
}
}
}
}

View File

@ -0,0 +1,47 @@
import BaseField, { IProps as IBaseFieldProps } from "../BaseField";
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
import React from "react";
import { ReactNode } from "react";
import classes from "./classes.module.scss";
import classnames from "classnames";
export type IProps = IBaseFieldProps & {};
export default class TextAreaField extends BaseField<IProps> {
constructor(props: IProps) {
super(props);
this.state = this.getDefaultState();
}
public override render(): ReactNode {
const value = this.state.value ?? "";
return (
<Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}>
<div className={classes["root"]} data-is-errored={this.hasError().toString()}>
<textarea
name={this.props.name}
rows={4}
data-value={value}
onChange={this.onChange}
className={classnames(classes["textarea"], this.props.className)}
value={value}
readOnly={this.props.readonly}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
<div className={classes["fake-placeholder"]}>
{this.props.placeholder} {!this.props.required && " (Facultatif)"}
</div>
{this.hasError() && <div className={classes["errors-container"]}>{this.renderErrors()}</div>}
</div>
</Typography>
);
}
public override componentDidMount() {
this.setState({
value: this.props.defaultValue ?? "",
});
}
}

View File

@ -1,180 +0,0 @@
import { isEmail, isNotEmpty, isNumberString, isString, max, maxLength, min, minLength } from "class-validator";
const Validators = {
/**
* **Parameters** : boolean
*
* This validator verifies the value is not empty
*/
required: { validate: isNotEmpty, message: "validation_messages.field_required" },
/**
* **Parameters** : boolean
*
* This validator verifies the value is a number
*/
numbersOnly: { validate: isNumberString, message: "validation_messages.only_numbers" },
/**
* **Parameters** : boolean
*
* This validator verifies the value is a number
*/
intOnly: {
validate: function (value: string) {
const regex = /^[0-9]*$/;
return regex.test(value);
},
message: "validation_messages.only_integers",
},
/**
* **Parameters** : number
*
* This validator verifies the number is not below the parameter
*/
minNumber: { validate: (value: string, minVal: number) => min(Number(value), minVal), message: "validation_messages.below_min" },
/**
* **Parameters** : number
*
* This validator verifies the number is not above the parameter
*/
maxNumber: { validate: (value: string, maxVal: number) => max(Number(value), maxVal), message: "validation_messages.above_max" },
/**
* **Parameters** : number
*
* This validator verifies the string minimum length is conform to the parameter
*/
minLength: {
validate: minLength,
message: "validation_messages.min_string_length",
},
/**
* **Parameters** : number
*
* This validator verifies the string maximum length is conform to the parameter
*/
maxLength: {
validate: maxLength,
message: "validation_messages.max_string_length",
},
/**
* **Parameters** : boolean
*
* This validator verifies the input's value is a string.
*/
isString: { validate: (value: string) => isString(value), message: "validation_messages.only_letters" },
/**
* **Parameters** : boolean
*
* This validator verifies the input's value is conform to the tag regex.
*/
isTag: {
validate: function (value: string) {
const regex = /^[a-zA-Z0-9][a-zA-Z0-9 ]*(,[a-zA-Z0-9][a-zA-Z0-9 ]*)*$/;
const isValid = regex.test(value);
if (!isValid) return false;
const splittedTag = value.split(",");
if (splittedTag.length !== new Set(splittedTag).size) {
return false;
}
return true;
},
message: "validation_messages.not_valid_tag",
},
/**
* **Parameters** : boolean
*
* This validator verifies the input's value is a valid email.
*
* If the **input is empty, it is considered valid**. If you do not wish this
* to happen please refer to the `required` validator.
*/
isEmail: {
validate: (value: string) => (Boolean(value) ? isEmail(value) : true),
message: "validation_messages.invalid_email",
},
isPseudo: {
validate: (value: string) => {
const pseudoRegex = /^[a-zA-Z][a-zA-Z0-9_-]{2,19}$/;
return pseudoRegex.test(value);
},
message: "validation_messages.is_pseudo",
},
noSpaceInString: {
validate: (value: string) => {
const regex = /^\S*$/;
return regex.test(value);
},
message: "validation_messages.no_space_in_string",
},
isPositiveNumber: {
validate: (value: string) => {
let nbr = parseFloat(value);
return !(isNaN(nbr) || nbr <= 0);
},
message: "validation_messages.positive_number",
},
floatPrecision: {
validate: (value: string, precision: number) => {
// If value is not a float
if (isNaN(parseFloat(value))) return false;
let splittedValue = value.split(".");
// If there is no decimals
if (!splittedValue[1]) return true;
// If there is more decimals than the required precision
if (splittedValue[1].length > precision) return false;
return true;
},
message: "validation_messages.float_precision",
},
isUrl: {
validate: (value: string, root: string | string[]) => {
try {
const url = new URL(value);
if (root) {
if (typeof root === "string") {
return url.hostname === root || url.hostname === `www.${root}`;
} else {
return root.some((r) => url.hostname === r || url.hostname === `www.${r}`);
}
}
return true;
} catch (e) {
return false;
}
},
message: "validation_messages.invalid_url",
},
// isUniqueEmail: { TODO : uncomment and implement DB request
// validate: async (value: string, actual: string) => {
// try {
// const users = await AppUser.getInstance().getUsers({email: value});
// if (!users.metadata.count) return true;
// if (users.data.length > 1) return false;
// if (users.data[0]?.email === actual) return true;
// return false;
// } catch {
// return true;
// }
// },
// message: "validation_messages.unique_email",
// },
};
export default Validators;
export type IValidationTypes = Partial<
Record<keyof typeof Validators, boolean | Partial<{ message: string; args: any[]; isErrored: (errored: boolean) => void }>>
>;

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