feat: creats frontend for the project
This commit is contained in:
130
frontend/src/components/RulesList.tsx
Normal file
130
frontend/src/components/RulesList.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user