Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
107 lines
3.6 KiB
TypeScript
107 lines
3.6 KiB
TypeScript
import { Popover, Spin, Tag } from 'antd';
|
||
import { useState } from 'react';
|
||
import { getDocument } from '../../services/api/documents';
|
||
import type { IssueOut, Severity } from '../../types/api';
|
||
import { severityLabel } from '../../utils/labels';
|
||
import { SeverityIcon } from '../DocumentReview/IssuesPanel';
|
||
import type { DocumentRow } from './documentRows';
|
||
import { warningText } from './fileWarnings';
|
||
|
||
type IssuesState = { status: 'loading' } | { status: 'error' } | { status: 'done'; issues: IssueOut[] };
|
||
|
||
const tagColor = (s: Severity) => (s === 'error' ? 'error' : 'processing');
|
||
|
||
function DocumentIssues({ state }: { state: IssuesState | null }) {
|
||
if (state === null || state.status === 'loading') return <Spin size='small' />;
|
||
if (state.status === 'error') return <span style={{ color: '#cf1322' }}>Не удалось загрузить</span>;
|
||
if (state.issues.length === 0) return <span style={{ color: '#8c8c8c' }}>Нет</span>;
|
||
return (
|
||
<>
|
||
{state.issues.map((i) => (
|
||
<div key={i.id}>
|
||
<Tag color={tagColor(i.severity)}>{severityLabel(i.severity)}</Tag>
|
||
{i.message}
|
||
</div>
|
||
))}
|
||
</>
|
||
);
|
||
}
|
||
|
||
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||
return (
|
||
<div style={{ marginBottom: 8 }}>
|
||
<div style={{ fontWeight: 600, marginBottom: 4 }}>{title}</div>
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** Значок с числом предупреждений под плашкой статуса; по клику — подробности (файл, обработка, документ). */
|
||
export function FileWarningsPopover({ row, badge }: { row: DocumentRow; badge: { count: number; level: Severity } | null }) {
|
||
const [issues, setIssues] = useState<IssuesState | null>(null);
|
||
|
||
if (!badge) return null;
|
||
|
||
// предупреждения документа грузим один раз за открытие: при закрытии сбрасываем, повторное открытие запрашивает заново
|
||
const onOpenChange = (open: boolean) => {
|
||
if (!open) {
|
||
setIssues(null);
|
||
return;
|
||
}
|
||
if (!row.documentId) return;
|
||
setIssues({ status: 'loading' });
|
||
getDocument(row.documentId)
|
||
.then((d) => setIssues({ status: 'done', issues: d.issues }))
|
||
.catch(() => setIssues({ status: 'error' }));
|
||
};
|
||
|
||
const content = (
|
||
<div style={{ maxWidth: 420, maxHeight: 320, overflowY: 'auto' }}>
|
||
{row.fileWarnings.length > 0 && (
|
||
<Section title='Предупреждения файла'>
|
||
{row.fileWarnings.map((w, i) => (
|
||
<div key={`${w.code}-${i}`}>
|
||
<Tag color={tagColor(w.severity)}>{severityLabel(w.severity)}</Tag>
|
||
{warningText(w)}
|
||
</div>
|
||
))}
|
||
</Section>
|
||
)}
|
||
{row.mappingWarnings.length > 0 && (
|
||
<Section title='Предупреждения обработки'>
|
||
{row.mappingWarnings.map((w) => (
|
||
<div key={w}>
|
||
<Tag color={tagColor('info')}>{severityLabel('info')}</Tag>
|
||
{w}
|
||
</div>
|
||
))}
|
||
</Section>
|
||
)}
|
||
{row.documentId && (
|
||
<Section title='Предупреждения документа'>
|
||
<DocumentIssues state={issues} />
|
||
</Section>
|
||
)}
|
||
</div>
|
||
);
|
||
|
||
return (
|
||
<Popover trigger='click' title='Предупреждения' content={content} onOpenChange={onOpenChange}>
|
||
<button
|
||
type='button'
|
||
onClick={(e) => e.stopPropagation()}
|
||
style={{
|
||
cursor: 'pointer',
|
||
background: 'none',
|
||
border: 'none',
|
||
padding: 0,
|
||
fontSize: 12,
|
||
whiteSpace: 'nowrap',
|
||
color: badge.level === 'error' ? '#cf1322' : '#1677ff',
|
||
}}>
|
||
<SeverityIcon severity={badge.level} /> {badge.count}
|
||
</button>
|
||
</Popover>
|
||
);
|
||
}
|