commit 669ffe38d8cac165c53e583a99035f5fd7e5c71b
Author: Jaroslav Hrubý <hrubyj@students.zcu.cz>
Date:   Thu May 5 17:05:29 2022 +0200

    Refactoring after code review

diff --git a/webapp/api/api.ts b/webapp/api/api.ts
index 608d4a9..b1fff5c 100644
--- a/webapp/api/api.ts
+++ b/webapp/api/api.ts
@@ -793,6 +793,12 @@ export interface UserInfo {
      * @memberof UserInfo
      */
     'assignedDocumentsCount'?: number;
+    /**
+     * 
+     * @type {ERole}
+     * @memberof UserInfo
+     */
+    'role'?: ERole;
 }
 /**
  * 
diff --git a/webapp/components/modals/AddUserModal.tsx b/webapp/components/modals/AddUserModal.tsx
index 8a608f2..34be33a 100644
--- a/webapp/components/modals/AddUserModal.tsx
+++ b/webapp/components/modals/AddUserModal.tsx
@@ -62,6 +62,7 @@ const AddUserModal: React.FC<ModalProps> = ({ onCancel }) => {
             visible={true}
             onCancel={handleCancel}
             footer={null}
+            centered
         >
             <Form
                 onFinish={onFinish}
diff --git a/webapp/components/modals/AssignDocumentModal.tsx b/webapp/components/modals/AssignDocumentModal.tsx
index 6e7bcab..a33ff15 100644
--- a/webapp/components/modals/AssignDocumentModal.tsx
+++ b/webapp/components/modals/AssignDocumentModal.tsx
@@ -5,6 +5,7 @@ import { UserInfo } from '../../api';
 import { annotationController, userController } from '../../controllers';
 import { useUnauthRedirect } from '../../hooks';
 import { LoggedUserContext } from '../../contexts/LoggedUserContext';
+import { ShowToast } from '../../utils/alerts';
 
 interface ModalProps {
     onCancel: () => void;
@@ -37,13 +38,22 @@ function AssignDocumentModal({ onCancel, documentsIds }: ModalProps) {
     const handleUpload = async () => {
         // console.log(documentsIds, selectedUsers);
         if (documentsIds && documentsIds.length !== 0) {
-            await annotationController.annotationsPost({
-                // @ts-ignore
-                documentIdList: documentsIds,
-                userIdList: selectedUsers,
-            });
+            await annotationController
+                .annotationsPost({
+                    // @ts-ignore
+                    documentIdList: documentsIds,
+                    userIdList: selectedUsers,
+                })
+                .then((r) => {
+                    ShowToast(
+                        'Přiřazení dokumentů proběhlo úspěšně',
+                        'success',
+                        3000,
+                        'top-end'
+                    );
+                    onCancel();
+                });
         }
-        onCancel();
     };
 
     function onChange(event: any) {
diff --git a/webapp/components/modals/EditUserModal.tsx b/webapp/components/modals/EditUserModal.tsx
index 94b224b..abe2296 100644
--- a/webapp/components/modals/EditUserModal.tsx
+++ b/webapp/components/modals/EditUserModal.tsx
@@ -38,9 +38,16 @@ const EditUserModal: React.FC<ModalProps> = ({ onCancel, userInfo }: ModalProps)
             role: values.role,
         };
         if (userInfo.id) {
-            await userController.userUserIdPut(userInfo.id, req);
+            userController.userUserIdPut(userInfo.id, req).then((r) => {
+                ShowToast(
+                    'Úprava uživatele proběhla úspěšně',
+                    'success',
+                    3000,
+                    'top-end'
+                );
+                onCancel();
+            });
         }
-        onCancel();
     };
 
     const onFinishFailed = (errorInfo: any) => {
@@ -54,6 +61,7 @@ const EditUserModal: React.FC<ModalProps> = ({ onCancel, userInfo }: ModalProps)
             visible={true}
             onCancel={handleCancel}
             footer={null}
+            centered
         >
             <Form
                 onFinish={onFinish}
@@ -62,17 +70,8 @@ const EditUserModal: React.FC<ModalProps> = ({ onCancel, userInfo }: ModalProps)
                 labelCol={{ span: 4 }}
                 layout="horizontal"
             >
-                <Form.Item
-                    label=""
-                    name="role"
-                    rules={[
-                        {
-                            required: true,
-                            message: 'Prosím vyberte roli',
-                        },
-                    ]}
-                >
-                    <Radio.Group>
+                <Form.Item label="" name="role">
+                    <Radio.Group defaultValue={userInfo.role}>
                         <Radio.Button value={ERole.Annotator}>Anotátor</Radio.Button>
                         <Radio.Button value={ERole.Administrator}>Admin</Radio.Button>
                     </Radio.Group>
@@ -89,13 +88,6 @@ const EditUserModal: React.FC<ModalProps> = ({ onCancel, userInfo }: ModalProps)
                 >
                     <Input placeholder="Uživatelské jméno" />
                 </Form.Item>
-                <Form.Item
-                    name="password"
-                    rules={[{ required: true, message: 'Prosím zadejte heslo' }]}
-                >
-                    <Input.Password placeholder="Heslo" />
-                </Form.Item>
-
                 <Form.Item
                     name="name"
                     initialValue={userInfo.name}
diff --git a/webapp/components/modals/EditUserPasswordModal.tsx b/webapp/components/modals/EditUserPasswordModal.tsx
new file mode 100644
index 0000000..1b646e3
--- /dev/null
+++ b/webapp/components/modals/EditUserPasswordModal.tsx
@@ -0,0 +1,81 @@
+import { Button, Form, Input, Modal, Radio } from 'antd';
+import 'antd/dist/antd.css';
+import React from 'react';
+import { ChangeUserInfoRequest, ERole, UserInfo } from '../../api';
+import { userController } from '../../controllers';
+import { ShowToast } from '../../utils/alerts';
+
+interface ModalProps {
+    onCancel: () => void;
+    userInfo: UserInfo;
+}
+
+/**
+ * Creates a modal window that loads documents to the app.
+ * @returns The modal window.
+ */
+const EditUserModal: React.FC<ModalProps> = ({ onCancel, userInfo }: ModalProps) => {
+    /**
+     * 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: ChangeUserInfoRequest = {
+            password: values.password,
+        };
+        if (userInfo.id) {
+            userController.userUserIdPut(userInfo.id, req).then((r) => {
+                ShowToast('Heslo úspěšně změněno', 'success', 3000, 'top-end');
+                onCancel();
+            });
+        }
+    };
+
+    const onFinishFailed = (errorInfo: any) => {
+        ShowToast('Zadané údaje nejsou validní', 'error', 3000, 'top-end');
+    };
+
+    return (
+        <Modal
+            title={'Změnit heslo uživatele ' + userInfo.username}
+            onOk={handleOk}
+            visible={true}
+            onCancel={handleCancel}
+            footer={null}
+            width={400}
+            centered
+        >
+            <Form
+                onFinish={onFinish}
+                onFinishFailed={onFinishFailed}
+                autoComplete="off"
+                labelCol={{ span: 4 }}
+                layout="horizontal"
+            >
+                <Form.Item
+                    name="password"
+                    rules={[{ required: true, message: 'Prosím zadejte heslo' }]}
+                >
+                    <Input.Password placeholder="Heslo" />
+                </Form.Item>
+                <Form.Item>
+                    <Button type="primary" htmlType="submit" className="w-100">
+                        Změnit
+                    </Button>
+                </Form.Item>
+            </Form>
+        </Modal>
+    );
+};
+
+export default EditUserModal;
diff --git a/webapp/components/modals/UserDocumentsModal.tsx b/webapp/components/modals/UserDocumentsModal.tsx
index 45109cd..01a3518 100644
--- a/webapp/components/modals/UserDocumentsModal.tsx
+++ b/webapp/components/modals/UserDocumentsModal.tsx
@@ -1,10 +1,15 @@
-import { List, Modal } from 'antd';
+import { List, Modal, Skeleton, Tag } from 'antd';
 import 'antd/dist/antd.css';
 import React, { useContext, useEffect, useState } from 'react';
-import { AnnotationListInfo } from '../../api';
+import { AnnotationListInfo, EState } from '../../api';
 import { userController } from '../../controllers';
 import { useUnauthRedirect } from '../../hooks';
 import { LoggedUserContext } from '../../contexts/LoggedUserContext';
+import {
+    CheckCircleOutlined,
+    ClockCircleOutlined,
+    SyncOutlined,
+} from '@ant-design/icons';
 
 interface ModalProps {
     onCancel: () => void;
@@ -24,6 +29,27 @@ function UserDocumentsModal({ onCancel, userId }: ModalProps) {
         onCancel();
     };
 
+    const getStateTag = (state: EState) => {
+        let color = 'green';
+        let label = 'Hotovo';
+        let icon = <CheckCircleOutlined />;
+        if (state === EState.New) {
+            color = 'volcano';
+            label = 'Nový';
+            icon = <ClockCircleOutlined />;
+        }
+        if (state === EState.InProgress) {
+            color = 'orange';
+            label = 'Rozpracováno';
+            icon = <SyncOutlined spin />;
+        }
+        return (
+            <Tag icon={icon} color={color} key={label}>
+                {label.toUpperCase()}
+            </Tag>
+        );
+    };
+
     useEffect(() => {
         if (!redirecting && role === 'ADMINISTRATOR' && userId) {
             userController
@@ -49,7 +75,11 @@ function UserDocumentsModal({ onCancel, userId }: ModalProps) {
                 style={{ maxHeight: 400, overflowY: 'auto', overflowX: 'hidden' }}
                 renderItem={(item) => (
                     <List.Item key={item.annotationId}>
-                        <List.Item.Meta title={item.documentName} />
+                        <List.Item.Meta
+                            title={item.documentName}
+                            // @ts-ignore
+                            avatar={getStateTag(item.state)}
+                        />
                     </List.Item>
                 )}
             />
diff --git a/webapp/pages/_app.tsx b/webapp/pages/_app.tsx
index d45f973..b529034 100644
--- a/webapp/pages/_app.tsx
+++ b/webapp/pages/_app.tsx
@@ -1,6 +1,5 @@
 // @ts-nocheck
 import '../styles/globals.css';
-import 'bootstrap/dist/css/bootstrap.css';
 import '@fortawesome/fontawesome-svg-core/styles.css';
 
 import type { AppProps } from 'next/app';
diff --git a/webapp/pages/documents/admin/index.tsx b/webapp/pages/documents/admin/index.tsx
index 173545e..c43975f 100644
--- a/webapp/pages/documents/admin/index.tsx
+++ b/webapp/pages/documents/admin/index.tsx
@@ -3,7 +3,7 @@ import React, { useContext, useEffect, useState } from 'react';
 
 import { useUnauthRedirect } from '../../../hooks';
 import { useRouter } from 'next/router';
-import { Button, Input, Space, Table, Typography } from 'antd';
+import { Button, Space, Table, Typography } from 'antd';
 import { faFileLines, faUser } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { LoggedUserContext } from '../../../contexts/LoggedUserContext';
@@ -12,10 +12,12 @@ import AddDocumentModal from '../../../components/modals/AddDocumentModal';
 import { DocumentListInfo, UserInfo } from '../../../api';
 import { documentController, userController } from '../../../controllers';
 import AssignDocumentModal from '../../../components/modals/AssignDocumentModal';
-import { ShowToast } from '../../../utils/alerts';
+import { ShowConfirmDelete, ShowToast } from '../../../utils/alerts';
 import { TableDocInfo } from '../../../components/types/TableDocInfo';
-import { SearchOutlined } from '@ant-design/icons';
 import { UserFilter } from '../../../components/types/UserFilter';
+import { getColumnSearchProps, getLocaleProps } from '../../../utils/tableUtils';
+import { UserOutlined } from '@ant-design/icons';
+import Swal from 'sweetalert2';
 
 function AdminDocumentPage() {
     const redirecting = useUnauthRedirect('/login');
@@ -76,63 +78,27 @@ function AdminDocumentPage() {
         setVisibleAssign(false);
     };
 
-    const handleSearch = (
-        selectedKeys: React.SetStateAction<string>[],
-        confirm: () => void
-    ) => {
-        confirm();
-    };
-
-    const handleReset = (clearFilters: () => void) => {
-        clearFilters();
+    const removeUserDocument = (userId: string, documentId: string) => {
+        Swal.fire({
+            title: 'Opravdu si přejete uživateli odebrat dokument?',
+            showCancelButton: true,
+            confirmButtonText: 'Odebrat',
+        }).then((result) => {
+            // TODO call API
+        });
     };
 
-    const getColumnSearchProps = (dataIndex: string, searchLabel: string) => ({
-        // @ts-ignore
-        filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => (
-            <div style={{ padding: 8 }}>
-                <Input
-                    placeholder={`Vyhledat ${searchLabel}`}
-                    value={selectedKeys[0]}
-                    onChange={(e) =>
-                        setSelectedKeys(e.target.value ? [e.target.value] : [])
-                    }
-                    onPressEnter={() => handleSearch(selectedKeys, confirm)}
-                    style={{ marginBottom: 8, display: 'block' }}
-                />
-                <Space>
-                    <Button
-                        type="primary"
-                        onClick={() => handleSearch(selectedKeys, confirm)}
-                        icon={<SearchOutlined />}
-                        style={{ width: 90 }}
-                    >
-                        Hledat
-                    </Button>
-                    <Button
-                        onClick={() => handleReset(clearFilters)}
-                        style={{ width: 90 }}
-                    >
-                        Smazat
-                    </Button>
-                </Space>
-            </div>
-        ),
-        filterIcon: (filtered: any) => (
-            <SearchOutlined style={{ color: filtered ? '#1890ff' : undefined }} />
-        ),
-        onFilter: (value: string, record: { [x: string]: { toString: () => string } }) =>
-            record[dataIndex]
-                ? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase())
-                : '',
-    });
-
     const columns = [
         {
             title: 'Název dokumentu',
             dataIndex: 'name',
             key: 'name',
             ...getColumnSearchProps('name', 'název'),
+            sorter: {
+                // @ts-ignore
+                compare: (a, b) => a.name.localeCompare(b.name),
+                multiple: 2,
+            },
         },
         {
             title: 'Délka',
@@ -143,7 +109,6 @@ function AdminDocumentPage() {
                 compare: (a, b) => a.length - b.length,
                 multiple: 1,
             },
-            sortDirections: ['descend', 'ascend'],
         },
         {
             title: 'Anotátoři',
@@ -152,18 +117,25 @@ function AdminDocumentPage() {
             render: (columnData: UserInfo[], record: DocumentListInfo, index: number) => {
                 return (
                     <div>
-                        {columnData.map((e) => (
-                            <span
-                                key={e.username + '.' + record.id}
-                                title={e.username ?? ''}
-                            >
-                                <FontAwesomeIcon
-                                    icon={faUser}
+                        <Space>
+                            {columnData.map((e) => (
+                                <span
+                                    key={e.username + '.' + record.id}
                                     title={e.username ?? ''}
-                                    className={'me-2'}
-                                />
-                            </span>
-                        ))}
+                                >
+                                    <FontAwesomeIcon
+                                        icon={faUser}
+                                        size={'2x'}
+                                        onClick={() =>
+                                            // @ts-ignore
+                                            removeUserDocument(e.id, record.id)
+                                        }
+                                        title={e.username ?? ''}
+                                        className={'me-2'}
+                                    />
+                                </span>
+                            ))}
+                        </Space>
                     </div>
                 );
             },
@@ -176,9 +148,8 @@ function AdminDocumentPage() {
             sorter: {
                 // @ts-ignore
                 compare: (a, b) => a.annotatingUsers.length - b.annotatingUsers.length,
-                multiple: 1,
+                multiple: 3,
             },
-            sortDirections: ['descend', 'ascend'],
         },
     ];
 
@@ -204,12 +175,7 @@ function AdminDocumentPage() {
             )}
 
             <Table
-                locale={{
-                    filterSearchPlaceholder: 'Hledat uživatele',
-                    triggerDesc: 'Seřadit sestupně',
-                    triggerAsc: 'Seřadit vzestupně',
-                    cancelSort: 'Vypnout řazení',
-                }}
+                locale={{ ...getLocaleProps() }}
                 rowSelection={{
                     type: 'checkbox',
                     ...rowSelection,
diff --git a/webapp/pages/documents/annotator/index.tsx b/webapp/pages/documents/annotator/index.tsx
index f29af2b..c697949 100644
--- a/webapp/pages/documents/annotator/index.tsx
+++ b/webapp/pages/documents/annotator/index.tsx
@@ -13,9 +13,9 @@ import { AnnotationListInfo } from '../../../api';
 import {
     CheckCircleOutlined,
     ClockCircleOutlined,
-    SearchOutlined,
     SyncOutlined,
 } from '@ant-design/icons';
+import { getColumnSearchProps, getLocaleProps } from '../../../utils/tableUtils';
 
 function UserDocumentPage() {
     const redirecting = useUnauthRedirect('/login');
@@ -39,63 +39,14 @@ function UserDocumentPage() {
         }
     }, [logout, redirecting, role, router]);
 
-    const handleSearch = (
-        selectedKeys: React.SetStateAction<string>[],
-        confirm: () => void
-    ) => {
-        confirm();
-    };
-
-    const handleReset = (clearFilters: () => void) => {
-        clearFilters();
-    };
-
-    const getColumnSearchProps = (dataIndex: string, searchLabel: string) => ({
-        // @ts-ignore
-        filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => (
-            <div style={{ padding: 8 }}>
-                <Input
-                    placeholder={`Vyhledat ${searchLabel}`}
-                    value={selectedKeys[0]}
-                    onChange={(e) =>
-                        setSelectedKeys(e.target.value ? [e.target.value] : [])
-                    }
-                    onPressEnter={() => handleSearch(selectedKeys, confirm)}
-                    style={{ marginBottom: 8, display: 'block' }}
-                />
-                <Space>
-                    <Button
-                        type="primary"
-                        onClick={() => handleSearch(selectedKeys, confirm)}
-                        icon={<SearchOutlined />}
-                        style={{ width: 90 }}
-                    >
-                        Hledat
-                    </Button>
-                    <Button
-                        onClick={() => handleReset(clearFilters)}
-                        style={{ width: 90 }}
-                    >
-                        Smazat
-                    </Button>
-                </Space>
-            </div>
-        ),
-        filterIcon: (filtered: any) => (
-            <SearchOutlined style={{ color: filtered ? '#1890ff' : undefined }} />
-        ),
-        onFilter: (value: string, record: { [x: string]: { toString: () => string } }) =>
-            record[dataIndex]
-                ? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase())
-                : '',
-    });
-
     const columns = [
         {
             title: 'Název dokumentu',
             dataIndex: 'documentName',
             key: 'documentName',
             ...getColumnSearchProps('documentName', 'název'),
+            // @ts-ignore
+            sorter: (a, b) => a.documentName.localeCompare(b.documentName),
         },
         {
             title: 'Stav anotace',
@@ -163,11 +114,7 @@ function UserDocumentPage() {
                 <FontAwesomeIcon icon={faFileLines} /> Dokumenty
             </Typography.Title>
             <Table
-                locale={{
-                    triggerDesc: 'Seřadit sestupně',
-                    triggerAsc: 'Seřadit vzestupně',
-                    cancelSort: 'Vypnout řazení',
-                }}
+                locale={{ ...getLocaleProps() }}
                 // @ts-ignore
                 columns={columns}
                 // @ts-ignore
diff --git a/webapp/pages/users/index.tsx b/webapp/pages/users/index.tsx
index 4a87581..8d1e2dc 100644
--- a/webapp/pages/users/index.tsx
+++ b/webapp/pages/users/index.tsx
@@ -3,7 +3,7 @@ import React, { useContext, useEffect, useState } from 'react';
 
 import { useUnauthRedirect } from '../../hooks';
 import { useRouter } from 'next/router';
-import { Badge, Button, Input, Space, Table, Typography } from 'antd';
+import { Badge, Button, Dropdown, Menu, Space, Table, Typography } from 'antd';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { faUsers } from '@fortawesome/free-solid-svg-icons';
 import { LoggedUserContext } from '../../contexts/LoggedUserContext';
@@ -12,9 +12,11 @@ import { UserInfo } from '../../api';
 import { userController } from '../../controllers';
 import AddUserModal from '../../components/modals/AddUserModal';
 import UserDocumentsModal from '../../components/modals/UserDocumentsModal';
-import { ShowConfirmDelete } from '../../utils/alerts';
+import { ShowConfirmDelete, ShowToast } from '../../utils/alerts';
 import EditUserModal from '../../components/modals/EditUserModal';
-import { SearchOutlined } from '@ant-design/icons';
+import { DeleteOutlined, EditOutlined, LockOutlined } from '@ant-design/icons';
+import { getColumnSearchProps, getLocaleProps } from '../../utils/tableUtils';
+import EditUserPasswordModal from '../../components/modals/EditUserPasswordModal';
 
 function UsersPage() {
     const redirecting = useUnauthRedirect('/login');
@@ -23,6 +25,7 @@ function UsersPage() {
 
     const [addVisible, setAddVisible] = React.useState(false);
     const [editVisible, setEditVisible] = React.useState(false);
+    const [passwordEditVisible, setPasswordEditVisible] = React.useState(false);
     const [docsVisible, setDocsVisible] = React.useState(false);
     const [users, setUsers] = useState<UserInfo[]>([]);
 
@@ -52,7 +55,10 @@ function UsersPage() {
 
     const deleteUser = (userId: string) => {
         ShowConfirmDelete(() => {
-            userController.userUserIdDelete(userId).then(() => fetchData());
+            userController.userUserIdDelete(userId).then(() => {
+                ShowToast('Uživatel úspěšně odstraněn', 'success', 3000, 'top-end');
+                fetchData();
+            });
         }, 'uživatele');
     };
 
@@ -63,6 +69,10 @@ function UsersPage() {
         setUserInfo(userId);
         setEditVisible(true);
     };
+    const showEditPasswordModal = (userId: string) => {
+        setUserInfo(userId);
+        setPasswordEditVisible(true);
+    };
     const showDocsModal = (userId: string) => {
         setUserInfo(userId);
         setDocsVisible(true);
@@ -73,58 +83,26 @@ function UsersPage() {
         setAddVisible(false);
         setDocsVisible(false);
         setEditVisible(false);
+        setPasswordEditVisible(false);
     };
 
-    const handleSearch = (
-        selectedKeys: React.SetStateAction<string>[],
-        confirm: () => void
-    ) => {
-        confirm();
-    };
-
-    const handleReset = (clearFilters: () => void) => {
-        clearFilters();
-    };
-
-    const getColumnSearchProps = (dataIndex: string, searchLabel: string) => ({
-        // @ts-ignore
-        filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => (
-            <div style={{ padding: 8 }}>
-                <Input
-                    placeholder={`Vyhledat ${searchLabel}`}
-                    value={selectedKeys[0]}
-                    onChange={(e) =>
-                        setSelectedKeys(e.target.value ? [e.target.value] : [])
-                    }
-                    onPressEnter={() => handleSearch(selectedKeys, confirm)}
-                    style={{ marginBottom: 8, display: 'block' }}
-                />
-                <Space>
-                    <Button
-                        type="primary"
-                        onClick={() => handleSearch(selectedKeys, confirm)}
-                        icon={<SearchOutlined />}
-                        style={{ width: 90 }}
-                    >
-                        Hledat
-                    </Button>
-                    <Button
-                        onClick={() => handleReset(clearFilters)}
-                        style={{ width: 90 }}
-                    >
-                        Smazat
-                    </Button>
-                </Space>
-            </div>
-        ),
-        filterIcon: (filtered: any) => (
-            <SearchOutlined style={{ color: filtered ? '#1890ff' : undefined }} />
-        ),
-        onFilter: (value: string, record: { [x: string]: { toString: () => string } }) =>
-            record[dataIndex]
-                ? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase())
-                : '',
-    });
+    const menu = (id: string) => (
+        <Menu>
+            <Menu.Item
+                icon={<EditOutlined />}
+                key={'edit'}
+                onClick={() => showEditModal(id)}
+            >
+                Upravit
+            </Menu.Item>
+            <Menu.Item icon={<LockOutlined />} onClick={() => showEditPasswordModal(id)}>
+                Změnit Heslo
+            </Menu.Item>
+            <Menu.Item icon={<DeleteOutlined />} onClick={() => deleteUser(id)}>
+                Smazat
+            </Menu.Item>
+        </Menu>
+    );
 
     const columns = [
         {
@@ -132,18 +110,53 @@ function UsersPage() {
             dataIndex: 'name',
             key: 'name',
             ...getColumnSearchProps('name', 'jméno'),
+            // @ts-ignore
+            sorter: (a, b) => a.name.localeCompare(b.name),
         },
         {
             title: 'Příjmení',
             dataIndex: 'surname',
             key: 'surname',
             ...getColumnSearchProps('surname', 'příjmení'),
+            // @ts-ignore
+            sorter: (a, b) => a.surname.localeCompare(b.surname),
         },
         {
             title: 'Uživatelské jméno',
             dataIndex: 'username',
             key: 'username',
             ...getColumnSearchProps('username', 'uživatelské jméno'),
+            // @ts-ignore
+            sorter: (a, b) => a.username.localeCompare(b.username),
+        },
+        {
+            title: 'Role',
+            dataIndex: 'role',
+            key: 'role',
+            filters: [
+                {
+                    text: 'Administrátor',
+                    value: 'ADMINISTRATOR',
+                },
+                {
+                    text: 'Anotátor',
+                    value: 'ANNOTATOR',
+                },
+            ],
+            // @ts-ignore
+            onFilter: (value, record) => record.role.indexOf(value) === 0,
+            // @ts-ignore
+            sorter: (a, b) => a.username.localeCompare(b.username),
+            render: (role: string) => {
+                let label = '';
+                if (role === 'ANNOTATOR') {
+                    label = 'Anotátor';
+                }
+                if (role === 'ADMINISTRATOR') {
+                    label = 'Administrátor';
+                }
+                return label;
+            },
         },
         {
             title: 'Přiřazeno dokumentů',
@@ -169,13 +182,14 @@ function UsersPage() {
             title: '',
             key: 'operations',
             dataIndex: 'id',
-            align: 'center' as 'center',
+            align: 'right' as 'right',
+            width: '150px',
             // @ts-ignore
             render: (id) => (
                 <div>
                     <Space size={'middle'}>
-                        <Button onClick={() => showEditModal(id)}>Upravit</Button>
-                        <Button onClick={() => deleteUser(id)}>Smazat</Button>
+                        {/*{menu(id)}*/}
+                        {<Dropdown.Button overlay={menu(id)} placement="bottom" />}
                     </Space>
                 </div>
             ),
@@ -193,18 +207,17 @@ function UsersPage() {
             </Button>
             {addVisible && <AddUserModal onCancel={hideModals} />}
             {editVisible && <EditUserModal userInfo={user} onCancel={hideModals} />}
+            {passwordEditVisible && (
+                <EditUserPasswordModal userInfo={user} onCancel={hideModals} />
+            )}
             {docsVisible && <UserDocumentsModal userId={user.id} onCancel={hideModals} />}
 
             <Table
-                locale={{
-                    triggerDesc: 'Seřadit sestupně',
-                    triggerAsc: 'Seřadit vzestupně',
-                    cancelSort: 'Vypnout řazení',
-                }}
+                locale={{ ...getLocaleProps() }}
                 // @ts-ignore
                 columns={columns}
                 dataSource={users}
-                scroll={{ y: 500 }}
+                scroll={{ y: 800 }}
             />
         </MainLayout>
     );
diff --git a/webapp/utils/tableUtils.tsx b/webapp/utils/tableUtils.tsx
new file mode 100644
index 0000000..78f81e6
--- /dev/null
+++ b/webapp/utils/tableUtils.tsx
@@ -0,0 +1,59 @@
+import { Button, Input, Space } from 'antd';
+import { SearchOutlined } from '@ant-design/icons';
+import React from 'react';
+import { style } from 'dom-helpers';
+
+export const getLocaleProps = () => ({
+    filterSearchPlaceholder: 'Hledat uživatele',
+    triggerDesc: 'Seřadit sestupně',
+    triggerAsc: 'Seřadit vzestupně',
+    cancelSort: 'Vypnout řazení',
+});
+
+const handleSearch = (
+    selectedKeys: React.SetStateAction<string>[],
+    confirm: () => void
+) => {
+    confirm();
+};
+
+const handleReset = (clearFilters: () => void) => {
+    clearFilters();
+};
+
+export const getColumnSearchProps = (dataIndex: string, searchLabel: string) => ({
+    // @ts-ignore
+    filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => (
+        <div style={{ padding: 8 }}>
+            <Input
+                placeholder={`Vyhledat ${searchLabel}`}
+                value={selectedKeys[0]}
+                onChange={(e) => setSelectedKeys(e.target.value ? [e.target.value] : [])}
+                onPressEnter={() => handleSearch(selectedKeys, confirm)}
+                style={{ marginBottom: 8, display: 'block' }}
+            />
+            <Space>
+                <Button
+                    type="primary"
+                    onClick={() => handleSearch(selectedKeys, confirm)}
+                    icon={<SearchOutlined />}
+                    style={{ width: 90 }}
+                >
+                    Hledat
+                </Button>
+                <Button onClick={() => handleReset(clearFilters)} style={{ width: 90 }}>
+                    Smazat
+                </Button>
+            </Space>
+        </div>
+    ),
+    filterIcon: (filtered: any) => (
+        <SearchOutlined
+            style={{ color: filtered ? '#1890ff' : '#757575', fontSize: '140%' }}
+        />
+    ),
+    onFilter: (value: string, record: { [x: string]: { toString: () => string } }) =>
+        record[dataIndex]
+            ? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase())
+            : '',
+});
