feat: add month switcher to Finance page - fix transactions not showing
All checks were successful
CI / ci (push) Successful in 40s

This commit is contained in:
Cosmo
2026-03-01 05:02:23 +00:00
parent c898c0063c
commit 72915aa6c4
6 changed files with 76 additions and 28 deletions

13
Dockerfile.dev Normal file
View File

@@ -0,0 +1,13 @@
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
ENV VITE_API_URL=http://192.168.31.60:8081
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -5,7 +5,9 @@ networks:
services: services:
web-dev: web-dev:
build: . build:
context: .
dockerfile: Dockerfile.dev
container_name: pulse-web-dev container_name: pulse-web-dev
restart: always restart: always
ports: ports:

View File

@@ -18,16 +18,16 @@ const MONTH_NAMES = [
"Июл", "Авг", "Сен", "Окт", "Ноя", "Дек", "Июл", "Авг", "Сен", "Окт", "Ноя", "Дек",
] ]
export default function FinanceAnalytics() { export default function FinanceAnalytics({ month, year }) {
const [analytics, setAnalytics] = useState(null) const [analytics, setAnalytics] = useState(null)
const [summary, setSummary] = useState(null) const [summary, setSummary] = useState(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
useEffect(() => { useEffect(() => {
const now = new Date() setLoading(true)
Promise.all([ Promise.all([
financeApi.getAnalytics({ months: 6 }), financeApi.getAnalytics({ months: 6 }),
financeApi.getSummary({ month: now.getMonth() + 1, year: now.getFullYear() }), financeApi.getSummary({ month, year }),
]) ])
.then(([a, s]) => { .then(([a, s]) => {
setAnalytics(a) setAnalytics(a)
@@ -35,7 +35,7 @@ export default function FinanceAnalytics() {
}) })
.catch(console.error) .catch(console.error)
.finally(() => setLoading(false)) .finally(() => setLoading(false))
}, []) }, [month, year])
if (loading) { if (loading) {
return ( return (
@@ -72,7 +72,6 @@ export default function FinanceAnalytics() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Summary cards */}
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div className="card p-4"> <div className="card p-4">
<p className="text-xs text-gray-500 dark:text-gray-400"> <p className="text-xs text-gray-500 dark:text-gray-400">
@@ -97,7 +96,6 @@ export default function FinanceAnalytics() {
</div> </div>
</div> </div>
{/* Bar chart */}
{barData.length > 0 && ( {barData.length > 0 && (
<div className="card p-5"> <div className="card p-5">
<h3 className="font-display font-bold text-gray-900 dark:text-white mb-4"> <h3 className="font-display font-bold text-gray-900 dark:text-white mb-4">
@@ -131,7 +129,6 @@ export default function FinanceAnalytics() {
</div> </div>
)} )}
{/* Donut chart */}
{pieData.length > 0 && ( {pieData.length > 0 && (
<div className="card p-5"> <div className="card p-5">
<h3 className="font-display font-bold text-gray-900 dark:text-white mb-4"> <h3 className="font-display font-bold text-gray-900 dark:text-white mb-4">
@@ -179,7 +176,6 @@ export default function FinanceAnalytics() {
</div> </div>
)} )}
{/* Monthly trend */}
{monthlyData.length > 0 && ( {monthlyData.length > 0 && (
<div className="card p-5"> <div className="card p-5">
<h3 className="font-display font-bold text-gray-900 dark:text-white mb-4"> <h3 className="font-display font-bold text-gray-900 dark:text-white mb-4">

View File

@@ -12,18 +12,18 @@ const COLORS = [
const fmt = (n) => Number(n).toLocaleString("ru-RU") + " ₽" const fmt = (n) => Number(n).toLocaleString("ru-RU") + " ₽"
export default function FinanceDashboard() { export default function FinanceDashboard({ month, year }) {
const [summary, setSummary] = useState(null) const [summary, setSummary] = useState(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
useEffect(() => { useEffect(() => {
const now = new Date() setLoading(true)
financeApi financeApi
.getSummary({ month: now.getMonth() + 1, year: now.getFullYear() }) .getSummary({ month, year })
.then(setSummary) .then(setSummary)
.catch(console.error) .catch(console.error)
.finally(() => setLoading(false)) .finally(() => setLoading(false))
}, []) }, [month, year])
if (loading) { if (loading) {
return ( return (
@@ -63,7 +63,6 @@ export default function FinanceDashboard() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Balance Card */}
<div className="card p-6 bg-gradient-to-br from-primary-950 to-primary-800 text-white"> <div className="card p-6 bg-gradient-to-br from-primary-950 to-primary-800 text-white">
<p className="text-sm opacity-70">Баланс за месяц</p> <p className="text-sm opacity-70">Баланс за месяц</p>
<p className="text-3xl font-bold mt-1">{fmt(summary.balance)}</p> <p className="text-3xl font-bold mt-1">{fmt(summary.balance)}</p>
@@ -83,7 +82,6 @@ export default function FinanceDashboard() {
</div> </div>
</div> </div>
{/* Top Categories */}
{expenseCategories.length > 0 && ( {expenseCategories.length > 0 && (
<div className="card p-5"> <div className="card p-5">
<h3 className="font-display font-bold text-gray-900 dark:text-white mb-4"> <h3 className="font-display font-bold text-gray-900 dark:text-white mb-4">
@@ -120,7 +118,6 @@ export default function FinanceDashboard() {
</div> </div>
)} )}
{/* Donut Chart */}
{pieData.length > 0 && ( {pieData.length > 0 && (
<div className="card p-5"> <div className="card p-5">
<h3 className="font-display font-bold text-gray-900 dark:text-white mb-4"> <h3 className="font-display font-bold text-gray-900 dark:text-white mb-4">
@@ -168,7 +165,6 @@ export default function FinanceDashboard() {
</div> </div>
)} )}
{/* Daily Line Chart */}
{dailyData.length > 0 && ( {dailyData.length > 0 && (
<div className="card p-5"> <div className="card p-5">
<h3 className="font-display font-bold text-gray-900 dark:text-white mb-4"> <h3 className="font-display font-bold text-gray-900 dark:text-white mb-4">

View File

@@ -8,7 +8,7 @@ const formatDate = (d) => {
return dt.toLocaleDateString("ru-RU", { day: "numeric", month: "long" }) return dt.toLocaleDateString("ru-RU", { day: "numeric", month: "long" })
} }
export default function TransactionList({ onAdd }) { export default function TransactionList({ onAdd, month, year }) {
const [transactions, setTransactions] = useState([]) const [transactions, setTransactions] = useState([])
const [categories, setCategories] = useState([]) const [categories, setCategories] = useState([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@@ -17,11 +17,12 @@ export default function TransactionList({ onAdd }) {
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
useEffect(() => { useEffect(() => {
setLoading(true)
Promise.all([ Promise.all([
financeApi.listCategories(), financeApi.listCategories(),
financeApi.listTransactions({ financeApi.listTransactions({
month: new Date().getMonth() + 1, month,
year: new Date().getFullYear(), year,
limit: 100, limit: 100,
}), }),
]) ])
@@ -31,7 +32,7 @@ export default function TransactionList({ onAdd }) {
}) })
.catch(console.error) .catch(console.error)
.finally(() => setLoading(false)) .finally(() => setLoading(false))
}, []) }, [month, year])
const filtered = transactions.filter((t) => { const filtered = transactions.filter((t) => {
if (filter !== "all" && t.type !== filter) return false if (filter !== "all" && t.type !== filter) return false
@@ -67,7 +68,6 @@ export default function TransactionList({ onAdd }) {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{/* Search */}
<input <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" 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="Поиск по описанию..." placeholder="Поиск по описанию..."
@@ -75,7 +75,6 @@ export default function TransactionList({ onAdd }) {
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
/> />
{/* Type filter */}
<div className="flex gap-2"> <div className="flex gap-2">
{[ {[
["all", "Все"], ["all", "Все"],
@@ -96,7 +95,6 @@ export default function TransactionList({ onAdd }) {
))} ))}
</div> </div>
{/* Category filter */}
<div className="flex gap-2 overflow-x-auto pb-1"> <div className="flex gap-2 overflow-x-auto pb-1">
<button <button
onClick={() => setCatFilter(null)} onClick={() => setCatFilter(null)}
@@ -123,7 +121,6 @@ export default function TransactionList({ onAdd }) {
))} ))}
</div> </div>
{/* Transaction groups */}
{Object.keys(grouped).length === 0 ? ( {Object.keys(grouped).length === 0 ? (
<div className="card p-12 text-center"> <div className="card p-12 text-center">
<span className="text-4xl block mb-3">🔍</span> <span className="text-4xl block mb-3">🔍</span>

View File

@@ -13,13 +13,32 @@ const tabs = [
{ key: "categories", label: "Категории", icon: "🏷️" }, { key: "categories", label: "Категории", icon: "🏷️" },
] ]
const MONTH_NAMES = [
"Январь", "Февраль", "Март", "Апрель", "Май", "Июнь",
"Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь",
]
export default function Finance() { export default function Finance() {
const now = new Date()
const [activeTab, setActiveTab] = useState("dashboard") const [activeTab, setActiveTab] = useState("dashboard")
const [showAdd, setShowAdd] = useState(false) const [showAdd, setShowAdd] = useState(false)
const [refreshKey, setRefreshKey] = useState(0) const [refreshKey, setRefreshKey] = useState(0)
const [month, setMonth] = useState(now.getMonth() + 1)
const [year, setYear] = useState(now.getFullYear())
const refresh = () => setRefreshKey((k) => k + 1) const refresh = () => setRefreshKey((k) => k + 1)
const prevMonth = () => {
if (month === 1) { setMonth(12); setYear(y => y - 1) }
else setMonth(m => m - 1)
}
const nextMonth = () => {
if (month === 12) { setMonth(1); setYear(y => y + 1) }
else setMonth(m => m + 1)
}
const isCurrentMonth = month === now.getMonth() + 1 && year === now.getFullYear()
return ( return (
<div className="min-h-screen bg-surface-50 dark:bg-gray-950 gradient-mesh pb-24"> <div className="min-h-screen bg-surface-50 dark:bg-gray-950 gradient-mesh pb-24">
<header className="bg-white/70 dark:bg-gray-900/70 backdrop-blur-xl border-b border-gray-100/50 dark:border-gray-800 sticky top-0 z-10"> <header className="bg-white/70 dark:bg-gray-900/70 backdrop-blur-xl border-b border-gray-100/50 dark:border-gray-800 sticky top-0 z-10">
@@ -36,6 +55,31 @@ export default function Finance() {
+ +
</button> </button>
</div> </div>
{/* Month Switcher */}
<div className="max-w-lg mx-auto px-4 pb-3">
<div className="flex items-center justify-center gap-4">
<button
onClick={prevMonth}
className="w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 flex items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700 transition text-sm font-bold"
>
</button>
<button
onClick={() => { setMonth(now.getMonth() + 1); setYear(now.getFullYear()) }}
className={"text-sm font-semibold min-w-[140px] text-center " + (isCurrentMonth ? "text-gray-900 dark:text-white" : "text-primary-600 dark:text-primary-400")}
>
{MONTH_NAMES[month - 1]} {year}
</button>
<button
onClick={nextMonth}
className="w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 flex items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700 transition text-sm font-bold"
>
</button>
</div>
</div>
<div className="max-w-lg mx-auto px-4 pb-3 flex gap-1.5 overflow-x-auto scrollbar-hide"> <div className="max-w-lg mx-auto px-4 pb-3 flex gap-1.5 overflow-x-auto scrollbar-hide">
{tabs.map((t) => ( {tabs.map((t) => (
<button <button
@@ -54,11 +98,11 @@ export default function Finance() {
</header> </header>
<div className="max-w-lg mx-auto px-4 py-6"> <div className="max-w-lg mx-auto px-4 py-6">
{activeTab === "dashboard" && <FinanceDashboard key={refreshKey} />} {activeTab === "dashboard" && <FinanceDashboard key={refreshKey + "-" + month + "-" + year} month={month} year={year} />}
{activeTab === "transactions" && ( {activeTab === "transactions" && (
<TransactionList key={refreshKey} onAdd={() => setShowAdd(true)} /> <TransactionList key={refreshKey + "-" + month + "-" + year} month={month} year={year} onAdd={() => setShowAdd(true)} />
)} )}
{activeTab === "analytics" && <FinanceAnalytics key={refreshKey} />} {activeTab === "analytics" && <FinanceAnalytics key={refreshKey + "-" + month + "-" + year} month={month} year={year} />}
{activeTab === "categories" && <CategoriesManager refreshKey={refreshKey} />} {activeTab === "categories" && <CategoriesManager refreshKey={refreshKey} />}
</div> </div>