mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 12:28:48 +02:00
Drop legacy pages, contexts, and dashboard widgets
Removes Dashboard / Transactions / Transfers pages, the section configuration UI, the legacy useSettings hook, and the standalone PrivacyContext/ThemeContext modules. Privacy/theme contexts now live under src/contexts/ and the API helper / push-notifications module move under src/lib/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,376 +0,0 @@
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
ArrowRight,
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
RefreshCw,
|
||||
CreditCard,
|
||||
Pencil,
|
||||
Check,
|
||||
X,
|
||||
BellRing,
|
||||
Landmark,
|
||||
} from 'lucide-react';
|
||||
|
||||
import api, { type Account, type Transaction } from '../api';
|
||||
import { useSettings } from '@/hooks/useSettings';
|
||||
import { formatAmount, formatDate, formatLocalDatetime } from '@/lib/format';
|
||||
import DashboardSection from '@/components/DashboardSection';
|
||||
import SectionConfigDialog from '@/components/SectionConfigDialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// --- Section definitions ---
|
||||
|
||||
interface SectionDef {
|
||||
filterFn: (a: Account) => boolean;
|
||||
totalCurrency: string; // empty string = no total
|
||||
}
|
||||
|
||||
const SECTION_DEFS: Record<string, SectionDef> = {
|
||||
crc_accounts: { filterFn: (a) => a.account_type === 'BANK' && a.currency === 'CRC', totalCurrency: 'CRC' },
|
||||
usd_accounts: { filterFn: (a) => a.account_type === 'BANK' && a.currency === 'USD', totalCurrency: 'USD' },
|
||||
pension: { filterFn: (a) => a.account_type === 'PENSION', totalCurrency: 'CRC' },
|
||||
savings: { filterFn: (a) => a.account_type === 'SAVINGS', totalCurrency: 'CRC' },
|
||||
liabilities: { filterFn: (a) => a.account_type === 'LIABILITY', totalCurrency: '' },
|
||||
crypto: { filterFn: (a) => a.account_type === 'CRYPTO', totalCurrency: '' },
|
||||
};
|
||||
|
||||
const BANK_ORDER = ['BAC', 'BCR', 'DAVIVIENDA'];
|
||||
|
||||
// --- AccountRow ---
|
||||
|
||||
interface AccountRowProps {
|
||||
account: Account;
|
||||
editingId: number | null;
|
||||
editValue: string;
|
||||
setEditValue: (v: string) => void;
|
||||
startEditing: (a: Account) => void;
|
||||
saveBalance: (id: number) => void;
|
||||
cancelEditing: () => void;
|
||||
}
|
||||
|
||||
function AccountRow({
|
||||
account,
|
||||
editingId,
|
||||
editValue,
|
||||
setEditValue,
|
||||
startEditing,
|
||||
saveBalance,
|
||||
cancelEditing,
|
||||
}: AccountRowProps) {
|
||||
const isLiability = account.account_type === 'LIABILITY';
|
||||
const isCrypto = account.account_type === 'CRYPTO';
|
||||
const label = isCrypto ? account.label : (account.bank === 'DAVIVIENDA' ? 'DAV' : account.bank);
|
||||
const isEditing = editingId === account.id;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between px-3 py-2.5 hover:bg-muted/30 transition-colors group">
|
||||
<span className="text-sm font-medium text-muted-foreground">{label}</span>
|
||||
|
||||
{isEditing ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') saveBalance(account.id);
|
||||
if (e.key === 'Escape') cancelEditing();
|
||||
}}
|
||||
autoFocus
|
||||
className="text-sm font-bold font-mono tracking-tight h-auto py-1 w-40"
|
||||
/>
|
||||
<Button variant="ghost" size="icon-xs" onClick={() => saveBalance(account.id)} title="Save" aria-label="Save balance">
|
||||
<Check className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon-xs" onClick={cancelEditing} title="Cancel" aria-label="Cancel editing">
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span data-sensitive className={cn('text-lg font-bold font-mono tracking-tight', isLiability && 'text-destructive')}>
|
||||
{formatAmount(account.balance, account.currency)}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => startEditing(account)}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-foreground cursor-pointer"
|
||||
title="Edit balance"
|
||||
aria-label="Edit balance"
|
||||
>
|
||||
<Pencil className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
{isLiability && account.next_payment != null && (
|
||||
<span data-sensitive className="text-xs font-mono text-destructive/60 ml-2">
|
||||
Next: {formatAmount(account.next_payment, account.currency)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Dashboard ---
|
||||
|
||||
export default function Dashboard() {
|
||||
const [accounts, setAccounts] = useState<Account[]>([]);
|
||||
const [recent, setRecent] = useState<Transaction[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [editValue, setEditValue] = useState('');
|
||||
const [exchangeRate, setExchangeRate] = useState<{ buy_rate: number; sell_rate: number } | null>(null);
|
||||
const [configSection, setConfigSection] = useState<string | null>(null);
|
||||
const [testingPush, setTestingPush] = useState(false);
|
||||
|
||||
const { settings, patchSection } = useSettings();
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [accRes, txRes] = await Promise.all([
|
||||
api.get('/accounts/'),
|
||||
api.get('/transactions/recent?limit=5'),
|
||||
]);
|
||||
setAccounts(accRes.data);
|
||||
setRecent(txRes.data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
api.get('/exchange-rate/').then((r) => setExchangeRate(r.data)).catch(() => {});
|
||||
};
|
||||
|
||||
useEffect(() => { fetchData(); }, []);
|
||||
|
||||
const startEditing = (account: Account) => {
|
||||
setEditingId(account.id);
|
||||
setEditValue(String(account.balance));
|
||||
};
|
||||
const cancelEditing = () => { setEditingId(null); setEditValue(''); };
|
||||
const saveBalance = async (accountId: number) => {
|
||||
const parsed = parseFloat(editValue);
|
||||
if (isNaN(parsed)) return cancelEditing();
|
||||
try {
|
||||
await api.patch(`/accounts/${accountId}`, { balance: parsed });
|
||||
setEditingId(null);
|
||||
setEditValue('');
|
||||
fetchData();
|
||||
} catch (e) { console.error(e); }
|
||||
};
|
||||
|
||||
const rowProps = { editingId, editValue, setEditValue, startEditing, saveBalance, cancelEditing };
|
||||
|
||||
// Sort sections by order, filter by visible
|
||||
const sortedSections = useMemo(() => {
|
||||
const sections = settings.dashboard.sections;
|
||||
return Object.entries(sections)
|
||||
.filter(([, s]) => s.visible)
|
||||
.sort(([, a], [, b]) => a.order - b.order);
|
||||
}, [settings]);
|
||||
|
||||
// Net worth calculation
|
||||
const netWorthBreakdown = useMemo(() => {
|
||||
if (accounts.length === 0) return null;
|
||||
let assets = 0;
|
||||
let liabilities = 0;
|
||||
for (const a of accounts) {
|
||||
const isLiability = a.account_type === 'LIABILITY';
|
||||
let crcValue = 0;
|
||||
if (a.currency === 'USD') {
|
||||
crcValue = Math.abs(a.balance) * (exchangeRate?.sell_rate ?? 0);
|
||||
} else if (a.currency === 'CRC') {
|
||||
crcValue = Math.abs(a.balance);
|
||||
}
|
||||
if (isLiability) {
|
||||
liabilities += crcValue;
|
||||
} else {
|
||||
assets += crcValue;
|
||||
}
|
||||
}
|
||||
return { assets, liabilities, net: assets - liabilities };
|
||||
}, [accounts, exchangeRate]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold font-heading">Dashboard</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">Financial overview</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" onClick={fetchData} title="Refresh" aria-label="Refresh">
|
||||
<RefreshCw className={cn('w-4 h-4', loading && 'animate-spin')} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Net Worth */}
|
||||
{netWorthBreakdown != null && (
|
||||
<Card>
|
||||
<CardContent className="px-4 py-3">
|
||||
<div className="flex items-center justify-between text-sm font-mono text-muted-foreground">
|
||||
<span>Net <span data-sensitive className="text-foreground font-bold">{netWorthBreakdown.net < 0 ? '-' : ''}{formatAmount(netWorthBreakdown.net, 'CRC')}</span></span>
|
||||
<div className="flex gap-4">
|
||||
<span>Assets <span data-sensitive className="text-foreground">{formatAmount(netWorthBreakdown.assets, 'CRC')}</span></span>
|
||||
<span>Liabilities <span data-sensitive className="text-foreground">{formatAmount(netWorthBreakdown.liabilities, 'CRC')}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Account sections */}
|
||||
{sortedSections.map(([sectionId, sectionSettings]) => {
|
||||
const def = SECTION_DEFS[sectionId];
|
||||
if (!def) return null;
|
||||
let accts = accounts.filter(def.filterFn);
|
||||
if (accts.length === 0) return null;
|
||||
|
||||
// Sort bank accounts by bank order
|
||||
if (sectionId === 'crc_accounts' || sectionId === 'usd_accounts') {
|
||||
accts = accts.sort((a, b) => BANK_ORDER.indexOf(a.bank) - BANK_ORDER.indexOf(b.bank));
|
||||
}
|
||||
|
||||
const total = accts.reduce((s, a) => s + a.balance, 0);
|
||||
|
||||
return (
|
||||
<DashboardSection
|
||||
key={sectionId}
|
||||
sectionId={sectionId}
|
||||
settings={sectionSettings}
|
||||
total={def.totalCurrency ? total : undefined}
|
||||
totalCurrency={def.totalCurrency || undefined}
|
||||
onToggleExpanded={(expanded) => patchSection(sectionId, { expanded })}
|
||||
onOpenConfig={() => setConfigSection(sectionId)}
|
||||
>
|
||||
{accts.map((a) => (
|
||||
<AccountRow key={a.id} account={a} {...rowProps} />
|
||||
))}
|
||||
</DashboardSection>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Exchange rate */}
|
||||
{exchangeRate && (
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">USD/CRC Exchange Rate</span>
|
||||
<div className="flex items-baseline gap-3 mt-1">
|
||||
<span data-sensitive className="text-lg font-bold font-mono">Buy: ₡{exchangeRate.buy_rate.toFixed(2)}</span>
|
||||
<span data-sensitive className="text-lg font-bold font-mono text-muted-foreground">Sell: ₡{exchangeRate.sell_rate.toFixed(2)}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Recent transactions */}
|
||||
<Card>
|
||||
<CardHeader className="border-b flex-row items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<CreditCard className="w-4 h-4 text-muted-foreground" />
|
||||
<CardTitle className="text-sm">Recent Charges</CardTitle>
|
||||
</div>
|
||||
<Link
|
||||
to="/transactions"
|
||||
className="flex items-center gap-1 text-xs font-medium text-primary hover:text-primary/80 transition-colors"
|
||||
>
|
||||
View all
|
||||
<ArrowRight className="w-3 h-3" />
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
{recent.length === 0 && !loading ? (
|
||||
<div className="px-5 py-12 text-center text-muted-foreground text-sm">No transactions yet. Add your first one!</div>
|
||||
) : (
|
||||
<div className="divide-y divide-border">
|
||||
{recent.map((tx) => (
|
||||
<div key={tx.id} className="flex items-center justify-between px-5 py-3.5 hover:bg-muted/50 transition-colors">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className={cn(
|
||||
'w-8 h-8 rounded-lg flex items-center justify-center shrink-0',
|
||||
tx.transaction_type === 'COMPRA' ? 'bg-destructive/10 text-destructive' : 'bg-primary/10 text-primary'
|
||||
)}>
|
||||
{tx.transaction_type === 'DEPOSITO' ? <Landmark className="w-4 h-4" /> : tx.transaction_type === 'DEVOLUCION' ? <TrendingUp className="w-4 h-4" /> : <TrendingDown className="w-4 h-4" />}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium truncate">{tx.merchant}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatDate(tx.date)}
|
||||
{tx.category && <span className="ml-2 text-muted-foreground/60">{tx.category.name}</span>}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span data-sensitive className={cn(
|
||||
'font-mono text-sm font-medium shrink-0 ml-4',
|
||||
tx.transaction_type !== 'COMPRA' && 'text-primary'
|
||||
)}>
|
||||
{tx.transaction_type === 'COMPRA' ? '-' : '+'}{formatAmount(tx.amount, tx.currency)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Test push notification */}
|
||||
<Card className="border-dashed border-yellow-500/50">
|
||||
<CardContent className="p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Test Push Notification</p>
|
||||
<p className="text-xs text-muted-foreground">Creates a mock transaction to trigger a push notification</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={testingPush}
|
||||
onClick={async () => {
|
||||
setTestingPush(true);
|
||||
try {
|
||||
const merchants = ['Walmart', 'AutoMercado', 'Uber Eats', 'Amazon', 'PriceSmart'];
|
||||
const amounts = [4500, 12350, 8900, 25000, 67800];
|
||||
const i = Math.floor(Math.random() * merchants.length);
|
||||
await api.post('/transactions/', {
|
||||
merchant: merchants[i],
|
||||
amount: amounts[i],
|
||||
currency: 'CRC',
|
||||
date: formatLocalDatetime(new Date()),
|
||||
bank: 'BAC',
|
||||
source: 'CREDIT_CARD',
|
||||
transaction_type: 'COMPRA',
|
||||
reference: `test-push-${Date.now()}`,
|
||||
notes: '[TEST] Push notification test — safe to delete',
|
||||
});
|
||||
fetchData();
|
||||
} catch (e) {
|
||||
console.error('Test push failed:', e);
|
||||
} finally {
|
||||
setTestingPush(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<BellRing className="w-4 h-4 mr-2" />
|
||||
{testingPush ? 'Sending...' : 'Send test'}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Section config dialog */}
|
||||
{configSection && settings.dashboard.sections[configSection] && (
|
||||
<SectionConfigDialog
|
||||
sectionId={configSection}
|
||||
settings={settings.dashboard.sections[configSection]}
|
||||
open={!!configSection}
|
||||
onOpenChange={(open) => { if (!open) setConfigSection(null); }}
|
||||
onSave={(id, partial) => patchSection(id, partial)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { Plus, ClipboardPaste } from 'lucide-react';
|
||||
|
||||
import api, { type Transaction, type Category } from '../api';
|
||||
import PasteImportModal from '../components/PasteImportModal';
|
||||
import BillingCycleSelector from '../components/BillingCycleSelector';
|
||||
import TransactionList from '../components/TransactionList';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
|
||||
function formatAmount(amount: number, currency: string) {
|
||||
const abs = Math.abs(amount);
|
||||
if (currency === 'USD') {
|
||||
return `$${abs.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
}
|
||||
return `₡${abs.toLocaleString('es-CR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
}
|
||||
|
||||
export default function Transactions() {
|
||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [search, setSearch] = useState('');
|
||||
const [categoryFilter, setCategoryFilter] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
const [cycle, setCycle] = useState<{ year: number; month: number } | null>(null);
|
||||
|
||||
const fetchTransactions = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: Record<string, string> = { source: 'CREDIT_CARD', limit: '200' };
|
||||
if (search) params.search = search;
|
||||
if (categoryFilter) params.category_id = categoryFilter;
|
||||
if (cycle) {
|
||||
params.cycle_year = String(cycle.year);
|
||||
params.cycle_month = String(cycle.month);
|
||||
}
|
||||
const { data } = await api.get('/transactions/', { params });
|
||||
setTransactions(data);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [search, categoryFilter, cycle]);
|
||||
|
||||
useEffect(() => {
|
||||
api.get('/categories/').then((r) => setCategories(r.data));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(fetchTransactions, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [fetchTransactions]);
|
||||
|
||||
const totalCRC = transactions
|
||||
.filter((tx) => tx.currency === 'CRC')
|
||||
.reduce((sum, tx) => sum + (tx.transaction_type === 'DEVOLUCION' ? -tx.amount : tx.amount), 0);
|
||||
const totalUSD = transactions
|
||||
.filter((tx) => tx.currency === 'USD')
|
||||
.reduce((sum, tx) => sum + (tx.transaction_type === 'DEVOLUCION' ? -tx.amount : tx.amount), 0);
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold font-heading">Credit Card Transactions</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{transactions.length} transactions
|
||||
{totalCRC !== 0 && (
|
||||
<> · <span data-sensitive className="font-mono text-foreground">{formatAmount(totalCRC, 'CRC')}</span></>
|
||||
)}
|
||||
{totalUSD !== 0 && (
|
||||
<> · <span data-sensitive className="font-mono text-foreground">{formatAmount(totalUSD, 'USD')}</span></>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => setImportOpen(true)}>
|
||||
<ClipboardPaste className="w-4 h-4" />
|
||||
Import
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Billing cycle */}
|
||||
<BillingCycleSelector value={cycle} onChange={setCycle} />
|
||||
|
||||
{/* Category filter */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Select
|
||||
value={categoryFilter || 'all'}
|
||||
onValueChange={(v) => setCategoryFilter(v === 'all' ? '' : v)}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Categories</SelectItem>
|
||||
{categories.map((c) => (
|
||||
<SelectItem key={c.id} value={String(c.id)}>
|
||||
{c.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<TransactionList
|
||||
transactions={transactions}
|
||||
loading={loading}
|
||||
source="CREDIT_CARD"
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
onRefresh={fetchTransactions}
|
||||
showCategory
|
||||
/>
|
||||
|
||||
{importOpen && (
|
||||
<PasteImportModal
|
||||
onClose={() => setImportOpen(false)}
|
||||
onImported={fetchTransactions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { ArrowLeftRight } from 'lucide-react';
|
||||
|
||||
import api, { type Transaction } from '../api';
|
||||
import TransactionList from '../components/TransactionList';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||
|
||||
type SourceTab = 'CASH' | 'TRANSFER';
|
||||
|
||||
export default function Transfers() {
|
||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||
const [search, setSearch] = useState('');
|
||||
const [sourceTab, setSourceTab] = useState<SourceTab>('CASH');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchTransactions = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: Record<string, string> = { source: sourceTab, limit: '200' };
|
||||
if (search) params.search = search;
|
||||
const { data } = await api.get('/transactions/', { params });
|
||||
setTransactions(data);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [search, sourceTab]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(fetchTransactions, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [fetchTransactions]);
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold font-heading">Cash & Transfers</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Track non-credit-card expenses
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs value={sourceTab} onValueChange={(v) => setSourceTab(v as SourceTab)}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="CASH">Cash</TabsTrigger>
|
||||
<TabsTrigger value="TRANSFER">Transfers</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value={sourceTab} className="mt-5 space-y-5">
|
||||
<TransactionList
|
||||
transactions={transactions}
|
||||
loading={loading}
|
||||
source={sourceTab}
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
onRefresh={fetchTransactions}
|
||||
showCategory={false}
|
||||
addLabel={sourceTab === 'CASH' ? 'Add Cash Expense' : 'Add Transfer'}
|
||||
emptyIcon={<ArrowLeftRight className="w-8 h-8 mx-auto mb-3 text-muted-foreground/50" />}
|
||||
emptyMessage={`No ${sourceTab.toLowerCase()} transactions yet`}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user