Super admin page users

This commit is contained in:
Maxime Lalo 2023-07-18 13:36:25 +02:00
parent a5d74176d5
commit 9332016dc1
16 changed files with 753 additions and 2 deletions

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

@ -9,7 +9,7 @@ import Image from "next/image";
import React, { ReactNode } from "react";
import classes from "./classes.module.scss";
import Users, { IGetUsersparams } from "@Front/Api/LeCoffreApi/SuperAdmin/Users/Users";
import Users, { IGetUsersparams } from "@Front/Api/LeCoffreApi/Admin/Users/Users";
import User from "le-coffre-resources/dist/Notary";
import CollaboratorListContainer from "./CollaboratorListContainer";
@ -88,7 +88,7 @@ export default class DefaultCollaboratorDashboard extends React.Component<IProps
public override async componentDidMount() {
this.onWindowResize = WindowStore.getInstance().onResize((window) => this.onResize(window));
const query: IGetUsersparams = {
where: { office_uid: "6981326f-8a0a-4437-b15c-4cd5c4d80f6e" },
where: { office_uid: "2af8694e-4dac-4e0d-854a-acb7fed8aa7d" },
include: { contact: true },
};

View File

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

View File

@ -0,0 +1,64 @@
import BlockList, { IBlock } from "@Front/Components/DesignSystem/BlockList";
import SearchBar from "@Front/Components/DesignSystem/SearchBar";
import Module from "@Front/Config/Module";
import User from "le-coffre-resources/dist/Notary";
import { useRouter } from "next/router";
import React, { useCallback, useState } from "react";
import classes from "./classes.module.scss";
type IProps = {
users: User[];
onSelectedUser?: (user: User) => void;
onCloseLeftSide?: () => void;
};
export default function UserListContainer(props: IProps) {
const [filteredUsers, setFilteredUsers] = useState<User[]>(props.users);
const router = useRouter();
const { userUid } = router.query;
const filterUsers = useCallback(
(input: string) => {
const filteredUsers = props.users.filter((user) => {
return (
user.contact?.first_name?.toLowerCase().includes(input.toLowerCase()) ||
user.contact?.last_name?.toLowerCase().includes(input.toLowerCase())
);
});
setFilteredUsers(filteredUsers);
},
[props.users],
);
const onSelectedBlock = useCallback(
(block: IBlock) => {
props.onCloseLeftSide && props.onCloseLeftSide();
const redirectPath = Module.getInstance().get().modules.pages.Users.pages.UsersInformations.props.path;
router.push(redirectPath.replace("[uid]", block.id));
},
[props, router],
);
return (
<div className={classes["root"]}>
<div className={classes["header"]}>
<div className={classes["searchbar"]}>
<SearchBar onChange={filterUsers} placeholder="Chercher un utilisateur" />
</div>
<div className={classes["folderlist-container"]}>
<BlockList
blocks={filteredUsers.map((user) => {
return {
name: user.contact?.first_name + " " + user.contact?.last_name,
id: user.uid!,
selected: user.uid === userUid,
};
})}
onSelectedBlock={onSelectedBlock}
/>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,117 @@
@import "@Themes/constants.scss";
@keyframes growWidth {
0% {
width: 100%;
}
100% {
width: 200%;
}
}
.root {
.content {
display: flex;
overflow: hidden;
height: calc(100vh - 83px);
.overlay {
position: absolute;
width: 100%;
height: 100%;
background-color: var(--white);
opacity: 0.5;
z-index: 2;
transition: all 0.3s $custom-easing;
}
.left-side {
background-color: $white;
z-index: 3;
display: flex;
width: 389px;
min-width: 389px;
transition: all 0.3s $custom-easing;
overflow: hidden;
@media (max-width: ($screen-m - 1px)) {
width: 56px;
min-width: 56px;
transform: translateX(-389px);
&.opened {
transform: translateX(0px);
width: 389px;
min-width: 389px;
}
}
@media (max-width: $screen-s) {
width: 0px;
min-width: 0px;
&.opened {
width: 100vw;
min-width: 100vw;
}
}
}
.closable-left-side {
position: absolute;
background-color: $white;
z-index: 0;
display: flex;
justify-content: center;
min-width: 56px;
max-width: 56px;
height: calc(100vh - 83px);
border-right: 1px $grey-medium solid;
@media (min-width: $screen-m) {
display: none;
}
.chevron-icon {
margin-top: 21px;
transform: rotate(180deg);
cursor: pointer;
}
@media (max-width: $screen-s) {
display: none;
}
}
.right-side {
min-width: calc(100vw - 389px);
padding: 64px 48px;
overflow-y: auto;
@media (max-width: ($screen-m - 1px)) {
min-width: calc(100vw - 56px);
}
@media (max-width: $screen-s) {
padding: 40px 16px 64px 16px;
flex: 1;
min-width: unset;
}
.back-arrow-mobile {
display: none;
@media (max-width: $screen-s) {
display: block;
margin-bottom: 24px;
}
}
.back-arrow-desktop {
@media (max-width: $screen-s) {
display: none;
}
}
}
}
}

View File

@ -0,0 +1,115 @@
import ChevronIcon from "@Assets/Icons/chevron.svg";
import Users, { IGetUsersparams } from "@Front/Api/LeCoffreApi/SuperAdmin/Users/Users";
import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button";
import Header from "@Front/Components/DesignSystem/Header";
import Version from "@Front/Components/DesignSystem/Version";
import BackArrow from "@Front/Components/Elements/BackArrow";
import WindowStore from "@Front/Stores/WindowStore";
import classNames from "classnames";
import User from "le-coffre-resources/dist/Notary";
import Image from "next/image";
import React, { ReactNode } from "react";
import classes from "./classes.module.scss";
import UserListContainer from "./UserListContainer";
type IProps = {
title: string;
children?: ReactNode;
onSelectedUser: (user: User) => void;
hasBackArrow: boolean;
backArrowUrl?: string;
mobileBackText?: string;
};
type IState = {
users: User[] | null;
isLeftSideOpen: boolean;
leftSideCanBeClosed: boolean;
};
export default class DefaultUserDashboard extends React.Component<IProps, IState> {
private onWindowResize = () => {};
public static defaultProps: Partial<IProps> = {
hasBackArrow: false,
};
public constructor(props: IProps) {
super(props);
this.state = {
users: null,
isLeftSideOpen: false,
leftSideCanBeClosed: typeof window !== "undefined" ? window.innerWidth < 1024 : false,
};
this.onOpenLeftSide = this.onOpenLeftSide.bind(this);
this.onCloseLeftSide = this.onCloseLeftSide.bind(this);
}
public override render(): JSX.Element {
return (
<div className={classes["root"]}>
<Header isUserConnected={true} />
<div className={classes["content"]}>
{this.state.isLeftSideOpen && <div className={classes["overlay"]} onClick={this.onCloseLeftSide} />}
<div className={classNames(classes["left-side"], this.state.isLeftSideOpen && classes["opened"])}>
{this.state.users && <UserListContainer users={this.state.users} onCloseLeftSide={this.onCloseLeftSide} />}
</div>
<div className={classNames(classes["closable-left-side"])}>
<Image alt="open side menu" src={ChevronIcon} className={classes["chevron-icon"]} onClick={this.onOpenLeftSide} />
</div>
<div className={classes["right-side"]}>
{this.props.hasBackArrow && (
<div className={classes["back-arrow-desktop"]}>
<BackArrow url={this.props.backArrowUrl ?? ""} />
</div>
)}
{this.props.mobileBackText && (
<div className={classes["back-arrow-mobile"]}>
<Button
icon={ChevronIcon}
iconposition={"left"}
iconstyle={{ transform: "rotate(180deg)", width: "22px", height: "22px" }}
variant={EButtonVariant.LINE}
onClick={this.onOpenLeftSide}>
{this.props.mobileBackText ?? "Retour"}
</Button>
</div>
)}
{this.props.children}
</div>
</div>
<Version />
</div>
);
}
public override async componentDidMount() {
this.onWindowResize = WindowStore.getInstance().onResize((window) => this.onResize(window));
const query: IGetUsersparams = {
include: { contact: true },
};
const users = await Users.getInstance().get(query);
this.setState({ users });
}
public override componentWillUnmount() {
this.onWindowResize();
}
private onOpenLeftSide() {
this.setState({ isLeftSideOpen: true });
}
private onCloseLeftSide() {
if (!this.state.leftSideCanBeClosed) return;
this.setState({ isLeftSideOpen: false });
}
private onResize(window: Window) {
if (window.innerWidth > 1023) {
if (!this.state.leftSideCanBeClosed) return;
this.setState({ leftSideCanBeClosed: false });
}
this.setState({ leftSideCanBeClosed: true });
}
}

View File

@ -0,0 +1,54 @@
@import "@Themes/constants.scss";
.root {
.user-infos {
background-color: var(--grey-soft);
display: flex;
justify-content: space-between;
padding: 24px;
margin-top: 32px;
@media (max-width: $screen-l) {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 32px;
}
@media (max-width: $screen-s) {
grid-template-columns: repeat(1, 1fr);
}
.user-infos-row {
display: flex;
flex-direction: column;
gap: 12px;
}
}
.role-container {
padding: 32px 16px;
border: 1px solid var(--grey);
margin-top: 32px;
display: flex;
gap: 32px;
.part {
flex: 1;
.first-line {
display: flex;
justify-content: space-between;
}
.second-line {
margin-top: 32px;
display: flex;
gap: 16px;
flex-direction: column;
}
}
.third-line {
margin-top: 32px;
}
}
}

View File

@ -0,0 +1,117 @@
import Users from "@Front/Api/LeCoffreApi/SuperAdmin/Users/Users";
import CheckBox from "@Front/Components/DesignSystem/CheckBox";
import SelectField, { IOption } from "@Front/Components/DesignSystem/Form/SelectField";
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
import DefaultUserDashboard from "@Front/Components/LayoutTemplates/DefaultUserDashboard";
import User from "le-coffre-resources/dist/Notary";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import classes from "./classes.module.scss";
import OfficeRoles from "@Front/Api/LeCoffreApi/Admin/OfficeRoles/OfficeRoles";
type IProps = {};
export default function UserInformations(props: IProps) {
const router = useRouter();
let { userUid } = router.query;
const [userSelected, setUserSelected] = useState<User | null>(null);
const [availableRoles, setAvailableRoles] = useState<IOption[]>([]);
useEffect(() => {
async function getUser() {
if (!userUid) return;
const user = await Users.getInstance().getByUid(userUid as string, {
q: {
contact: true,
office_role: true,
},
});
if (!user) return;
const roles = await OfficeRoles.getInstance().get();
if (!roles) return;
setAvailableRoles(roles.map((role) => ({ value: role.uid, label: role.name })));
setUserSelected(user);
}
getUser();
}, [userUid]);
return (
<DefaultUserDashboard mobileBackText={"Liste des collaborateurs"}>
<div className={classes["root"]}>
<div className={classes["folder-header"]}>
<Typography typo={ITypo.H1Bis}>{userSelected?.contact?.first_name + " " + userSelected?.contact?.last_name}</Typography>
</div>
<div className={classes["user-infos"]}>
<div className={classes["user-infos-row"]}>
<Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}>
Nom
</Typography>
<Typography typo={ITypo.P_18}>{userSelected?.contact?.first_name}</Typography>
</div>
<div className={classes["user-infos-row"]}>
<Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}>
Prénom
</Typography>
<Typography typo={ITypo.P_18}>{userSelected?.contact?.last_name}</Typography>
</div>
<div className={classes["user-infos-row"]}>
<Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}>
Numéro de téléphone
</Typography>
<Typography typo={ITypo.P_18}>{userSelected?.contact?.phone_number}</Typography>
</div>
<div className={classes["user-infos-row"]}>
<Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}>
Email
</Typography>
<Typography typo={ITypo.P_18}>{userSelected?.contact?.email}</Typography>
</div>
</div>
<div className={classes["role-container"]}>
<div className={classes["part"]}>
<div className={classes["first-line"]}>
<Typography typo={ITypo.P_SB_18}>Rôle au sein de son office</Typography>
</div>
<div className={classes["second-line"]}>
<SelectField
placeholder="Rôle"
name="role"
options={availableRoles}
selectedOption={{
value: userSelected?.office_role?.uid,
label: userSelected?.office_role?.name!,
}}
/>
</div>
</div>
<div className={classes["part"]}>
<div className={classes["first-line"]}>
<Typography typo={ITypo.P_SB_18}>Attribuer un titre</Typography>
</div>
<div className={classes["second-line"]}>
<CheckBox
option={{
label: "Nommer admin de son office",
value: "title",
}}
name="admin"
toolTip="tooltip"
/>
<CheckBox
option={{
label: "Nommer super admin LEcoffre.io",
value: "title",
}}
name="superadmin"
toolTip="tooltip"
/>
</div>
</div>
</div>
</div>
</DefaultUserDashboard>
);
}

View File

@ -0,0 +1,72 @@
@import "@Themes/constants.scss";
.root {
display: flex;
align-items: center;
flex-direction: column;
min-height: 100%;
.no-folder-selected {
width: 100%;
.choose-a-folder {
margin-top: 96px;
text-align: center;
}
}
.folder-informations {
width: 100%;
min-height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: column;
flex-grow: 1;
.folder-header {
width: 100%;
.header {
margin-bottom: 32px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
}
.second-box {
margin-top: 24px;
margin-bottom: 32px;
}
.progress-bar {
margin-bottom: 32px;
}
.button-container {
width: 100%;
text-align: center;
:first-child {
margin-right: 12px;
}
> * {
margin: auto;
}
@media (max-width: $screen-m) {
:first-child {
margin-right: 0;
margin-bottom: 12px;
}
> * {
width: 100%;
}
}
}
.modal-title {
margin-bottom: 24px;
}
}
}

View File

@ -0,0 +1,26 @@
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
import BasePage from "../Base";
import classes from "./classes.module.scss";
import DefaultUserDashboard from "@Front/Components/LayoutTemplates/DefaultUserDashboard";
type IProps = {};
type IState = {};
export default class Users extends BasePage<IProps, IState> {
public override render(): JSX.Element {
return (
<DefaultUserDashboard title={"Dossier"} mobileBackText={"Liste des utilisateurs"}>
<div className={classes["root"]}>
<div className={classes["no-folder-selected"]}>
<Typography typo={ITypo.H1Bis}>Informations des utilisateurs</Typography>
<div className={classes["choose-a-folder"]}>
<Typography typo={ITypo.P_18} color={ITypoColor.GREY}>
Sélectionnez un utilisateur
</Typography>
</div>
</div>
</div>
</DefaultUserDashboard>
);
}
}

View File

@ -181,6 +181,22 @@
}
}
},
"Users": {
"enabled": true,
"props": {
"path": "/users",
"labelKey": "users"
},
"pages": {
"UsersInformations": {
"enabled": true,
"props": {
"path": "/users/[uid]",
"labelKey": "users_informations"
}
}
}
},
"404": {
"enabled": true,
"props": {

View File

@ -181,6 +181,22 @@
}
}
},
"Users": {
"enabled": true,
"props": {
"path": "/users",
"labelKey": "users"
},
"pages": {
"UsersInformations": {
"enabled": true,
"props": {
"path": "/users/[uid]",
"labelKey": "users_informations"
}
}
}
},
"404": {
"enabled": true,
"props": {

View File

@ -181,6 +181,22 @@
}
}
},
"Users": {
"enabled": true,
"props": {
"path": "/users",
"labelKey": "users"
},
"pages": {
"UsersInformations": {
"enabled": true,
"props": {
"path": "/users/[uid]",
"labelKey": "users_informations"
}
}
}
},
"404": {
"enabled": true,
"props": {

View File

@ -181,6 +181,22 @@
}
}
},
"Users": {
"enabled": true,
"props": {
"path": "/users",
"labelKey": "users"
},
"pages": {
"UsersInformations": {
"enabled": true,
"props": {
"path": "/users/[uid]",
"labelKey": "users_informations"
}
}
}
},
"404": {
"enabled": true,
"props": {

View File

@ -0,0 +1,5 @@
import UserInformations from "@Front/Components/Layouts/Users/UserInformations";
export default function Route() {
return <UserInformations />;
}

View File

@ -0,0 +1,5 @@
import Users from "@Front/Components/Layouts/Users";
export default function Route() {
return <Users />;
}