Перенесён диапазон arm-web 8345f5e..a3c0010 (3 коммитов, 22 файлов набора синхронизации) скриптом docs/scripts/sync-web.sh. После переноса web/ и arm-web@test идентичны по набору. Коммиты arm-web: - ff1e45c file id as option value for react rendering - 0242486 fix(#49): зелёный make check и исправления после ревью (AURORA-144) - 0fdbd80 feat: add comments frontend, checklist modal, files delete, select options in document review Co-Authored-By: sync-web.sh <noreply@aepif.ru>
192 lines
7.0 KiB
TypeScript
192 lines
7.0 KiB
TypeScript
import { CloseOutlined, UploadOutlined } from '@ant-design/icons';
|
||
import { Button, Drawer, Switch, Tooltip, Typography, message } from 'antd';
|
||
import { useEffect, useState } from 'react';
|
||
import { ApiError } from '../../api/client';
|
||
import { fetchLegalEntity } from '../../services/api/directories/company_groups';
|
||
import { postComment } from '../../services/api/documents';
|
||
import { summarizeUpload, uploadFiles } from '../../services/api/files';
|
||
import { isInWork, useCurrentConsolidation } from '../../store/slices/consolidations/selectors';
|
||
import { useAppSelector } from '../../store/store';
|
||
import type { FileOut, LegalEntityBrief } from '../../types/api';
|
||
import DeleteReasonModal from './DeleteModal';
|
||
import { FilesList } from './FilesList';
|
||
import ReplaceDocModal from './ReplaceDocModal';
|
||
import { UploadDocumentsModal } from './UploadDocumentsModal';
|
||
import { consolidationLabel } from './consolidationLabel';
|
||
import type { DocumentRow } from './documentRows';
|
||
import { useLegalEntityFiles } from './useLegalEntityFiles';
|
||
|
||
const UPLOAD_DISABLED_HINT = 'Загрузка доступна только в версии в работе';
|
||
|
||
export function DocumentsDrawer({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||
const [uploadOpen, setUploadOpen] = useState(false);
|
||
const [entity, setEntity] = useState<LegalEntityBrief | null>(null);
|
||
const [showAll, setShowAll] = useState(false);
|
||
const { current_legal_entity_id, consolidations } = useAppSelector((s) => s.consolidations);
|
||
const current = useCurrentConsolidation();
|
||
// консолидация не определена (версии нет в списке ГК) — режим «все» без переключателя (design D-3)
|
||
const desk = current !== null && !showAll;
|
||
const consolidationId = desk ? current.consolidation_id : null;
|
||
const { files, loading, reload } = useLegalEntityFiles(current_legal_entity_id, consolidationId, open);
|
||
const [targetRow, setTargetRow] = useState<DocumentRow | null>(null);
|
||
const [removalOpen, setRemovalOpen] = useState(false);
|
||
const canUpload = current !== null && isInWork(current.version_status);
|
||
const [replaceOpen, setReplaceOpen] = useState(false);
|
||
const [replacing, setReplacing] = useState(false);
|
||
|
||
const onDelete = () => {
|
||
void reload();
|
||
setRemovalOpen(false);
|
||
};
|
||
|
||
const handleReplaceClick = () => {
|
||
setRemovalOpen(false);
|
||
setReplaceOpen(true);
|
||
};
|
||
const labelFor = desk
|
||
? undefined
|
||
: (f: FileOut) => consolidationLabel(f.consolidation_id, consolidations, current?.consolidation_id ?? null);
|
||
|
||
useEffect(() => {
|
||
setShowAll(false); // новая панель открывается столом консоли
|
||
setEntity(null);
|
||
if (!open || !current_legal_entity_id) return;
|
||
let cancelled = false;
|
||
fetchLegalEntity(current_legal_entity_id)
|
||
.then((e) => {
|
||
if (!cancelled) setEntity(e);
|
||
})
|
||
.catch(() => {
|
||
// без имени: шапка остаётся с подписью, панель не падает
|
||
});
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, [open, current_legal_entity_id]);
|
||
|
||
const handleRowDelete = (row: DocumentRow) => {
|
||
setTargetRow(row);
|
||
setRemovalOpen(true);
|
||
};
|
||
// Замена = загрузка нового файла в ту же консолидацию: прежний ядро переводит в SUPERSEDED после разбора,
|
||
// если совпали тип, счёт и период (05-domain-rules §9.5 п. 3). Причина — комментарием к заменяемому документу.
|
||
const handleReplaceSubmit = async (file: File, reason: string) => {
|
||
if (!current_legal_entity_id || !targetRow) return;
|
||
setReplacing(true);
|
||
try {
|
||
const result = summarizeUpload(await uploadFiles(current_legal_entity_id, [file], current?.consolidation_id ?? null));
|
||
if (result.accepted.length === 0) {
|
||
message.error(result.rejected[0]?.reason ?? 'Файл не принят');
|
||
return;
|
||
}
|
||
if (targetRow.documentId) {
|
||
await postComment(targetRow.documentId, `Замена документа: ${reason}`).catch(() =>
|
||
message.warning('Файл загружен, но причину замены сохранить не удалось'),
|
||
);
|
||
}
|
||
message.success('Файл загружен: прежний будет заменён после разбора, если совпадут тип, счёт и период');
|
||
setReplaceOpen(false);
|
||
void reload();
|
||
} catch (e: unknown) {
|
||
message.error(e instanceof ApiError ? e.message : 'Не удалось загрузить файл');
|
||
} finally {
|
||
setReplacing(false);
|
||
}
|
||
};
|
||
|
||
const uploadButton = (
|
||
<Button
|
||
type='primary'
|
||
icon={<UploadOutlined />}
|
||
disabled={!current_legal_entity_id || !canUpload}
|
||
onClick={() => setUploadOpen(true)}
|
||
style={canUpload ? { background: '#237804' } : { pointerEvents: 'none' }}>
|
||
Загрузить документы
|
||
</Button>
|
||
);
|
||
|
||
return (
|
||
<Drawer
|
||
open={open}
|
||
onClose={onClose}
|
||
destroyOnHidden
|
||
size={820}
|
||
closable={false}
|
||
extra={<Button icon={<CloseOutlined />} aria-label='Закрыть' onClick={onClose} />}
|
||
title={
|
||
<div>
|
||
<Typography.Text
|
||
type='secondary'
|
||
style={{
|
||
fontSize: 11,
|
||
letterSpacing: 0.5,
|
||
textTransform: 'uppercase',
|
||
}}>
|
||
Первичные документы
|
||
</Typography.Text>
|
||
{entity && (
|
||
<>
|
||
<div style={{ fontSize: 20, fontWeight: 500, marginTop: 4 }}>{entity.name}</div>
|
||
<Typography.Text type='secondary' style={{ fontSize: 13, fontWeight: 400 }}>
|
||
{entity.inn}
|
||
</Typography.Text>
|
||
</>
|
||
)}
|
||
</div>
|
||
}>
|
||
<div
|
||
style={{
|
||
marginBottom: 16,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 16,
|
||
}}>
|
||
{canUpload ? (
|
||
uploadButton
|
||
) : (
|
||
<Tooltip title={UPLOAD_DISABLED_HINT}>
|
||
<span style={{ display: 'inline-block', cursor: 'not-allowed' }}>{uploadButton}</span>
|
||
</Tooltip>
|
||
)}
|
||
{current !== null && (
|
||
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<Switch size='small' checked={showAll} onChange={setShowAll} />
|
||
<Typography.Text>Все документы организации</Typography.Text>
|
||
</span>
|
||
)}
|
||
</div>
|
||
<FilesList files={files} loading={loading} labelFor={labelFor} onDeleteClick={handleRowDelete} />
|
||
{current_legal_entity_id && (
|
||
<UploadDocumentsModal
|
||
legalEntityId={current_legal_entity_id}
|
||
consolidationId={current?.consolidation_id ?? null}
|
||
organizationName={entity?.name ?? null}
|
||
open={uploadOpen}
|
||
onClose={() => setUploadOpen(false)}
|
||
onUploaded={() => void reload()}
|
||
/>
|
||
)}
|
||
{targetRow && (
|
||
<>
|
||
<DeleteReasonModal
|
||
targetRow={targetRow}
|
||
open={removalOpen}
|
||
onCancel={() => setRemovalOpen(false)}
|
||
onDelete={onDelete}
|
||
onReplace={handleReplaceClick}
|
||
canReplace={canUpload}
|
||
/>
|
||
<ReplaceDocModal
|
||
open={replaceOpen}
|
||
docName={targetRow.name}
|
||
organizationName={entity?.name ?? null}
|
||
submitting={replacing}
|
||
onCancel={() => setReplaceOpen(false)}
|
||
onSubmit={handleReplaceSubmit}
|
||
/>
|
||
</>
|
||
)}
|
||
</Drawer>
|
||
);
|
||
}
|