Projekt

Obecné

Profil

Stáhnout (21.2 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, Input, 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

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

    
50
    const router = useRouter();
51

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

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

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

    
76
        let annotationCountRes =
77
            await documentController.documentsRequiredAnnotationsGlobalGet();
78
        setAnnotationCount(annotationCountRes.data.requiredAnnotationsGlobal);
79

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

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

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

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

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

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

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

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

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

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

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

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

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

    
227
    const deleteDocuments = () => {
228
        const req: DeleteDocumentsRequest = { documentIds: selectedDocs };
229

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

    
238
    const exportDocuments = () => {
239
        const req: ExportRequest = { documentIds: selectedDocs };
240
        documentController.documentsExportPost(req);
241
    };
242

    
243
    const changeDefaultAnotationCount = (e: FocusEvent<HTMLInputElement>) => {
244
        documentController.documentsRequiredAnnotationsGlobalPost({
245
            requiredAnnotations: parseInt(e.currentTarget.value),
246
        });
247
    };
248

    
249
    const hideModal = () => {
250
        fetchData();
251
        setVisibleAdd(false);
252
        setVisibleAssign(false);
253
        setVisibleSetCount(false);
254
        setVisiblePreview(false);
255
    };
256

    
257
    function getUserTag(user: DocumentUserInfo, record: DocumentListInfo) {
258
        return (
259
            <span
260
                className={'userTagWrapper'}
261
                title={getUserInfoAlt(user) + '\nStav: ' + user.state}
262
                onClick={() => {
263
                    ShowConfirm(
264
                        async () => {
265
                            if (!record.id || !user?.id) {
266
                                return;
267
                            }
268
                            await removeUserFromDocument(record.id, user.id);
269
                        },
270
                        'odebrat uživatele ' +
271
                            user.name +
272
                            ' ' +
273
                            user.surname +
274
                            ' (' +
275
                            user.username +
276
                            ') z tohoto dokumentu',
277
                        'Dosavadní postup tohoto anotátora na daném dokumentu bude nenávratně smazán'
278
                    );
279
                }}
280
            >
281
                <span
282
                    key={user.username + '.' + record.id}
283
                    style={{
284
                        color: getAnnotationStateColor(user.state),
285
                    }}
286
                    className={'me-3 userTag'}
287
                >
288
                    <FontAwesomeIcon
289
                        icon={faUser}
290
                        title={getUserInfoAlt(user)}
291
                        className={'me-2'}
292
                    />
293
                    {record.finalAnnotations?.some(
294
                        (annot) => annot.userId === user.id
295
                    ) ? (
296
                        <u>{getNameTruncated(user)}</u>
297
                    ) : (
298
                        getNameTruncated(user)
299
                    )}
300
                </span>
301
                <span className={'remove'}>Odebrat</span>
302
            </span>
303
        );
304
    }
305

    
306
    const columns = [
307
        {
308
            title: 'Název dokumentu',
309
            dataIndex: 'name',
310
            key: 'name',
311
            width: '30%',
312
            ...getColumnSearchProps('name', 'název'),
313
            sorter: {
314
                // @ts-ignore
315
                compare: (a, b) => a.name.localeCompare(b.name),
316
                multiple: 2,
317
            },
318
        },
319
        {
320
            title: 'Délka',
321
            dataIndex: 'length',
322
            key: 'length',
323
            width: '5%',
324
            align: 'center' as 'center',
325
            sorter: {
326
                // @ts-ignore
327
                compare: (a, b) => a.length - b.length,
328
                multiple: 1,
329
            },
330
        },
331
        {
332
            title: 'Dokončeno | přiřazeno | vyžadováno',
333
            key: 'annotationCounts',
334
            width: '15%',
335
            align: 'center' as 'center',
336
            render: (
337
                columnData: DocumentListResponse,
338
                record: DocumentListInfo,
339
                index: number
340
            ) => {
341
                const finished =
342
                    record.annotatingUsers?.filter((d) => d.state === EState.Done)
343
                        .length ?? 0;
344

    
345
                return (
346
                    <div>
347
                        <ABadge
348
                            style={
349
                                finished === record.annotatingUsers?.length
350
                                    ? BadgeStyle.SUCCESS
351
                                    : BadgeStyle.WARNING
352
                            }
353
                        >
354
                            {finished}
355
                        </ABadge>
356
                        {' | '}
357
                        <ABadge
358
                            style={
359
                                (record.annotatingUsers?.length ?? 0) >=
360
                                (record.requiredAnnotations ?? 0)
361
                                    ? BadgeStyle.SUCCESS
362
                                    : BadgeStyle.WARNING
363
                            }
364
                        >
365
                            {record.annotatingUsers?.length}
366
                        </ABadge>
367
                        {' | '}
368
                        <ABadge style={BadgeStyle.GENERAL}>
369
                            {record.requiredAnnotations}
370
                        </ABadge>
371
                    </div>
372
                );
373
            },
374
        },
375
        {
376
            title: 'Anotátoři',
377
            dataIndex: 'annotatingUsers',
378
            key: 'annotatingUsers',
379
            width: '20%',
380
            render: (
381
                columnData: DocumentUserInfo[],
382
                record: DocumentListInfo,
383
                index: number
384
            ) => {
385
                return <div>{columnData.map((e) => getUserTag(e, record))}</div>;
386
            },
387
            filters: userFilters,
388
            filterSearch: true,
389
            // @ts-ignore
390
            onFilter: (value, record) =>
391
                // @ts-ignore
392
                record.annotatingUsers.find((user) => user['username'] === value),
393
            sorter: {
394
                // @ts-ignore
395
                compare: (a, b) => a.annotatingUsers.length - b.annotatingUsers.length,
396
                multiple: 3,
397
            },
398
        },
399
        {
400
            title: '',
401
            key: 'action',
402
            dataIndex: ['id', 'name'],
403
            align: 'center' as 'center',
404
            width: '10%',
405
            // @ts-ignore
406
            render: (text, row) => (
407
                <Button
408
                    style={{ width: '80px' }}
409
                    key={row.id}
410
                    onClick={() => showPreviewModal(row.id, row.name)}
411
                >
412
                    Náhled
413
                </Button>
414
            ),
415
        },
416
        {
417
            title: 'Finální verze dokumentu',
418
            key: 'final',
419
            dataIndex: ['id', 'finalizedExists'],
420
            width: '20%',
421
            // @ts-ignore
422
            render: (text, row) =>
423
                row.finalizedExists ? (
424
                    <>
425
                        <Row>
426
                            <Space>
427
                                <Button
428
                                    disabled={finalizing}
429
                                    onClick={() => finalizeDocumentConfirm(row.id, true)}
430
                                >
431
                                    Znovu vytvořit
432
                                </Button>
433
                            </Space>
434
                        </Row>
435
                        <Row style={{ marginTop: '5px' }}>
436
                            <Space>
437
                                <Button
438
                                    disabled={finalizing}
439
                                    onClick={() =>
440
                                        editFinalizedDocument(row.finalizedAnnotationId)
441
                                    }
442
                                >
443
                                    Upravit
444
                                </Button>
445
                                {getFinalizationStateIcon(row.finalizedState)}
446
                            </Space>
447
                        </Row>
448
                    </>
449
                ) : (
450
                    <Button
451
                        disabled={finalizing}
452
                        onClick={() => finalizeDocumentConfirm(row, false)}
453
                    >
454
                        Finalizovat
455
                    </Button>
456
                ),
457
            filters: [
458
                {
459
                    text: 'Nefinalizováno',
460
                    value: null,
461
                },
462
                {
463
                    text: 'Nový',
464
                    value: 'NEW',
465
                },
466
                {
467
                    text: 'Rozpracováno',
468
                    value: 'IN_PROGRESS',
469
                },
470
                {
471
                    text: 'Hotovo',
472
                    value: 'DONE',
473
                },
474
            ],
475
            // @ts-ignore
476
            onFilter: (value, record) => record.finalizedState === value,
477
        },
478
    ];
479

    
480
    const rowSelection = {
481
        onChange: (selectedRowKeys: React.Key[], selectedRows: DocumentListInfo[]) => {
482
            // @ts-ignore
483
            setSelectedDocs(selectedRows.map((row) => row.id));
484
        },
485
    };
486

    
487
    return redirecting || role !== 'ADMINISTRATOR' ? null : (
488
        <MainLayout>
489
            <div
490
                style={{
491
                    display: 'flex',
492
                    flexDirection: 'row',
493
                    justifyContent: 'space-between',
494
                    flexWrap: 'wrap',
495
                }}
496
            >
497
                <Typography.Title level={2}>
498
                    <FontAwesomeIcon icon={faFileLines} /> Dokumenty
499
                </Typography.Title>
500

    
501
                <Stack
502
                    style={{
503
                        width: '400px',
504
                        border: '1px solid lightgray',
505
                        borderRadius: 3,
506
                        padding: 10,
507
                        marginBottom: 30,
508
                        display: 'flex',
509
                        flexDirection: 'row',
510
                        justifyContent: 'space-between',
511
                    }}
512
                    direction="horizontal"
513
                    key={annotationCount}
514
                >
515
                    <span>Výchozí požadovaný počet anotací:</span>
516
                    <Input
517
                        style={{ width: '100px' }}
518
                        defaultValue={annotationCount}
519
                        onBlur={changeDefaultAnotationCount}
520
                    />
521
                </Stack>
522
            </div>
523

    
524
            <div
525
                style={{
526
                    padding: '10px',
527
                    display: 'flex',
528
                    flexDirection: 'row',
529
                    justifyContent: 'flex-start',
530
                    gap: '20px',
531
                    marginBottom: '20px',
532
                }}
533
            >
534
                <Button type={'primary'} onClick={showAddModal}>
535
                    Nahrát dokument
536
                </Button>
537

    
538
                <div style={{ display: 'flex', gap: '3px' }}>
539
                    <Button
540
                        danger
541
                        disabled={!(selectedDocs?.length > 0)}
542
                        onClick={() => deleteDocuments()}
543
                    >
544
                        Smazat dokumenty
545
                    </Button>
546

    
547
                    <Button
548
                        disabled={!(selectedDocs?.length > 0)}
549
                        onClick={() => exportDocuments()}
550
                    >
551
                        Export
552
                    </Button>
553
                    <Button
554
                        onClick={showAssignModal}
555
                        disabled={!(selectedDocs?.length > 0)}
556
                    >
557
                        Přiřadit vybrané dokumenty uživatelům
558
                    </Button>
559
                    <Button
560
                        onClick={showRequiredAnnotationsCountModal}
561
                        disabled={!(selectedDocs?.length > 0)}
562
                    >
563
                        Nastavit požadovaný počet anotací vybraným dokumentům
564
                    </Button>
565
                </div>
566
            </div>
567

    
568
            {visibleAdd && <AddDocumentModal onCancel={hideModal} />}
569
            {visibleAssign && (
570
                <AssignDocumentModal documentsIds={selectedDocs} onCancel={hideModal} />
571
            )}
572
            {visiblePreview && (
573
                <DocPreviewModal
574
                    onCancel={hideModal}
575
                    documentName={previewDocName ?? ''}
576
                    content={
577
                        previewDocContent ?? 'Nastala chyba při načítání obsahu dokumentu'
578
                    }
579
                />
580
            )}
581
            {visibleSetCount && (
582
                <SetRequiredAnnotationsCountModal
583
                    documentsIds={selectedDocs}
584
                    onCancel={hideModal}
585
                />
586
            )}
587

    
588
            <Table
589
                locale={{ ...getLocaleProps() }}
590
                rowSelection={{
591
                    type: 'checkbox',
592
                    ...rowSelection,
593
                }}
594
                // @ts-ignore
595
                columns={columns}
596
                dataSource={documents}
597
                size="middle"
598
                scroll={{ y: 'calc(65vh - 4em)' }}
599
                pagination={false}
600
            />
601
        </MainLayout>
602
    );
603
}
604

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