86 lines
3.2 KiB
React
86 lines
3.2 KiB
React
|
|
import { useState } from 'react';
|
|||
|
|
import { RotateCcw, Trash2, Loader2 } from 'lucide-react';
|
|||
|
|
import {
|
|||
|
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription,
|
|||
|
|
} from '@/components/ui/dialog';
|
|||
|
|
import { Button } from '@/components/ui/button';
|
|||
|
|
import { formatUSD } from '@/lib/money';
|
|||
|
|
|
|||
|
|
function daysLeftLabel(days) {
|
|||
|
|
if (days == null) return null;
|
|||
|
|
if (days <= 0) return 'purges today';
|
|||
|
|
if (days === 1) return '1 day left';
|
|||
|
|
return `${days} days left`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Lists bills that were soft-deleted within the 30-day recovery window and lets
|
|||
|
|
* the user restore them — a durable path beyond the transient "Undo" toast.
|
|||
|
|
*
|
|||
|
|
* Presentational: the parent owns the list (`bills`) and the async `onRestore`,
|
|||
|
|
* so restoring refreshes the page's active bills too.
|
|||
|
|
*/
|
|||
|
|
export default function RecentlyDeletedBillsDialog({ open, onOpenChange, bills = [], onRestore }) {
|
|||
|
|
const [busyId, setBusyId] = useState(null);
|
|||
|
|
|
|||
|
|
async function handleRestore(bill) {
|
|||
|
|
setBusyId(bill.id);
|
|||
|
|
try {
|
|||
|
|
await onRestore(bill);
|
|||
|
|
} finally {
|
|||
|
|
setBusyId(null);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|||
|
|
<DialogContent className="max-w-lg">
|
|||
|
|
<DialogHeader>
|
|||
|
|
<DialogTitle>Recently deleted</DialogTitle>
|
|||
|
|
<DialogDescription>
|
|||
|
|
Deleted bills are kept for 30 days before they’re permanently removed. Restore one to bring it back.
|
|||
|
|
</DialogDescription>
|
|||
|
|
</DialogHeader>
|
|||
|
|
|
|||
|
|
{bills.length === 0 ? (
|
|||
|
|
<div className="flex flex-col items-center gap-2 py-8 text-center">
|
|||
|
|
<Trash2 className="h-8 w-8 text-muted-foreground/40" />
|
|||
|
|
<p className="text-sm text-muted-foreground">Nothing to recover</p>
|
|||
|
|
<p className="text-xs text-muted-foreground/70">Bills you delete will appear here for 30 days.</p>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<ul className="-mx-2 max-h-[55vh] divide-y divide-border/60 overflow-y-auto">
|
|||
|
|
{bills.map(bill => {
|
|||
|
|
const left = daysLeftLabel(bill.days_left);
|
|||
|
|
const busy = busyId === bill.id;
|
|||
|
|
return (
|
|||
|
|
<li key={bill.id} className="flex items-center gap-3 px-2 py-2.5">
|
|||
|
|
<div className="min-w-0 flex-1">
|
|||
|
|
<p className="truncate text-sm font-medium">{bill.name}</p>
|
|||
|
|
<p className="truncate text-xs text-muted-foreground">
|
|||
|
|
{formatUSD(bill.expected_amount)}
|
|||
|
|
{bill.category_name ? ` · ${bill.category_name}` : ''}
|
|||
|
|
{left ? <span className="text-muted-foreground/60"> · {left}</span> : null}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
<Button
|
|||
|
|
type="button"
|
|||
|
|
size="sm"
|
|||
|
|
variant="outline"
|
|||
|
|
className="h-8 shrink-0 gap-1.5"
|
|||
|
|
disabled={busy}
|
|||
|
|
onClick={() => handleRestore(bill)}
|
|||
|
|
>
|
|||
|
|
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <RotateCcw className="h-3.5 w-3.5" />}
|
|||
|
|
Restore
|
|||
|
|
</Button>
|
|||
|
|
</li>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</ul>
|
|||
|
|
)}
|
|||
|
|
</DialogContent>
|
|||
|
|
</Dialog>
|
|||
|
|
);
|
|||
|
|
}
|