Files
family_budget/frontend/src/components/ImportModal.tsx
vakabunga 8b57dd987e Revert SSE streaming for PDF import, use synchronous flow
SSE streaming added unnecessary complexity and latency due to
buffering issues across Node.js event loop, Nginx proxy, and
Docker layers. Reverted to a simple synchronous request/response
for PDF conversion. Kept extractLlmErrorMessage for user-friendly
LLM errors, lazy-loaded pdf-parse, and extended Nginx timeout.
2026-03-14 20:12:27 +03:00

169 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useRef } from 'react';
import type { ImportStatementResponse } from '@family-budget/shared';
import { importStatement } from '../api/import';
import { updateAccount } from '../api/accounts';
interface Props {
onClose: () => void;
onDone: () => void;
}
export function ImportModal({ onClose, onDone }: Props) {
const [result, setResult] = useState<ImportStatementResponse | null>(null);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [alias, setAlias] = useState('');
const [aliasSaved, setAliasSaved] = useState(false);
const fileRef = useRef<HTMLInputElement>(null);
const handleFileChange = async (
e: React.ChangeEvent<HTMLInputElement>,
) => {
const file = e.target.files?.[0];
if (!file) return;
const name = file.name.toLowerCase();
const type = file.type;
const isPdf = type === 'application/pdf' || name.endsWith('.pdf');
const isJson = type === 'application/json' || name.endsWith('.json');
if (!isPdf && !isJson) {
setError('Допустимы только файлы PDF или JSON');
return;
}
setLoading(true);
setError('');
setResult(null);
try {
const resp = await importStatement(file);
setResult(resp);
} catch (err: unknown) {
const msg =
err instanceof Error ? err.message : 'Ошибка импорта';
setError(msg);
} finally {
setLoading(false);
}
};
const handleSaveAlias = async () => {
if (!result || !alias.trim()) return;
try {
await updateAccount(result.accountId, { alias: alias.trim() });
setAliasSaved(true);
} catch {
// handled globally
}
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>Импорт выписки</h2>
<button className="btn-close" onClick={onClose}>
&times;
</button>
</div>
<div className="modal-body">
{error && <div className="alert alert-error">{error}</div>}
{!result && (
<div className="import-upload">
<p>Выберите файл выписки (PDF или JSON, формат 1.0)</p>
<input
ref={fileRef}
type="file"
accept=".pdf,.json,application/pdf,application/json"
onChange={handleFileChange}
className="file-input"
/>
{loading && (
<div className="import-loading">Импорт...</div>
)}
</div>
)}
{result && (
<div className="import-result">
<div className="import-result-icon" aria-hidden="true"></div>
<h3>Импорт завершён</h3>
<table className="import-stats">
<tbody>
<tr>
<td>Счёт</td>
<td>{result.accountNumberMasked}</td>
</tr>
<tr>
<td>Новый счёт</td>
<td>{result.isNewAccount ? 'Да' : 'Нет'}</td>
</tr>
<tr>
<td>Импортировано</td>
<td>{result.imported}</td>
</tr>
<tr>
<td>Дубликатов пропущено</td>
<td>{result.duplicatesSkipped}</td>
</tr>
<tr>
<td>Всего в файле</td>
<td>{result.totalInFile}</td>
</tr>
</tbody>
</table>
{result.isNewAccount && !aliasSaved && (
<div className="import-alias">
<label>Алиас для нового счёта</label>
<div className="import-alias-row">
<input
type="text"
placeholder="Напр.: Текущий, Накопительный"
value={alias}
onChange={(e) => setAlias(e.target.value)}
maxLength={50}
/>
<button
className="btn btn-sm btn-primary"
onClick={handleSaveAlias}
disabled={!alias.trim()}
>
Сохранить
</button>
</div>
</div>
)}
{aliasSaved && (
<div className="import-alias-saved">
Алиас сохранён
</div>
)}
</div>
)}
</div>
<div className="modal-footer">
{result ? (
<button className="btn btn-primary" onClick={onDone}>
Готово
</button>
) : (
<button
className="btn btn-secondary"
onClick={onClose}
disabled={loading}
>
Отмена
</button>
)}
</div>
</div>
</div>
);
}