"use client"; import { useRef, useState } from "react"; import type { FormEvent } from "react"; import { useRouter } from "next/navigation"; import type { Entry, Schedule } from "@/lib/shared/types"; import type { DeleteTarget, EntryDetailsFormState, EntryFormState, ScheduleDetailsFormState, ScheduleFormState } from "@/features/entries/components/entries-panel.types"; import { getTodayIsoDate, normalizeTagList } from "@/features/entries/components/entries-panel.utils"; type CreateEntryInput = { entryType: "SPENDING" | "INCOME"; amountDollars: number; occurredAt: string; necessity: "NECESSARY" | "BOTH" | "UNNECESSARY"; purchaseType: string; notes?: string; tags?: string[]; bucketId?: number | null; }; type UpdateEntryInput = CreateEntryInput & { id: number }; type ScheduleInput = { entryType: "SPENDING" | "INCOME"; amountDollars: number; necessity: "NECESSARY" | "BOTH" | "UNNECESSARY"; purchaseType: string; notes?: string; tags?: string[]; startsOn: string; frequency: "DAILY" | "WEEKLY" | "MONTHLY" | "YEARLY"; intervalCount?: number; endCondition?: "NEVER" | "AFTER_COUNT" | "BY_DATE"; endCount?: number | null; endDate?: string | null; }; type NotifyInput = { title: string; message?: string; tone?: "info" | "success" | "danger"; durationMs?: number; }; type UseEntriesPanelCrudParams = { filteredEntries: Entry[]; schedules: Schedule[]; createEntry: (input: CreateEntryInput) => Promise; updateEntry: (input: UpdateEntryInput) => Promise; deleteEntry: (id: number | string) => Promise; createSchedule: (input: ScheduleInput & { createEntryNow?: boolean }) => Promise; updateSchedule: (input: ScheduleInput & { id: number; nextRunOn?: string; isActive?: boolean }) => Promise; deleteSchedule: (id: number | string) => Promise; notify: (input: NotifyInput) => void; notifyEntryMutation: () => void; canManageTags: boolean; }; function createInitialEntryForm(today: string): EntryFormState { return { amountDollars: "", occurredAt: today, necessity: "NECESSARY", notes: "", tags: [], entryType: "SPENDING" }; } function createInitialScheduleForm(today: string): ScheduleFormState { return { amountDollars: "", startsOn: today, necessity: "NECESSARY", notes: "", tags: [], entryType: "SPENDING", frequency: "MONTHLY", intervalCount: 1, endCondition: "NEVER", endCount: "", endDate: "", createEntryNow: false }; } function createInitialScheduleDetailsForm(today: string): ScheduleDetailsFormState { return { amountDollars: "", startsOn: today, necessity: "NECESSARY", notes: "", tags: [], entryType: "SPENDING", frequency: "MONTHLY", intervalCount: 1, endCondition: "NEVER", endCount: "", endDate: "", nextRunOn: today, isActive: true }; } export default function useEntriesPanelCrud({ filteredEntries, schedules, createEntry, updateEntry, deleteEntry, createSchedule, updateSchedule, deleteSchedule, notify, notifyEntryMutation, canManageTags }: UseEntriesPanelCrudParams) { const router = useRouter(); const today = getTodayIsoDate(); const [newEntryOpen, setNewEntryOpen] = useState(false); const [newScheduleOpen, setNewScheduleOpen] = useState(false); const [entryDetailsOpen, setEntryDetailsOpen] = useState(false); const [scheduleDetailsOpen, setScheduleDetailsOpen] = useState(false); const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); const [deleteTarget, setDeleteTarget] = useState("ENTRY"); const [selectedEntryId, setSelectedEntryId] = useState(null); const [selectedEntryIndex, setSelectedEntryIndex] = useState(null); const [selectedScheduleId, setSelectedScheduleId] = useState(null); const [entryRemovedTags, setEntryRemovedTags] = useState([]); const [scheduleRemovedTags, setScheduleRemovedTags] = useState([]); const [entryForm, setEntryForm] = useState(() => createInitialEntryForm(today)); const [entryDetailsForm, setEntryDetailsForm] = useState(() => createInitialEntryForm(today)); const [entryDetailsOriginal, setEntryDetailsOriginal] = useState(null); const [scheduleForm, setScheduleForm] = useState(() => createInitialScheduleForm(today)); const [scheduleDetailsForm, setScheduleDetailsForm] = useState(() => createInitialScheduleDetailsForm(today)); const [scheduleDetailsOriginal, setScheduleDetailsOriginal] = useState(null); const amountInputRef = useRef(null); const tagsInputRef = useRef(null); function handleEmptyTagAction() { if (!canManageTags) return; router.push("/groups/settings"); } function hasEntryChanges() { if (!entryDetailsOriginal) return false; const tags = entryDetailsForm.tags.filter(tag => !entryRemovedTags.includes(tag)); return ( entryDetailsForm.amountDollars !== entryDetailsOriginal.amountDollars || entryDetailsForm.occurredAt !== entryDetailsOriginal.occurredAt || entryDetailsForm.necessity !== entryDetailsOriginal.necessity || entryDetailsForm.notes !== entryDetailsOriginal.notes || entryDetailsForm.entryType !== entryDetailsOriginal.entryType || normalizeTagList(tags) !== normalizeTagList(entryDetailsOriginal.tags) ); } function hasScheduleChanges() { if (!scheduleDetailsOriginal) return false; const tags = scheduleDetailsForm.tags.filter(tag => !scheduleRemovedTags.includes(tag)); return ( scheduleDetailsForm.amountDollars !== scheduleDetailsOriginal.amountDollars || scheduleDetailsForm.startsOn !== scheduleDetailsOriginal.startsOn || scheduleDetailsForm.necessity !== scheduleDetailsOriginal.necessity || scheduleDetailsForm.notes !== scheduleDetailsOriginal.notes || scheduleDetailsForm.entryType !== scheduleDetailsOriginal.entryType || scheduleDetailsForm.frequency !== scheduleDetailsOriginal.frequency || scheduleDetailsForm.intervalCount !== scheduleDetailsOriginal.intervalCount || scheduleDetailsForm.endCondition !== scheduleDetailsOriginal.endCondition || scheduleDetailsForm.endCount !== scheduleDetailsOriginal.endCount || scheduleDetailsForm.endDate !== scheduleDetailsOriginal.endDate || scheduleDetailsForm.nextRunOn !== scheduleDetailsOriginal.nextRunOn || scheduleDetailsForm.isActive !== scheduleDetailsOriginal.isActive || normalizeTagList(tags) !== normalizeTagList(scheduleDetailsOriginal.tags) ); } async function submitNewEntry(event: FormEvent) { event.preventDefault(); if (!event.currentTarget.reportValidity()) return; const amountDollars = Number(entryForm.amountDollars || 0); if (!Number.isFinite(amountDollars) || amountDollars <= 0 || !entryForm.tags.length) return; const created = await createEntry({ entryType: entryForm.entryType, amountDollars, occurredAt: entryForm.occurredAt, necessity: entryForm.necessity, purchaseType: entryForm.tags.join(", ") || "General", notes: entryForm.notes.trim() || undefined, tags: entryForm.tags }); if (!created) return; setNewEntryOpen(false); setEntryForm(createInitialEntryForm(today)); notify({ title: "Entry added", message: `${created.tags.join(", ")} - $${created.amountDollars.toFixed(2)}`, tone: "success" }); notifyEntryMutation(); } async function submitNewSchedule(event: FormEvent) { event.preventDefault(); const amountDollars = Number(scheduleForm.amountDollars || 0); if (!Number.isFinite(amountDollars) || amountDollars <= 0 || !scheduleForm.tags.length) return; const created = await createSchedule({ entryType: scheduleForm.entryType, amountDollars, startsOn: scheduleForm.startsOn, necessity: scheduleForm.necessity, purchaseType: scheduleForm.tags.join(", ") || "General", notes: scheduleForm.notes.trim() || undefined, tags: scheduleForm.tags, frequency: scheduleForm.frequency, intervalCount: scheduleForm.intervalCount, endCondition: scheduleForm.endCondition, endCount: scheduleForm.endCondition === "AFTER_COUNT" ? Number(scheduleForm.endCount || 0) || null : null, endDate: scheduleForm.endCondition === "BY_DATE" ? scheduleForm.endDate || null : null, createEntryNow: scheduleForm.createEntryNow }); if (!created) return; setNewScheduleOpen(false); setScheduleForm(createInitialScheduleForm(today)); notify({ title: "Schedule added", message: `${created.tags.join(", ") || "Schedule"} - $${created.amountDollars.toFixed(2)}`, tone: "success" }); if (scheduleForm.createEntryNow) notifyEntryMutation(); } function openEntryDetails(id: number) { const index = filteredEntries.findIndex(entry => Number(entry.id) === Number(id)); if (index < 0) return; const entry = filteredEntries[index]; const form: EntryDetailsFormState = { amountDollars: String(entry.amountDollars), occurredAt: entry.occurredAt, necessity: entry.necessity, notes: entry.notes || "", tags: entry.tags || [], entryType: entry.entryType }; setSelectedEntryId(Number(id)); setSelectedEntryIndex(index); setEntryDetailsForm(form); setEntryDetailsOriginal(form); setEntryRemovedTags([]); setEntryDetailsOpen(true); } function openScheduleDetails(id: number) { const schedule = schedules.find(item => Number(item.id) === Number(id)); if (!schedule) return; const form: ScheduleDetailsFormState = { amountDollars: String(schedule.amountDollars), startsOn: schedule.startsOn, necessity: schedule.necessity, notes: schedule.notes || "", tags: schedule.tags || [], entryType: schedule.entryType, frequency: schedule.frequency, intervalCount: schedule.intervalCount, endCondition: schedule.endCondition, endCount: schedule.endCount == null ? "" : String(schedule.endCount), endDate: schedule.endDate || "", nextRunOn: schedule.nextRunOn, isActive: schedule.isActive }; setSelectedScheduleId(Number(id)); setScheduleDetailsForm(form); setScheduleDetailsOriginal(form); setScheduleRemovedTags([]); setScheduleDetailsOpen(true); } async function submitEntryUpdate(event: FormEvent) { event.preventDefault(); if (!selectedEntryId || !hasEntryChanges()) return; const amount = Number(entryDetailsForm.amountDollars || 0); const tags = entryDetailsForm.tags.filter(tag => !entryRemovedTags.includes(tag)); if (!Number.isFinite(amount) || amount <= 0 || !tags.length) return; const updated = await updateEntry({ id: selectedEntryId, entryType: entryDetailsForm.entryType, amountDollars: amount, occurredAt: entryDetailsForm.occurredAt, necessity: entryDetailsForm.necessity, purchaseType: tags.join(", ") || "General", notes: entryDetailsForm.notes.trim() || undefined, tags }); if (!updated) return; setEntryDetailsOpen(false); notify({ title: "Entry updated", message: `${updated.tags.join(", ")} - $${updated.amountDollars.toFixed(2)}` }); notifyEntryMutation(); } async function submitScheduleUpdate(event: FormEvent) { event.preventDefault(); if (!selectedScheduleId || !hasScheduleChanges()) return; const amount = Number(scheduleDetailsForm.amountDollars || 0); const tags = scheduleDetailsForm.tags.filter(tag => !scheduleRemovedTags.includes(tag)); if (!Number.isFinite(amount) || amount <= 0 || !tags.length) return; const updated = await updateSchedule({ id: selectedScheduleId, entryType: scheduleDetailsForm.entryType, amountDollars: amount, startsOn: scheduleDetailsForm.startsOn, necessity: scheduleDetailsForm.necessity, purchaseType: tags.join(", ") || "General", notes: scheduleDetailsForm.notes.trim() || undefined, tags, frequency: scheduleDetailsForm.frequency, intervalCount: scheduleDetailsForm.intervalCount, endCondition: scheduleDetailsForm.endCondition, endCount: scheduleDetailsForm.endCondition === "AFTER_COUNT" ? Number(scheduleDetailsForm.endCount || 0) || null : null, endDate: scheduleDetailsForm.endCondition === "BY_DATE" ? scheduleDetailsForm.endDate || null : null, nextRunOn: scheduleDetailsForm.nextRunOn, isActive: scheduleDetailsForm.isActive }); if (!updated) return; setScheduleDetailsOpen(false); notify({ title: "Schedule updated", message: `${updated.tags.join(", ")} - $${updated.amountDollars.toFixed(2)}` }); } async function confirmDelete() { if (deleteTarget === "ENTRY" && selectedEntryId) { const removed = await deleteEntry(selectedEntryId); if (!removed) return; setEntryDetailsOpen(false); notify({ title: "Entry deleted", message: removed.tags.join(", ") || "Entry removed", tone: "danger" }); notifyEntryMutation(); } if (deleteTarget === "SCHEDULE" && selectedScheduleId) { const removed = await deleteSchedule(selectedScheduleId); if (!removed) return; setScheduleDetailsOpen(false); notify({ title: "Schedule deleted", message: removed.tags.join(", ") || "Schedule removed", tone: "danger" }); } } function prevEntry() { if (!filteredEntries.length) return; const current = selectedEntryIndex ?? 0; const index = current === 0 ? filteredEntries.length - 1 : current - 1; openEntryDetails(filteredEntries[index].id); } function nextEntry() { if (!filteredEntries.length) return; const current = selectedEntryIndex ?? 0; const index = current === filteredEntries.length - 1 ? 0 : current + 1; openEntryDetails(filteredEntries[index].id); } return { newEntryOpen, setNewEntryOpen, newScheduleOpen, setNewScheduleOpen, entryDetailsOpen, setEntryDetailsOpen, scheduleDetailsOpen, setScheduleDetailsOpen, confirmDeleteOpen, setConfirmDeleteOpen, deleteTarget, setDeleteTarget, selectedEntryIndex, selectedScheduleId, entryRemovedTags, setEntryRemovedTags, scheduleRemovedTags, setScheduleRemovedTags, entryForm, setEntryForm, entryDetailsForm, setEntryDetailsForm, entryDetailsOriginal, scheduleForm, setScheduleForm, scheduleDetailsForm, setScheduleDetailsForm, scheduleDetailsOriginal, amountInputRef, tagsInputRef, handleEmptyTagAction, hasEntryChanges, hasScheduleChanges, submitNewEntry, submitNewSchedule, openEntryDetails, openScheduleDetails, submitEntryUpdate, submitScheduleUpdate, confirmDelete, prevEntry, nextEntry }; } export type EntriesPanelCrudState = ReturnType;