import { useId, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { api } from "@/lib/api"; import { useFinanceData } from "@/context/FinanceDataContext"; import { useMoney } from "@/hooks/useMoney"; import { formatMoney, toDateString } from "@/lib/format"; import { parseLocalDate } from "@/lib/utils"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { CategoryPicker } from "@/components/elements/CategoryPicker"; import { DatePicker } from "@/components/elements/DatePicker"; import { Link } from "react-router-dom"; import { ToolCard, QueryError, useToolAction } from "./shared"; import { useAccountCurrencies } from "./CurrencyTools"; type Scheduled = { id: string; title: string; amount: number; currency: string; type: string; category: string; accountId: string; nextDate: string; cadence: string; active: boolean }; type Forecast = { basis: string; unconfirmedAccounts: string[]; groups: { currency: string; opening: number; scheduledEnd: number; scenarioEnd: number; events: { date: string; title: string; amount: number; balance: number; overdue: boolean }[] }[] }; export function PlanningTools({ compact = false }: { compact?: boolean }) { const { accounts } = useFinanceData(); const { hidden } = useMoney(); const id = useId(); const [addOpen, setAddOpen] = useState(false); const [listOpen, setListOpen] = useState(false); const [forecastOpen, setForecastOpen] = useState(false); const currencies = useAccountCurrencies(); const schedules = useQuery({ queryKey: ["enhanced", "schedules"], queryFn: () => api.enhanced.get("schedules") }); const [title, setTitle] = useState(""); const [amount, setAmount] = useState(""); const [accountId, setAccount] = useState(""); const [type, setType] = useState("expense"); const [category, setCategory] = useState("Bills & Utilities"); const [nextDate, setDate] = useState(toDateString(new Date())); const [cadence, setCadence] = useState("monthly"); const [extraSpend, setSpend] = useState("0"); const [end, setEnd] = useState(toDateString(new Date(Date.now() + 30 * 86400000))); const action = useToolAction(); const today = toDateString(new Date()); const validScenario = !!end && end >= today && extraSpend !== "" && Number.isFinite(Number(extraSpend)) && Number(extraSpend) >= 0; const forecast = useQuery({ queryKey: ["enhanced", "forecast", today, end, extraSpend], enabled: validScenario && (compact || forecastOpen), queryFn: () => api.enhanced.get(`forecast?start=${today}&end=${end}&extra_spend=${Number(extraSpend)}`) }); const available = accounts.filter(a => ["checking", "savings", "cash", "credit", "loan"].includes(a.type) && currencies.data?.[a.id]); const valid = !!title.trim() && Number(amount) > 0 && Number.isFinite(Number(amount)) && available.some(account => account.id === accountId) && !!nextDate; const forecastResults =
{!validScenario &&

Choose a valid period and nonnegative scenario spending.

} {forecast.isFetching &&

Updating forecast…

} {forecast.data?.unconfirmedAccounts.length ?

Confirm the currency of {forecast.data.unconfirmedAccounts.length} accounts in Data Sources to include them.

: null} {forecast.data?.groups.map(group =>

{group.currency} · Current {formatMoney(group.opening, group.currency)} · Scheduled end {formatMoney(group.scheduledEnd, group.currency)}{!compact && <> · Scenario {formatMoney(group.scenarioEnd, group.currency)}}

{(compact ? group.events.slice(0, 5) : group.events).map((event, i) =>

{event.date} · {event.title}{event.overdue ? " (overdue)" : ""}{formatMoney(event.amount, group.currency)}

)}
)}

{forecast.data?.basis || "Based on current balances and scheduled items."}

; return <> {!compact && <>

Plan expected bills and income in one place.

{schedules.data?.length || 0} scheduled items

Add a scheduled bill or incomePlan a future transaction. This does not make a bank payment. {!accounts.length ?

Connect your bank to choose an account for this schedule.

: !available.length ?

Choose an account currency in Settings → Data Sources to start.

: null}
{ event.preventDefault(); if (valid) action.mutate(async () => { await api.enhanced.post("schedules", { title: title.trim(), amount: Number(amount), type, category, accountId, currency: currencies.data![accountId], nextDate, cadence }); setTitle(""); setAmount(""); setAddOpen(false); }); }}>
setTitle(event.target.value)} maxLength={120} required />
setAmount(event.target.value)} required />
setDate(value || "")} aria-label="Due date" />
Scheduled itemsSkip a payment already in your transactions. Record manually adds a ledger entry without making a bank payment. {!schedules.data?.length &&

No scheduled items yet. Add a bill or income to get started.

} {schedules.data?.map(schedule =>

{schedule.title} · {formatMoney(schedule.amount, schedule.currency)}

{schedule.nextDate} · {schedule.cadence} · {schedule.type}{!schedule.active ? " · paused / completed" : schedule.nextDate < today ? " · overdue" : ""}

{schedule.active && <>}
)}
} {compact ? forecastResults : <>

Explore how upcoming payments and extra spending could affect your balance.

}
{!compact && Cashflow forecastCurrent balances plus scheduled items and your spending scenario. This is an estimate.
setEnd(value || "")} />
setSpend(event.target.value)} />
{forecastResults}
} ; }