"use client"; import { AlertTriangle, ChevronLeft, ChevronRight, Copy, EllipsisVertical, History, Pencil, } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; import { toast } from "sonner"; import { IconBadge } from "@/components/appearance/swatch"; import { MoneyInput } from "@/components/money-input"; import { ResponsiveDialog } from "@/components/responsive-dialog"; 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, CardContent } from "@/components/ui/card"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { tr } from "@/i18n/tr"; import { cn } from "@/lib/utils"; import { shiftMonth, type BudgetStatus } from "@/shared/budget"; import { formatDate, formatMoney } from "@/shared/format"; import { formatMoneyInput, parseMoney } from "@/shared/money"; import { budgetFromActualsAction, copyPreviousBudgetAction, saveBudgetAction } from "../actions"; import type { BudgetLineView, BudgetView as Data } from "../service"; const t = tr.budget; const d = (ymd: string) => formatDate(`${ymd}T12:00:00+03:00`); const monthFmt = new Intl.DateTimeFormat("tr-TR", { month: "long", year: "numeric", timeZone: "Europe/Istanbul", }); const monthLabel = (m: string) => monthFmt.format(new Date(`${m}-15T12:00:00+03:00`)); const BAR: Record = { ok: "bg-budget", warning: "bg-amber-500", over: "bg-expense", none: "bg-muted-foreground/40", }; /** Yüzde çubuğu; `marker` ayın geçen kısmını (yüzde) gösterir. */ function Bar({ ratioBp, status, marker, label, }: { ratioBp: number | null; status: BudgetStatus; marker?: number; label: string; }) { const pct = ratioBp === null ? 0 : Math.min(100, ratioBp / 100); return (
{marker !== undefined && marker > 0 && marker < 100 ? ( ) : null}
); } function StatusBadge({ status, forecastOver, exceeded, }: { status: BudgetStatus; forecastOver: boolean; /** Limit aşıldı mı (tam %100'de "doldu"). */ exceeded: boolean; }) { if (status === "over") return ( {exceeded ? t.status.over : t.status.full} ); if (status === "warning") return ( {t.status.warning} ); if (forecastOver) return ( {t.status.forecastOver} ); return null; } function Stat({ label, value, tone }: { label: string; value: string; tone?: string }) { return (

{label}

{value}

); } type Pending = { kind: "edit" } | { kind: "replace"; source: "copy" | "actuals" } | null; export function BudgetView({ data }: { data: Data }) { const [pending, setPending] = useState(null); const [busy, setBusy] = useState(false); const close = () => setPending(null); const tt = data.totals; const monthPct = Math.round((data.elapsedDays * 100) / data.totalDays); const warnings = data.lines.filter((l) => l.status === "warning" || l.status === "over"); async function generate(source: "copy" | "actuals") { setBusy(true); const r = source === "copy" ? await copyPreviousBudgetAction(data.month) : await budgetFromActualsAction(data.month); setBusy(false); close(); if (r.ok) toast.success(source === "copy" ? t.toast.copied : t.toast.created); else toast.error(r.error); } const askOrRun = (source: "copy" | "actuals") => data.hasBudget ? setPending({ kind: "replace", source }) : generate(source); return (

{monthLabel(data.month)}

{t.period(d(data.period.from), d(data.period.to))}

{data.month !== data.currentMonth ? ( ) : null}
{data.canWrite ? (
askOrRun("copy")} > {t.copyPrevious} askOrRun("actuals")}> {t.fromActuals}
) : null}
{data.hasBudget ? ( ) : null}
{t.totals.monthProgress(data.elapsedDays, data.totalDays, String(monthPct))} {tt.ratioBp !== null ? ` · %${Math.round(tt.ratioBp / 100)}` : ""} {tt.plannedMinor ? {t.totals.planned(formatMoney(tt.plannedMinor))} : null} {data.hasBudget && tt.unbudgetedMinor ? ( {t.totals.unbudgeted}: {formatMoney(tt.unbudgetedMinor)} ) : null}
{data.includesHidden ? (

{t.includesHidden}

) : null}
{warnings.length ? (
{t.warnings(warnings.length)}
) : null} {!data.hasBudget ? (

{t.empty}

{t.emptyHint}

{data.canWrite ? (
) : null}
) : null} {data.lines.length ? (
    {data.lines.map((l) => ( ))}
) : data.hasBudget ? (

{t.noSpending}

) : null} {pending?.kind === "edit" ? : null} !o && close()}> {t.replaceTitle} {t.replaceDescription} {pending?.kind === "replace" && pending.source === "actuals" ? ` ${t.fromActualsHint}` : ""} {tr.common.cancel}
); } function Line({ line: l, marker }: { line: BudgetLineView; marker: number }) { const hasLimit = l.limitMinor !== null; return (
  • {l.name}

    {hasLimit ? t.line.ofLimit(formatMoney(l.actualMinor), formatMoney(l.limitMinor!)) : formatMoney(l.actualMinor)}

    {hasLimit ? ( ) : null}

    {hasLimit ? ( {l.remainingMinor! < 0 ? t.line.over(formatMoney(-l.remainingMinor!)) : t.line.remaining(formatMoney(l.remainingMinor!))} ) : ( {l.covered ? t.status.covered : t.status.none} )} {l.plannedMinor ? {t.line.planned(formatMoney(l.plannedMinor))} : null} {l.forecastMinor !== l.actualMinor ? ( {t.line.forecast(formatMoney(l.forecastMinor))} ) : null}

  • ); } function EditDialog({ data, onClose }: { data: Data; onClose: () => void }) { const [values, setValues] = useState>(() => Object.fromEntries( data.editor.categories.map((c) => { const v = data.editor.limits[c.id]; return [c.id, v ? formatMoneyInput(v) : ""]; }), ), ); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); // Toplam: limiti olan ve üstünde limit olmayan kategoriler (alt limitler iki kez sayılmaz). let total = 0; const stack: { depth: number; limited: boolean }[] = []; for (const c of data.editor.categories) { while (stack.length && stack.at(-1)!.depth >= c.depth) stack.pop(); const covered = stack.some((s) => s.limited); const v = parseMoney(values[c.id] ?? "") ?? 0; if (v > 0 && !covered) total += v; stack.push({ depth: c.depth, limited: v > 0 }); } async function submit(e: React.FormEvent) { e.preventDefault(); setBusy(true); const r = await saveBudgetAction({ month: data.month, limits: Object.entries(values).map(([categoryId, amount]) => ({ categoryId, amount })), }); setBusy(false); if (r.ok) { toast.success(t.toast.saved); onClose(); } else setError(r.error); } return ( !o && onClose()} title={t.editTitle(monthLabel(data.month))} description={t.editHint} footer={ <> {t.editTotal(formatMoney(total))} } >
    {data.editor.categories.map((c) => ( ))} {error ?

    {error}

    : null}
    ); }