BillTracker/client/components/data/SeedDemoDataSection.tsx

149 lines
6.6 KiB
TypeScript

import { useState, useEffect } from 'react';
import { toast } from 'sonner';
import { Loader2 } from 'lucide-react';
import { api } from '@/api';
import { errMessage } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import {
AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { SectionCard, type SectionCardProps } from './dataShared';
export default function SeedDemoDataSection({ onSeeded, cardProps = {} }: {
onSeeded?: () => void;
cardProps?: Partial<SectionCardProps>;
}) {
const [loading, setLoading] = useState(false);
const [seeded, setSeeded] = useState(false);
const [counts, setCounts] = useState<{ bills: number; categories: number; payments: number; transactions: number }>(
{ bills: 0, categories: 0, payments: 0, transactions: 0 }
);
const [clearing, setClearing] = useState(false);
const [showClearConfirm, setShowClearConfirm] = useState(false);
const [statusLoading, setStatusLoading] = useState(true);
useEffect(() => {
api.seededStatus()
.then(raw => {
const data = raw as { seeded?: boolean; seededBills?: number; seededCategories?: number; seededPayments?: number; seededTransactions?: number };
setSeeded(!!data.seeded);
if (data.seeded) setCounts({
bills: data.seededBills || 0,
categories: data.seededCategories || 0,
payments: data.seededPayments || 0,
transactions: data.seededTransactions || 0,
});
})
.catch(err => console.error('Failed to check seeded status:', err))
.finally(() => setStatusLoading(false));
}, []);
const handleSeed = async () => {
setLoading(true);
try {
const data = await api.seedDemoData() as { billsCreated?: number; categoriesCreated?: number; paymentsCreated?: number; transactionsCreated?: number } | null;
if (!data || typeof data !== 'object') throw new Error('Invalid response from server');
setCounts({
bills: data.billsCreated || 0,
categories: data.categoriesCreated || 0,
payments: data.paymentsCreated || 0,
transactions: data.transactionsCreated || 0,
});
setSeeded(true);
toast.success(`Seeded ${data.billsCreated || 0} bills with ${data.paymentsCreated || 0} payments and ${data.transactionsCreated || 0} transactions.`);
setTimeout(() => onSeeded?.(), 100);
} catch (err) {
console.error('Seed error:', err);
toast.error(errMessage(err, 'Failed to seed demo data.'));
} finally {
setLoading(false);
}
};
const handleClearDemoData = async () => {
setClearing(true);
try {
const data = await api.clearDemoData() as { removed?: number };
setSeeded(false);
setCounts({ bills: 0, categories: 0, payments: 0, transactions: 0 });
setShowClearConfirm(false);
toast.success(`Removed all demo data (${data.removed || 0} records). Your own data is untouched.`);
onSeeded?.();
} catch (err) {
toast.error(errMessage(err, 'Failed to clear demo data.'));
} finally {
setClearing(false);
}
};
return (
<SectionCard title="Demo Data" subtitle="Seed your database with demo data for testing" {...cardProps}>
<div className="rounded-lg border border-border/60 bg-background/50 p-4">
{statusLoading ? (
<p className="text-sm text-muted-foreground">Loading</p>
) : seeded ? (
<>
<p className="text-sm font-medium text-emerald-600 dark:text-emerald-400">Demo data seeded</p>
<div className="mt-3 grid grid-cols-2 gap-4 text-xs sm:grid-cols-4">
<div>
<p className="text-muted-foreground">Bills</p>
<p className="font-semibold">{counts.bills}</p>
</div>
<div>
<p className="text-muted-foreground">Payments</p>
<p className="font-semibold">{counts.payments}</p>
</div>
<div>
<p className="text-muted-foreground">Transactions</p>
<p className="font-semibold">{counts.transactions}</p>
</div>
<div>
<p className="text-muted-foreground">Categories</p>
<p className="font-semibold">{counts.categories}</p>
</div>
</div>
</>
) : (
<p className="text-sm text-muted-foreground">
Populate every page with a realistic dataset bills, months of payment history,
bank transactions, budgets, a debt snowball, and more so you can explore the whole app.
It's tied to your account and fully removable; your own data is never touched.
</p>
)}
<div className="mt-4 flex items-center justify-between gap-3 border-t border-border pt-4">
<Button size="sm" variant="outline" onClick={handleSeed} disabled={loading || seeded || statusLoading}>
{loading ? <><Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />Seeding…</> : 'Seed Demo Data'}
</Button>
<AlertDialog open={showClearConfirm} onOpenChange={setShowClearConfirm}>
<AlertDialogTrigger asChild>
<Button size="sm" variant="destructive" disabled={!seeded || clearing || statusLoading}>
{clearing ? <><Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />Clearing…</> : 'Clear Demo Data'}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Clear Demo Data</AlertDialogTitle>
<AlertDialogDescription>
This removes all seeded demo data — {counts.bills} bills, {counts.payments} payments,
{' '}{counts.transactions} transactions and related records. Your own bills and categories
are kept. This cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleClearDemoData} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
{clearing ? <><Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />Clearing…</> : 'Clear Data'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
</SectionCard>
);
}