feat: add comments frontend, checklist modal, files delete, select options in document review #49
@ -43,7 +43,7 @@ function App() {
|
||||
<Route path='/catalogue/groups' element={<CatalogueGroup />} />
|
||||
<Route path='/catalogue/jur_l' element={<CatalogueJur />} />
|
||||
<Route path='/docstorage' element={<DocStorage />} />
|
||||
<Route path='/documents/:document_id' element={<DocumentPage />} />
|
||||
<Route path='/documents' element={<DocumentPage />} />
|
||||
<Route path='/logging' element={<AuditLogging />} />
|
||||
</Routes>
|
||||
</Layout.Content>
|
||||
|
||||
@ -19,8 +19,16 @@ const crumbs = (location: string | string[]) =>
|
||||
export default function PageTitle({ location, header, description, backable = false, extra }: PageTitleProps) {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<header style={{ display: 'flex', alignItems: 'flex-start', gap: 16, marginBottom: 12, flex: '0 0 auto' }}>
|
||||
<div style={{ flex: '1 1 auto', minWidth: 0 }}>
|
||||
<header
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: 16,
|
||||
marginBottom: 12,
|
||||
flex: '0 0 auto',
|
||||
}}>
|
||||
{/* <div style={{ flex: '1 1 auto', minWidth: 0 }}> */}
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontSize: 12, color: '#667085' }}>
|
||||
{backable && (
|
||||
<Button
|
||||
@ -45,7 +53,17 @@ export default function PageTitle({ location, header, description, backable = fa
|
||||
</h1>
|
||||
{description && <div style={{ color: '#667085' }}>{description}</div>}
|
||||
</div>
|
||||
{extra && <div style={{ flex: '0 0 auto' }}>{extra}</div>}
|
||||
{extra && (
|
||||
<div
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
width: 'min(520px, 48%)',
|
||||
minWidth: 0,
|
||||
display: 'flex',
|
||||
}}>
|
||||
{extra}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
@ -29,7 +29,9 @@ createRoot(rootElement).render(
|
||||
components: {
|
||||
Layout: {
|
||||
siderBg: '#fff',
|
||||
fontSize: 13,
|
||||
},
|
||||
|
||||
Button: {
|
||||
defaultBorderColor: '#c7cdd5',
|
||||
defaultHoverBg: '#f8fafc',
|
||||
@ -46,6 +48,10 @@ createRoot(rootElement).render(
|
||||
headerColor: '#667085',
|
||||
headerBg: 'rgb(248, 250, 252)',
|
||||
},
|
||||
Form: {
|
||||
labelFontSize: 12,
|
||||
labelColor: '#665087',
|
||||
},
|
||||
},
|
||||
}}>
|
||||
<App />
|
||||
|
||||
@ -1,30 +1,51 @@
|
||||
import { Button, Empty, Result, Spin, Tag } from 'antd';
|
||||
import { Button, Empty, Form, message, Result, Select, Spin, Tooltip } from 'antd';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { ApiError } from '../api/client';
|
||||
import PageTitle from '../components/PageTitle';
|
||||
import { fetchLegalEntity } from '../services/api/directories/company_groups';
|
||||
import { getDocument } from '../services/api/documents';
|
||||
import { getFile } from '../services/api/files';
|
||||
import { getFile, listLegalEntityFiles } from '../services/api/files';
|
||||
import type { DocumentOut } from '../types/api';
|
||||
import { documentTypeLabel, periodLabel } from '../utils/labels';
|
||||
import { OsvDocument } from '../widgets/DocumentReview/OsvDocument';
|
||||
import { StatementReview } from '../widgets/DocumentReview/StatementReview';
|
||||
import { documentPageTitle, documentSubtitle, statusBadge } from '../widgets/DocumentReview/reviewView';
|
||||
import { documentPageTitle, documentSubtitle, optionStatusBadge, statusBadge } from '../widgets/DocumentReview/reviewView';
|
||||
import { documentViewKind } from '../widgets/DocumentReview/viewKind';
|
||||
import { buildDocumentRows } from '../widgets/FilesDrawer/documentRows';
|
||||
import { TableTag } from '../components/TableTag/TableTag';
|
||||
import { TAG_PRESETS } from '../components/TableTag/presets';
|
||||
import { CommentOutlined } from '@ant-design/icons';
|
||||
import CommentModal from '../widgets/DocumentReview/CommentModal';
|
||||
|
||||
export function DocumentPage() {
|
||||
const { document_id } = useParams();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const document_id = searchParams.get('document_id');
|
||||
const legal_entity_id = searchParams.get('legal_entity_id');
|
||||
const consolidation_id = searchParams.get('consolidation_id');
|
||||
const navigate = useNavigate();
|
||||
const [doc, setDoc] = useState<DocumentOut | null>(null);
|
||||
const [entityName, setEntityName] = useState('');
|
||||
const [subtitle, setSubtitle] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [filesOptions, setFilesOptions] = useState<any[]>([]);
|
||||
const [commentOpen, setCommentOpen] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!document_id) return;
|
||||
try {
|
||||
const d = await getDocument(document_id);
|
||||
const filesJson = await listLegalEntityFiles(legal_entity_id, consolidation_id);
|
||||
const documentOptions = buildDocumentRows(filesJson);
|
||||
|
||||
const options = documentOptions.map((d) => ({
|
||||
disabled: d.documentId === null,
|
||||
label: d.name,
|
||||
value: d.documentId,
|
||||
...d,
|
||||
title: '',
|
||||
}));
|
||||
setFilesOptions(options);
|
||||
setDoc(d);
|
||||
setError(null);
|
||||
fetchLegalEntity(d.legal_entity_id)
|
||||
@ -36,7 +57,7 @@ export function DocumentPage() {
|
||||
} catch (e: unknown) {
|
||||
setError(e instanceof ApiError ? e.message : 'Не удалось открыть документ');
|
||||
}
|
||||
}, [document_id]);
|
||||
}, [document_id, legal_entity_id, consolidation_id]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
@ -53,7 +74,14 @@ export function DocumentPage() {
|
||||
// Документ в статусе ERROR — данных формы/строк нет: показываем карточку и понятный текст, а не пустой экран.
|
||||
const broken = doc.status === 'ERROR' && kind !== 'none';
|
||||
return (
|
||||
<div style={{ padding: 12, display: 'flex', flexDirection: 'column', height: '100vh', gap: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
padding: 12,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100vh',
|
||||
gap: 12,
|
||||
}}>
|
||||
<PageTitle
|
||||
backable
|
||||
location={['Отчётность', entityName, 'Проверка документа']}
|
||||
@ -63,11 +91,72 @@ export function DocumentPage() {
|
||||
`${documentTypeLabel(doc.document_type, doc.osv_account_code)} · ${periodLabel(doc.period_year, doc.period_quarter, doc.is_cumulative)}`
|
||||
}
|
||||
extra={
|
||||
<Tag variant='filled' color={badge.color} style={{ fontWeight: 600, fontSize: 14, padding: '4px 12px' }}>
|
||||
{badge.label}
|
||||
</Tag>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
alignItems: 'flex-end',
|
||||
maxWidth: '100%',
|
||||
width: '100%',
|
||||
}}>
|
||||
<Form style={{ flex: '1 1 auto', minWidth: 0, overflow: 'hidden' }} layout='vertical'>
|
||||
<Form.Item style={{ marginBottom: 0, width: '100%' }} label='Документ'>
|
||||
{document_id && (
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
onChange={(e, option) => {
|
||||
if (option.documentId === null) {
|
||||
message.error('Нельзя перейти к просмотру файла');
|
||||
} else {
|
||||
setSearchParams((old) => {
|
||||
old.set('document_id', option.documentId);
|
||||
return old;
|
||||
});
|
||||
}
|
||||
}}
|
||||
defaultValue={document_id}
|
||||
options={filesOptions}
|
||||
optionRender={(d) => {
|
||||
const currentBadge = optionStatusBadge(d.data.status);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: 10,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
overflowX: 'hidden',
|
||||
}}>
|
||||
<Tooltip title={d.data.name}>{d.data.name}</Tooltip>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 auto' }}>
|
||||
<TableTag {...currentBadge} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}></Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<div style={{ flex: '0 0 auto' }}>
|
||||
<TableTag text={badge.label} {...TAG_PRESETS[badge.color]} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{true && (
|
||||
<div style={{ alignSelf: 'flex-end' }}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setCommentOpen(true);
|
||||
}}>
|
||||
<CommentOutlined />
|
||||
Добавить комментарий
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<main
|
||||
style={{
|
||||
flex: '1 1 auto',
|
||||
@ -82,6 +171,13 @@ export function DocumentPage() {
|
||||
{!broken && kind === 'statement' && <StatementReview key={doc.id} document={doc} onChanged={load} />}
|
||||
{kind === 'none' && <Empty description='Документ без данных для проверки' />}
|
||||
</main>
|
||||
<CommentModal
|
||||
open={commentOpen}
|
||||
document_id={document_id}
|
||||
onClose={() => {
|
||||
setCommentOpen(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -8,5 +8,6 @@ export const getConsolidations = (groupId: string) => api.get<IConsolidation[]>(
|
||||
export const getVersionCard = (versionId: string) => api.get<VersionCard>(`/consolidations/versions/${versionId}`);
|
||||
export const createConsolidation = (body: ConsolidationCreate) => api.post<VersionCard>('/consolidations', body);
|
||||
export const downloadChecklistCsv = (versionId: string) => apiFetchBlob(`/consolidations/versions/${versionId}/checklist?format=csv`);
|
||||
export const getChecklist = (versionId: string) => api.get(`/consolidations/versions/${versionId}/checklist`);
|
||||
/** «Взять в работу»: 201 — взята, 200 — уже ваша; чужая — 409 `LOCKED_BY_OTHER` (перехват — с `takeover_reason`). */
|
||||
export const lockVersion = (versionId: string) => api.post<LockOut>(`/consolidations/versions/${versionId}/lock`);
|
||||
|
||||
@ -21,3 +21,8 @@ export async function getAllEntries(id: string): Promise<EntryOut[]> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function postComment(document_id: string, comment: string) {
|
||||
const body = { text: comment };
|
||||
return api.post(`/documents/${document_id}/comments/`, body);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ApiError, apiFetch, apiFetchBlob } from '../../api/client';
|
||||
import { api, ApiError, apiFetch, apiFetchBlob } from '../../api/client';
|
||||
import type { FileOut, Page, UploadResult } from '../../types/api';
|
||||
|
||||
export function isUploadResult(body: unknown): body is UploadResult {
|
||||
@ -56,3 +56,7 @@ export function fetchFileContent(fileId: string): Promise<{ blob: Blob; filename
|
||||
export function getFile(id: string): Promise<FileOut> {
|
||||
return apiFetch<FileOut>(`/files/${id}`);
|
||||
}
|
||||
|
||||
export function removeFile(fileId: string) {
|
||||
return api.del(`/files/${fileId}`);
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ const initialState: ConsolidationsSliceType = {
|
||||
consolidations: [],
|
||||
current_version_id: null,
|
||||
current_legal_entity_id: null,
|
||||
current_legal_entity_name: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
@ -17,9 +18,12 @@ const consolidationsSlice = createSlice({
|
||||
setSelectedVersion: (state, action: PayloadAction<string | null>) => {
|
||||
state.current_version_id = action.payload;
|
||||
},
|
||||
setSelectedLegalEntity: (state, action: PayloadAction<string | null>) => {
|
||||
setSelectedLegalEntityId: (state, action: PayloadAction<string | null>) => {
|
||||
state.current_legal_entity_id = action.payload;
|
||||
},
|
||||
setSelectedLegalEntityName: (state, action: PayloadAction<string | null>) => {
|
||||
state.current_legal_entity_name = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
@ -39,4 +43,4 @@ const consolidationsSlice = createSlice({
|
||||
});
|
||||
|
||||
export default consolidationsSlice.reducer;
|
||||
export const { setSelectedVersion, setSelectedLegalEntity } = consolidationsSlice.actions;
|
||||
export const { setSelectedVersion, setSelectedLegalEntityId, setSelectedLegalEntityName } = consolidationsSlice.actions;
|
||||
|
||||
@ -6,4 +6,5 @@ export type ConsolidationsSliceType = {
|
||||
current_version_id: string | null;
|
||||
error: string | null;
|
||||
current_legal_entity_id: string | null;
|
||||
current_legal_entity_name: string | null;
|
||||
};
|
||||
|
||||
@ -35,6 +35,24 @@ type ChecklistItem = {
|
||||
account?: string | null;
|
||||
};
|
||||
|
||||
type ChecklistRow = {
|
||||
cells: ChecklistItem[];
|
||||
legal_entity_id: string;
|
||||
legal_entity_name: string;
|
||||
legal_entity_inn: string;
|
||||
};
|
||||
|
||||
type ChecklistRule = {
|
||||
document_type: DocumentType;
|
||||
account: string;
|
||||
is_mandatory: boolean;
|
||||
};
|
||||
|
||||
export type ChecklistBackendShape = {
|
||||
rows: ChecklistRow[];
|
||||
rules: ChecklistRule[];
|
||||
};
|
||||
|
||||
type VersionMember = {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
81
src/widgets/ConsoleLegalEntities/ChecklistModal.tsx
Normal file
81
src/widgets/ConsoleLegalEntities/ChecklistModal.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { getChecklist } from '../../services/api/consolidations';
|
||||
import { message, Modal, Table, TableColumnsType } from 'antd';
|
||||
import { ChecklistBackendShape, type IVersionCard } from '../../types/consolidations/version_members';
|
||||
import { DownloadOutlined } from '@ant-design/icons';
|
||||
import { buildChecklistColumns, getMarksItems } from './checklist_utils';
|
||||
// import { useMe } from '../../auth/useMe';
|
||||
|
||||
type ChecklistProps = {
|
||||
open: boolean;
|
||||
onDownload: () => void;
|
||||
consolidationData: IVersionCard;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function ChecklistModal({ open, onDownload, consolidationData, onClose }: ChecklistProps) {
|
||||
const [checklistData, setChecklistData] = useState<ChecklistBackendShape | null>(null);
|
||||
|
||||
const displayedData = useMemo(() => {
|
||||
if (!checklistData) return;
|
||||
const result = [];
|
||||
for (const row of checklistData.rows) {
|
||||
const rowObj = {
|
||||
name: row.legal_entity_name,
|
||||
inn: row.legal_entity_inn,
|
||||
ofr: 'MISSING',
|
||||
balance: 'MISSING',
|
||||
};
|
||||
for (const cell of row.cells) {
|
||||
if (cell.document_type === 'OSV') {
|
||||
const code = cell.account;
|
||||
const key = `osv_${code}`;
|
||||
rowObj[key] = cell.status;
|
||||
} else if (cell.document_type === 'BALANCE') {
|
||||
rowObj.balance = cell.status;
|
||||
} else if (cell.document_type === 'OFR') {
|
||||
rowObj.ofr = cell.status;
|
||||
}
|
||||
}
|
||||
result.push(rowObj);
|
||||
}
|
||||
return result;
|
||||
}, [checklistData]);
|
||||
const columns: TableColumnsType = useMemo(() => {
|
||||
if (!checklistData) return [];
|
||||
return buildChecklistColumns(checklistData);
|
||||
}, [checklistData]);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchChecklist() {
|
||||
if (!consolidationData.id || !open) return;
|
||||
try {
|
||||
const res = await getChecklist(consolidationData.id);
|
||||
setChecklistData(res);
|
||||
} catch (e) {
|
||||
message.error(e.message);
|
||||
}
|
||||
}
|
||||
fetchChecklist();
|
||||
}, [open, consolidationData.id]);
|
||||
return (
|
||||
<Modal
|
||||
width='100%'
|
||||
open={open}
|
||||
cancelText='Отмена'
|
||||
onCancel={onClose}
|
||||
title='Чеклист'
|
||||
destroyOnHidden
|
||||
onOk={() => {
|
||||
onDownload();
|
||||
onClose();
|
||||
}}
|
||||
okText='Скачать чек-лист'
|
||||
okButtonProps={{ icon: <DownloadOutlined /> }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
{getMarksItems()}
|
||||
<Table columns={columns} dataSource={displayedData} pagination={false} />
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -6,18 +6,24 @@ import { ColumnSettings, DataTable, useFilteredRows, useVisibleColumns } from '.
|
||||
import { useTableFilters } from '../../components/GenericTable/useTableFilters';
|
||||
import { downloadChecklistCsv } from '../../services/api/consolidations';
|
||||
import { getVersionCard } from '../../services/api/consolidations';
|
||||
import { setSelectedLegalEntity, setSelectedVersion } from '../../store/slices/consolidations/consolidationsSlice';
|
||||
import {
|
||||
setSelectedLegalEntityId,
|
||||
setSelectedLegalEntityName,
|
||||
setSelectedVersion,
|
||||
} from '../../store/slices/consolidations/consolidationsSlice';
|
||||
import { useAppDispatch, useAppSelector } from '../../store/store';
|
||||
import type { IVersionCard, MemberRow } from '../../types/consolidations/version_members';
|
||||
import { saveBlob } from '../../utils/download';
|
||||
import { membersConfig } from './config';
|
||||
import { normalizeData } from './normalize';
|
||||
import { checklistFilename, responsibleName } from './panelInfo';
|
||||
import ChecklistModal from './ChecklistModal';
|
||||
|
||||
export function ConsoleLegalEntitiesTable({ onDrawerOpen, reloadKey }: { onDrawerOpen: () => void; reloadKey: number }) {
|
||||
const dispatch = useAppDispatch();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const consolidation_id = useAppSelector((s) => s.consolidations.current_version_id);
|
||||
const [checklistOpen, setChecklistOpen] = useState(false);
|
||||
|
||||
const [consolidationData, setConsolidationData] = useState<IVersionCard | null>(null);
|
||||
|
||||
@ -47,6 +53,10 @@ export function ConsoleLegalEntitiesTable({ onDrawerOpen, reloadKey }: { onDrawe
|
||||
message.error(e instanceof ApiError ? e.message : 'Не удалось скачать чек-лист');
|
||||
}
|
||||
};
|
||||
const viewChecklist = () => {
|
||||
if (!consolidationData) return;
|
||||
setChecklistOpen(true);
|
||||
};
|
||||
const { visibleKeys, setVisibleKeys } = useVisibleColumns(membersConfig);
|
||||
const { rawQuery, columnFilters, setColumnFilters, sort, setSort, resetAll, hasActiveFilters } = useTableFilters<MemberRow>();
|
||||
// single filtered array — title count and table share it
|
||||
@ -54,67 +64,85 @@ export function ConsoleLegalEntitiesTable({ onDrawerOpen, reloadKey }: { onDrawe
|
||||
const onRow = (row: MemberRow) => ({
|
||||
onClick: () => {
|
||||
dispatch(setSelectedVersion(row.version_id));
|
||||
dispatch(setSelectedLegalEntity(row.id));
|
||||
dispatch(setSelectedLegalEntityId(row.id));
|
||||
dispatch(setSelectedLegalEntityName(row.name));
|
||||
onDrawerOpen();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<Space
|
||||
style={{
|
||||
width: '100%',
|
||||
justifyContent: 'space-between',
|
||||
padding: '12px 16px',
|
||||
flex: 0,
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, margin: 0 }}>
|
||||
<h2
|
||||
style={{
|
||||
display: 'block',
|
||||
boxSizing: 'border-box',
|
||||
fontSize: 17,
|
||||
fontWeight: 600,
|
||||
margin: 0,
|
||||
}}>
|
||||
Организации
|
||||
</h2>
|
||||
<span style={{ fontSize: 13, color: 'rgb(102, 112, 133)' }}>{rawRows.length} шт.</span>
|
||||
{consolidationData && (
|
||||
// «Утверждён в РФ» / «Верифицирован в ГО» — маршрут согласования, Срез 7
|
||||
<span style={{ fontSize: 13, color: 'rgb(102, 112, 133)', marginLeft: 8 }}>
|
||||
Ответственный: {responsibleName(consolidationData)} Утверждён в РФ: — Верифицирован в ГО: —
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{hasActiveFilters && (
|
||||
<Button
|
||||
type='default'
|
||||
onClick={() => {
|
||||
resetAll();
|
||||
<>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<Space
|
||||
style={{
|
||||
width: '100%',
|
||||
justifyContent: 'space-between',
|
||||
padding: '12px 16px',
|
||||
flex: 0,
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, margin: 0 }}>
|
||||
<h2
|
||||
style={{
|
||||
display: 'block',
|
||||
boxSizing: 'border-box',
|
||||
fontSize: 17,
|
||||
fontWeight: 600,
|
||||
margin: 0,
|
||||
}}>
|
||||
Сбросить фильтры
|
||||
Организации
|
||||
</h2>
|
||||
<span style={{ fontSize: 13, color: 'rgb(102, 112, 133)' }}>{rawRows.length} шт.</span>
|
||||
{consolidationData && (
|
||||
// «Утверждён в РФ» / «Верифицирован в ГО» — маршрут согласования, Срез 7
|
||||
<span
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: 'rgb(102, 112, 133)',
|
||||
marginLeft: 8,
|
||||
}}>
|
||||
Ответственный: {responsibleName(consolidationData)} Утверждён в РФ: — Верифицирован в ГО: —
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{hasActiveFilters && (
|
||||
<Button
|
||||
type='default'
|
||||
onClick={() => {
|
||||
resetAll();
|
||||
}}>
|
||||
Сбросить фильтры
|
||||
</Button>
|
||||
)}
|
||||
<ColumnSettings defs={membersConfig.columns} visibleKeys={visibleKeys} onChange={setVisibleKeys} />
|
||||
<Button icon={<DownloadOutlined />} disabled={!consolidationData} onClick={viewChecklist} style={{ marginLeft: 8 }}>
|
||||
Скачать чек-лист
|
||||
</Button>
|
||||
)}
|
||||
<ColumnSettings defs={membersConfig.columns} visibleKeys={visibleKeys} onChange={setVisibleKeys} />
|
||||
<Button icon={<DownloadOutlined />} disabled={!consolidationData} onClick={downloadChecklist} style={{ marginLeft: 8 }}>
|
||||
Скачать чек-лист
|
||||
</Button>
|
||||
</div>
|
||||
</Space>
|
||||
<DataTable
|
||||
config={membersConfig}
|
||||
rows={filtered}
|
||||
allRows={rawRows}
|
||||
loading={loading}
|
||||
visibleKeys={visibleKeys}
|
||||
columnFilters={columnFilters}
|
||||
onColumnFiltersChange={setColumnFilters}
|
||||
sortState={sort}
|
||||
onSortChange={setSort}
|
||||
onRow={onRow}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Space>
|
||||
<DataTable
|
||||
config={membersConfig}
|
||||
rows={filtered}
|
||||
allRows={rawRows}
|
||||
loading={loading}
|
||||
visibleKeys={visibleKeys}
|
||||
columnFilters={columnFilters}
|
||||
onColumnFiltersChange={setColumnFilters}
|
||||
sortState={sort}
|
||||
onSortChange={setSort}
|
||||
onRow={onRow}
|
||||
/>
|
||||
</div>
|
||||
{consolidationData && (
|
||||
<ChecklistModal
|
||||
open={checklistOpen}
|
||||
onClose={() => {
|
||||
setChecklistOpen(false);
|
||||
}}
|
||||
onDownload={downloadChecklist}
|
||||
consolidationData={consolidationData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
94
src/widgets/ConsoleLegalEntities/checklist_utils.tsx
Normal file
94
src/widgets/ConsoleLegalEntities/checklist_utils.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
EllipsisOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
EyeOutlined,
|
||||
MinusCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { TAG_PRESETS } from '../../components/TableTag/presets';
|
||||
import { ChecklistBackendShape, FileStatus } from '../../types/consolidations/version_members';
|
||||
import { Avatar, TableColumnsType } from 'antd';
|
||||
import { statusMapping } from './consts';
|
||||
|
||||
export const mapStatus = (status: FileStatus) => {
|
||||
const presetMapping = {
|
||||
MISSING: TAG_PRESETS['default'],
|
||||
CONFIRMED: TAG_PRESETS['green'],
|
||||
NEEDS_CHECK: TAG_PRESETS['orange'],
|
||||
RECOGNIZED: TAG_PRESETS['gold'],
|
||||
ERROR: TAG_PRESETS['red'],
|
||||
PROCESSING: TAG_PRESETS['geekblue'],
|
||||
};
|
||||
const preset = presetMapping[status];
|
||||
|
||||
const iconMapping = {
|
||||
MISSING: <MinusCircleOutlined />,
|
||||
NEEDS_CHECK: <EyeOutlined />,
|
||||
ERROR: <ExclamationCircleOutlined />,
|
||||
CONFIRMED: <CheckCircleOutlined />,
|
||||
RECOGNIZED: <EllipsisOutlined />,
|
||||
PROCESSING: <ClockCircleOutlined />,
|
||||
};
|
||||
return (
|
||||
<Avatar style={{ color: preset.color, backgroundColor: preset.bgColor, borderColor: preset.borderColor }} icon={iconMapping[status]} />
|
||||
);
|
||||
};
|
||||
export function getMarksItems() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
backgroundColor: '#f8fafc',
|
||||
padding: '5px 10px',
|
||||
borderRadius: 10,
|
||||
fontSize: 11,
|
||||
}}>
|
||||
<b>Условные обозначения</b>
|
||||
{Object.entries(statusMapping).map((item) => {
|
||||
const avatar = mapStatus(item[0] as FileStatus);
|
||||
return (
|
||||
<div key={item[0]}>
|
||||
{avatar} <span style={{ color: '#667085' }}>{item[1]}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function buildChecklistColumns(checklistData: ChecklistBackendShape) {
|
||||
const osv_children = [];
|
||||
const osv_codes = new Set();
|
||||
for (const row of checklistData.rows) {
|
||||
for (const cell of row.cells) {
|
||||
if (cell.document_type === 'OSV') {
|
||||
const code = cell.account;
|
||||
if (!osv_codes.has(code)) {
|
||||
osv_children.push({ key: `osv_${code}`, dataIndex: `osv_${code}`, title: code, render: (v) => mapStatus(v) });
|
||||
osv_codes.add(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const result: TableColumnsType = [
|
||||
{
|
||||
key: 'name',
|
||||
dataIndex: 'leName',
|
||||
title: 'ОРГАНИЗАЦИЯ',
|
||||
render: (v, r) => (
|
||||
<div>
|
||||
<h5>
|
||||
{r.name}: {r.inn}
|
||||
</h5>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ key: 'balance', dataIndex: 'balance', title: 'БУХ.БАЛАНС', render: (v) => mapStatus(v) },
|
||||
{ key: 'ofr', dataIndex: 'ofr', title: 'ОФР', render: (v) => mapStatus(v) },
|
||||
{ title: 'ОСВ', children: osv_children, render: (v) => mapStatus(v) },
|
||||
];
|
||||
|
||||
return result;
|
||||
}
|
||||
43
src/widgets/DocumentReview/CommentModal.tsx
Normal file
43
src/widgets/DocumentReview/CommentModal.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { Form, Input, message, Modal } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import { postComment } from '../../services/api/documents';
|
||||
|
||||
export default function CommentModal({
|
||||
document_id, // () => void
|
||||
open,
|
||||
onClose,
|
||||
}) {
|
||||
const [comment, setComment] = useState('');
|
||||
const [isLoading, setIsloading] = useState(false);
|
||||
const handleOk = async () => {
|
||||
setIsloading(true);
|
||||
try {
|
||||
const res = await postComment(document_id, comment);
|
||||
} catch (e) {
|
||||
message.error(e.message);
|
||||
} finally {
|
||||
setIsloading(false);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Modal
|
||||
title={<div style={{ height: '50px' }}>Добавить комментарий</div>}
|
||||
okText='Добавить'
|
||||
onOk={handleOk}
|
||||
okButtonProps={{ disabled: comment.length === 0 }}
|
||||
open={open}
|
||||
cancelText='Отмена'
|
||||
onCancel={onClose}
|
||||
destroyOnHidden>
|
||||
<Form layout='vertical'>
|
||||
<Form.Item label={<span style={{ fontWeight: 600 }}>Комментарий</span>}>
|
||||
<Input
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
placeholder='Опишите результат проверки или внесенные правки'></Input>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -295,6 +295,11 @@ export function StatementReview({ document, onChanged }: { document: DocumentOut
|
||||
</span>
|
||||
)}
|
||||
{serverErrorOffScreen && <Alert type='error' showIcon title={lineServerError.text} />}
|
||||
{grid.changedValues !== null && (
|
||||
<p style={{ color: 'red' }}>
|
||||
Измененных значений: {grid.changedValues}. Значений, требующих проверки: {grid.unrecognizedValues}
|
||||
</p>
|
||||
)}
|
||||
<StatementTable
|
||||
columns={grid.columns}
|
||||
layout={layout}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { Preset, TAG_PRESETS } from '../../components/TableTag/presets';
|
||||
import type { ColumnOut, DocumentStatus, DocumentType, FileOut } from '../../types/api';
|
||||
import { formatMoscowDate } from '../../utils/dates';
|
||||
import { documentStatusLabel } from '../../utils/labels';
|
||||
import { RowStatus } from '../FilesDrawer/documentRows';
|
||||
|
||||
/** '2025-09-30' (или ISO с временем) → '30.09.2025'; пусто → null; нераспознанное — как есть. */
|
||||
export function formatReportDate(value: string | null | undefined): string | null {
|
||||
@ -32,6 +34,18 @@ export function statusBadge(status: DocumentStatus): { label: string; color: 'gr
|
||||
}
|
||||
return { label, color: status === 'ERROR' ? 'red' : 'orange' };
|
||||
}
|
||||
export function optionStatusBadge(status: RowStatus): { text: string } & Preset {
|
||||
if (status === 'success') {
|
||||
const preset = TAG_PRESETS.green;
|
||||
const text = 'Загружен';
|
||||
return { text, ...preset };
|
||||
} else if (status === 'warning') {
|
||||
return { text: 'Требуется проверка', ...TAG_PRESETS.orange };
|
||||
} else if (status === 'processing') {
|
||||
return { text: 'Обработка', ...TAG_PRESETS.geekblue };
|
||||
}
|
||||
return { text: 'Ошибка', ...TAG_PRESETS.red };
|
||||
}
|
||||
|
||||
export function documentSubtitle(file: Pick<FileOut, 'original_name' | 'file_type' | 'uploaded_at'>): string {
|
||||
return [file.original_name, file.file_type, formatMoscowDate(file.uploaded_at)].filter((p) => p).join(' · ');
|
||||
|
||||
@ -28,7 +28,39 @@ export type GridRow = {
|
||||
/** Ключ строки: id из БД, у достроенной строки (id = null) — код; двоеточия в ключе нет (разделитель cellKey). */
|
||||
const rowKey = (id: string | null, code: string | null) => id ?? `code-${code ?? ''}`;
|
||||
|
||||
export function buildStatementGrid(statement: StatementOut): { columns: ColumnOut[]; rows: GridRow[] } {
|
||||
export function buildStatementGrid(statement: StatementOut): {
|
||||
columns: ColumnOut[];
|
||||
rows: GridRow[];
|
||||
changedValues: number | null;
|
||||
unrecognizedValues: number | null;
|
||||
} {
|
||||
let changedValues: number | null = 0;
|
||||
let unrecognizedValues: number | null = 0;
|
||||
if (statement.document.status === 'NEEDS_CHECK') {
|
||||
for (const key of ['inn', 'report_date', 'units']) {
|
||||
const val = statement.header[key];
|
||||
if (!val['verified'] && !val['modified']) {
|
||||
unrecognizedValues++;
|
||||
}
|
||||
if (val['modified']) {
|
||||
changedValues++;
|
||||
}
|
||||
}
|
||||
for (const l of statement.lines) {
|
||||
for (const c of l.values) {
|
||||
if (!c.is_recognized && !c.is_modified) {
|
||||
unrecognizedValues++;
|
||||
}
|
||||
if (c.is_modified) {
|
||||
changedValues++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unrecognizedValues = null;
|
||||
changedValues = null;
|
||||
}
|
||||
|
||||
const columns = [...statement.columns].sort((a, b) => a.column_index - b.column_index);
|
||||
const rows = statement.lines.map((line) => {
|
||||
const cells: Record<string, ValueOut | undefined> = {};
|
||||
@ -54,7 +86,7 @@ export function buildStatementGrid(statement: StatementOut): { columns: ColumnOu
|
||||
cells,
|
||||
};
|
||||
});
|
||||
return { columns, rows };
|
||||
return { columns, rows, changedValues, unrecognizedValues };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
77
src/widgets/FilesDrawer/DeleteModal.tsx
Normal file
77
src/widgets/FilesDrawer/DeleteModal.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Modal, Button, Typography, theme, message } from 'antd';
|
||||
import { removeFile } from '../../services/api/files';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const OptionCard = ({ title, detail, action }) => {
|
||||
const { token } = theme.useToken();
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 16,
|
||||
padding: 12,
|
||||
border: `1px solid ${token.colorBorder}`,
|
||||
borderRadius: token.borderRadius,
|
||||
}}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Text strong>{title}</Text>
|
||||
<Text type='secondary'>{detail}</Text>
|
||||
</div>
|
||||
{action}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function DeleteReasonModal({
|
||||
targetRow,
|
||||
open,
|
||||
onCancel,
|
||||
onDelete, // () => void
|
||||
onReplace, // () => void
|
||||
}) {
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const handleDelete = useCallback(async () => {
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await removeFile(targetRow.key);
|
||||
message.success('Файл удалён');
|
||||
} catch (e) {
|
||||
message.error(e.message);
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
onDelete();
|
||||
}
|
||||
}, [targetRow.key, onDelete]);
|
||||
return (
|
||||
<Modal title='Удаление документа' open={open} onCancel={onCancel} footer={null} destroyOnHidden>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<p style={{ color: '#667085', margin: 0 }}>Выберите причину удаления документа «{targetRow.name}»</p>
|
||||
<p style={{ color: 'red', fontSize: 11, margin: 0 }}>Будет удален/заменён сам файл и все его документы</p>
|
||||
|
||||
<OptionCard
|
||||
title='Ошибочный документ'
|
||||
detail='Документ будет удален без возможности восстановления.'
|
||||
action={
|
||||
<Button type='primary' danger loading={isDeleting} onClick={handleDelete}>
|
||||
Удалить
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<OptionCard
|
||||
title='Замена документа'
|
||||
detail='Загрузите актуальный файл и укажите причину замены.'
|
||||
action={
|
||||
<Button type='primary' onClick={onReplace}>
|
||||
Заменить
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { CloseOutlined, UploadOutlined } from '@ant-design/icons';
|
||||
import { Button, Drawer, Switch, Tooltip, Typography } from 'antd';
|
||||
import { Button, Drawer, message, Switch, Tooltip, Typography } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { fetchLegalEntity } from '../../services/api/directories/company_groups';
|
||||
import { isInWork, useCurrentConsolidation } from '../../store/slices/consolidations/selectors';
|
||||
@ -9,6 +9,9 @@ import { FilesList } from './FilesList';
|
||||
import { UploadDocumentsModal } from './UploadDocumentsModal';
|
||||
import { consolidationLabel } from './consolidationLabel';
|
||||
import { useLegalEntityFiles } from './useLegalEntityFiles';
|
||||
import { DocumentRow } from './documentRows';
|
||||
import DeleteReasonModal from './DeleteModal';
|
||||
import ReplaceDocModal from './ReplaceDocModal';
|
||||
|
||||
const UPLOAD_DISABLED_HINT = 'Загрузка доступна только в версии в работе';
|
||||
|
||||
@ -22,7 +25,21 @@ export function DocumentsDrawer({ open, onClose }: { open: boolean; onClose: ()
|
||||
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);
|
||||
@ -44,6 +61,23 @@ export function DocumentsDrawer({ open, onClose }: { open: boolean; onClose: ()
|
||||
};
|
||||
}, [open, current_legal_entity_id]);
|
||||
|
||||
const handleRowDelete = (row: DocumentRow) => {
|
||||
setTargetRow(row);
|
||||
setRemovalOpen(true);
|
||||
};
|
||||
const handleReplaceSubmit = ({ file, comment }) => {
|
||||
try {
|
||||
setReplacing(true);
|
||||
message.success('Документ заменен');
|
||||
setReplaceOpen(false);
|
||||
reload();
|
||||
} catch (e) {
|
||||
message.error(e.message);
|
||||
} finally {
|
||||
setReplacing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const uploadButton = (
|
||||
<Button
|
||||
type='primary'
|
||||
@ -65,7 +99,13 @@ export function DocumentsDrawer({ open, onClose }: { open: boolean; onClose: ()
|
||||
extra={<Button icon={<CloseOutlined />} aria-label='Закрыть' onClick={onClose} />}
|
||||
title={
|
||||
<div>
|
||||
<Typography.Text type='secondary' style={{ fontSize: 11, letterSpacing: 0.5, textTransform: 'uppercase' }}>
|
||||
<Typography.Text
|
||||
type='secondary'
|
||||
style={{
|
||||
fontSize: 11,
|
||||
letterSpacing: 0.5,
|
||||
textTransform: 'uppercase',
|
||||
}}>
|
||||
Первичные документы
|
||||
</Typography.Text>
|
||||
{entity && (
|
||||
@ -78,7 +118,13 @@ export function DocumentsDrawer({ open, onClose }: { open: boolean; onClose: ()
|
||||
)}
|
||||
</div>
|
||||
}>
|
||||
<div style={{ marginBottom: 16, display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
}}>
|
||||
{canUpload ? (
|
||||
uploadButton
|
||||
) : (
|
||||
@ -93,7 +139,7 @@ export function DocumentsDrawer({ open, onClose }: { open: boolean; onClose: ()
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<FilesList files={files} loading={loading} labelFor={labelFor} />
|
||||
<FilesList files={files} loading={loading} labelFor={labelFor} onDeleteClick={handleRowDelete} />
|
||||
{current_legal_entity_id && (
|
||||
<UploadDocumentsModal
|
||||
legalEntityId={current_legal_entity_id}
|
||||
@ -104,6 +150,24 @@ export function DocumentsDrawer({ open, onClose }: { open: boolean; onClose: ()
|
||||
onUploaded={() => void reload()}
|
||||
/>
|
||||
)}
|
||||
{targetRow && (
|
||||
<>
|
||||
<DeleteReasonModal
|
||||
targetRow={targetRow}
|
||||
open={removalOpen}
|
||||
onCancel={() => setRemovalOpen(false)}
|
||||
onDelete={onDelete}
|
||||
onReplace={handleReplaceClick}
|
||||
/>
|
||||
<ReplaceDocModal
|
||||
open={replaceOpen}
|
||||
docName={targetRow?.name}
|
||||
submitting={replacing}
|
||||
onCancel={() => setReplaceOpen(false)}
|
||||
onSubmit={handleReplaceSubmit}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { CheckOutlined, CloseOutlined, FileExcelOutlined, FileImageOutlined, FilePdfOutlined } from '@ant-design/icons';
|
||||
import { Spin, Table, Tooltip, Typography } from 'antd';
|
||||
import { CheckOutlined, CloseOutlined, DeleteOutlined, FileExcelOutlined, FileImageOutlined, FilePdfOutlined } from '@ant-design/icons';
|
||||
import { Button, message, Spin, Table, Tooltip, Typography } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@ -55,10 +55,25 @@ function StatusPill({ row }: { row: DocumentRow }) {
|
||||
return row.errorReason ? <Tooltip title={row.errorReason}>{pill}</Tooltip> : pill;
|
||||
}
|
||||
|
||||
const HEADER = { fontSize: 11, letterSpacing: 0.5, color: '#8c8c8c', textTransform: 'uppercase' as const };
|
||||
const HEADER = {
|
||||
fontSize: 11,
|
||||
letterSpacing: 0.5,
|
||||
color: '#8c8c8c',
|
||||
textTransform: 'uppercase' as const,
|
||||
};
|
||||
const head = (text: string) => <span style={HEADER}>{text}</span>;
|
||||
|
||||
export function FilesList({ files, loading, labelFor }: { files: FileOut[]; loading: boolean; labelFor?: (f: FileOut) => string | null }) {
|
||||
export function FilesList({
|
||||
files,
|
||||
loading,
|
||||
labelFor,
|
||||
onDeleteClick,
|
||||
}: {
|
||||
files: FileOut[];
|
||||
loading: boolean;
|
||||
labelFor?: (f: FileOut) => string | null;
|
||||
onDeleteClick: (row: DocumentRow) => void;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const rows = buildDocumentRows(files, labelFor);
|
||||
|
||||
@ -67,7 +82,13 @@ export function FilesList({ files, loading, labelFor }: { files: FileOut[]; load
|
||||
title: head('Документ'),
|
||||
key: 'document',
|
||||
render: (_, r) => (
|
||||
<div style={{ display: 'flex', gap: 12, alignItems: 'center', minWidth: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
alignItems: 'center',
|
||||
minWidth: 0,
|
||||
}}>
|
||||
<FormatIcon type={r.fileType} />
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<Tooltip title={r.name}>
|
||||
@ -82,7 +103,12 @@ export function FilesList({ files, loading, labelFor }: { files: FileOut[]; load
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ title: head('Период'), key: 'period', width: 110, render: (_, r) => r.period },
|
||||
{
|
||||
title: head('Период'),
|
||||
key: 'period',
|
||||
width: 110,
|
||||
render: (_, r) => r.period,
|
||||
},
|
||||
{
|
||||
title: head('Проверка'),
|
||||
key: 'check',
|
||||
@ -106,6 +132,21 @@ export function FilesList({ files, loading, labelFor }: { files: FileOut[]; load
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
key: 'actions',
|
||||
title: '',
|
||||
dataIndex: 'action',
|
||||
render: (_, r) => (
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteClick(r);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (loading && files.length === 0) {
|
||||
@ -125,7 +166,14 @@ export function FilesList({ files, loading, labelFor }: { files: FileOut[]; load
|
||||
dataSource={rows}
|
||||
locale={{ emptyText: 'Документов нет' }}
|
||||
onRow={(r) => ({
|
||||
onClick: r.linkable && r.documentId ? () => navigate(`/documents/${r.documentId}`) : undefined,
|
||||
onClick:
|
||||
r.linkable && r.documentId && r.legalEntityId && r.consolidationId
|
||||
? () => {
|
||||
navigate(`/documents?consolidation_id=${r.consolidationId}&legal_entity_id=${r.legalEntityId}&document_id=${r.documentId}`);
|
||||
}
|
||||
: () => {
|
||||
message.warning('Просмотр данного документа недоступен');
|
||||
},
|
||||
style: r.linkable ? { cursor: 'pointer' } : undefined,
|
||||
})}
|
||||
/>
|
||||
|
||||
75
src/widgets/FilesDrawer/ReplaceDocModal.tsx
Normal file
75
src/widgets/FilesDrawer/ReplaceDocModal.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, Input, Upload, Button } from 'antd';
|
||||
import { UploadOutlined, FileAddOutlined } from '@ant-design/icons';
|
||||
import { useAppSelector } from '../../store/store';
|
||||
|
||||
const { Dragger } = Upload;
|
||||
|
||||
export default function ReplaceDocModal({ open, docName, submitting, onCancel, onSubmit }) {
|
||||
const [comment, setComment] = useState('');
|
||||
const [fileList, setFileList] = useState([]);
|
||||
const leName = useAppSelector((s) => s.consolidations.current_legal_entity_name);
|
||||
|
||||
// reset form every time the modal is opened
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setComment('');
|
||||
setFileList([]);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleOk = () => {
|
||||
const file = fileList[0]?.originFileObj ?? fileList[0];
|
||||
onSubmit({ file, comment });
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title='Замена документа'
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
onOk={handleOk}
|
||||
okText='Загрузить'
|
||||
okButtonProps={{
|
||||
icon: <UploadOutlined />, // icon is rendered before the text
|
||||
disabled: fileList.length === 0 || comment.length === 0,
|
||||
}}
|
||||
confirmLoading={submitting}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<span>{leName}</span>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'rgb(248, 250, 252)',
|
||||
borderRadius: 6,
|
||||
fontSize: 12,
|
||||
color: '#667085',
|
||||
padding: '8px 10px',
|
||||
}}>
|
||||
Заменяемый документ: {docName}
|
||||
</div>
|
||||
|
||||
<Input placeholder='Укажите причину замены' value={comment} onChange={(e) => setComment(e.target.value)} />
|
||||
|
||||
<Dragger
|
||||
multiple={false}
|
||||
style={{ borderStyle: 'dashed' }}
|
||||
fileList={fileList}
|
||||
maxCount={1}
|
||||
accept='.xls,.xlsx,.xlsb,.pdf,.png,.jpg,.jpeg'
|
||||
beforeUpload={(file) => {
|
||||
setFileList([file]);
|
||||
return false;
|
||||
}}
|
||||
onRemove={() => setFileList([])}>
|
||||
<p className='ant-upload-drag-icon'>
|
||||
<FileAddOutlined />
|
||||
</p>
|
||||
<p className='ant-upload-text' style={{ color: '#667085' }}>
|
||||
Перетащите файлы сюда
|
||||
</p>
|
||||
<Button>Выбрать документ</Button>
|
||||
</Dragger>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -20,6 +20,8 @@ export interface DocumentRow {
|
||||
statusLabel: string;
|
||||
errorReason: string | null;
|
||||
linkable: boolean;
|
||||
consolidationId: string | null;
|
||||
legalEntityId: string | null;
|
||||
/** предупреждения разбора файла (общие для всех документов файла) */
|
||||
fileWarnings: FileWarningOut[];
|
||||
mappingWarnings: string[];
|
||||
@ -72,6 +74,8 @@ export function buildDocumentRows(files: FileOut[], labelFor?: (f: FileOut) => s
|
||||
statusLabel: failed ? 'Ошибка' : 'Обработка',
|
||||
errorReason: errorReason(f),
|
||||
linkable: false,
|
||||
consolidationId: f.consolidation_id,
|
||||
legalEntityId: f.legal_entity_id,
|
||||
fileWarnings,
|
||||
mappingWarnings,
|
||||
issueCounts: { error: 0, info: 0 },
|
||||
@ -86,7 +90,10 @@ export function buildDocumentRows(files: FileOut[], labelFor?: (f: FileOut) => s
|
||||
const status = documentRowStatus(d.status);
|
||||
return {
|
||||
key: d.id,
|
||||
consolidationId: f.consolidation_id,
|
||||
documentId: d.id,
|
||||
file_id: f.id,
|
||||
legalEntityId: f.legal_entity_id,
|
||||
name,
|
||||
fileType: f.file_type,
|
||||
subtitle: join([typeText, f.file_type, date, extra]),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user