131 lines
3.8 KiB
TypeScript
131 lines
3.8 KiB
TypeScript
import { useState, useEffect } from 'react';
|
||
import type { CategoryRule } from '@family-budget/shared';
|
||
import {
|
||
getCategoryRules,
|
||
updateCategoryRule,
|
||
applyRule,
|
||
} from '../api/rules';
|
||
import { formatDate } from '../utils/format';
|
||
|
||
export function RulesList() {
|
||
const [rules, setRules] = useState<CategoryRule[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [applyingId, setApplyingId] = useState<number | null>(null);
|
||
const [applyResult, setApplyResult] = useState<{
|
||
id: number;
|
||
count: number;
|
||
} | null>(null);
|
||
|
||
useEffect(() => {
|
||
setLoading(true);
|
||
getCategoryRules()
|
||
.then(setRules)
|
||
.catch(() => {})
|
||
.finally(() => setLoading(false));
|
||
}, []);
|
||
|
||
const handleToggle = async (rule: CategoryRule) => {
|
||
try {
|
||
const updated = await updateCategoryRule(rule.id, {
|
||
isActive: !rule.isActive,
|
||
});
|
||
setRules((prev) =>
|
||
prev.map((r) => (r.id === rule.id ? updated : r)),
|
||
);
|
||
} catch {
|
||
// error handled globally
|
||
}
|
||
};
|
||
|
||
const handleApply = async (id: number) => {
|
||
setApplyingId(id);
|
||
try {
|
||
const resp = await applyRule(id);
|
||
setApplyResult({ id, count: resp.applied });
|
||
setTimeout(() => setApplyResult(null), 4000);
|
||
} catch {
|
||
// error handled globally
|
||
} finally {
|
||
setApplyingId(null);
|
||
}
|
||
};
|
||
|
||
if (loading) {
|
||
return <div className="section-loading">Загрузка...</div>;
|
||
}
|
||
|
||
return (
|
||
<div className="settings-section">
|
||
<table className="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Шаблон</th>
|
||
<th>Категория</th>
|
||
<th className="th-center">Приоритет</th>
|
||
<th className="th-center">Подтверждение</th>
|
||
<th>Создано</th>
|
||
<th className="th-center">Активно</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{rules.map((r) => (
|
||
<tr
|
||
key={r.id}
|
||
className={!r.isActive ? 'row-inactive' : ''}
|
||
>
|
||
<td>
|
||
<code>{r.pattern}</code>
|
||
</td>
|
||
<td>{r.categoryName}</td>
|
||
<td className="td-center">{r.priority}</td>
|
||
<td className="td-center">
|
||
{r.requiresConfirmation ? 'Да' : 'Нет'}
|
||
</td>
|
||
<td className="td-nowrap">
|
||
{formatDate(r.createdAt)}
|
||
</td>
|
||
<td className="td-center">
|
||
<button
|
||
className={`toggle ${r.isActive ? 'toggle-on' : 'toggle-off'}`}
|
||
onClick={() => handleToggle(r)}
|
||
title={
|
||
r.isActive ? 'Деактивировать' : 'Активировать'
|
||
}
|
||
>
|
||
{r.isActive ? 'Вкл' : 'Выкл'}
|
||
</button>
|
||
</td>
|
||
<td>
|
||
<div className="rules-actions">
|
||
{r.isActive && (
|
||
<button
|
||
className="btn btn-sm btn-secondary"
|
||
onClick={() => handleApply(r.id)}
|
||
disabled={applyingId === r.id}
|
||
>
|
||
{applyingId === r.id ? '...' : 'Применить'}
|
||
</button>
|
||
)}
|
||
{applyResult?.id === r.id && (
|
||
<span className="apply-result">
|
||
Применено: {applyResult.count}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
{rules.length === 0 && (
|
||
<tr>
|
||
<td colSpan={7} className="td-center text-muted">
|
||
Нет правил
|
||
</td>
|
||
</tr>
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
);
|
||
}
|