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 React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
|
|
@ -12,6 +11,7 @@ import {
|
||||||
import { api } from '@/api';
|
import { api } from '@/api';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import BillModal from '@/components/BillModal';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const FIELD_LABELS = {
|
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 sortedIssues = [...bill.issues].sort((a, b) => severityWeight(a.severity) - severityWeight(b.severity));
|
||||||
|
const opening = openingBillId === bill.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="overflow-hidden">
|
<Card className="overflow-hidden">
|
||||||
|
|
@ -86,8 +87,16 @@ function BillIssueCard({ bill }) {
|
||||||
{!bill.active && ' - Inactive'}
|
{!bill.active && ' - Inactive'}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild variant="outline" size="sm" className="shrink-0">
|
<Button
|
||||||
<Link to="/bills">Open Bills</Link>
|
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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
@ -112,6 +121,9 @@ export default function HealthPage() {
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [includeInactive, setIncludeInactive] = useState(false);
|
const [includeInactive, setIncludeInactive] = useState(false);
|
||||||
|
const [categories, setCategories] = useState([]);
|
||||||
|
const [modalBill, setModalBill] = useState(null);
|
||||||
|
const [openingBillId, setOpeningBillId] = useState(null);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
@ -126,6 +138,27 @@ export default function HealthPage() {
|
||||||
|
|
||||||
useEffect(() => { load(); }, [load]);
|
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 summary = data?.summary || {};
|
||||||
const bills = data?.bills || [];
|
const bills = data?.bills || [];
|
||||||
const sortedBills = useMemo(() => [...bills].sort((a, b) => {
|
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>
|
<p>Fix errors first; warnings are cleanup items that improve confidence and projections.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-3">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{modalBill && (
|
||||||
|
<BillModal
|
||||||
|
bill={modalBill}
|
||||||
|
categories={categories}
|
||||||
|
onClose={() => setModalBill(null)}
|
||||||
|
onSave={handleBillSaved}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</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) => {
|
router.post('/seed-demo-data', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const result = seedDemoData(req.user.id);
|
const result = seedDemoData(req.user.id);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* Seed Demo Data Script
|
* 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.
|
* 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: '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: '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: '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: '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: 'Student Loan', category: 'Loans', amount: 250, dueDay: 15, cycle: 'monthly', autopay: true, interestRate: 5.5, currentBalance: 12500, minPayment: 150, snowballOrder: 1, 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: '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: '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: '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: '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: '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 },
|
{ 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',
|
billData.cycle || 'monthly',
|
||||||
amount,
|
amount,
|
||||||
billData.autopay !== undefined ? (billData.autopay ? 1 : 0) : Math.random() > 0.5 ? 1 : 0,
|
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.currentBalance ?? null,
|
||||||
billData.minPayment ?? null,
|
billData.minPayment ?? null,
|
||||||
billData.snowballOrder ?? null,
|
billData.snowballOrder ?? null,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue