"use client"; import { useEffect, useMemo, useState } from "react"; import { useGroupsContext } from "@/hooks/groups-context"; import useBuckets from "@/features/buckets/hooks/use-buckets"; import useTags from "@/features/tags/hooks/use-tags"; import NewBucketModal from "@/components/new-bucket-modal"; import ConfirmSlideModal from "@/components/confirm-slide-modal"; import { bucketIcons } from "@/lib/shared/bucket-icons"; import BucketCard from "./bucket-card"; import { useEntryMutation } from "@/hooks/entry-mutation-context"; export default function BucketsPanel() { const { activeGroupId } = useGroupsContext(); const { buckets, loading, error, createBucket, updateBucket, deleteBucket, reload } = useBuckets(activeGroupId); const { mutationVersion } = useEntryMutation(); const { tags: tagSuggestions } = useTags(activeGroupId); const [modalOpen, setModalOpen] = useState(false); const [editId, setEditId] = useState(null); const [confirmDeleteId, setConfirmDeleteId] = useState(null); const [menuOpenId, setMenuOpenId] = useState(null); const [expandedIds, setExpandedIds] = useState([]); const [form, setForm] = useState({ name: "", description: "", iconKey: "none", budgetLimitDollars: "", tags: [] as string[], necessity: "BOTH", windowDays: "30" }); const iconMap = useMemo(() => new Map(bucketIcons.map(item => [item.key, item.icon])), []); const orderedBuckets = useMemo(() => [...buckets].sort((a, b) => a.position - b.position || a.name.localeCompare(b.name)), [buckets]); useEffect(() => { function handleClickOutside(event: MouseEvent) { if (!menuOpenId) return; const target = event.target as HTMLElement | null; if (!target) return; if (target.closest("[data-bucket-menu]") || target.closest("[data-bucket-menu-button]")) return; setMenuOpenId(null); } document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [menuOpenId]); useEffect(() => { if (!activeGroupId) return; if (mutationVersion === 0) return; reload(); }, [mutationVersion, activeGroupId, reload]); function resetForm() { setForm({ name: "", description: "", iconKey: "none", budgetLimitDollars: "", tags: [], necessity: "BOTH", windowDays: "30" }); setEditId(null); } function openCreate() { resetForm(); setModalOpen(true); } function openEdit(bucketId: number) { const bucket = buckets.find(item => item.id === bucketId); if (!bucket) return; setEditId(bucketId); setForm({ name: bucket.name, description: bucket.description || "", iconKey: bucket.iconKey || "none", budgetLimitDollars: bucket.budgetLimitDollars != null ? String(bucket.budgetLimitDollars) : "", tags: bucket.tags || [], necessity: bucket.necessity, windowDays: bucket.windowDays ? String(bucket.windowDays) : "30" }); setModalOpen(true); setMenuOpenId(null); } async function handleSubmit(e: React.FormEvent) { e.preventDefault(); const budget = form.budgetLimitDollars ? Number(form.budgetLimitDollars) : null; const windowDays = form.windowDays ? Number(form.windowDays) : 30; if (!form.name.trim()) return; const iconKey = form.iconKey && form.iconKey !== "none" ? form.iconKey : null; if (!Number.isFinite(windowDays) || windowDays < 1 || windowDays > 365) return; if (editId) { const ok = await updateBucket({ id: editId, name: form.name.trim(), description: form.description.trim() || undefined, iconKey, budgetLimitDollars: budget, tags: form.tags, necessity: form.necessity as "NECESSARY" | "BOTH" | "UNNECESSARY", windowDays }); if (ok) setModalOpen(false); } else { const ok = await createBucket({ name: form.name.trim(), description: form.description.trim() || undefined, iconKey, budgetLimitDollars: budget, tags: form.tags, necessity: form.necessity as "NECESSARY" | "BOTH" | "UNNECESSARY", windowDays }); if (ok) setModalOpen(false); } } function toggleExpanded(bucketId: number) { setExpandedIds(prev => prev.includes(bucketId) ? prev.filter(id => id !== bucketId) : [...prev, bucketId]); } function budgetUsage(bucket: typeof buckets[number]) { const limit = bucket.budgetLimitDollars || 0; const spent = bucket.totalUsage || 0; return { limit, spent }; } return ( <>

Buckets

{!activeGroupId ? (
Select a group to view buckets.
) : loading ? (
{[0, 1].map(row => (
))}
) : orderedBuckets.length ? ( orderedBuckets.map(bucket => { const icon = bucket.iconKey ? iconMap.get(bucket.iconKey) : null; const { limit, spent } = budgetUsage(bucket); const isExpanded = expandedIds.includes(bucket.id); const usageLabel = limit ? `$${spent.toFixed(2)} / $${limit.toFixed(2)}` : ""; return }) ) : (
No buckets yet.
)}
setModalOpen(false)} onSubmit={handleSubmit} onChange={next => setForm(prev => ({ ...prev, ...next }))} tagSuggestions={tagSuggestions} /> setConfirmDeleteId(null)} onConfirm={async () => { if (!confirmDeleteId) return; const ok = await deleteBucket(confirmDeleteId); if (ok) setConfirmDeleteId(null); }} /> ); }