"use client"; import { CalendarClock, Check, EllipsisVertical, Loader2, Pencil, Plus, SkipForward, Trash2, Repeat, } from "lucide-react"; import { useState, useTransition } from "react"; import { toast } from "sonner"; import { MoneyInput } from "@/components/money-input"; import { ResponsiveDialog } from "@/components/responsive-dialog"; import { useQuickAdd } from "@/components/layout/quick-add"; import { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardDescription, CardHeader } from "@/components/ui/card"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { getTransactionAction } from "@/features/transactions/actions"; import { Amount, RowIcon, rowSubtitle, rowTitle, } from "@/features/transactions/components/transaction-parts"; import { tr } from "@/i18n/tr"; import { todayIstanbul } from "@/shared/dates"; import { formatDate, formatMoney } from "@/shared/format"; import { formatMoneyInput } from "@/shared/money"; import { addDays } from "@/shared/recurrence"; import { confirmOccurrenceAction, deleteRuleAction, getRuleFormAction, postponeOccurrenceAction, skipOccurrenceAction, } from "../actions"; import type { PlannedItem, PlannedOverview, RuleView } from "../service"; import { RuleFormDialog, type RuleFormMode } from "./rule-form"; const t = tr.recurring; const dateLabel = (ymd: string) => formatDate(`${ymd}T12:00:00+03:00`); type Pending = | { kind: "confirm"; item: PlannedItem } | { kind: "postpone"; item: PlannedItem } | { kind: "scope"; item: PlannedItem } | { kind: "delete"; ruleId: string; item?: PlannedItem } | null; export function PlannedView({ data, canWrite }: { data: PlannedOverview; canWrite: boolean }) { const [tab, setTab] = useState(data.overdue.length ? "overdue" : "upcoming"); const [pending, setPending] = useState(null); const [ruleForm, setRuleForm] = useState(null); const [, startTransition] = useTransition(); const { openTransactionForm } = useQuickAdd(); function skip(item: PlannedItem) { startTransition(async () => { const r = await skipOccurrenceAction(item.id); if (r.ok) toast.success(t.toast.skipped); else toast.error(r.error); }); } async function editScope(item: PlannedItem, scope: "one" | "following" | "all") { setPending(null); if (scope === "one" || !item.recurringRuleId) { const d = await getTransactionAction(item.id); if (d.ok) openTransactionForm({ kind: "edit", transaction: d.data }); else toast.error(d.error); return; } const rule = await getRuleFormAction(item.recurringRuleId); if (!rule.ok) return void toast.error(rule.error); const [pos, total] = (item.seriesPosition ?? "").split("/").map(Number); setRuleForm({ kind: "edit", rule: rule.data, scope, fromTransactionId: scope === "following" ? item.id : undefined, fromDate: scope === "following" ? item.date : undefined, remainingCount: pos && total ? total - pos + 1 : null, }); } async function editRule(rule: RuleView) { const r = await getRuleFormAction(rule.id); if (r.ok) setRuleForm({ kind: "edit", rule: r.data, scope: "all" }); else toast.error(r.error); } const itemList = (items: PlannedItem[], empty: string) => items.length === 0 ? ( {empty} ) : ( ); return (
{canWrite ? (
) : null} {t.tabs.overdue} {data.overdue.length ? ( {data.overdue.length} ) : null} {t.tabs.upcoming} {t.tabs.rules} {itemList(data.overdue, t.empty.overdue)} {itemList(data.upcoming, t.empty.upcoming)} {data.rules.length === 0 ? ( {t.empty.rules} ) : (
    {data.rules.map((rule) => (
  • {rule.title} {rule.isEstimate ? {t.estimate} : null} {rule.recurrence} ·{" "} {rule.toAccount ? `${rule.account} → ${rule.toAccount}` : rule.account} {rule.nextDate ? ` · ${t.next(dateLabel(rule.nextDate))}` : ""} {rule.remaining !== null ? ` · ${t.remaining(rule.remaining)}` : ` · ${t.endless}`} {canWrite ? ( void editRule(rule)}> {t.actions.edit} setPending({ kind: "delete", ruleId: rule.id })} > {t.actions.delete} ) : null}
  • ))}
)}
setPending(null)} /> setPending(null)} /> setPending(null)} onPick={(item, scope) => void editScope(item, scope)} /> setPending(null)} /> !o && setRuleForm(null)} />
); } function ConfirmDialog({ item, onClose }: { item: PlannedItem | null; onClose: () => void }) { return item ? : null; } function ConfirmForm({ item, onClose }: { item: PlannedItem; onClose: () => void }) { const today = todayIstanbul(); const [amount, setAmount] = useState(formatMoneyInput(item.amountMinor)); // Gelecekteki kaydı erken onaylarken bugünün tarihi; geçmiştekini kendi tarihiyle. const [date, setDate] = useState(item.date > today ? today : item.date); const [errors, setErrors] = useState>({}); const [busy, setBusy] = useState(false); async function submit() { setBusy(true); const r = await confirmOccurrenceAction(item.id, { amount, date }); setBusy(false); if (r.ok) { toast.success(t.toast.confirmed); onClose(); } else setErrors({ ...r.fieldErrors, root: r.fieldErrors ? undefined : r.error }); } return ( !o && onClose()} title={`${t.confirmTitle}: ${rowTitle(item)}`} description={t.confirmDescription} footer={ <> } > {tr.transactions.fields.amount} {errors.amount ? {errors.amount} : null} {tr.transactions.fields.date} setDate(e.target.value)} /> {errors.date ? {errors.date} : null} {item.isEstimate ? (

{t.estimate}: {formatMoney(item.amountMinor)}

) : null} {errors.root ? {errors.root} : null}
); } function PostponeDialog({ item, onClose }: { item: PlannedItem | null; onClose: () => void }) { return item ? : null; } function PostponeForm({ item, onClose }: { item: PlannedItem; onClose: () => void }) { const base = item.date > todayIstanbul() ? item.date : todayIstanbul(); const [date, setDate] = useState(addDays(base, 7)); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); async function submit() { setBusy(true); const r = await postponeOccurrenceAction(item.id, date); setBusy(false); if (r.ok) { toast.success(t.toast.postponed); onClose(); } else setError(r.fieldErrors?.date ?? r.error); } return ( !o && onClose()} title={`${t.postponeTitle}: ${rowTitle(item)}`} footer={ <> } > {t.postponeDate} setDate(e.target.value)} /> {error ? {error} : null} ); } function ScopeDialog({ item, onClose, onPick, }: { item: PlannedItem | null; onClose: () => void; onPick: (item: PlannedItem, scope: "one" | "following" | "all") => void; }) { return ( !o && onClose()} title={t.scopeTitle} >
{(["one", "following", "all"] as const).map((scope) => ( ))}
); } function DeleteDialog({ target, onClose, }: { target: { ruleId: string; item?: PlannedItem } | null; onClose: () => void; }) { const [busy, setBusy] = useState(false); async function run(scope: "all" | "following") { if (!target) return; setBusy(true); const r = await deleteRuleAction(target.ruleId, scope, target.item?.id); setBusy(false); if (r.ok) { toast.success(t.toast.deleted); onClose(); } else toast.error(r.error); } return ( !o && onClose()}> {t.deleteTitle} {t.deleteDescription} {tr.common.cancel} {target?.item ? ( ) : null} ); }