feat: creats frontend for the project
This commit is contained in:
164
frontend/src/components/ImportModal.tsx
Normal file
164
frontend/src/components/ImportModal.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
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;
|
||||
|
||||
setLoading(true);
|
||||
setError('');
|
||||
setResult(null);
|
||||
|
||||
try {
|
||||
const text = await file.text();
|
||||
const json = JSON.parse(text);
|
||||
const resp = await importStatement(json);
|
||||
setResult(resp);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof SyntaxError) {
|
||||
setError('Некорректный JSON-файл');
|
||||
} else {
|
||||
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}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body">
|
||||
{error && <div className="alert alert-error">{error}</div>}
|
||||
|
||||
{!result && (
|
||||
<div className="import-upload">
|
||||
<p>Выберите JSON-файл выписки (формат 1.0)</p>
|
||||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleFileChange}
|
||||
className="file-input"
|
||||
/>
|
||||
{loading && (
|
||||
<div className="import-loading">Импорт...</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<div className="import-result">
|
||||
<div className="import-result-icon">✓</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user