Files
pulse-web/src/components/finance/TransactionList.jsx
Cosmo 72915aa6c4
All checks were successful
CI / ci (push) Successful in 40s
feat: add month switcher to Finance page - fix transactions not showing
2026-03-01 05:02:23 +00:00

168 lines
5.4 KiB
JavaScript
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 { useState, useEffect } from "react"
import { financeApi } from "../../api/finance"
const fmt = (n) => Number(n).toLocaleString("ru-RU") + " ₽"
const formatDate = (d) => {
const dt = new Date(d)
return dt.toLocaleDateString("ru-RU", { day: "numeric", month: "long" })
}
export default function TransactionList({ onAdd, month, year }) {
const [transactions, setTransactions] = useState([])
const [categories, setCategories] = useState([])
const [loading, setLoading] = useState(true)
const [filter, setFilter] = useState("all")
const [catFilter, setCatFilter] = useState(null)
const [search, setSearch] = useState("")
useEffect(() => {
setLoading(true)
Promise.all([
financeApi.listCategories(),
financeApi.listTransactions({
month,
year,
limit: 100,
}),
])
.then(([cats, txs]) => {
setCategories(cats || [])
setTransactions(txs || [])
})
.catch(console.error)
.finally(() => setLoading(false))
}, [month, year])
const filtered = transactions.filter((t) => {
if (filter !== "all" && t.type !== filter) return false
if (catFilter && t.category_id !== catFilter) return false
if (search && !t.description.toLowerCase().includes(search.toLowerCase()))
return false
return true
})
const grouped = filtered.reduce((acc, t) => {
const d = t.date.slice(0, 10)
;(acc[d] = acc[d] || []).push(t)
return acc
}, {})
const handleDelete = async (id) => {
if (!confirm("Удалить транзакцию?")) return
await financeApi.deleteTransaction(id)
setTransactions((txs) => txs.filter((t) => t.id !== id))
}
if (loading) {
return (
<div className="space-y-3">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="card p-4 animate-pulse">
<div className="h-5 bg-gray-200 dark:bg-gray-800 rounded w-3/4" />
</div>
))}
</div>
)
}
return (
<div className="space-y-4">
<input
className="w-full px-4 py-2.5 rounded-xl bg-gray-100 dark:bg-gray-800 text-sm text-gray-900 dark:text-white placeholder-gray-400 outline-none"
placeholder="Поиск по описанию..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="flex gap-2">
{[
["all", "Все"],
["income", "Доходы"],
["expense", "Расходы"],
].map(([k, l]) => (
<button
key={k}
onClick={() => setFilter(k)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition ${
filter === k
? "bg-primary-500 text-white"
: "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400"
}`}
>
{l}
</button>
))}
</div>
<div className="flex gap-2 overflow-x-auto pb-1">
<button
onClick={() => setCatFilter(null)}
className={`px-3 py-1 rounded-lg text-xs font-medium whitespace-nowrap transition ${
!catFilter
? "bg-accent-500 text-white"
: "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400"
}`}
>
Все
</button>
{categories.map((c) => (
<button
key={c.id}
onClick={() => setCatFilter(c.id)}
className={`px-3 py-1 rounded-lg text-xs font-medium whitespace-nowrap transition ${
catFilter === c.id
? "bg-accent-500 text-white"
: "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400"
}`}
>
{c.emoji} {c.name}
</button>
))}
</div>
{Object.keys(grouped).length === 0 ? (
<div className="card p-12 text-center">
<span className="text-4xl block mb-3">🔍</span>
<p className="text-gray-500 dark:text-gray-400">Ничего не найдено</p>
</div>
) : (
Object.entries(grouped).map(([date, txs]) => (
<div key={date}>
<p className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase mb-2">
{formatDate(date)}
</p>
<div className="card divide-y divide-gray-100 dark:divide-gray-800">
{txs.map((t) => (
<div
key={t.id}
className="px-4 py-3 flex items-center gap-3"
onClick={() => handleDelete(t.id)}
>
<span className="text-xl">{t.category_emoji}</span>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
{t.description || t.category_name}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{t.category_emoji} {t.category_name}
</p>
</div>
<span
className={`text-sm font-bold ${
t.type === "income" ? "text-green-500" : "text-red-500"
}`}
>
{t.type === "income" ? "+" : "-"}
{fmt(t.amount)}
</span>
</div>
))}
</div>
</div>
))
)}
</div>
)
}