Projekt

Obecné

Profil

Stáhnout (23.7 KB) Statistiky
| Větev: | Tag: | Revize:
1
import 'antd/dist/antd.css';
2
import React, { FocusEvent, useContext, useEffect, useState } from 'react';
3

    
4
import { useUnauthRedirect } from '../../../hooks';
5
import { useRouter } from 'next/router';
6
import { Button, Dropdown, Input, Menu, Row, Space, Table, Tag, Typography } from 'antd';
7
import { faArrowsRotate, faFileLines, faUser } from '@fortawesome/free-solid-svg-icons';
8
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
9
import { LoggedUserContext } from '../../../contexts/LoggedUserContext';
10
import { MainLayout } from '../../../layouts/MainLayout';
11
import AddDocumentModal from '../../../components/modals/AddDocumentModal';
12
import {
13
    DeleteDocumentsRequest,
14
    DocumentListInfo,
15
    DocumentListResponse,
16
    DocumentUserInfo,
17
    EState,
18
    ExportRequest,
19
} from '../../../api';
20
import { documentController, userController } from '../../../controllers';
21
import AssignDocumentModal from '../../../components/modals/AssignDocumentModal';
22
import { ShowConfirm, ShowConfirmDelete, ShowToast } from '../../../utils/alerts';
23
import { TableDocInfo } from '../../../components/types/TableDocInfo';
24
import {
25
    getAnnotationStateColor,
26
    getAnnotationStateString,
27
    getNameTruncated,
28
    getUserInfoAlt,
29
} from '../../../utils/strings';
30
import { ABadge, BadgeStyle } from '../../../components/common/ABadge';
31
import SetRequiredAnnotationsCountModal from '../../../components/modals/SetRequiredAnnotationsCountModal';
32
import DocPreviewModal from '../../../components/modals/DocPreviewModal';
33
import { UserFilter } from '../../../components/types/UserFilter';
34
import { getColumnSearchProps, getLocaleProps } from '../../../utils/tableUtils';
35
import { SweetAlertIcon } from 'sweetalert2';
36
import { Stack } from 'react-bootstrap';
37

    
38
import { faCircleCheck, faClock } from '@fortawesome/free-regular-svg-icons';
39
import styles from '/styles/Icon.module.scss';
40
import { FileSearchOutlined, UserDeleteOutlined } from '@ant-design/icons';
41

    
42
function AdminDocumentPage() {
43
    const redirecting = useUnauthRedirect('/login');
44
    const { logout, role } = useContext(LoggedUserContext);
45
    const [finalizing, setFinalizing] = React.useState(false);
46
    const [visibleAdd, setVisibleAdd] = React.useState(false);
47
    const [visibleAssign, setVisibleAssign] = React.useState(false);
48
    const [visiblePreview, setVisiblePreview] = React.useState(false);
49
    const [visibleSetCount, setVisibleSetCount] = React.useState(false);
50

    
51
    const router = useRouter();
52

    
53
    const [documents, setDocuments] = useState<TableDocInfo[]>([]);
54
    const [userFilters, setUserFilters] = useState<UserFilter[]>([]);
55
    const [selectedDocs, setSelectedDocs] = useState<string[]>([]);
56
    const [selectedRows, setSelectedRows] = useState([]);
57
    const [previewDocContent, setPreviewDocContent] = useState<string>();
58
    const [previewDocName, setPreviewDocName] = useState<string>();
59
    const [annotationCount, setAnnotationCount] = useState<number>();
60

    
61
    async function fetchData() {
62
        const docs = (await documentController.documentsGet()).data.documents;
63
        // @ts-ignore
64
        const tableDocs: TableDocInfo[] = docs?.map((doc, index) => {
65
            return { key: index, ...doc };
66
        });
67

    
68
        const users = (await userController.usersGet()).data.users;
69
        // @ts-ignore
70
        const filters: UserFilter[] = users?.map((user) => {
71
            return {
72
                text: user.surname + ' ' + user.name,
73
                value: user.username,
74
            };
75
        });
76
        setUserFilters(filters);
77

    
78
        let annotationCountRes =
79
            await documentController.documentsRequiredAnnotationsGlobalGet();
80
        setAnnotationCount(annotationCountRes.data.requiredAnnotationsGlobal);
81

    
82
        if (!docs) {
83
            setDocuments([]);
84
        } else {
85
            setDocuments(tableDocs);
86
        }
87
    }
88

    
89
    useEffect(() => {
90
        if (!redirecting && role === 'ADMINISTRATOR') {
91
            fetchData();
92
        }
93
    }, [logout, redirecting, role, router]);
94

    
95
    const finalizeDocumentConfirm = async (
96
        document: DocumentListInfo,
97
        recreate: boolean
98
    ) => {
99
        let desc = recreate ? 'Dosavadní změny finální verze budou smazány' : '';
100
        let icon: SweetAlertIcon = recreate ? 'warning' : 'question';
101

    
102
        const doneAnnotations = document.annotatingUsers?.filter(
103
            (usr) => usr.state === EState.Done
104
        ).length;
105

    
106
        if (
107
            doneAnnotations !== undefined &&
108
            document.requiredAnnotations !== undefined &&
109
            doneAnnotations < document.requiredAnnotations
110
        ) {
111
            icon = 'warning';
112
            desc =
113
                'Není dokončen požadovaný počet anotací <br /> (dokončeno ' +
114
                doneAnnotations +
115
                ' z ' +
116
                document.requiredAnnotations +
117
                ')';
118
        }
119
        recreate
120
            ? ShowConfirm(
121
                  () => finalizeDocument(document.id),
122
                  'vytvořit novou finální verzi dokumentu',
123
                  desc,
124
                  icon
125
              )
126
            : ShowConfirm(
127
                  () => finalizeDocument(document.id),
128
                  'vytvořit finální verzi dokumentu',
129
                  desc,
130
                  icon
131
              );
132
    };
133

    
134
    const finalizeDocument = async (documentId: string | undefined) => {
135
        setFinalizing(true);
136
        const finalAnnotationId = (
137
            await documentController.documentDocumentIdFinalPost(
138
                documentId ? documentId : ''
139
            )
140
        ).data.finalAnnotationId;
141
        if (!finalAnnotationId) {
142
            ShowToast('Finální verzi se nepovedlo vytvořit', 'error');
143
        } else {
144
            router.push({
145
                pathname: '/annotation/[annotationId]',
146
                query: { annotationId: finalAnnotationId, final: true },
147
            });
148
        }
149
        setFinalizing(false);
150
    };
151

    
152
    const editFinalizedDocument = async (finalAnnotationId: string) => {
153
        setFinalizing(true);
154
        if (!finalAnnotationId) {
155
            ShowToast('Finální verze dosud neexistuje', 'warning');
156
        } else {
157
            router.push({
158
                pathname: '/annotation/[annotationId]',
159
                query: { annotationId: finalAnnotationId, final: true },
160
            });
161
        }
162
        setFinalizing(false);
163
    };
164

    
165
    async function removeUserFromDocument(documentID: string, annotatorID: string) {
166
        const res =
167
            await documentController.documentsDocumentIdAnnotatorsAnnotatorIdDelete(
168
                documentID,
169
                annotatorID
170
            );
171

    
172
        if (res.status === 200) {
173
            ShowToast('Uživatel byl úspěšně odebrán z dokumentu');
174
        }
175
        await fetchData();
176
    }
177

    
178
    const getFinalizationStateIcon = (state: EState) => {
179
        const color = getAnnotationStateColor(state);
180
        const label = getAnnotationStateString(state);
181
        let icon = <FontAwesomeIcon icon={faCircleCheck} className={styles.iconLeft} />;
182
        if (state === 'NEW') {
183
            icon = <FontAwesomeIcon icon={faClock} className={styles.iconLeft} />;
184
        }
185
        if (state === 'IN_PROGRESS') {
186
            icon = <FontAwesomeIcon icon={faArrowsRotate} className={styles.iconLeft} />;
187
        }
188

    
189
        return (
190
            <Tag icon={icon} color={color} key={label}>
191
                {label.toUpperCase()}
192
            </Tag>
193
        );
194
    };
195

    
196
    const showAssignModal = () => {
197
        if (selectedDocs.length == 0) {
198
            ShowToast('Vyberte dokument pro přiřazení', 'warning', 3000, 'top-end');
199
        } else {
200
            setVisibleAssign(true);
201
        }
202
    };
203
    const showRequiredAnnotationsCountModal = () => {
204
        if (selectedDocs.length == 0) {
205
            ShowToast(
206
                'Vyberte dokument, pro které chcete nastavit požadovaný počet anotací',
207
                'warning',
208
                3000,
209
                'top-end'
210
            );
211
        } else {
212
            setVisibleSetCount(true);
213
        }
214
    };
215
    const showAddModal = () => {
216
        setVisibleAdd(true);
217
    };
218

    
219
    const showPreviewModal = async (id: string, name: string) => {
220
        const documentContent = (await documentController.documentDocumentIdGet(id)).data
221
            .content;
222
        if (documentContent) {
223
            setPreviewDocName(name);
224
            setPreviewDocContent(documentContent);
225
            setVisiblePreview(true);
226
        }
227
    };
228

    
229
    const deleteDocuments = () => {
230
        const req: DeleteDocumentsRequest = { documentIds: selectedDocs };
231

    
232
        ShowConfirmDelete(() => {
233
            documentController.documentsDelete(req).then(() => {
234
                ShowToast('Dokumenty byly úspěšně odstraněny');
235
                fetchData();
236
                clearSelection();
237
            });
238
        }, 'dokumenty');
239
    };
240

    
241
    const exportDocuments = async () => {
242
        const req: ExportRequest = { documentIds: selectedDocs };
243
        const res = await documentController.documentsExportPost(req);
244
        if (res?.data) {
245
            download(res.data, 'export.zip');
246
            clearSelection();
247
        }
248
    };
249

    
250
    function download(baseData: string, filename: string) {
251
        if (!baseData) {
252
            console.log('base data null');
253
            return;
254
        }
255

    
256
        const linkSource = `data:application/zip;base64,${baseData}`;
257
        const downloadLink = document.createElement('a');
258

    
259
        downloadLink.href = linkSource;
260
        downloadLink.download = filename;
261
        downloadLink.click();
262

    
263
        // Clean up and remove the link
264
        // downloadLink.parentNode.removeChild(downloadLink);
265
    }
266

    
267
    const changeDefaultAnotationCount = (e: FocusEvent<HTMLInputElement>) => {
268
        documentController.documentsRequiredAnnotationsGlobalPost({
269
            requiredAnnotations: parseInt(e.currentTarget.value),
270
        });
271
    };
272

    
273
    const hideModalWithClear = (clear: boolean) => {
274
        fetchData();
275
        setVisibleAssign(false);
276
        setVisibleSetCount(false);
277
        if (clear) {
278
            clearSelection();
279
        }
280
    };
281

    
282
    const hideModal = () => {
283
        fetchData();
284
        setVisibleAdd(false);
285
        setVisiblePreview(false);
286
    };
287

    
288
    const removeUserDocument = (user: DocumentUserInfo, record: DocumentListInfo) => {
289
        ShowConfirm(
290
            async () => {
291
                if (!record.id || !user?.id) {
292
                    return;
293
                }
294
                await removeUserFromDocument(record.id, user.id);
295
            },
296
            'odebrat uživatele ' +
297
                user.name +
298
                ' ' +
299
                user.surname +
300
                ' (' +
301
                user.username +
302
                ') z tohoto dokumentu',
303
            'Dosavadní postup tohoto anotátora na daném dokumentu bude nenávratně smazán'
304
        );
305
    };
306

    
307
    const menu = (user: DocumentUserInfo, record: DocumentListInfo) => {
308
        return (
309
            <Menu>
310
                <Menu.ItemGroup
311
                    title={
312
                        <div>
313
                            <Row>
314
                                <Typography.Text>
315
                                    {getNameTruncated(user) + ' (' + user.username + ')'}
316
                                </Typography.Text>
317
                            </Row>
318
                            <Row>
319
                                <Typography.Text>
320
                                    {user.state
321
                                        ? 'stav anotace: ' +
322
                                          getAnnotationStateString(user.state)
323
                                        : ''}
324
                                </Typography.Text>
325
                            </Row>
326
                        </div>
327
                    }
328
                >
329
                    <Menu.Item
330
                        icon={<FileSearchOutlined />}
331
                        onClick={() =>
332
                            router.push({
333
                                pathname: '/annotation/[annotationId]',
334
                                query: { annotationId: user.annotationId, final: false },
335
                            })
336
                        }
337
                    >
338
                        Zobrazit anotaci
339
                    </Menu.Item>
340
                    <Menu.Item
341
                        icon={<UserDeleteOutlined />}
342
                        onClick={() => removeUserDocument(user, record)}
343
                    >
344
                        Odebrat
345
                    </Menu.Item>
346
                </Menu.ItemGroup>
347
            </Menu>
348
        );
349
    };
350

    
351
    function getUserTag(user: DocumentUserInfo, record: DocumentListInfo) {
352
        return (
353
            <Dropdown overlay={menu(user, record)}>
354
                <Space
355
                    size={2}
356
                    style={{
357
                        paddingRight: 10,
358
                        color: getAnnotationStateColor(user.state),
359
                    }}
360
                >
361
                    <FontAwesomeIcon
362
                        icon={faUser}
363
                        title={getUserInfoAlt(user)}
364
                        className={'me-2'}
365
                    />
366
                    {record.finalAnnotations?.some(
367
                        (annot) => annot.userId === user.id
368
                    ) ? (
369
                        <u>{getNameTruncated(user)}</u>
370
                    ) : (
371
                        getNameTruncated(user)
372
                    )}
373
                </Space>
374
            </Dropdown>
375
        );
376
    }
377

    
378
    const columns = [
379
        {
380
            title: 'Název dokumentu',
381
            dataIndex: 'name',
382
            key: 'name',
383
            width: '30%',
384
            ...getColumnSearchProps('name', 'název'),
385
            sorter: {
386
                // @ts-ignore
387
                compare: (a, b) => a.name.localeCompare(b.name),
388
                multiple: 2,
389
            },
390
        },
391
        {
392
            title: 'Délka',
393
            dataIndex: 'length',
394
            key: 'length',
395
            width: '5%',
396
            align: 'center' as 'center',
397
            sorter: {
398
                // @ts-ignore
399
                compare: (a, b) => a.length - b.length,
400
                multiple: 1,
401
            },
402
        },
403
        {
404
            title: 'Dokončeno | přiřazeno | vyžadováno',
405
            key: 'annotationCounts',
406
            width: '15%',
407
            align: 'center' as 'center',
408
            render: (
409
                columnData: DocumentListResponse,
410
                record: DocumentListInfo,
411
                index: number
412
            ) => {
413
                const finished =
414
                    record.annotatingUsers?.filter((d) => d.state === EState.Done)
415
                        .length ?? 0;
416

    
417
                return (
418
                    <div>
419
                        <ABadge
420
                            style={
421
                                finished === record.annotatingUsers?.length
422
                                    ? BadgeStyle.SUCCESS
423
                                    : BadgeStyle.WARNING
424
                            }
425
                        >
426
                            {finished}
427
                        </ABadge>
428
                        {' | '}
429
                        <ABadge
430
                            style={
431
                                (record.annotatingUsers?.length ?? 0) >=
432
                                (record.requiredAnnotations ?? 0)
433
                                    ? BadgeStyle.SUCCESS
434
                                    : BadgeStyle.WARNING
435
                            }
436
                        >
437
                            {record.annotatingUsers?.length}
438
                        </ABadge>
439
                        {' | '}
440
                        <ABadge style={BadgeStyle.GENERAL}>
441
                            {record.requiredAnnotations}
442
                        </ABadge>
443
                    </div>
444
                );
445
            },
446
        },
447
        {
448
            title: 'Anotátoři',
449
            dataIndex: 'annotatingUsers',
450
            key: 'annotatingUsers',
451
            width: '20%',
452
            render: (
453
                columnData: DocumentUserInfo[],
454
                record: DocumentListInfo,
455
                index: number
456
            ) => {
457
                return <div>{columnData.map((e) => getUserTag(e, record))}</div>;
458
            },
459
            filters: userFilters,
460
            filterSearch: true,
461
            // @ts-ignore
462
            onFilter: (value, record) =>
463
                // @ts-ignore
464
                record.annotatingUsers.find((user) => user['username'] === value),
465
            sorter: {
466
                // @ts-ignore
467
                compare: (a, b) => a.annotatingUsers.length - b.annotatingUsers.length,
468
                multiple: 3,
469
            },
470
        },
471
        {
472
            title: '',
473
            key: 'action',
474
            dataIndex: ['id', 'name'],
475
            align: 'center' as 'center',
476
            width: '10%',
477
            // @ts-ignore
478
            render: (text, row) => (
479
                <Button
480
                    style={{ width: '80px' }}
481
                    key={row.id}
482
                    onClick={() => showPreviewModal(row.id, row.name)}
483
                >
484
                    Náhled
485
                </Button>
486
            ),
487
        },
488
        {
489
            title: 'Finální verze dokumentu',
490
            key: 'final',
491
            dataIndex: ['id', 'finalizedExists'],
492
            width: '20%',
493
            // @ts-ignore
494
            render: (text, row) =>
495
                row.finalizedExists ? (
496
                    <>
497
                        <Row>
498
                            <Space>
499
                                <Button
500
                                    disabled={finalizing}
501
                                    onClick={() => finalizeDocumentConfirm(row, true)}
502
                                >
503
                                    Znovu vytvořit
504
                                </Button>
505
                            </Space>
506
                        </Row>
507
                        <Row style={{ marginTop: '5px' }}>
508
                            <Space>
509
                                <Button
510
                                    disabled={finalizing}
511
                                    onClick={() =>
512
                                        editFinalizedDocument(row.finalizedAnnotationId)
513
                                    }
514
                                >
515
                                    Upravit
516
                                </Button>
517
                                {getFinalizationStateIcon(row.finalizedState)}
518
                            </Space>
519
                        </Row>
520
                    </>
521
                ) : (
522
                    <Button
523
                        disabled={finalizing}
524
                        onClick={() => finalizeDocumentConfirm(row, false)}
525
                    >
526
                        Finalizovat
527
                    </Button>
528
                ),
529
            filters: [
530
                {
531
                    text: 'Nefinalizováno',
532
                    value: null,
533
                },
534
                {
535
                    text: 'Nový',
536
                    value: 'NEW',
537
                },
538
                {
539
                    text: 'Rozpracováno',
540
                    value: 'IN_PROGRESS',
541
                },
542
                {
543
                    text: 'Hotovo',
544
                    value: 'DONE',
545
                },
546
            ],
547
            // @ts-ignore
548
            onFilter: (value, record) => record.finalizedState === value,
549
        },
550
    ];
551

    
552
    const clearSelection = () => {
553
        setSelectedRows([]);
554
    };
555

    
556
    const onSelectChange = (
557
        selectedRowKeys: React.Key[],
558
        selectedRows: DocumentListInfo[]
559
    ) => {
560
        // @ts-ignore
561
        setSelectedDocs(selectedRows.map((row) => row.id));
562
        // @ts-ignore
563
        setSelectedRows(selectedRowKeys);
564
    };
565

    
566
    const rowSelection = {
567
        selectedRowKeys: selectedRows,
568
        onChange: onSelectChange,
569
    };
570

    
571
    return redirecting || role !== 'ADMINISTRATOR' ? null : (
572
        <MainLayout>
573
            <div
574
                style={{
575
                    display: 'flex',
576
                    flexDirection: 'row',
577
                    justifyContent: 'space-between',
578
                    flexWrap: 'wrap',
579
                }}
580
            >
581
                <Typography.Title level={2}>
582
                    <FontAwesomeIcon icon={faFileLines} /> Dokumenty
583
                </Typography.Title>
584

    
585
                <Stack
586
                    style={{
587
                        width: '400px',
588
                        border: '1px solid lightgray',
589
                        borderRadius: 3,
590
                        padding: 10,
591
                        marginBottom: 30,
592
                        display: 'flex',
593
                        flexDirection: 'row',
594
                        justifyContent: 'space-between',
595
                    }}
596
                    direction="horizontal"
597
                    key={annotationCount}
598
                >
599
                    <span>Výchozí požadovaný počet anotací:</span>
600
                    <Input
601
                        style={{ width: '100px' }}
602
                        defaultValue={annotationCount}
603
                        onBlur={changeDefaultAnotationCount}
604
                    />
605
                </Stack>
606
            </div>
607

    
608
            <div
609
                style={{
610
                    padding: '10px',
611
                    display: 'flex',
612
                    flexDirection: 'row',
613
                    justifyContent: 'flex-start',
614
                    gap: '20px',
615
                    marginBottom: '20px',
616
                }}
617
            >
618
                <Button type={'primary'} onClick={showAddModal}>
619
                    Nahrát dokument
620
                </Button>
621

    
622
                <div style={{ display: 'flex', gap: '3px', flexWrap: 'wrap' }}>
623
                    <Button
624
                        danger
625
                        disabled={!(selectedDocs?.length > 0)}
626
                        onClick={() => deleteDocuments()}
627
                    >
628
                        Smazat dokumenty
629
                    </Button>
630

    
631
                    <Button
632
                        disabled={!(selectedDocs?.length > 0)}
633
                        onClick={() => exportDocuments()}
634
                    >
635
                        Export
636
                    </Button>
637
                    <Button
638
                        onClick={showAssignModal}
639
                        disabled={!(selectedDocs?.length > 0)}
640
                    >
641
                        Přiřadit vybrané dokumenty uživatelům
642
                    </Button>
643
                    <Button
644
                        onClick={showRequiredAnnotationsCountModal}
645
                        disabled={!(selectedDocs?.length > 0)}
646
                    >
647
                        Nastavit požadovaný počet anotací vybraným dokumentům
648
                    </Button>
649
                </div>
650
            </div>
651

    
652
            {visibleAdd && <AddDocumentModal onCancel={hideModal} />}
653
            {visibleAssign && (
654
                <AssignDocumentModal
655
                    documentsIds={selectedDocs}
656
                    onCancel={hideModalWithClear}
657
                />
658
            )}
659
            {visiblePreview && (
660
                <DocPreviewModal
661
                    onCancel={hideModal}
662
                    documentName={previewDocName ?? ''}
663
                    content={
664
                        previewDocContent ?? 'Nastala chyba při načítání obsahu dokumentu'
665
                    }
666
                />
667
            )}
668
            {visibleSetCount && (
669
                <SetRequiredAnnotationsCountModal
670
                    documentsIds={selectedDocs}
671
                    onCancel={hideModalWithClear}
672
                />
673
            )}
674

    
675
            <Table
676
                locale={{ ...getLocaleProps() }}
677
                rowSelection={rowSelection}
678
                // @ts-ignore
679
                columns={columns}
680
                dataSource={documents}
681
                size="middle"
682
                scroll={{ y: 'calc(65vh - 4em)' }}
683
                pagination={false}
684
            />
685
        </MainLayout>
686
    );
687
}
688

    
689
export default AdminDocumentPage;
    (1-1/1)