commit 82ba2ee5bef9a27bfd345841d20bacc7b0892a15
Author: Jaroslav Hrubý <hrubyj@students.zcu.cz>
Date:   Sun Apr 24 20:59:56 2022 +0200

    Users table implemented

diff --git a/webapp/api/api.ts b/webapp/api/api.ts
index 89c8e56..c75e374 100644
--- a/webapp/api/api.ts
+++ b/webapp/api/api.ts
@@ -170,6 +170,43 @@ export interface AnnotationsAddRequest {
      */
     'documentIdList'?: Array<string> | null;
 }
+/**
+ * 
+ * @export
+ * @interface ChangeUserInfoRequest
+ */
+export interface ChangeUserInfoRequest {
+    /**
+     * 
+     * @type {string}
+     * @memberof ChangeUserInfoRequest
+     */
+    'password'?: string | null;
+    /**
+     * 
+     * @type {string}
+     * @memberof ChangeUserInfoRequest
+     */
+    'username'?: string | null;
+    /**
+     * 
+     * @type {string}
+     * @memberof ChangeUserInfoRequest
+     */
+    'name'?: string | null;
+    /**
+     * 
+     * @type {string}
+     * @memberof ChangeUserInfoRequest
+     */
+    'surname'?: string | null;
+    /**
+     * 
+     * @type {ERole}
+     * @memberof ChangeUserInfoRequest
+     */
+    'role'?: ERole;
+}
 /**
  * 
  * @export
@@ -195,6 +232,43 @@ export interface ClientInfo {
      */
     'ip'?: string | null;
 }
+/**
+ * 
+ * @export
+ * @interface CreateUserRequest
+ */
+export interface CreateUserRequest {
+    /**
+     * 
+     * @type {string}
+     * @memberof CreateUserRequest
+     */
+    'username'?: string | null;
+    /**
+     * 
+     * @type {string}
+     * @memberof CreateUserRequest
+     */
+    'password'?: string | null;
+    /**
+     * 
+     * @type {string}
+     * @memberof CreateUserRequest
+     */
+    'name'?: string | null;
+    /**
+     * 
+     * @type {string}
+     * @memberof CreateUserRequest
+     */
+    'surname'?: string | null;
+    /**
+     * 
+     * @type {ERole}
+     * @memberof CreateUserRequest
+     */
+    'role'?: ERole;
+}
 /**
  * 
  * @export
@@ -1547,6 +1621,76 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
                 options: localVarRequestOptions,
             };
         },
+        /**
+         * 
+         * @param {string} userId 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        userUserIdAnnotationsGet: async (userId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'userId' is not null or undefined
+            assertParamExists('userUserIdAnnotationsGet', 'userId', userId)
+            const localVarPath = `/user/{userId}/annotations`
+                .replace(`{${"userId"}}`, encodeURIComponent(String(userId)));
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+
+    
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @param {string} userId 
+         * @param {ChangeUserInfoRequest} [changeUserInfoRequest] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        userUserIdPut: async (userId: string, changeUserInfoRequest?: ChangeUserInfoRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'userId' is not null or undefined
+            assertParamExists('userUserIdPut', 'userId', userId)
+            const localVarPath = `/user/{userId}`
+                .replace(`{${"userId"}}`, encodeURIComponent(String(userId)));
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+
+    
+            localVarHeaderParameter['Content-Type'] = 'application/json';
+
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+            localVarRequestOptions.data = serializeDataIfNeeded(changeUserInfoRequest, localVarRequestOptions, configuration)
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
         /**
          * 
          * @param {*} [options] Override http request option.
@@ -1571,6 +1715,39 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @param {CreateUserRequest} [createUserRequest] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        usersPost: async (createUserRequest?: CreateUserRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/users`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+
+    
+            localVarHeaderParameter['Content-Type'] = 'application/json';
+
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+            localVarRequestOptions.data = serializeDataIfNeeded(createUserRequest, localVarRequestOptions, configuration)
+
             return {
                 url: toPathString(localVarUrlObj),
                 options: localVarRequestOptions,
@@ -1595,6 +1772,27 @@ export const UserApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.userAnnotationsGet(options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * 
+         * @param {string} userId 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async userUserIdAnnotationsGet(userId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AnnotationListResponse>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.userUserIdAnnotationsGet(userId, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
+        /**
+         * 
+         * @param {string} userId 
+         * @param {ChangeUserInfoRequest} [changeUserInfoRequest] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async userUserIdPut(userId: string, changeUserInfoRequest?: ChangeUserInfoRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.userUserIdPut(userId, changeUserInfoRequest, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
          * 
          * @param {*} [options] Override http request option.
@@ -1604,6 +1802,16 @@ export const UserApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.usersGet(options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * 
+         * @param {CreateUserRequest} [createUserRequest] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async usersPost(createUserRequest?: CreateUserRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.usersPost(createUserRequest, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
     }
 };
 
@@ -1622,6 +1830,25 @@ export const UserApiFactory = function (configuration?: Configuration, basePath?
         userAnnotationsGet(options?: any): AxiosPromise<AnnotationListResponse> {
             return localVarFp.userAnnotationsGet(options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @param {string} userId 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        userUserIdAnnotationsGet(userId: string, options?: any): AxiosPromise<AnnotationListResponse> {
+            return localVarFp.userUserIdAnnotationsGet(userId, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @param {string} userId 
+         * @param {ChangeUserInfoRequest} [changeUserInfoRequest] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        userUserIdPut(userId: string, changeUserInfoRequest?: ChangeUserInfoRequest, options?: any): AxiosPromise<void> {
+            return localVarFp.userUserIdPut(userId, changeUserInfoRequest, options).then((request) => request(axios, basePath));
+        },
         /**
          * 
          * @param {*} [options] Override http request option.
@@ -1630,6 +1857,15 @@ export const UserApiFactory = function (configuration?: Configuration, basePath?
         usersGet(options?: any): AxiosPromise<UserList> {
             return localVarFp.usersGet(options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @param {CreateUserRequest} [createUserRequest] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        usersPost(createUserRequest?: CreateUserRequest, options?: any): AxiosPromise<void> {
+            return localVarFp.usersPost(createUserRequest, options).then((request) => request(axios, basePath));
+        },
     };
 };
 
@@ -1650,6 +1886,29 @@ export class UserApi extends BaseAPI {
         return UserApiFp(this.configuration).userAnnotationsGet(options).then((request) => request(this.axios, this.basePath));
     }
 
+    /**
+     * 
+     * @param {string} userId 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof UserApi
+     */
+    public userUserIdAnnotationsGet(userId: string, options?: AxiosRequestConfig) {
+        return UserApiFp(this.configuration).userUserIdAnnotationsGet(userId, options).then((request) => request(this.axios, this.basePath));
+    }
+
+    /**
+     * 
+     * @param {string} userId 
+     * @param {ChangeUserInfoRequest} [changeUserInfoRequest] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof UserApi
+     */
+    public userUserIdPut(userId: string, changeUserInfoRequest?: ChangeUserInfoRequest, options?: AxiosRequestConfig) {
+        return UserApiFp(this.configuration).userUserIdPut(userId, changeUserInfoRequest, options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
      * 
      * @param {*} [options] Override http request option.
@@ -1659,6 +1918,17 @@ export class UserApi extends BaseAPI {
     public usersGet(options?: AxiosRequestConfig) {
         return UserApiFp(this.configuration).usersGet(options).then((request) => request(this.axios, this.basePath));
     }
+
+    /**
+     * 
+     * @param {CreateUserRequest} [createUserRequest] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof UserApi
+     */
+    public usersPost(createUserRequest?: CreateUserRequest, options?: AxiosRequestConfig) {
+        return UserApiFp(this.configuration).usersPost(createUserRequest, options).then((request) => request(this.axios, this.basePath));
+    }
 }
 
 
diff --git a/webapp/components/modals/AddUserModal.tsx b/webapp/components/modals/AddUserModal.tsx
new file mode 100644
index 0000000..4c77b45
--- /dev/null
+++ b/webapp/components/modals/AddUserModal.tsx
@@ -0,0 +1,139 @@
+import { Button, Form, Input, Modal, Radio, Upload } from 'antd';
+import { InboxOutlined, LockOutlined, UserOutlined } from '@ant-design/icons';
+import 'antd/dist/antd.css';
+import React from 'react';
+import {
+    CreateUserRequest,
+    DocumentAddInfo,
+    EAddDocumentFormat,
+    ERole,
+    LoginResponse,
+} from '../../api';
+import { documentController, userController } from '../../controllers';
+import { ShowToast } from '../../utils/alerts';
+import { getTokenData } from '../../utils/login';
+
+const { Dragger } = Upload;
+
+interface ModalProps {
+    onCancel: () => void;
+}
+
+/**
+ * Creates a modal window that loads documents to the app.
+ * @returns The modal window.
+ */
+const AddUserModal: React.FC<ModalProps> = ({ onCancel }) => {
+    /**
+     * Handles successful closing of the modal window.
+     */
+    const handleOk = () => {
+        onCancel();
+    };
+
+    /**
+     * Handles cancelling of the model window.
+     */
+    const handleCancel = () => {
+        onCancel();
+    };
+
+    const onFinish = async (values: any) => {
+        const req: CreateUserRequest = {
+            username: values.username,
+            password: values.password,
+            name: values.name,
+            surname: values.surname,
+            role: values.role,
+        };
+
+        const loginRes = await userController.usersPost(req);
+        onCancel();
+    };
+
+    const onFinishFailed = (errorInfo: any) => {
+        ShowToast('Zadané údaje nejsou validní', 'error', 3000, 'top-end');
+    };
+
+    return (
+        <Modal
+            title="Nový uživatel"
+            onOk={handleOk}
+            visible={true}
+            onCancel={handleCancel}
+            footer={null}
+        >
+            <Form
+                onFinish={onFinish}
+                onFinishFailed={onFinishFailed}
+                autoComplete="off"
+                labelCol={{ span: 4 }}
+                layout="horizontal"
+            >
+                <Form.Item
+                    label=""
+                    name="role"
+                    rules={[
+                        {
+                            required: true,
+                            message: 'Prosím vyberte roli',
+                        },
+                    ]}
+                >
+                    <Radio.Group>
+                        <Radio.Button value={ERole.Annotator}>Anotátor</Radio.Button>
+                        <Radio.Button value={ERole.Administrator}>Admin</Radio.Button>
+                    </Radio.Group>
+                </Form.Item>
+                <Form.Item
+                    name="username"
+                    rules={[
+                        {
+                            required: true,
+                            message: 'Prosím zadejte uživatelské jméno',
+                        },
+                    ]}
+                >
+                    <Input placeholder="Uživatelské jméno" />
+                </Form.Item>
+                <Form.Item
+                    name="password"
+                    rules={[{ required: true, message: 'Prosím zadejte heslo' }]}
+                >
+                    <Input placeholder="Heslo" />
+                </Form.Item>
+
+                <Form.Item
+                    name="name"
+                    rules={[
+                        {
+                            required: true,
+                            message: 'Prosím zadejte jméno',
+                        },
+                    ]}
+                >
+                    <Input placeholder="Jméno" />
+                </Form.Item>
+
+                <Form.Item
+                    name="surname"
+                    rules={[
+                        {
+                            required: true,
+                            message: 'Prosím zadejte příjmení',
+                        },
+                    ]}
+                >
+                    <Input placeholder="Příjmení" />
+                </Form.Item>
+                <Form.Item>
+                    <Button type="primary" htmlType="submit" className="w-100">
+                        Vytvořit
+                    </Button>
+                </Form.Item>
+            </Form>
+        </Modal>
+    );
+};
+
+export default AddUserModal;
diff --git a/webapp/pages/users/index.tsx b/webapp/pages/users/index.tsx
index eb1b9b0..6dbe101 100644
--- a/webapp/pages/users/index.tsx
+++ b/webapp/pages/users/index.tsx
@@ -1,30 +1,83 @@
 import 'antd/dist/antd.css';
-import React, { useContext, useEffect } from 'react';
+import React, { useContext, useEffect, useState } from 'react';
 
 import { useUnauthRedirect } from '../../hooks';
 import { useRouter } from 'next/router';
-import { Typography } from 'antd';
+import { Button, Space, Table, Typography } from 'antd';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { faUsers } from '@fortawesome/free-solid-svg-icons';
 import { LoggedUserContext } from '../../contexts/LoggedUserContext';
 import { MainLayout } from '../../layouts/MainLayout';
+import { UserInfo } from '../../api';
+import { userController } from '../../controllers';
+import AddUserModal from '../../components/modals/AddUserModal';
 
 function UsersPage() {
     const redirecting = useUnauthRedirect('/login');
     const { logout, role } = useContext(LoggedUserContext);
     const router = useRouter();
 
+    const [visible, setVisible] = React.useState(false);
+    const [users, setUsers] = useState<UserInfo[]>([]);
+
     useEffect(() => {
+        async function fetchData() {
+            let usrs = (await userController.usersGet()).data.users;
+            if (!usrs) {
+                setUsers([]);
+            } else {
+                setUsers(usrs);
+            }
+        }
+
         if (!redirecting && role === 'ADMINISTRATOR') {
-            // TODO load users
+            fetchData();
         }
     }, [logout, redirecting, role, router]);
 
+    const showModal = () => {
+        setVisible(true);
+    };
+
+    const hideModal = () => {
+        setVisible(false);
+    };
+
+    const columns = [
+        { title: 'Jméno', dataIndex: 'name', key: 'name' },
+        { title: 'Příjmení', dataIndex: 'surname', key: 'surname' },
+        { title: 'Uživatelské jméno', dataIndex: 'username', key: 'username' },
+        {
+            title: '',
+            key: 'operation',
+            render: () => (
+                <div>
+                    <Space size={'middle'}>
+                        <Button>Dokumenty</Button>
+                        <Button>Upravit</Button>
+                        <Button>Smazat</Button>
+                    </Space>
+                </div>
+            ),
+        },
+    ];
+
     return redirecting || role !== 'ADMINISTRATOR' ? null : (
         <MainLayout>
             <Typography.Title level={2}>
                 <FontAwesomeIcon icon={faUsers} /> Uživatelé
             </Typography.Title>
+            <Button type={'primary'} onClick={showModal}>
+                Přidat uživatele
+            </Button>
+            {visible && <AddUserModal onCancel={hideModal} />}
+
+            <Table
+                columns={columns}
+                dataSource={users}
+                size="small"
+                scroll={{ y: 500 }}
+            />
         </MainLayout>
     );
 }
