import { differenceInCalendarDays } from "date-fns"; import { X } from "lucide-react"; import { CategoryIcon } from "@/components/elements/CategoryIcon"; import { Button } from "@/components/ui/button"; import { useMoney } from "@/hooks/useMoney"; import { parseLocalDate, cn } from "@/lib/utils"; import { useUpcomingRecurring, useRecurringMutations } from "@/hooks/useRecurring"; import { CADENCE_LABEL } from "@/lib/recurring"; import type { RecurringDto } from "@/lib/api"; import { Skeleton } from "@/components/ui/skeleton"; function dueLabel(dateStr: string, today = new Date()): string { const days = differenceInCalendarDays(parseLocalDate(dateStr), today); if (days <= 0) return "Due today"; if (days === 1) return "Tomorrow"; return `In ${days} days`; } /** * Predicted next occurrences of the user's detected bills, subscriptions and * paychecks, soonest first. Shared by the dashboard's Upcoming widget and * the Transactions page's Upcoming section, one query and one dismiss * action so a merchant dismissed in either place disappears from both. */ export function UpcomingList({ days = 30, limit }: { days?: number; limit?: number }) { const { data, isLoading } = useUpcomingRecurring(days); const { dismiss } = useRecurringMutations(days); const { money } = useMoney(); const rows = limit ? (data ?? []).slice(0, limit) : data ?? []; // This query is in flight from the moment the page mounts, same as // everything else on it, but returning null while it resolves left this // one section visibly blank until it did - a pop-in the rest of the page // (which shows a skeleton instead) doesn't have. A plain `null` here // was never noticeable on the dashboard, where it sits well below the // fold, but the Transactions page puts it right in view. if (isLoading) { return (
{Array.from({ length: limit ?? 3 }).map((_, i) => (
))}
); } if (rows.length === 0) { return (

Nothing recurring due in the next {days} days.

); } return (
{rows.map((r: RecurringDto) => (

{r.merchantName}

{CADENCE_LABEL[r.cadence]} · {dueLabel(r.nextDate)}

{r.type === "income" ? "+" : "-"}{money(r.typicalAmount)}
))}
); }