From ee7026872c35bfd2ecd62b5b1210fba379cd3e5b Mon Sep 17 00:00:00 2001 From: null Date: Fri, 12 Jun 2026 03:59:42 -0500 Subject: [PATCH] feat(banking): bank transactions ledger page with route, sidebar link, and API endpoint --- client/App.jsx | 2 + client/api.js | 1 + client/components/layout/Sidebar.jsx | 36 +- client/pages/BankTransactionsPage.jsx | 567 ++++++++++++++++++++++++++ routes/transactions.js | 179 ++++++++ 5 files changed, 777 insertions(+), 8 deletions(-) create mode 100644 client/pages/BankTransactionsPage.jsx diff --git a/client/App.jsx b/client/App.jsx index 4ae2b72..13dbe8e 100644 --- a/client/App.jsx +++ b/client/App.jsx @@ -48,6 +48,7 @@ const SnowballPage = lazy(() => import('@/pages/SnowballPage')); const HealthPage = lazy(() => import('@/pages/HealthPage')); const PayoffPage = lazy(() => import('@/pages/PayoffPage')); const SpendingPage = lazy(() => import('@/pages/SpendingPage')); +const BankTransactionsPage = lazy(() => import('@/pages/BankTransactionsPage')); function RequireAuth({ children, role }) { const { user, singleUserMode } = useAuth(); @@ -221,6 +222,7 @@ export default function App() { }>} /> }>} /> }>} /> + }>} /> }>} /> }>} /> } /> diff --git a/client/api.js b/client/api.js index c38f20a..3db848a 100644 --- a/client/api.js +++ b/client/api.js @@ -403,6 +403,7 @@ export const api = { // Transactions transactions: (params = {}) => get(`/transactions${queryString(params)}`), + bankTransactionsLedger: (params = {}) => get(`/transactions/bank-ledger${queryString(params)}`), createManualTransaction: (data) => post('/transactions/manual', data), updateTransaction: (id, data) => put(`/transactions/${id}`, data), deleteTransaction: (id) => del(`/transactions/${id}`), diff --git a/client/components/layout/Sidebar.jsx b/client/components/layout/Sidebar.jsx index 34d58ee..476f8b4 100644 --- a/client/components/layout/Sidebar.jsx +++ b/client/components/layout/Sidebar.jsx @@ -1,10 +1,11 @@ -import { useState, useMemo } from 'react'; +import { useEffect, useState, useMemo } from 'react'; import { NavLink, useLocation, useNavigate } from 'react-router-dom'; import { Activity, BarChart3, Calculator, CalendarDays, ChevronDown, ClipboardCheck, ClipboardList, Database, Info, LayoutGrid, LogOut, Map, Menu, Receipt, Search, Settings, ShieldCheck, Tag, TrendingDown, User, X, - Repeat, ShoppingCart, + Landmark, Repeat, ShoppingCart, } from 'lucide-react'; +import { api } from '@/api'; import { cn } from '@/lib/utils'; import { useAuth } from '@/hooks/useAuth'; import { useOverdueCount } from '@/hooks/useQueries'; @@ -42,16 +43,17 @@ const trackerItems = [ { to: '/categories', icon: Tag, label: 'Categories' }, { to: '/health', icon: ClipboardCheck, label: 'Health' }, { to: '/spending', icon: ShoppingCart, label: 'Spending' }, + { to: '/bank-transactions', icon: Landmark, label: 'Banking', simplefinOnly: true }, { to: '/snowball', icon: TrendingDown, label: 'Snowball' }, { to: '/payoff', icon: Calculator, label: 'Payoff' }, ]; -function TrackerMenu({ onNavigate, badge, badgeNames = [] }) { +function TrackerMenu({ onNavigate, badge, badgeNames = [], items = trackerItems }) { const location = useLocation(); const navigate = useNavigate(); - const isTrackerActive = useMemo(() => trackerItems.some(item => ( + const isTrackerActive = useMemo(() => items.some(item => ( item.end ? location.pathname === item.to : location.pathname.startsWith(item.to) - )), [location.pathname]); + )), [items, location.pathname]); return ( @@ -93,7 +95,7 @@ function TrackerMenu({ onNavigate, badge, badgeNames = [] }) { - {trackerItems.map(item => { + {items.map(item => { const Icon = item.icon; return ( { navigate(item.to); onNavigate?.(); }}> @@ -193,9 +195,27 @@ export default function Sidebar({ adminMode = false }) { const [mobileOpen, setMobileOpen] = useState(false); const { user } = useAuth(); const items = useMemo(() => adminMode ? adminNavItems : userNavItems, [adminMode]); + const [simplefinReady, setSimplefinReady] = useState(false); const { data: overdueData } = useOverdueCount(); const overdueCount = (!adminMode && overdueData?.count) ? overdueData.count : 0; const overdueNames = (!adminMode && overdueData?.names) ? overdueData.names : []; + const trackerMenuItems = useMemo( + () => trackerItems.filter(item => !item.simplefinOnly || simplefinReady), + [simplefinReady], + ); + + useEffect(() => { + if (adminMode) return undefined; + let cancelled = false; + api.simplefinStatus() + .then(status => { + if (!cancelled) setSimplefinReady(Boolean(status?.enabled && status?.has_connections)); + }) + .catch(() => { + if (!cancelled) setSimplefinReady(false); + }); + return () => { cancelled = true; }; + }, [adminMode]); return (
@@ -203,7 +223,7 @@ export default function Sidebar({ adminMode = false }) {