v0.28.0
This commit is contained in:
parent
88c1374d97
commit
53670b3745
|
|
@ -1,5 +1,4 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
AlertTriangle,
|
||||
|
|
@ -12,6 +11,7 @@ import {
|
|||
import { api } from '@/api';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import BillModal from '@/components/BillModal';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const FIELD_LABELS = {
|
||||
|
|
@ -66,8 +66,9 @@ function IssuePill({ issue }) {
|
|||
);
|
||||
}
|
||||
|
||||
function BillIssueCard({ bill }) {
|
||||
function BillIssueCard({ bill, onOpenBill, openingBillId }) {
|
||||
const sortedIssues = [...bill.issues].sort((a, b) => severityWeight(a.severity) - severityWeight(b.severity));
|
||||
const opening = openingBillId === bill.id;
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
|
|
@ -86,8 +87,16 @@ function BillIssueCard({ bill }) {
|
|||
{!bill.active && ' - Inactive'}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button asChild variant="outline" size="sm" className="shrink-0">
|
||||
<Link to="/bills">Open Bills</Link>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
onClick={() => onOpenBill(bill.id)}
|
||||
disabled={!!openingBillId}
|
||||
>
|
||||
{opening && <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />}
|
||||
{opening ? 'Opening...' : 'Open Bill'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
|
@ -112,6 +121,9 @@ export default function HealthPage() {
|
|||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [includeInactive, setIncludeInactive] = useState(false);
|
||||
const [categories, setCategories] = useState([]);
|
||||
const [modalBill, setModalBill] = useState(null);
|
||||
const [openingBillId, setOpeningBillId] = useState(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
|
@ -126,6 +138,27 @@ export default function HealthPage() {
|
|||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const openBill = useCallback(async (billId) => {
|
||||
setOpeningBillId(billId);
|
||||
try {
|
||||
const [bill, cats] = await Promise.all([
|
||||
api.bill(billId),
|
||||
categories.length ? Promise.resolve(categories) : api.categories(),
|
||||
]);
|
||||
if (!categories.length) setCategories(cats);
|
||||
setModalBill(bill);
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Could not open bill.');
|
||||
} finally {
|
||||
setOpeningBillId(null);
|
||||
}
|
||||
}, [categories]);
|
||||
|
||||
const handleBillSaved = useCallback(() => {
|
||||
setModalBill(null);
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const summary = data?.summary || {};
|
||||
const bills = data?.bills || [];
|
||||
const sortedBills = useMemo(() => [...bills].sort((a, b) => {
|
||||
|
|
@ -206,10 +239,26 @@ export default function HealthPage() {
|
|||
<p>Fix errors first; warnings are cleanup items that improve confidence and projections.</p>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
{sortedBills.map(bill => <BillIssueCard key={bill.id} bill={bill} />)}
|
||||
{sortedBills.map(bill => (
|
||||
<BillIssueCard
|
||||
key={bill.id}
|
||||
bill={bill}
|
||||
onOpenBill={openBill}
|
||||
openingBillId={openingBillId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{modalBill && (
|
||||
<BillModal
|
||||
bill={modalBill}
|
||||
categories={categories}
|
||||
onClose={() => setModalBill(null)}
|
||||
onSave={handleBillSaved}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ router.post('/clear-demo-data', demoDataLimiter, (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// POST /api/user/seed-demo-data — seeds 20 demo bills for the requesting user
|
||||
// POST /api/user/seed-demo-data — seeds demo bills for the requesting user
|
||||
router.post('/seed-demo-data', (req, res) => {
|
||||
try {
|
||||
const result = seedDemoData(req.user.id);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Seed Demo Data Script
|
||||
* Creates 20 realistic bills across 8 categories for demo purposes.
|
||||
* Creates realistic bills across common categories for demo purposes.
|
||||
* Idempotent: can be run multiple times safely.
|
||||
*/
|
||||
|
||||
|
|
@ -36,12 +36,14 @@ const BILLS = [
|
|||
{ name: 'Internet Provider', category: 'Utilities', amount: 70, dueDay: 18, cycle: 'monthly', autopay: true, interestRate: 0 },
|
||||
{ name: 'Cell Phone', category: 'Utilities', amount: 65, dueDay: 25, cycle: 'monthly', autopay: true, interestRate: 0 },
|
||||
{ name: 'Health Insurance', category: 'Healthcare', amount: 200, dueDay: 1, cycle: 'quarterly', autopay: true, interestRate: 0 },
|
||||
{ name: 'Credit Card', category: 'Credit Cards', amount: 150, dueDay: 28, cycle: 'monthly', autopay: true, interestRate: 19.99, currentBalance: 2800, minPayment: 75, snowballOrder: 0, snowballInclude: 1 },
|
||||
{ name: 'Student Loan', category: 'Loans', amount: 250, dueDay: 15, cycle: 'monthly', autopay: true, interestRate: 5.5, currentBalance: 12500, minPayment: 150, snowballOrder: 1, snowballInclude: 1 },
|
||||
{ name: 'Discover It Card', category: 'Credit Cards', amount: 65, dueDay: 26, cycle: 'monthly', autopay: true, interestRate: 22.99, currentBalance: 920, minPayment: 35, snowballOrder: 0, snowballInclude: 1 },
|
||||
{ name: 'Capital One Quicksilver', category: 'Credit Cards', amount: 95, dueDay: 28, cycle: 'monthly', autopay: true, interestRate: 24.49, currentBalance: 1850, minPayment: 55, snowballOrder: 1, snowballInclude: 1 },
|
||||
{ name: 'Chase Freedom', category: 'Credit Cards', amount: 140, dueDay: 12, cycle: 'monthly', autopay: true, interestRate: 21.49, currentBalance: 3200, minPayment: 90, snowballOrder: 2, snowballInclude: 1 },
|
||||
{ name: 'Student Loan', category: 'Loans', amount: 250, dueDay: 15, cycle: 'monthly', autopay: true, interestRate: 5.5, currentBalance: 12500, minPayment: 150, snowballOrder: 4, snowballInclude: 1 },
|
||||
{ name: 'Gas Utility', category: 'Utilities', amount: 35, dueDay: 12, cycle: 'monthly', autopay: true, interestRate: 0 },
|
||||
{ name: 'Trash Service', category: 'Utilities', amount: 25, dueDay: 28, cycle: 'monthly', autopay: true, interestRate: 0 },
|
||||
{ name: 'Homeowners Insurance', category: 'Insurance', amount: 300, dueDay: 10, cycle: 'annually', autopay: false, interestRate: 0 },
|
||||
{ name: 'Car Payment', category: 'Loans', amount: 350, dueDay: 22, cycle: 'monthly', autopay: true, interestRate: 4.5, currentBalance: 8400, minPayment: 350, snowballOrder: 2, snowballInclude: 1 },
|
||||
{ name: 'Car Payment', category: 'Loans', amount: 350, dueDay: 22, cycle: 'monthly', autopay: true, interestRate: 4.5, currentBalance: 8400, minPayment: 350, snowballOrder: 3, snowballInclude: 1 },
|
||||
{ name: 'Spotify', category: 'Entertainment', amount: 9.99, dueDay: 14, cycle: 'monthly', autopay: true, interestRate: 0 },
|
||||
{ name: 'Adobe Creative Cloud', category: 'Subscriptions', amount: 54.99, dueDay: 8, cycle: 'monthly', autopay: true, interestRate: 0 },
|
||||
{ name: 'Amazon Prime', category: 'Subscriptions', amount: 14.99, dueDay: 1, cycle: 'annually', autopay: true, interestRate: 0 },
|
||||
|
|
@ -148,7 +150,7 @@ function seedDemoData(userId = null) {
|
|||
billData.cycle || 'monthly',
|
||||
amount,
|
||||
billData.autopay !== undefined ? (billData.autopay ? 1 : 0) : Math.random() > 0.5 ? 1 : 0,
|
||||
billData.interestRate || (Math.random() > 0.7 ? Math.round(Math.random() * 15 * 100) / 100 : 0),
|
||||
billData.interestRate ?? (Math.random() > 0.7 ? Math.round(Math.random() * 15 * 100) / 100 : 0),
|
||||
billData.currentBalance ?? null,
|
||||
billData.minPayment ?? null,
|
||||
billData.snowballOrder ?? null,
|
||||
|
|
|
|||
Loading…
Reference in New Issue