feat: Модуль Финансы + Трекер + CI/CD #1
13
Dockerfile.dev
Normal file
13
Dockerfile.dev
Normal 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;"]
|
||||
@@ -5,7 +5,9 @@ networks:
|
||||
|
||||
services:
|
||||
web-dev:
|
||||
build: .
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
container_name: pulse-web-dev
|
||||
restart: always
|
||||
ports:
|
||||
|
||||
@@ -18,16 +18,16 @@ const MONTH_NAMES = [
|
||||
"Июл", "Авг", "Сен", "Окт", "Ноя", "Дек",
|
||||
]
|
||||
|
||||
export default function FinanceAnalytics() {
|
||||
export default function FinanceAnalytics({ month, year }) {
|
||||
const [analytics, setAnalytics] = useState(null)
|
||||
const [summary, setSummary] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const now = new Date()
|
||||
setLoading(true)
|
||||
Promise.all([
|
||||
financeApi.getAnalytics({ months: 6 }),
|
||||
financeApi.getSummary({ month: now.getMonth() + 1, year: now.getFullYear() }),
|
||||
financeApi.getSummary({ month, year }),
|
||||
])
|
||||
.then(([a, s]) => {
|
||||
setAnalytics(a)
|
||||
@@ -35,7 +35,7 @@ export default function FinanceAnalytics() {
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
}, [month, year])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -72,7 +72,6 @@ export default function FinanceAnalytics() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Summary cards */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="card p-4">
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
@@ -97,7 +96,6 @@ export default function FinanceAnalytics() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bar chart */}
|
||||
{barData.length > 0 && (
|
||||
<div className="card p-5">
|
||||
<h3 className="font-display font-bold text-gray-900 dark:text-white mb-4">
|
||||
@@ -131,7 +129,6 @@ export default function FinanceAnalytics() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Donut chart */}
|
||||
{pieData.length > 0 && (
|
||||
<div className="card p-5">
|
||||
<h3 className="font-display font-bold text-gray-900 dark:text-white mb-4">
|
||||
@@ -179,7 +176,6 @@ export default function FinanceAnalytics() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Monthly trend */}
|
||||
{monthlyData.length > 0 && (
|
||||
<div className="card p-5">
|
||||
<h3 className="font-display font-bold text-gray-900 dark:text-white mb-4">
|
||||
|
||||
@@ -12,18 +12,18 @@ const COLORS = [
|
||||
|
||||
const fmt = (n) => Number(n).toLocaleString("ru-RU") + " ₽"
|
||||
|
||||
export default function FinanceDashboard() {
|
||||
export default function FinanceDashboard({ month, year }) {
|
||||
const [summary, setSummary] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const now = new Date()
|
||||
setLoading(true)
|
||||
financeApi
|
||||
.getSummary({ month: now.getMonth() + 1, year: now.getFullYear() })
|
||||
.getSummary({ month, year })
|
||||
.then(setSummary)
|
||||
.catch(console.error)
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
}, [month, year])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -63,7 +63,6 @@ export default function FinanceDashboard() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Balance Card */}
|
||||
<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-3xl font-bold mt-1">{fmt(summary.balance)}</p>
|
||||
@@ -83,7 +82,6 @@ export default function FinanceDashboard() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top Categories */}
|
||||
{expenseCategories.length > 0 && (
|
||||
<div className="card p-5">
|
||||
<h3 className="font-display font-bold text-gray-900 dark:text-white mb-4">
|
||||
@@ -120,7 +118,6 @@ export default function FinanceDashboard() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Donut Chart */}
|
||||
{pieData.length > 0 && (
|
||||
<div className="card p-5">
|
||||
<h3 className="font-display font-bold text-gray-900 dark:text-white mb-4">
|
||||
@@ -168,7 +165,6 @@ export default function FinanceDashboard() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Daily Line Chart */}
|
||||
{dailyData.length > 0 && (
|
||||
<div className="card p-5">
|
||||
<h3 className="font-display font-bold text-gray-900 dark:text-white mb-4">
|
||||
|
||||
@@ -8,7 +8,7 @@ const formatDate = (d) => {
|
||||
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 [categories, setCategories] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -17,11 +17,12 @@ export default function TransactionList({ onAdd }) {
|
||||
const [search, setSearch] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
Promise.all([
|
||||
financeApi.listCategories(),
|
||||
financeApi.listTransactions({
|
||||
month: new Date().getMonth() + 1,
|
||||
year: new Date().getFullYear(),
|
||||
month,
|
||||
year,
|
||||
limit: 100,
|
||||
}),
|
||||
])
|
||||
@@ -31,7 +32,7 @@ export default function TransactionList({ onAdd }) {
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
}, [month, year])
|
||||
|
||||
const filtered = transactions.filter((t) => {
|
||||
if (filter !== "all" && t.type !== filter) return false
|
||||
@@ -67,7 +68,6 @@ export default function TransactionList({ onAdd }) {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Search */}
|
||||
<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="Поиск по описанию..."
|
||||
@@ -75,7 +75,6 @@ export default function TransactionList({ onAdd }) {
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
{/* Type filter */}
|
||||
<div className="flex gap-2">
|
||||
{[
|
||||
["all", "Все"],
|
||||
@@ -96,7 +95,6 @@ export default function TransactionList({ onAdd }) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Category filter */}
|
||||
<div className="flex gap-2 overflow-x-auto pb-1">
|
||||
<button
|
||||
onClick={() => setCatFilter(null)}
|
||||
@@ -123,7 +121,6 @@ export default function TransactionList({ onAdd }) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Transaction groups */}
|
||||
{Object.keys(grouped).length === 0 ? (
|
||||
<div className="card p-12 text-center">
|
||||
<span className="text-4xl block mb-3">🔍</span>
|
||||
|
||||
@@ -13,13 +13,32 @@ const tabs = [
|
||||
{ key: "categories", label: "Категории", icon: "🏷️" },
|
||||
]
|
||||
|
||||
const MONTH_NAMES = [
|
||||
"Январь", "Февраль", "Март", "Апрель", "Май", "Июнь",
|
||||
"Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь",
|
||||
]
|
||||
|
||||
export default function Finance() {
|
||||
const now = new Date()
|
||||
const [activeTab, setActiveTab] = useState("dashboard")
|
||||
const [showAdd, setShowAdd] = useState(false)
|
||||
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 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 (
|
||||
<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">
|
||||
@@ -36,6 +55,31 @@ export default function Finance() {
|
||||
+
|
||||
</button>
|
||||
</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">
|
||||
{tabs.map((t) => (
|
||||
<button
|
||||
@@ -54,11 +98,11 @@ export default function Finance() {
|
||||
</header>
|
||||
|
||||
<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" && (
|
||||
<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} />}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user