Files
family_budget/frontend/src/components/TransactionTable.tsx
Anton 56b5c81ec5 feat(frontend): адаптация под мобильные устройства
- Мобильная навигация: hamburger-меню и drawer вместо фиксированного sidebar
- Модальные окна на весь экран при ширине < 480px
- Адаптивные заголовки страниц и фильтры (touch-friendly)
- Card view для таблицы операций при ширине < 600px
- Горизонтальный скролл вкладок настроек
- Увеличенные touch-targets (44px) для пагинации и кнопок
- Уменьшенная высота графиков на мобильных
- Поддержка safe-area-inset для устройств с вырезами
- theme-color в index.html

Made-with: Cursor
2026-03-10 11:50:36 +03:00

173 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Transaction } from '@family-budget/shared';
import { formatAmount, formatDateTime } from '../utils/format';
interface Props {
transactions: Transaction[];
loading: boolean;
onEdit: (tx: Transaction) => void;
}
const DIRECTION_CLASSES: Record<string, string> = {
income: 'amount-income',
expense: 'amount-expense',
transfer: 'amount-transfer',
};
function TransactionCard({
tx,
onEdit,
}: {
tx: Transaction;
onEdit: (tx: Transaction) => void;
}) {
const directionClass = DIRECTION_CLASSES[tx.direction] ?? '';
const isUnconfirmed =
!tx.isCategoryConfirmed && tx.categoryId != null;
return (
<div
className={`transaction-card ${isUnconfirmed ? 'row-unconfirmed' : ''}`}
>
<div className="transaction-card-header">
<span className="transaction-card-date">
{formatDateTime(tx.operationAt)}
</span>
<span className={`transaction-card-amount ${directionClass}`}>
{formatAmount(tx.amountSigned)}
</span>
</div>
<div className="transaction-card-body">
<span className="description-text">{tx.description}</span>
{tx.comment && (
<span className="comment-badge" title={tx.comment}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
</svg>
</span>
)}
</div>
<div className="transaction-card-footer">
<span className="transaction-card-meta">
{tx.accountAlias || '—'} · {tx.categoryName || '—'}
</span>
<div className="transaction-card-actions">
{tx.categoryId != null && !tx.isCategoryConfirmed && (
<span
className="badge badge-warning"
title="Категория не подтверждена"
>
?
</span>
)}
<button
type="button"
className="btn-icon btn-icon-touch"
onClick={() => onEdit(tx)}
title="Редактировать"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
</button>
</div>
</div>
</div>
);
}
export function TransactionTable({ transactions, loading, onEdit }: Props) {
if (loading) {
return <div className="table-loading">Загрузка операций...</div>;
}
if (transactions.length === 0) {
return <div className="table-empty">Операции не найдены</div>;
}
return (
<>
<div className="table-wrapper table-desktop">
<table className="data-table">
<thead>
<tr>
<th>Дата</th>
<th>Счёт</th>
<th>Сумма</th>
<th>Описание</th>
<th>Категория</th>
<th className="th-center">Статус</th>
<th></th>
</tr>
</thead>
<tbody>
{transactions.map((tx) => (
<tr
key={tx.id}
className={
!tx.isCategoryConfirmed && tx.categoryId
? 'row-unconfirmed'
: ''
}
>
<td className="td-nowrap">
{formatDateTime(tx.operationAt)}
</td>
<td className="td-nowrap">{tx.accountAlias || '—'}</td>
<td
className={`td-nowrap td-amount ${DIRECTION_CLASSES[tx.direction] ?? ''}`}
>
{formatAmount(tx.amountSigned)}
</td>
<td className="td-description">
<span className="description-text">{tx.description}</span>
{tx.comment && (
<span className="comment-badge" title={tx.comment}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
</svg>
</span>
)}
</td>
<td className="td-nowrap">
{tx.categoryName || (
<span className="text-muted"></span>
)}
</td>
<td className="td-center">
{tx.categoryId != null && !tx.isCategoryConfirmed && (
<span
className="badge badge-warning"
title="Категория не подтверждена"
>
?
</span>
)}
</td>
<td>
<button
className="btn-icon"
onClick={() => onEdit(tx)}
title="Редактировать"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="transaction-cards transaction-mobile">
{transactions.map((tx) => (
<TransactionCard key={tx.id} tx={tx} onEdit={onEdit} />
))}
</div>
</>
);
}