mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-05-19 11:28:49 +02:00
Add accounts expansion, analytics, exchange rates, API tokens, PWA support, and UI overhaul
All checks were successful
Deploy to VPS / deploy (push) Successful in 45s
All checks were successful
Deploy to VPS / deploy (push) Successful in 45s
- Expand Account model with account_type (pension, savings, liability, crypto), new banks/currencies (BTC, XMR, FCL, ROP, VOL, MEMP, MPAT, MORTGAGE), and next_payment field - Add exchange rate endpoint (BCCR integration), analytics endpoint, paste-import for transactions, and API token management - Add PWA manifest, service worker, and app icons - Redesign dashboard, transactions, transfers, and login pages with theme support - Add billing cycle selector, confirm dialog, and paste import modal components - One-time DB reset in deploy workflow for schema migration Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { Plus, Search, Pencil, Trash2, ArrowLeftRight } from 'lucide-react';
|
||||
|
||||
import api, { type Transaction } from '../api';
|
||||
import TransactionModal from '../components/TransactionModal';
|
||||
import ConfirmDialog from '../components/ConfirmDialog';
|
||||
|
||||
function formatAmount(amount: number, currency: string) {
|
||||
const abs = Math.abs(amount);
|
||||
@@ -21,6 +22,8 @@ export default function Transfers() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<Transaction | null>(null);
|
||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
const fetchTransactions = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -39,10 +42,16 @@ export default function Transfers() {
|
||||
return () => clearTimeout(timer);
|
||||
}, [fetchTransactions]);
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('Delete this transaction?')) return;
|
||||
await api.delete(`/transactions/${id}`);
|
||||
fetchTransactions();
|
||||
const handleDelete = async () => {
|
||||
if (deleteId === null) return;
|
||||
setDeleting(true);
|
||||
try {
|
||||
await api.delete(`/transactions/${deleteId}`);
|
||||
setDeleteId(null);
|
||||
fetchTransactions();
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -50,7 +59,7 @@ export default function Transfers() {
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Cash & Transfers</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
<p className="text-sm text-text-muted mt-1">
|
||||
Track non-credit-card expenses
|
||||
</p>
|
||||
</div>
|
||||
@@ -59,7 +68,7 @@ export default function Transfers() {
|
||||
setEditing(null);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
className="flex items-center gap-2 bg-emerald-500 hover:bg-emerald-400 text-slate-950 font-semibold px-4 py-2.5 rounded-lg text-sm transition-colors"
|
||||
className="flex items-center gap-2 bg-[#606C38] hover:bg-[#7a8a4a] text-white dark:text-[#FEFAE0] font-semibold px-4 py-2.5 rounded-lg text-sm transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add {sourceTab === 'CASH' ? 'Cash Expense' : 'Transfer'}
|
||||
@@ -67,15 +76,15 @@ export default function Transfers() {
|
||||
</div>
|
||||
|
||||
{/* Source tabs */}
|
||||
<div className="flex gap-1 bg-slate-900/40 border border-slate-800/60 rounded-lg p-1 w-fit">
|
||||
<div className="flex gap-1 bg-surface-card border border-border rounded-lg p-1 w-fit">
|
||||
{(['CASH', 'TRANSFER'] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setSourceTab(tab)}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
sourceTab === tab
|
||||
? 'bg-emerald-500/10 text-emerald-400'
|
||||
: 'text-slate-500 hover:text-white'
|
||||
? 'bg-[#606C38]/10 text-[#606C38] dark:text-[#7a8a4a]'
|
||||
: 'text-text-muted hover:text-text-primary'
|
||||
}`}
|
||||
>
|
||||
{tab === 'CASH' ? 'Cash' : 'Transfers'}
|
||||
@@ -85,20 +94,20 @@ export default function Transfers() {
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative max-w-sm">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
|
||||
<input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full bg-slate-900/60 border border-slate-800/60 rounded-lg pl-10 pr-4 py-2.5 text-sm text-white placeholder-slate-600 focus:outline-none focus:border-emerald-500/50 transition-colors"
|
||||
className="w-full bg-input-bg border border-border rounded-lg pl-10 pr-4 py-2.5 text-sm text-text-primary placeholder-text-faint focus:outline-none focus:border-[#606C38]/50 transition-colors"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
<div className="bg-slate-900/40 border border-slate-800/60 rounded-xl divide-y divide-slate-800/30">
|
||||
<div className="bg-surface-card border border-border rounded-xl divide-y divide-border-subtle">
|
||||
{transactions.length === 0 && !loading ? (
|
||||
<div className="px-5 py-16 text-center text-slate-600 text-sm">
|
||||
<ArrowLeftRight className="w-8 h-8 mx-auto mb-3 text-slate-700" />
|
||||
<div className="px-5 py-16 text-center text-text-faint text-sm">
|
||||
<ArrowLeftRight className="w-8 h-8 mx-auto mb-3 text-text-faint" />
|
||||
No {sourceTab.toLowerCase()} transactions yet
|
||||
</div>
|
||||
) : (
|
||||
@@ -106,25 +115,25 @@ export default function Transfers() {
|
||||
{transactions.map((tx) => (
|
||||
<div
|
||||
key={tx.id}
|
||||
className="flex items-center justify-between px-5 py-4 hover:bg-slate-800/20 transition-colors group"
|
||||
className="flex items-center justify-between px-5 py-4 hover:bg-surface-hover transition-colors group"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium truncate">{tx.merchant}</p>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
<p className="text-xs text-text-muted mt-0.5">
|
||||
{new Date(tx.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
{tx.category && (
|
||||
<span className="ml-2 bg-slate-800/60 text-slate-400 px-2 py-0.5 rounded">
|
||||
<span className="ml-2 bg-surface-hover text-text-secondary px-2 py-0.5 rounded">
|
||||
{tx.category.name}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 flex-shrink-0 ml-4">
|
||||
<span className="font-mono text-sm font-medium text-white">
|
||||
<span className="font-mono text-sm font-medium">
|
||||
{formatAmount(tx.amount, tx.currency)}
|
||||
</span>
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
@@ -133,13 +142,13 @@ export default function Transfers() {
|
||||
setEditing(tx);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
className="p-1.5 rounded hover:bg-slate-700/50 text-slate-500 hover:text-white transition-colors"
|
||||
className="p-1.5 rounded hover:bg-surface-hover text-text-muted hover:text-text-primary transition-colors"
|
||||
>
|
||||
<Pencil className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(tx.id)}
|
||||
className="p-1.5 rounded hover:bg-red-500/10 text-slate-500 hover:text-red-400 transition-colors"
|
||||
onClick={() => setDeleteId(tx.id)}
|
||||
className="p-1.5 rounded hover:bg-red-500/10 text-text-muted hover:text-red-500 dark:hover:text-red-400 transition-colors"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -159,6 +168,16 @@ export default function Transfers() {
|
||||
onSaved={fetchTransactions}
|
||||
/>
|
||||
)}
|
||||
|
||||
{deleteId !== null && (
|
||||
<ConfirmDialog
|
||||
title="Delete Transaction"
|
||||
message="This transaction will be permanently deleted. This action cannot be undone."
|
||||
onConfirm={handleDelete}
|
||||
onCancel={() => setDeleteId(null)}
|
||||
loading={deleting}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user