"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import useEntries from "@/features/entries/hooks/use-entries"; import useSchedules from "@/features/entries/hooks/use-schedules"; import { useGroupsContext } from "@/hooks/groups-context"; import { useNotificationsContext } from "@/hooks/notifications-context"; import { useEntryMutation } from "@/hooks/entry-mutation-context"; import useTags from "@/features/tags/hooks/use-tags"; import useGroupSettings from "@/features/groups/hooks/use-group-settings"; import useUserSettings from "@/hooks/use-user-settings"; import ToggleButtonGroup from "@/components/toggle-button-group"; import NewEntryModal from "@/components/new-entry-modal"; import EntryDetailsModal from "@/components/entry-details-modal"; import NewScheduleModal, { type NewScheduleForm } from "@/components/new-schedule-modal"; import ScheduleDetailsModal, { type ScheduleDetailsForm } from "@/components/schedule-details-modal"; import EntriesList from "@/features/entries/components/entries-list"; import SchedulesList from "@/features/entries/components/schedules-list"; import EntriesFilterModal, { type EntriesFilters } from "@/features/entries/components/entries-filter-modal"; import ConfirmSlideModal from "@/components/confirm-slide-modal"; const EMPTY_FILTERS: EntriesFilters = { amountMin: "", amountMax: "", dateFrom: "", dateTo: "", necessity: "ANY", notesQuery: "", tags: [], tagsMode: "ANY" }; function normalizeTagList(tags: string[]) { return tags.map(tag => tag.toLowerCase()).sort().join("|"); } function isEditableTarget(target: EventTarget | null) { if (!(target instanceof HTMLElement)) return false; if (target.isContentEditable) return true; const tag = target.tagName; return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT"; } function ListProgressSignal({ hasMore, shownCount, totalCount, noun }: { hasMore: boolean; shownCount: number; totalCount: number; noun: "entries" | "schedules"; }) { if (totalCount <= 0) return null; return (
{hasMore ? "\u21e3" : "\u2713"} {hasMore ? `Keep scrolling for more ${noun} (${shownCount} of ${totalCount})` : `You have reached the end of ${noun} (${totalCount} total)`}
); } export default function EntriesPanel() { const today = new Date().toISOString().slice(0, 10); const { groups, activeGroupId } = useGroupsContext(); const { entries, loading: entriesLoading, error: entriesError, createEntry, updateEntry, deleteEntry } = useEntries(activeGroupId); const { schedules, loading: schedulesLoading, error: schedulesError, createSchedule, updateSchedule, deleteSchedule } = useSchedules(activeGroupId); const { settings: userSettings } = useUserSettings(); const { notify } = useNotificationsContext(); const { notifyEntryMutation } = useEntryMutation(); const { tags: tagSuggestions } = useTags(activeGroupId); const { settings: groupSettings } = useGroupSettings(activeGroupId); const router = useRouter(); const activeGroup = groups.find(group => group.id === activeGroupId) || null; const canManageTags = Boolean(activeGroup && (activeGroup.role !== "MEMBER" || groupSettings.allowMemberTagManage)); const emptyTagActionLabel = canManageTags ? "No Tags Assigned Yet - Click To Assign Tags" : "No Tags Assigned Yet - Contact Your Group Admin"; const pageSize = Math.max(1, Number(userSettings.entryPanelPageSize || 10)); const [entryTab, setEntryTab] = useState<"ENTRIES" | "SCHEDULES">("ENTRIES"); const [filters, setFilters] = useState(EMPTY_FILTERS); const [entryVisibleCount, setEntryVisibleCount] = useState(pageSize); const [scheduleVisibleCount, setScheduleVisibleCount] = useState(pageSize); const [filterOpen, setFilterOpen] = useState(false); 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" | "SCHEDULE">("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({ amountDollars: "", occurredAt: today, necessity: "NECESSARY", notes: "", tags: [] as string[], entryType: "SPENDING" as "SPENDING" | "INCOME" }); const [entryDetailsForm, setEntryDetailsForm] = useState({ amountDollars: "", occurredAt: today, necessity: "NECESSARY", notes: "", tags: [] as string[], entryType: "SPENDING" as "SPENDING" | "INCOME" }); const [entryDetailsOriginal, setEntryDetailsOriginal] = useState(null); const [scheduleForm, setScheduleForm] = useState({ amountDollars: "", startsOn: today, necessity: "NECESSARY", notes: "", tags: [], entryType: "SPENDING", frequency: "MONTHLY", intervalCount: 1, endCondition: "NEVER", endCount: "", endDate: "", createEntryNow: false }); const [scheduleDetailsForm, setScheduleDetailsForm] = useState({ amountDollars: "", startsOn: today, necessity: "NECESSARY", notes: "", tags: [], entryType: "SPENDING", frequency: "MONTHLY", intervalCount: 1, endCondition: "NEVER", endCount: "", endDate: "", nextRunOn: today, isActive: true }); const [scheduleDetailsOriginal, setScheduleDetailsOriginal] = useState(null); const amountInputRef = useRef(null); const tagsInputRef = useRef(null); const entriesLoadSentinelRef = useRef(null); const schedulesLoadSentinelRef = useRef(null); useEffect(() => { setEntryVisibleCount(pageSize); setScheduleVisibleCount(pageSize); }, [pageSize]); const activeFilterCount = useMemo(() => { let count = 0; if (filters.amountMin) count += 1; if (filters.amountMax) count += 1; if (filters.dateFrom) count += 1; if (filters.dateTo) count += 1; if (filters.necessity !== "ANY") count += 1; if (filters.notesQuery.trim()) count += 1; if (filters.tags.length) count += 1; return count; }, [filters]); const filteredEntries = useMemo(() => { const min = filters.amountMin ? Number(filters.amountMin) : null; const max = filters.amountMax ? Number(filters.amountMax) : null; const from = filters.dateFrom ? new Date(filters.dateFrom).getTime() : null; const to = filters.dateTo ? new Date(filters.dateTo).getTime() : null; const query = filters.notesQuery.trim().toLowerCase(); const tagsFilter = filters.tags.map(tag => tag.toLowerCase()); return entries.filter(entry => { if (min != null && entry.amountDollars < min) return false; if (max != null && entry.amountDollars > max) return false; const time = new Date(entry.occurredAt).getTime(); if (from != null && !Number.isNaN(from) && time < from) return false; if (to != null && !Number.isNaN(to) && time > to + 24 * 60 * 60 * 1000 - 1) return false; if (filters.necessity !== "ANY" && entry.necessity !== filters.necessity) return false; if (query && !(entry.notes || "").toLowerCase().includes(query)) return false; if (tagsFilter.length) { const entryTags = (entry.tags || []).map(tag => tag.toLowerCase()); if (filters.tagsMode === "ALL") { if (!tagsFilter.every(tag => entryTags.includes(tag))) return false; } else if (!tagsFilter.some(tag => entryTags.includes(tag))) return false; } return true; }); }, [entries, filters]); const filteredSchedules = useMemo(() => { const min = filters.amountMin ? Number(filters.amountMin) : null; const max = filters.amountMax ? Number(filters.amountMax) : null; const from = filters.dateFrom ? new Date(filters.dateFrom).getTime() : null; const to = filters.dateTo ? new Date(filters.dateTo).getTime() : null; const query = filters.notesQuery.trim().toLowerCase(); const tagsFilter = filters.tags.map(tag => tag.toLowerCase()); return schedules.filter(schedule => { if (min != null && schedule.amountDollars < min) return false; if (max != null && schedule.amountDollars > max) return false; const time = new Date(schedule.startsOn).getTime(); if (from != null && !Number.isNaN(from) && time < from) return false; if (to != null && !Number.isNaN(to) && time > to + 24 * 60 * 60 * 1000 - 1) return false; if (filters.necessity !== "ANY" && schedule.necessity !== filters.necessity) return false; if (query && !(schedule.notes || "").toLowerCase().includes(query)) return false; if (tagsFilter.length) { const scheduleTags = (schedule.tags || []).map(tag => tag.toLowerCase()); if (filters.tagsMode === "ALL") { if (!tagsFilter.every(tag => scheduleTags.includes(tag))) return false; } else if (!tagsFilter.some(tag => scheduleTags.includes(tag))) return false; } return true; }); }, [schedules, filters]); const visibleEntries = useMemo(() => filteredEntries.slice(0, entryVisibleCount), [filteredEntries, entryVisibleCount]); const visibleSchedules = useMemo(() => filteredSchedules.slice(0, scheduleVisibleCount), [filteredSchedules, scheduleVisibleCount]); const hasMoreEntries = filteredEntries.length > visibleEntries.length; const hasMoreSchedules = filteredSchedules.length > visibleSchedules.length; useEffect(() => { if (entryTab !== "ENTRIES" || !hasMoreEntries) return; let touchY: number | null = null; let lastLoadAt = 0; let lastScrollY = window.scrollY; function shouldLoadMore() { const sentinel = entriesLoadSentinelRef.current; if (!sentinel) return false; const rect = sentinel.getBoundingClientRect(); return rect.top <= window.innerHeight + 48; } function tryLoadMore() { if (!shouldLoadMore()) return; const now = Date.now(); if (now - lastLoadAt < 150) return; lastLoadAt = now; setEntryVisibleCount(prev => { if (prev >= filteredEntries.length) return prev; return Math.min(prev + pageSize, filteredEntries.length); }); } function onWheel(event: WheelEvent) { if (event.deltaY <= 0) return; tryLoadMore(); } function onScroll() { const nextY = window.scrollY; if (nextY <= lastScrollY) { lastScrollY = nextY; return; } lastScrollY = nextY; tryLoadMore(); } function onKeyDown(event: KeyboardEvent) { if (isEditableTarget(event.target)) return; if (event.key === "ArrowDown" || event.key === "PageDown" || event.key === "End" || (event.key === " " && !event.shiftKey)) { tryLoadMore(); } } function onTouchStart(event: TouchEvent) { touchY = event.touches[0]?.clientY ?? null; } function onTouchMove(event: TouchEvent) { const nextY = event.touches[0]?.clientY; if (touchY == null || nextY == null) return; const delta = touchY - nextY; touchY = nextY; if (delta > 10) tryLoadMore(); } window.addEventListener("wheel", onWheel, { passive: true }); window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("keydown", onKeyDown); window.addEventListener("touchstart", onTouchStart, { passive: true }); window.addEventListener("touchmove", onTouchMove, { passive: true }); return () => { window.removeEventListener("wheel", onWheel); window.removeEventListener("scroll", onScroll); window.removeEventListener("keydown", onKeyDown); window.removeEventListener("touchstart", onTouchStart); window.removeEventListener("touchmove", onTouchMove); }; }, [entryTab, hasMoreEntries, filteredEntries.length, pageSize]); useEffect(() => { if (entryTab !== "SCHEDULES" || !hasMoreSchedules) return; let touchY: number | null = null; let lastLoadAt = 0; let lastScrollY = window.scrollY; function shouldLoadMore() { const sentinel = schedulesLoadSentinelRef.current; if (!sentinel) return false; const rect = sentinel.getBoundingClientRect(); return rect.top <= window.innerHeight + 48; } function tryLoadMore() { if (!shouldLoadMore()) return; const now = Date.now(); if (now - lastLoadAt < 150) return; lastLoadAt = now; setScheduleVisibleCount(prev => { if (prev >= filteredSchedules.length) return prev; return Math.min(prev + pageSize, filteredSchedules.length); }); } function onWheel(event: WheelEvent) { if (event.deltaY <= 0) return; tryLoadMore(); } function onScroll() { const nextY = window.scrollY; if (nextY <= lastScrollY) { lastScrollY = nextY; return; } lastScrollY = nextY; tryLoadMore(); } function onKeyDown(event: KeyboardEvent) { if (isEditableTarget(event.target)) return; if (event.key === "ArrowDown" || event.key === "PageDown" || event.key === "End" || (event.key === " " && !event.shiftKey)) { tryLoadMore(); } } function onTouchStart(event: TouchEvent) { touchY = event.touches[0]?.clientY ?? null; } function onTouchMove(event: TouchEvent) { const nextY = event.touches[0]?.clientY; if (touchY == null || nextY == null) return; const delta = touchY - nextY; touchY = nextY; if (delta > 10) tryLoadMore(); } window.addEventListener("wheel", onWheel, { passive: true }); window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("keydown", onKeyDown); window.addEventListener("touchstart", onTouchStart, { passive: true }); window.addEventListener("touchmove", onTouchMove, { passive: true }); return () => { window.removeEventListener("wheel", onWheel); window.removeEventListener("scroll", onScroll); window.removeEventListener("keydown", onKeyDown); window.removeEventListener("touchstart", onTouchStart); window.removeEventListener("touchmove", onTouchMove); }; }, [entryTab, hasMoreSchedules, filteredSchedules.length, pageSize]); function clearFilters() { setFilters(EMPTY_FILTERS); setEntryVisibleCount(pageSize); setScheduleVisibleCount(pageSize); } 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(e: React.FormEvent) { e.preventDefault(); if (!e.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 as "NECESSARY" | "BOTH" | "UNNECESSARY", purchaseType: entryForm.tags.join(", ") || "General", notes: entryForm.notes.trim() || undefined, tags: entryForm.tags }); if (!created) return; setNewEntryOpen(false); setEntryForm({ amountDollars: "", occurredAt: today, necessity: "NECESSARY", notes: "", tags: [], entryType: "SPENDING" }); notify({ title: "Entry added", message: `${created.tags.join(", ")} - $${created.amountDollars.toFixed(2)}`, tone: "success" }); notifyEntryMutation(); } async function submitNewSchedule(e: React.FormEvent) { e.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({ amountDollars: "", startsOn: today, necessity: "NECESSARY", notes: "", tags: [], entryType: "SPENDING", frequency: "MONTHLY", intervalCount: 1, endCondition: "NEVER", endCount: "", endDate: "", createEntryNow: false }); 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 = { 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: ScheduleDetailsForm = { 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(e: React.FormEvent) { e.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 as "NECESSARY" | "BOTH" | "UNNECESSARY", 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(e: React.FormEvent) { e.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 ( <>
{entryTab === "ENTRIES" ? ( <> openEntryDetails(entry.id)} onClearFilters={clearFilters} /> setNewEntryOpen(false)} onSubmit={submitNewEntry} onChange={next => setEntryForm(prev => ({ ...prev, ...next }))} tagSuggestions={tagSuggestions} emptyTagActionLabel={emptyTagActionLabel} emptyTagActionDisabled={!canManageTags} onEmptyTagAction={handleEmptyTagAction} amountInputRef={amountInputRef} tagsInputRef={tagsInputRef} /> setNewScheduleOpen(false)} onSubmit={submitNewSchedule} onChange={next => setScheduleForm(prev => ({ ...prev, ...next }))} tagSuggestions={tagSuggestions} emptyTagActionLabel={emptyTagActionLabel} emptyTagActionDisabled={!canManageTags} onEmptyTagAction={handleEmptyTagAction} /> setEntryDetailsOpen(false)} onSubmit={submitEntryUpdate} onRequestDelete={() => { setDeleteTarget("ENTRY"); setConfirmDeleteOpen(true); }} onRevert={() => { if (!entryDetailsOriginal) return; setEntryDetailsForm(entryDetailsOriginal); setEntryRemovedTags([]); }} onChange={next => setEntryDetailsForm(prev => ({ ...prev, ...next }))} onAddTag={tag => { setEntryDetailsForm(prev => ({ ...prev, tags: prev.tags.includes(tag) ? prev.tags : [...prev.tags, tag] })); setEntryRemovedTags(prev => prev.filter(item => item !== tag)); }} onToggleTag={tag => setEntryRemovedTags(prev => prev.includes(tag) ? prev.filter(item => item !== tag) : [...prev, tag])} removedTags={entryRemovedTags} tagSuggestions={tagSuggestions} emptyTagActionLabel={emptyTagActionLabel} emptyTagActionDisabled={!canManageTags} onEmptyTagAction={handleEmptyTagAction} onPrev={prevEntry} onNext={nextEntry} loopHintPrev={selectedEntryIndex === 0 && filteredEntries.length > 1 ? "Loop" : ""} loopHintNext={selectedEntryIndex === filteredEntries.length - 1 && filteredEntries.length > 1 ? "Loop" : ""} canNavigate={filteredEntries.length > 1} /> setScheduleDetailsOpen(false)} onSubmit={submitScheduleUpdate} onRequestDelete={() => { setDeleteTarget("SCHEDULE"); setConfirmDeleteOpen(true); }} onRevert={() => { if (!scheduleDetailsOriginal) return; setScheduleDetailsForm(scheduleDetailsOriginal); setScheduleRemovedTags([]); }} onChange={next => setScheduleDetailsForm(prev => ({ ...prev, ...next }))} onAddTag={tag => { setScheduleDetailsForm(prev => ({ ...prev, tags: prev.tags.includes(tag) ? prev.tags : [...prev.tags, tag] })); setScheduleRemovedTags(prev => prev.filter(item => item !== tag)); }} onToggleTag={tag => setScheduleRemovedTags(prev => prev.includes(tag) ? prev.filter(item => item !== tag) : [...prev, tag])} removedTags={scheduleRemovedTags} tagSuggestions={tagSuggestions} emptyTagActionLabel={emptyTagActionLabel} emptyTagActionDisabled={!canManageTags} onEmptyTagAction={handleEmptyTagAction} /> setFilters(prev => (prev.tags.includes(tag) ? prev : { ...prev, tags: [...prev.tags, tag] }))} onFilterToggleTag={tag => setFilters(prev => ({ ...prev, tags: prev.tags.filter(item => item !== tag) }))} onClose={() => setFilterOpen(false)} /> setConfirmDeleteOpen(false)} onConfirm={() => { setConfirmDeleteOpen(false); confirmDelete(); }} /> ); }