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,197 @@
import { useState, type FormEvent } from 'react';
import type {
Transaction,
Category,
CreateCategoryRuleRequest,
} from '@family-budget/shared';
import { updateTransaction } from '../api/transactions';
import { createCategoryRule } from '../api/rules';
import { formatAmount, formatDateTime } from '../utils/format';
interface Props {
transaction: Transaction;
categories: Category[];
onClose: () => void;
onSave: () => void;
}
function extractPattern(description: string): string {
return description
.replace(/Оплата товаров и услуг\.\s*/i, '')
.replace(/\s*по карте\s*\*\d+.*/i, '')
.replace(/\s*Перевод средств.*/i, '')
.trim()
.slice(0, 50);
}
export function EditTransactionModal({
transaction,
categories,
onClose,
onSave,
}: Props) {
const [categoryId, setCategoryId] = useState<string>(
transaction.categoryId != null ? String(transaction.categoryId) : '',
);
const [comment, setComment] = useState(transaction.comment || '');
const [createRule, setCreateRule] = useState(true);
const [pattern, setPattern] = useState(
extractPattern(transaction.description),
);
const [requiresConfirmation, setRequiresConfirmation] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setSaving(true);
setError('');
try {
await updateTransaction(transaction.id, {
categoryId: categoryId ? Number(categoryId) : null,
comment: comment || null,
});
if (createRule && categoryId && pattern.trim()) {
const ruleData: CreateCategoryRuleRequest = {
pattern: pattern.trim(),
matchType: 'contains',
categoryId: Number(categoryId),
priority: 100,
requiresConfirmation,
};
await createCategoryRule(ruleData);
}
onSave();
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : 'Ошибка сохранения';
setError(msg);
} finally {
setSaving(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>
<form onSubmit={handleSubmit}>
<div className="modal-body">
{error && <div className="alert alert-error">{error}</div>}
<div className="modal-tx-info">
<div className="modal-tx-row">
<span className="modal-tx-label">Дата</span>
<span>{formatDateTime(transaction.operationAt)}</span>
</div>
<div className="modal-tx-row">
<span className="modal-tx-label">Сумма</span>
<span>{formatAmount(transaction.amountSigned)}</span>
</div>
<div className="modal-tx-row">
<span className="modal-tx-label">Описание</span>
<span className="modal-tx-description">
{transaction.description}
</span>
</div>
</div>
<div className="form-group">
<label htmlFor="edit-category">Категория</label>
<select
id="edit-category"
value={categoryId}
onChange={(e) => setCategoryId(e.target.value)}
>
<option value=""> Без категории </option>
{categories.map((c) => (
<option key={c.id} value={c.id}>
{c.name}
</option>
))}
</select>
</div>
<div className="form-group">
<label htmlFor="edit-comment">Комментарий</label>
<textarea
id="edit-comment"
rows={2}
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="Комментарий к операции..."
/>
</div>
<div className="form-divider" />
<div className="form-group form-group-checkbox">
<label>
<input
type="checkbox"
checked={createRule}
onChange={(e) => setCreateRule(e.target.checked)}
/>
Создать правило для похожих транзакций
</label>
</div>
{createRule && (
<>
<div className="form-group">
<label htmlFor="edit-pattern">
Шаблон (ключевая строка)
</label>
<input
id="edit-pattern"
type="text"
value={pattern}
onChange={(e) => setPattern(e.target.value)}
maxLength={200}
/>
</div>
<div className="form-group form-group-checkbox">
<label>
<input
type="checkbox"
checked={requiresConfirmation}
onChange={(e) =>
setRequiresConfirmation(e.target.checked)
}
/>
Требовать подтверждения категории
</label>
</div>
</>
)}
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
onClick={onClose}
>
Отмена
</button>
<button
type="submit"
className="btn btn-primary"
disabled={saving}
>
{saving ? 'Сохранение...' : 'Сохранить'}
</button>
</div>
</form>
</div>
</div>
);
}