Merge pull request 'feat(imports): import history and delete by import' (#13) from feat/import-history into main

Reviewed-on: #13
This commit was merged in pull request #13.
This commit is contained in:
2026-03-16 14:48:47 +00:00
13 changed files with 331 additions and 6 deletions

View File

@@ -1,4 +1,7 @@
import type { ImportStatementResponse } from '@family-budget/shared';
import type {
ImportStatementResponse,
Import,
} from '@family-budget/shared';
import { api } from './client';
export async function importStatement(
@@ -11,3 +14,13 @@ export async function importStatement(
formData,
);
}
export async function getImports(): Promise<Import[]> {
return api.get<Import[]>('/api/imports');
}
export async function deleteImport(
id: number,
): Promise<{ deleted: number }> {
return api.delete<{ deleted: number }>(`/api/imports/${id}`);
}

View File

@@ -1,13 +1,87 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { ClearHistoryModal } from './ClearHistoryModal';
import { DeleteImportModal } from './DeleteImportModal';
import { getImports } from '../api/import';
import type { Import } from '@family-budget/shared';
function formatDate(iso: string): string {
const d = new Date(iso);
return d.toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
export function DataSection() {
const [showClearModal, setShowClearModal] = useState(false);
const [imports, setImports] = useState<Import[]>([]);
const [impToDelete, setImpToDelete] = useState<Import | null>(null);
const navigate = useNavigate();
useEffect(() => {
getImports().then(setImports).catch(() => {});
}, []);
const handleImportDeleted = () => {
setImpToDelete(null);
getImports().then(setImports).catch(() => {});
navigate('/history');
};
return (
<div className="data-section">
<div className="section-block">
<h3>История импортов</h3>
<p className="section-desc">
Список импортов выписок. Можно удалить операции конкретного импорта.
</p>
{imports.length === 0 ? (
<p className="muted">Импортов пока нет.</p>
) : (
<div className="table-responsive">
<table className="table">
<thead>
<tr>
<th>Дата</th>
<th>Счёт</th>
<th>Банк</th>
<th>Импортировано</th>
<th>Дубликаты</th>
<th></th>
</tr>
</thead>
<tbody>
{imports.map((imp) => (
<tr key={imp.id}>
<td>{formatDate(imp.importedAt)}</td>
<td>
{imp.accountAlias || imp.accountNumberMasked || '—'}
</td>
<td>{imp.bank}</td>
<td>{imp.importedCount}</td>
<td>{imp.duplicatesSkipped}</td>
<td>
<button
type="button"
className="btn btn-sm btn-danger"
onClick={() => setImpToDelete(imp)}
disabled={imp.importedCount === 0}
>
Удалить
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
<div className="section-block">
<h3>Очистка данных</h3>
<p className="section-desc">
@@ -32,6 +106,14 @@ export function DataSection() {
}}
/>
)}
{impToDelete && (
<DeleteImportModal
imp={impToDelete}
onClose={() => setImpToDelete(null)}
onDone={handleImportDeleted}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,70 @@
import { useState } from 'react';
import { deleteImport } from '../api/import';
import type { Import } from '@family-budget/shared';
interface Props {
imp: Import;
onClose: () => void;
onDone: () => void;
}
export function DeleteImportModal({ imp, onClose, onDone }: Props) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const accountLabel =
imp.accountAlias || imp.accountNumberMasked || `ID ${imp.accountId}`;
const handleConfirm = async () => {
setLoading(true);
setError('');
try {
await deleteImport(imp.id);
onDone();
} catch (e) {
setError(
e instanceof Error ? e.message : 'Ошибка при удалении импорта',
);
} finally {
setLoading(false);
}
};
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">
<p className="clear-history-warn">
Будут удалены все операции этого импорта ({imp.importedCount}{' '}
шт.): {imp.bank} / {accountLabel}
</p>
{error && <div className="alert alert-error">{error}</div>}
<p>Действие необратимо.</p>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-danger"
onClick={handleConfirm}
disabled={loading}
>
{loading ? 'Удаление…' : 'Удалить'}
</button>
<button type="button" className="btn btn-secondary" onClick={onClose}>
Отмена
</button>
</div>
</div>
</div>
);
}