Projekt

Obecné

Profil

Stáhnout (20 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, Row, Space, Table, Tag, Typography, Input } from 'antd';
7
import { 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
    DocumentListInfo,
14
    DocumentListResponse,
15
    DocumentUserInfo,
16
    EState,
17
} from '../../../api';
18
import { documentController, userController } from '../../../controllers';
19
import AssignDocumentModal from '../../../components/modals/AssignDocumentModal';
20
import { ShowConfirm, ShowToast } from '../../../utils/alerts';
21
import { TableDocInfo } from '../../../components/types/TableDocInfo';
22
import {
23
    getAnnotationStateColor,
24
    getAnnotationStateString,
25
    getNameTruncated,
26
    getUserInfoAlt,
27
} from '../../../utils/strings';
28
import { ABadge, BadgeStyle } from '../../../components/common/ABadge';
29
import SetRequiredAnnotationsCountModal from '../../../components/modals/SetRequiredAnnotationsCountModal';
30
import DocPreviewModal from '../../../components/modals/DocPreviewModal';
31
import { UserFilter } from '../../../components/types/UserFilter';
32
import { getColumnSearchProps, getLocaleProps } from '../../../utils/tableUtils';
33
import {
34
    CheckCircleOutlined,
35
    ClockCircleOutlined,
36
    SyncOutlined,
37
    UserOutlined,
38
} from '@ant-design/icons';
39
import { SweetAlertIcon } from 'sweetalert2';
40
import { Stack } from 'react-bootstrap';
41
import { AxiosRequestConfig } from 'axios';
42
import { constants } from 'http2';
43
import HTTP_STATUS_ACCEPTED = module;
44
import HTTP_STATUS_OK = module;
45

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

    
55
    const router = useRouter();
56

    
57
    const [documents, setDocuments] = useState<TableDocInfo[]>([]);
58
    const [userFilters, setUserFilters] = useState<UserFilter[]>([]);
59
    const [selectedDocs, setSelectedDocs] = useState<string[]>([]);
60
    const [previewDocContent, setPreviewDocContent] = useState<string>();
61
    const [previewDocName, setPreviewDocName] = useState<string>();
62
    const [annotationCount, setAnnotationCount] = useState<number>();
63

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

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

    
81
        let annotationCountRes =
82
            await documentController.documentsRequiredAnnotationsGlobalGet();
83
        setAnnotationCount(annotationCountRes.data.requiredAnnotationsGlobal);
84

    
85
        if (!docs) {
86
            setDocuments([]);
87
        } else {
88
            setDocuments(tableDocs);
89
        }
90
    }
91

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

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

    
105
        const doneAnnotations = document.annotatingUsers?.filter(
106
            (usr) => usr.state === EState.Done
107
        ).length;
108

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

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

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

    
168
    async function removeUserFromDocument(documentID: string, annotatorID: string) {
169
        const res =
170
            await documentController.documentsDocumentIdAnnotatorsAnnotatorIdDelete(
171
                documentID,
172
                annotatorID
173
            );
174

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

    
181
    const getFinalizationStateIcon = (state: EState) => {
182
        const color = getAnnotationStateColor(state);
183
        const label = getAnnotationStateString(state);
184
        let icon = <CheckCircleOutlined />;
185
        if (state === 'NEW') {
186
            icon = <ClockCircleOutlined />;
187
        }
188
        if (state === 'IN_PROGRESS') {
189
            icon = <SyncOutlined />;
190
        }
191

    
192
        return (
193
            <Tag icon={icon} color={color} key={label}>
194
                {label.toUpperCase()}
195
            </Tag>
196
        );
197
    };
198

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

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

    
232
    const changeDefaultAnotationCount = (e: FocusEvent<HTMLInputElement>) => {
233
        documentController.documentsRequiredAnnotationsGlobalPost({
234
            requiredAnnotations: parseInt(e.currentTarget.value),
235
        });
236
    };
237

    
238
    const hideModal = () => {
239
        fetchData();
240
        setVisibleAdd(false);
241
        setVisibleAssign(false);
242
        setVisibleSetCount(false);
243
        setVisiblePreview(false);
244
    };
245

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

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

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

    
469
    const rowSelection = {
470
        onChange: (selectedRowKeys: React.Key[], selectedRows: DocumentListInfo[]) => {
471
            // @ts-ignore
472
            setSelectedDocs(selectedRows.map((row) => row.id));
473
        },
474
    };
475

    
476
    return redirecting || role !== 'ADMINISTRATOR' ? null : (
477
        <MainLayout>
478
            <div
479
                style={{
480
                    display: 'flex',
481
                    flexDirection: 'row',
482
                    justifyContent: 'space-between',
483
                    flexWrap: 'wrap',
484
                }}
485
            >
486
                <Typography.Title level={2}>
487
                    <FontAwesomeIcon icon={faFileLines} /> Dokumenty
488
                </Typography.Title>
489

    
490
                <Stack
491
                    style={{
492
                        width: '400px',
493
                        border: '1px solid lightgray',
494
                        borderRadius: 3,
495
                        padding: 10,
496
                        marginBottom: 30,
497
                        display: 'flex',
498
                        flexDirection: 'row',
499
                        justifyContent: 'space-between',
500
                    }}
501
                    direction="horizontal"
502
                    key={annotationCount}
503
                >
504
                    <span>Výchozí požadovaný počet anotací:</span>
505
                    <Input
506
                        style={{ width: '100px' }}
507
                        defaultValue={annotationCount}
508
                        onBlur={changeDefaultAnotationCount}
509
                    />
510
                </Stack>
511
            </div>
512

    
513
            <div
514
                style={{
515
                    padding: '10px',
516
                    display: 'flex',
517
                    flexDirection: 'row',
518
                    justifyContent: 'flex-start',
519
                    gap: '20px',
520
                }}
521
            >
522
                <Button type={'primary'} onClick={showAddModal}>
523
                    Nahrát dokument
524
                </Button>
525

    
526
                <div>
527
                    <Button
528
                        onClick={showAssignModal}
529
                        disabled={!(selectedDocs?.length > 0)}
530
                    >
531
                        Přiřadit vybrané dokumenty uživatelům
532
                    </Button>
533
                    <Button
534
                        onClick={showRequiredAnnotationsCountModal}
535
                        disabled={!(selectedDocs?.length > 0)}
536
                    >
537
                        Nastavit požadovaný počet anotací vybraným dokumentům
538
                    </Button>
539
                </div>
540
            </div>
541

    
542
            {visibleAdd && <AddDocumentModal onCancel={hideModal} />}
543
            {visibleAssign && (
544
                <AssignDocumentModal documentsIds={selectedDocs} onCancel={hideModal} />
545
            )}
546
            {visiblePreview && (
547
                <DocPreviewModal
548
                    onCancel={hideModal}
549
                    documentName={previewDocName ?? ''}
550
                    content={
551
                        previewDocContent ?? 'Nastala chyba při načítání obsahu dokumentu'
552
                    }
553
                />
554
            )}
555
            {visibleSetCount && (
556
                <SetRequiredAnnotationsCountModal
557
                    documentsIds={selectedDocs}
558
                    onCancel={hideModal}
559
                />
560
            )}
561

    
562
            <Table
563
                locale={{ ...getLocaleProps() }}
564
                rowSelection={{
565
                    type: 'checkbox',
566
                    ...rowSelection,
567
                }}
568
                // @ts-ignore
569
                columns={columns}
570
                dataSource={documents}
571
                size="middle"
572
                scroll={{ y: 600 }}
573
            />
574
        </MainLayout>
575
    );
576
}
577

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