From c8ebcc1755726a1f9de105493e760f06f563c201 Mon Sep 17 00:00:00 2001 From: Artur T <81235055+a-trzewik@users.noreply.github.com> Date: Tue, 27 Jul 2021 11:21:21 +0200 Subject: [PATCH] Feat/user mgnt (#200) * user mgnt without api * confirm dialogs * validation * api enabled - no subgroups * ui fixes * fix user edit after goup delete * sub groups handling * fix user create * fix label parent group --- src/api.tsx | 157 ++++++++ src/assets/SCSS/custom.scss | 3 + src/assets/i18n/de/translation.json | 4 +- src/assets/i18n/en/translation.json | 3 +- src/assets/images/icon_add.svg | 8 + src/assets/images/icon_delete.svg | 8 + src/assets/images/icon_edit.svg | 8 + src/components/confirm-modal.component.tsx | 73 ++++ src/components/group-modal.component.tsx | 192 +++++++++ src/components/landing-page.component.tsx | 22 +- .../modules/form-group.component.tsx | 28 ++ src/components/user-management.component.tsx | 369 ++++++++++++++++++ src/components/user-modal.component.tsx | 170 ++++++++ src/misc/useNavigation.tsx | 6 +- src/misc/user.tsx | 55 +++ src/routing.component.tsx | 9 + 16 files changed, 1106 insertions(+), 9 deletions(-) create mode 100644 src/assets/images/icon_add.svg create mode 100644 src/assets/images/icon_delete.svg create mode 100644 src/assets/images/icon_edit.svg create mode 100644 src/components/confirm-modal.component.tsx create mode 100644 src/components/group-modal.component.tsx create mode 100644 src/components/user-management.component.tsx create mode 100644 src/components/user-modal.component.tsx create mode 100644 src/misc/user.tsx diff --git a/src/api.tsx b/src/api.tsx index 17f5e9f..a32db63 100644 --- a/src/api.tsx +++ b/src/api.tsx @@ -29,6 +29,7 @@ import StatisticData from './misc/statistic-data'; import ITestResult from './misc/test-result'; import IQTArchiv from './misc/qt-archiv'; import { Sex, TestResult } from './misc/enum'; +import { IUser, IGroupDetails } from './misc/user'; export const api = axios.create({ baseURL: '' @@ -502,3 +503,159 @@ export const useGetPDF = (hash: string | undefined, onSuccess?: (status: number) return result; } + +export const useGetUsers = (onError?: (error: any) => void) => { + const { keycloak, initialized } = useKeycloak(); + const [result, setResult] = React.useState(); + + const refreshUsers = () => { + const header = { + "Authorization": initialized ? `Bearer ${keycloak.token}` : "", + 'Content-Type': 'application/json' + }; + const uri = '/api/usermanagement/users'; + + api.get(uri, { headers: header }) + .then(response => { + setResult(response.data); + }) + .catch(error => { + if (onError) { + onError(error); + } + }); + } + + React.useEffect(refreshUsers, []); + + return [result, refreshUsers]; +} + +export const createUser = (user: IUser, token: string) => { + const header = { + "Authorization": `Bearer ${token}`, + 'Content-Type': 'application/json' + }; + const uri = '/api/usermanagement/users'; + return api.post(uri, JSON.stringify(user), { headers: header }) +} + +export const deleteUser = (userId: string, token: string) => { + const header = { + "Authorization": `Bearer ${token}`, + 'Content-Type': 'application/json' + }; + const uri = '/api/usermanagement/users/'+userId; + return api.delete(uri, { headers: header }) +} + +export const updateUser = (user: IUser, token: string) => { + const header = { + "Authorization": `Bearer ${token}`, + 'Content-Type': 'application/json' + }; + const uri = '/api/usermanagement/users/'+user.id; + return api.patch(uri, JSON.stringify(user), { headers: header }) +} + +export const useGetGroups = (onError?: (error: any) => void) => { + const { keycloak, initialized } = useKeycloak(); + const [result, setResult] = React.useState(); + + const refreshGroups = () => { + const header = { + "Authorization": initialized ? `Bearer ${keycloak.token}` : "", + 'Content-Type': 'application/json' + }; + const uri = '/api/usermanagement/groups'; + + api.get(uri, { headers: header }) + .then(response => { + setResult(response.data); + }) + .catch(error => { + if (onError) { + onError(error); + } + }); + } + + React.useEffect(refreshGroups, []); + + return [result, refreshGroups]; +} + +export const useGetGroupDetails = (groupReloaded: (group: IGroupDetails) => void, onError?: (error: any) => void) => { + const { keycloak, initialized } = useKeycloak(); + const [result, setResult] = React.useState(); + + const updateGroup = (groupId: string) => { + if (groupId) { + const header = { + "Authorization": initialized ? `Bearer ${keycloak.token}` : "", + 'Content-Type': 'application/json' + }; + const uri = '/api/usermanagement/groups/' + groupId; + + api.get(uri, { headers: header }) + .then(response => { + const group = response.data + groupReloaded(group); + setResult(group); + }) + .catch(error => { + if (onError) { + onError(error); + } + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + } + + return [result, updateGroup, setResult]; +} + +export const createGroup = (group: IGroupDetails, token: string) => { + const header = { + "Authorization": `Bearer ${token}`, + 'Content-Type': 'application/json' + }; + const uri = '/api/usermanagement/groups'; + return api.post(uri, JSON.stringify(group), { headers: header }) +} + +export const updateGroup = (group: IGroupDetails, token: string) => { + const header = { + "Authorization": `Bearer ${token}`, + 'Content-Type': 'application/json' + }; + const uri = '/api/usermanagement/groups/'+group.id; + return api.put(uri, JSON.stringify(group), { headers: header }) +} + +export const deleteGroup = (groupId: string, token: string) => { + const header = { + "Authorization": `Bearer ${token}`, + 'Content-Type': 'application/json' + }; + const uri = '/api/usermanagement/groups/'+groupId; + return api.delete(uri, { headers: header }) +} + +export const addUserToGroup = (userId: string ,groupId: string, token: string) => { + const header = { + "Authorization": `Bearer ${token}`, + 'Content-Type': 'application/json' + }; + const uri = '/api/usermanagement/groups/'+groupId+'/users'; + return api.post(uri,JSON.stringify({userId: userId}), { headers: header }) +} + +export const addGroupAsChild = (childGroupId: string, parentGroupId: string, token: string) => { + const header = { + "Authorization": `Bearer ${token}`, + 'Content-Type': 'application/json' + }; + const uri = '/api/usermanagement/groups/'+parentGroupId+'/subgroups'; + return api.post(uri,JSON.stringify({groupId: childGroupId}), { headers: header }) +} \ No newline at end of file diff --git a/src/assets/SCSS/custom.scss b/src/assets/SCSS/custom.scss index 9e51183..9b75cd2 100644 --- a/src/assets/SCSS/custom.scss +++ b/src/assets/SCSS/custom.scss @@ -369,4 +369,7 @@ hr { position: absolute; left: 30px; top: 6px; +} +.btn-icon { + padding: 0px; } \ No newline at end of file diff --git a/src/assets/i18n/de/translation.json b/src/assets/i18n/de/translation.json index 9a8a952..4403729 100644 --- a/src/assets/i18n/de/translation.json +++ b/src/assets/i18n/de/translation.json @@ -79,5 +79,7 @@ "testId-input-header": "Eingabe Proben-ID", "error-processId-data-load": "Die eingegebene Proben-ID ist uns nicht bekannt!", "ok":"OK", - "change-password":"Passwort ändern" + "change-password":"Passwort ändern", + "dccConsent":"Patient wünscht ein offizielles COVID-Testzertifikat der EU (DCC).", + "user-management":"Benutzerverwaltung" } diff --git a/src/assets/i18n/en/translation.json b/src/assets/i18n/en/translation.json index 5cfcc4a..b7a000f 100644 --- a/src/assets/i18n/en/translation.json +++ b/src/assets/i18n/en/translation.json @@ -76,5 +76,6 @@ "testId-input-header": "Process number input", "error-processId-data-load": "The entered sample ID is not known!", "ok":"OK", - "change-password":"Change Password" + "change-password":"Change Password", + "user-management":"User Management" } \ No newline at end of file diff --git a/src/assets/images/icon_add.svg b/src/assets/images/icon_add.svg new file mode 100644 index 0000000..865ecb1 --- /dev/null +++ b/src/assets/images/icon_add.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/images/icon_delete.svg b/src/assets/images/icon_delete.svg new file mode 100644 index 0000000..65d9eff --- /dev/null +++ b/src/assets/images/icon_delete.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/images/icon_edit.svg b/src/assets/images/icon_edit.svg new file mode 100644 index 0000000..1b0bf62 --- /dev/null +++ b/src/assets/images/icon_edit.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/components/confirm-modal.component.tsx b/src/components/confirm-modal.component.tsx new file mode 100644 index 0000000..2496575 --- /dev/null +++ b/src/components/confirm-modal.component.tsx @@ -0,0 +1,73 @@ +/* + * Corona-Warn-App / cwa-quick-test-frontend + * + * (C) 2021, T-Systems International GmbH + * + * Deutsche Telekom AG and all other contributors / + * copyright owners license this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { Button, Col, Container, Modal, Row } from 'react-bootstrap' + +import '../i18n'; +import { useTranslation } from 'react-i18next'; + +const ConfirmModal = (props: any) => { + const { t } = useTranslation(); + + return ( + + + + + {props.message} + + + + + + + + + + + + + + + ) +} + +export default ConfirmModal; \ No newline at end of file diff --git a/src/components/group-modal.component.tsx b/src/components/group-modal.component.tsx new file mode 100644 index 0000000..5901411 --- /dev/null +++ b/src/components/group-modal.component.tsx @@ -0,0 +1,192 @@ +/* + * Corona-Warn-App / cwa-quick-test-frontend + * + * (C) 2021, T-Systems International GmbH + * + * Deutsche Telekom AG and all other contributors / + * copyright owners license this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { Button, Modal, Form, Col, Row } from 'react-bootstrap' + +import '../i18n'; +import { useTranslation } from 'react-i18next'; +import { FormGroupTextarea, FormGroupInput } from './modules/form-group.component'; +import { IGroupDetails, IGroupNode, IGroup } from '../misc/user'; +import { useGetGroupDetails } from '../api'; +import CwaSpinner from './spinner/spinner.component'; + +const emptyGroup: IGroupDetails = { + id: '', + pocId: '', + name: '', + pocDetails: '', +} + +const GroupModal = (props: any) => { + + const [btnOkDisabled, setBtnOkDisabled] = React.useState(true); + const { t } = useTranslation(); + + const [data, setData] = React.useState(''); + const [validated, setValidated] = React.useState(false); + + const groupReloaded = (group: IGroupDetails) => { + if (group) { + setData(unpackData(group.pocDetails)) + group.parentGroup = props.parentGroupId; + } + setBtnOkDisabled(false); + } + + const [group, updateGroup, setGroup] = useGetGroupDetails(groupReloaded, props.handleError); + + const handleCancel = () => { + props.onCancel(); + } + + const unpackData = (data:string) => { + if (data) { + data = data.replaceAll(',','\n') + } else { + data = '' + } + return data; + } + + const packData = (data:string) => { + if (data) { + data = data.replaceAll('\n',',') + } + return data; + } + + const handleOk = () => { + setBtnOkDisabled(true); + if (props.handleOk) { + group.pocDetails = packData(data); + props.handleOk(group); + } + } + + const handleEnter = () => { + setBtnOkDisabled(false); + if (props.groupId) { + updateGroup(props.groupId); + } else { + setGroup({...emptyGroup}); + setData('') + } + } + + const updateGroupProp = (name:string, value:any) => { + const ngroup = { ...group, [name]: value}; + setGroup(ngroup); + } + + const handleSubmit = (event: React.FormEvent) => { + const form = event.currentTarget; + event.preventDefault(); + event.stopPropagation(); + + setValidated(true); + + if (form.checkValidity()) { + handleOk(); + } + } + + const isNew = !(group && group.id); + + const selfIdOrChildren: string[] = []; + const collectChildren = (idlist: string[], parentNode: IGroup) => { + if (parentNode) { + idlist.push(parentNode.id); + parentNode.children.forEach(child => collectChildren(idlist, child as IGroup)); + } + } + if (!isNew) { + const node = props.groups.find((groupNode: IGroupNode) => groupNode.group.id === group.id); + if (node) { + collectChildren(selfIdOrChildren, node.group); + } + } + const fList = props.groups.filter((groupNode: IGroupNode) => selfIdOrChildren.indexOf(groupNode.group.id)<0) + const groupOptions = fList.map((groupNode: IGroupNode) => + + ); + groupOptions.push(); + + return ( + +
+ + {isNew ? 'Neue Gruppe anlegen' : 'Gruppe bearbeiten'} + + + {!isNew ? + + Übergeordnete Gruppe + + updateGroupProp('parentGroup',ent.target.value)} + > + {groupOptions} + + + : null} + < FormGroupInput controlId='formFirstName' title="Name" + value={group ? group.name : ''} + required + onChange={(evt: any) => updateGroupProp('name',evt.target.value)} + maxLength={50} + /> + < FormGroupInput controlId='formFirstName' title="POC Id" + value={group && group.pocId ? group.pocId : ''} + onChange={(evt: any) => updateGroupProp('pocId',evt.target.value)} + maxLength={50} + /> + < FormGroupTextarea controlId='formLastName' title="Data" + value={data} + onChange={(evt: any) => setData(evt.target.value)} + type='textarea' + maxLength={300} + /> + + + {btnOkDisabled ? : null} + + + +
+
+ ) +} + +export default GroupModal; \ No newline at end of file diff --git a/src/components/landing-page.component.tsx b/src/components/landing-page.component.tsx index 64e8629..cb866db 100644 --- a/src/components/landing-page.component.tsx +++ b/src/components/landing-page.component.tsx @@ -27,12 +27,15 @@ import { useTranslation } from 'react-i18next'; import useNavigation from '../misc/useNavigation'; import CwaSpinner from './spinner/spinner.component'; +import { useKeycloak } from '@react-keycloak/web'; const LandingPage = (props: any) => { const navigation = useNavigation(); const { t } = useTranslation(); + const { keycloak, initialized } = useKeycloak(); + const [isInit, setIsInit] = React.useState(false) React.useEffect(() => { @@ -40,18 +43,25 @@ const LandingPage = (props: any) => { setIsInit(true); }, [navigation]) + const hasRole = (role: string) => keycloak && (keycloak.hasRealmRole(role) || keycloak.hasRealmRole(role)); + return (!isInit ? :

{t('translation:welcome')}

- - - - - - + {hasRole('c19_quick_test_counter') ? + : null} + {hasRole('c19_quick_test_lab') ? + : null} + {hasRole('c19_quick_test_counter') ? + : null} + {hasRole('c19_quick_test_lab') ? + <> + : null} + {hasRole('c19_quick_test_admin') ? + : null}
) diff --git a/src/components/modules/form-group.component.tsx b/src/components/modules/form-group.component.tsx index f2c43a8..a4fca0a 100644 --- a/src/components/modules/form-group.component.tsx +++ b/src/components/modules/form-group.component.tsx @@ -23,6 +23,7 @@ export const FormGroupInput = (props: any) => { type={props.type ? props.type : 'text'} required={props.required} maxLength={props.maxLength} + minLength={props.minLength} min={props.min} max={props.max} pattern={props.pattern} @@ -38,6 +39,32 @@ export const FormGroupInput = (props: any) => { ) } +export const FormGroupTextarea = (props: any) => { + + return (!props ? <> : + + ) +} + + export const FormGroupAddressInput = (props: any) => { return (!props ? <> : @@ -84,6 +111,7 @@ export const FormGroupConsentCkb = (props: any) => { onChange={props.onChange} type={props.type} name={props.name} + disabled={props.readOnly} checked={props.checked} required={props.required} id={props.controlId} diff --git a/src/components/user-management.component.tsx b/src/components/user-management.component.tsx new file mode 100644 index 0000000..f436263 --- /dev/null +++ b/src/components/user-management.component.tsx @@ -0,0 +1,369 @@ +/* + * Corona-Warn-App / cwa-quick-test-frontend + * + * (C) 2021, T-Systems International GmbH + * + * Deutsche Telekom AG and all other contributors / + * copyright owners license this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { Card, Fade, Table, Button, Row, Col } from 'react-bootstrap'; +import CwaSpinner from './spinner/spinner.component'; +import CardHeader from './modules/card-header.component'; +import { useTranslation } from 'react-i18next'; +import UserModal from './user-modal.component'; +import GroupModal from './group-modal.component'; +import ConfirmModal from './confirm-modal.component'; + +import AppContext from '../misc/appContext'; +import { IUser, IGroup, IGroupDetails, IGroupNode } from '../misc/user'; +import { useGetUsers, useGetGroups, createUser, createGroup, deleteUser, deleteGroup, updateGroup, addUserToGroup, updateUser, addGroupAsChild } from '../api'; +import { useKeycloak } from '@react-keycloak/web'; + +import imageEdit from '../assets/images/icon_edit.svg' +import imageDelete from '../assets/images/icon_delete.svg' +import imageAdd from '../assets/images/icon_add.svg' + +const emptyUser: IUser = { + id: '', + firstName: '', + lastName: '', + username: '', + password: '', + subGroup: null, + roleLab: false, + roleCounter: false, +} + +const UserManagement = (props: any) => { + const context = React.useContext(AppContext); + const { t } = useTranslation(); + + + const handleError = (error: any) => { + let msg = ''; + + if (error) { + msg = error.message + } + props.setError({ error: error, message: msg, onCancel: context.navigation!.toLanding }); + } + + const { keycloak, initialized } = useKeycloak(); + const [bUsers, refreshUsers] = useGetUsers(handleError); + const [bGroups, refreshGroups] = useGetGroups(handleError); + const [users, setUsers] = React.useState([]); + const [groups, setGroups] = React.useState([]); + const [isInit, setIsInit] = React.useState(true); + const [isUserData, setIsUserData] = React.useState(false); + const [editUser, setEditUser] = React.useState(emptyUser); + const [isGroupEdit, setIsGroupEdit] = React.useState(false); + const [editGroupId, setEditGroupId] = React.useState(''); + const [editGroupParentId, setEditGroupParentId] = React.useState(''); + + const [showConfirm, setShowConfirm] = React.useState(false); + const [confirmMessage, setConfirmMessage] = React.useState(''); + const [confirmHandle, setConfirmHandle] = React.useState<() => void>(); + const [isUpdating, setIsUpdating] = React.useState(false); + + React.useEffect(() => { + if (bUsers) { + setUsers(bUsers); + } + setIsUpdating(false); + }, [bUsers]); + + React.useEffect(() => { + if (bGroups) { + setGroups(bGroups); + } + setIsUpdating(false); + }, [bGroups]); + + React.useEffect(() => { + setIsInit(!!(bGroups && bUsers)); + }, [bUsers,bGroups]); + + + const userUpdate = (user:IUser) => { + if (editUser && editUser.username && keycloak.token) { + const fuser = users.find(u => u.username === user.username); + if (!user.password) { + user.password = undefined; + } + setIsUpdating(true); + updateUser(user, keycloak.token).then(() => { + if (fuser && fuser.subGroup!==user.subGroup + && keycloak.token && user.subGroup && user.subGroup!=='empty') { + addUserToGroup(user.id, user.subGroup, keycloak.token).then(() => { + refreshUsers(); + }).catch(e => { + handleError(e); + }); + } else { + refreshUsers(); + } + }); + } else { + const newUser: any = {...user}; + if (newUser.subGroup) { + // The attribute has different name in backend for create + newUser.subgroup = newUser.subGroup; + delete newUser.subGroup; + } + if (keycloak.token) { + setIsUpdating(true); + createUser(newUser, keycloak.token).then(() => { + refreshUsers(); + }).catch(e => { + handleError(e); + }); + } + } + setIsUserData(false); + } + + const flattenGroups = (groups: IGroup[], groupNodes: IGroupNode[], level: number, parentGroup?: string): void => { + groups.forEach((group: IGroup) => { + const gNode: IGroupNode = { + group: group, + parentGroup: parentGroup, + level: level, + }; + groupNodes.push(gNode); + if (group.children) { + flattenGroups(group.children, groupNodes, level+1, group.id); + } + }); + } + + const groupNodes: IGroupNode[] = []; + flattenGroups(groups, groupNodes, 0); + + const groupUpdate = (group:IGroupDetails) => { + if (group.id) { + if (keycloak.token) { + setIsUpdating(true); + const uGroup: any = {...group}; + delete uGroup.parentGroup; + updateGroup(uGroup, keycloak.token).then(() => { + console.log("update group finished"); + const fgroupNode = groupNodes.find((groupNode: IGroupNode) => groupNode.group.id === group.id); + if (keycloak.token && fgroupNode && group.id && group.parentGroup && group.parentGroup!=='empty' && fgroupNode.parentGroup !== group.parentGroup) { + addGroupAsChild(group.id, group.parentGroup, keycloak.token).then(() => { + refreshGroups(); + }).catch(e => { + handleError(e); + }); + } else { + refreshGroups(); + } + }).catch(e => { + handleError(e); + }); + } + } else { + if (keycloak.token) { + createGroup(group, keycloak.token).then(() => { + refreshGroups(); + }).catch(e => { + handleError(e); + }); + } + } + setIsGroupEdit(false); + } + + const startEditGroup = (groupNode:IGroupNode) => { + setEditGroupId(groupNode.group.id); + setEditGroupParentId(groupNode.parentGroup ? groupNode.parentGroup : ''); + setIsGroupEdit(true); + } + + const handleDeleteGroup = (group:IGroup) => { + setConfirmMessage("Wollen Sie wirklich die Gruppe "+group.name+ " löschen?") + setShowConfirm(true); + const handle = () => { + if (keycloak.token && group.id) { + deleteGroup(group.id, keycloak.token).then(() => { + refreshGroups(); + }).catch(e => { + handleError(e); + }); + } + }; + // need to wrap a function again because react apply each function passed to hook + setConfirmHandle(() => handle); + } + + const startEditUser = (user: IUser) => { + setEditUser({...user}); + setIsUserData(true); + } + + const handleDeleteUser = (user:IUser) => { + setConfirmMessage("Wollen Sie wirklich den User "+user.username+ " löschen?") + setShowConfirm(true); + const handle = () => { + if (keycloak.token && user.username) { + deleteUser(user.id, keycloak.token).then(() => { + refreshUsers(); + }).catch(e => { + handleError(e); + }); + } + }; + // need to wrap a function again because react apply each function passed to hook + setConfirmHandle(() => handle); + } + + const rolesAsString = (user:IUser) => { + let roleString = ''; + if (user.roleLab) { + roleString = 'lab' + } + if (user.roleCounter) { + if (roleString) { + roleString += ' '; + } + roleString += 'counter'; + } + return roleString; + } + + + const nameWithIdent = (groupNode: IGroupNode) : string => { + return "\u00A0\u00A0\u00A0\u00A0".repeat(groupNode.level)+groupNode.group.name; + } + + const groupRows = groupNodes.map(g => {nameWithIdent(g)} +    + ); + + const groupName = (groupId: string|null): string => { + let groupName = '' + if (groupId) { + const fNode = groupNodes.find(gnode => gnode.group.id === groupId); + if (fNode) { + groupName = fNode.group.path; + } + } + return groupName; + } + + + const userRows = users.map(u => {u.username}{u.firstName}{u.lastName} + {groupName(u.subGroup)}{rolesAsString(u)} +    + ); + + + return (!(isInit && context && context.valueSets) + ? + : <> + + + + +

Benutzer

+ + + + + + + + + + + + + {userRows} + +
BenutzernameVornameNameGruppeRollen
+ +
+

Gruppen

+ + + + + + + + + {groupRows} + +
Name
+ + {isUpdating ? : null} +
+ + + + + + + +
+
+ setIsUserData(false)} + groups={groupNodes} + handleOk={userUpdate} + user={editUser} + /> + setIsGroupEdit(false)} + groupId={editGroupId} + parentGroupId={editGroupParentId} + handleOk={groupUpdate} + groups={groupNodes} + handleError={(err: any) => { + setIsGroupEdit(false); + handleError(err); + }} + /> + { + setConfirmHandle(undefined); + setShowConfirm(false); + }} + handleOk={() => { + setShowConfirm(false); + if (confirmHandle) { + confirmHandle(); + } + }} + /> + ); + +} + +export default UserManagement; \ No newline at end of file diff --git a/src/components/user-modal.component.tsx b/src/components/user-modal.component.tsx new file mode 100644 index 0000000..e190140 --- /dev/null +++ b/src/components/user-modal.component.tsx @@ -0,0 +1,170 @@ +/* + * Corona-Warn-App / cwa-quick-test-frontend + * + * (C) 2021, T-Systems International GmbH + * + * Deutsche Telekom AG and all other contributors / + * copyright owners license this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { Button, Col, Modal, Row, Form } from 'react-bootstrap' + +import '../i18n'; +import { useTranslation } from 'react-i18next'; +import { FormGroupConsentCkb, FormGroupInput } from './modules/form-group.component'; +import { IUser, IGroupNode } from '../misc/user'; + +const UserModal = (props: any) => { + + const { t } = useTranslation(); + + const [user, setUser] = React.useState(props.user); + const [isNew, setIsNew] = React.useState(true); + const [validated, setValidated] = React.useState(false); + + React.useEffect(() => { + if (props.user.username !== user.username) { + if (!props.user.username + && !props.user.subGroup + && props.groups + && props.groups.length>0) { + // props.user.subGroup = props.groups[0].group.id; + } + setUser(props.user); + setIsNew(!props.user.username); + } + },[props.user]); + + const handleCancel = () => { + props.onCancel(); + // props.onHide(); + } + + const updateUserProp = (name:string, value:any) => { + const nuser = { ...user, [name]: value}; + setUser(nuser); + } + + const handleOk = () => { + if (props.handleOk) { + props.handleOk(user, setUser); + } + } + + const handleEnter = () => { + setValidated(false); + } + + const handleSubmit = (event: React.FormEvent) => { + const form = event.currentTarget; + event.preventDefault(); + event.stopPropagation(); + + setValidated(true); + + if (form.checkValidity()) { + handleOk(); + } + } + + const groupOptions = props.groups.map((groupNode: IGroupNode) => + + ); + + if (!user.subGroup || !props.groups.find((groupNode: IGroupNode) => groupNode.group.id === user.subGroup)) { + user.subGroup = null; + groupOptions.push(); + } + + return ( + +
+ + {isNew ? 'Neuen Benutzer anlegen' : 'Benutzer bearbeiten'} + + + < FormGroupInput controlId='formEmailInput' title="Benutzername" + value={user.username} + required + readOnly={!isNew} + onChange={(evt: any) => updateUserProp('username',evt.target.value)} + minLength={3} + maxLength={50} + /> + < FormGroupInput controlId='formFirstName' title="Vorname" + value={user.firstName} + required + onChange={(evt: any) => updateUserProp('firstName',evt.target.value)} + maxLength={30} + /> + < FormGroupInput controlId='formLastName' title="Nachname" + value={user.lastName} + onChange={(evt: any) => updateUserProp('lastName',evt.target.value)} + required + maxLength={30} + /> + < FormGroupInput controlId='formPassword' title="Passwort" + value={user.password} + onChange={(evt: any) => updateUserProp('password',evt.target.value)} + required={isNew} + type='password' + minLength={8} + maxLength={64} + /> + updateUserProp('roleLab',evt.currentTarget.checked)} + type='checkbox' + checked={user.roleLab} + /> + updateUserProp('roleCounter',evt.currentTarget.checked)} + type='checkbox' + checked={user.roleCounter} + /> + + Gruppe + + updateUserProp('subGroup',ent.target.value)} + > + {groupOptions} + + + + + + + + +
+
+ ) +} + +export default UserModal; \ No newline at end of file diff --git a/src/misc/useNavigation.tsx b/src/misc/useNavigation.tsx index d734138..9fce00b 100644 --- a/src/misc/useNavigation.tsx +++ b/src/misc/useNavigation.tsx @@ -37,6 +37,7 @@ export interface INavigation { toQRScan: () => void, toStatistics: () => void, toFailedReport: () => void, + toUserManagement: () => void, } export const useRoutes = () => { @@ -54,7 +55,8 @@ export const useRoutes = () => { recordTestResult: basePath + '/record/result', qrScan: basePath + '/qr/scan', statistics: basePath + '/statistics', - failedReport: basePath + '/failedreport' + failedReport: basePath + '/failedreport', + userManagement: basePath + '/usermanagement' }); }, []) @@ -81,6 +83,7 @@ export const useNavigation = () => { c.qrScan = routes.qrScan.replace(':mandant', mandant as string); c.statistics = routes.statistics.replace(':mandant', mandant as string); c.failedReport = routes.failedReport.replace(':mandant', mandant as string); + c.userManagement = routes.userManagement.replace(':mandant', mandant as string); setCalculatedRoutes(c); } @@ -100,6 +103,7 @@ export const useNavigation = () => { toQRScan: () => { history.push(calculatedRoutes.qrScan); }, toStatistics: () => { history.push(calculatedRoutes.statistics); }, toFailedReport: () => { history.push(calculatedRoutes.failedReport); }, + toUserManagement: () => { history.push(calculatedRoutes.userManagement); }, }); } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/misc/user.tsx b/src/misc/user.tsx new file mode 100644 index 0000000..42a9375 --- /dev/null +++ b/src/misc/user.tsx @@ -0,0 +1,55 @@ +/* + * Corona-Warn-App / cwa-quick-test-frontend + * + * (C) 2021, T-Systems International GmbH + * + * Deutsche Telekom AG and all other contributors / + * copyright owners license this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import InputGroupWithExtras from "react-bootstrap/esm/InputGroup" + +export interface IUser { + id: string, + username: string, + firstName: string, + lastName: string, + roleCounter: boolean, + roleLab: boolean, + subGroup: string | null, + password?: string, +} + +export interface IGroup { + name: string, + id: string, + path: string, + children: IGroup[], +} + +export interface IGroupNode { + group: IGroup, + parentGroup?: string, + level: number, +} + +export interface IGroupDetails { + id?: string, + name: string, + pocDetails: string, + pocId: string, + parentGroup?: string, +} + diff --git a/src/routing.component.tsx b/src/routing.component.tsx index c462364..99de49a 100644 --- a/src/routing.component.tsx +++ b/src/routing.component.tsx @@ -38,6 +38,7 @@ import RecordTestResult from './components/record-test-result.component'; import QrScan from './components/qr-scan.component'; import Statistics from './components/statistics.component'; import FailedReport from './components/failed-report.component'; +import UserManagement from './components/user-management.component'; import PrivateRoute from './components/private-route.component'; import IError from './misc/error'; @@ -167,6 +168,14 @@ const Routing = () => { render={(props) => } /> + } + /> + {/*