feat: creats frontend for the project

This commit is contained in:
vakabunga
2026-03-02 00:33:09 +03:00
parent 4d67636633
commit cd56e2bf9d
37 changed files with 3762 additions and 0 deletions

View 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}>
&times;
</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">&check;</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>
);
}