557 lines
23 KiB
TypeScript
557 lines
23 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import useEntries from "@/features/entries/hooks/use-entries";
|
|
import { useGroupsContext } from "@/hooks/groups-context";
|
|
import NewEntryModal from "@/components/new-entry-modal";
|
|
import EntryDetailsModal from "@/components/entry-details-modal";
|
|
import { useNotificationsContext } from "@/hooks/notifications-context";
|
|
import useTags from "@/features/tags/hooks/use-tags";
|
|
import ConfirmSlideModal from "@/components/confirm-slide-modal";
|
|
import useGroupSettings from "@/features/groups/hooks/use-group-settings";
|
|
import { useEntryMutation } from "@/hooks/entry-mutation-context";
|
|
import EntriesList from "@/features/entries/components/entries-list";
|
|
import EntriesFilterModal, { type EntriesFilters } from "@/features/entries/components/entries-filter-modal";
|
|
import EntriesDiscardModal from "@/features/entries/components/entries-discard-modal";
|
|
import ToggleButtonGroup from "@/components/toggle-button-group";
|
|
|
|
export default function EntriesPanel() {
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
const { groups, activeGroupId } = useGroupsContext();
|
|
const router = useRouter();
|
|
const { entries, loading, error, createEntry, updateEntry, deleteEntry } = useEntries(activeGroupId);
|
|
const { notifyEntryMutation } = useEntryMutation();
|
|
const { notify } = useNotificationsContext();
|
|
const { tags: tagSuggestions } = useTags(activeGroupId);
|
|
const { settings } = useGroupSettings(activeGroupId);
|
|
const activeGroup = groups.find(group => group.id === activeGroupId) || null;
|
|
const canManageTags = Boolean(activeGroup && (activeGroup.role !== "MEMBER" || settings.allowMemberTagManage));
|
|
const emptyTagActionLabel = canManageTags
|
|
? "No Tags Assigned Yet - Click To Assign Tags"
|
|
: "No Tags Assigned Yet - Contact Your Group Admin";
|
|
const [form, setForm] = useState({
|
|
amountDollars: "",
|
|
occurredAt: today,
|
|
necessity: "NECESSARY",
|
|
notes: "",
|
|
tags: [] as string[],
|
|
entryType: "SPENDING" as "SPENDING" | "INCOME",
|
|
isRecurring: false,
|
|
frequency: "MONTHLY" as "DAILY" | "WEEKLY" | "BIWEEKLY" | "MONTHLY" | "QUARTERLY" | "YEARLY",
|
|
intervalCount: 1,
|
|
endCondition: "NEVER" as "NEVER" | "AFTER_COUNT" | "BY_DATE",
|
|
endCount: "",
|
|
endDate: ""
|
|
});
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [detailsOpen, setDetailsOpen] = useState(false);
|
|
const [selectedId, setSelectedId] = useState<number | null>(null);
|
|
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
|
const [detailsForm, setDetailsForm] = useState({
|
|
amountDollars: "",
|
|
occurredAt: today,
|
|
necessity: "NECESSARY",
|
|
notes: "",
|
|
tags: [] as string[],
|
|
entryType: "SPENDING" as "SPENDING" | "INCOME",
|
|
isRecurring: false,
|
|
frequency: "MONTHLY" as "DAILY" | "WEEKLY" | "BIWEEKLY" | "MONTHLY" | "QUARTERLY" | "YEARLY",
|
|
intervalCount: 1,
|
|
endCondition: "NEVER" as "NEVER" | "AFTER_COUNT" | "BY_DATE",
|
|
endCount: "",
|
|
endDate: ""
|
|
});
|
|
const [detailsOriginal, setDetailsOriginal] = useState<typeof detailsForm | null>(null);
|
|
const [removedTags, setRemovedTags] = useState<string[]>([]);
|
|
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
|
const [discardOpen, setDiscardOpen] = useState(false);
|
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
const [entryTab, setEntryTab] = useState<"SINGLE" | "RECURRING">("SINGLE");
|
|
const emptyFilters: EntriesFilters = {
|
|
amountMin: "",
|
|
amountMax: "",
|
|
dateFrom: "",
|
|
dateTo: "",
|
|
necessity: "ANY",
|
|
notesQuery: "",
|
|
tags: [] as string[],
|
|
tagsMode: "ANY" as "ANY" | "ALL"
|
|
};
|
|
const [filters, setFilters] = useState(emptyFilters);
|
|
const amountInputRef = useRef<HTMLInputElement>(null);
|
|
const tagsInputRef = useRef<HTMLInputElement>(null);
|
|
const pendingDiscardRef = useRef<
|
|
| { type: "close" }
|
|
| { type: "prev" }
|
|
| { type: "next" }
|
|
| { type: "open"; entry: typeof entries[number]; index: number }
|
|
| null
|
|
>(null);
|
|
const filteredEntries = useMemo(() => {
|
|
if (!entries.length) return entries;
|
|
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;
|
|
if (from != null) {
|
|
const time = new Date(entry.occurredAt).getTime();
|
|
if (!Number.isNaN(from) && time < from) return false;
|
|
}
|
|
if (to != null) {
|
|
const time = new Date(entry.occurredAt).getTime();
|
|
if (!Number.isNaN(to) && time > to + 24 * 60 * 60 * 1000 - 1) return false;
|
|
}
|
|
if (filters.necessity !== "ANY" && entry.necessity !== filters.necessity) return false;
|
|
if (query) {
|
|
const notes = (entry.notes || "").toLowerCase();
|
|
if (!notes.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 visibleEntries = useMemo(() => filteredEntries.filter(entry => entry.isRecurring === (entryTab === "RECURRING")), [filteredEntries, entryTab]);
|
|
const totalEntries = visibleEntries.length;
|
|
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]);
|
|
|
|
function handleEmptyTagAction() {
|
|
if (!activeGroupId || !canManageTags) return;
|
|
router.push("/groups/settings");
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (tagsInputRef.current)
|
|
tagsInputRef.current.setCustomValidity(form.tags.length ? "" : "Please fill out this field");
|
|
}, [form.tags.length]);
|
|
|
|
useEffect(() => {
|
|
if (!filterOpen && !discardOpen) return;
|
|
function handleKeyDown(event: KeyboardEvent) {
|
|
if (event.key === "Escape") {
|
|
if (discardOpen) handleCancelDiscard();
|
|
if (filterOpen) setFilterOpen(false);
|
|
}
|
|
}
|
|
window.addEventListener("keydown", handleKeyDown);
|
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
}, [discardOpen, filterOpen]);
|
|
|
|
async function handleCreate(e: React.FormEvent<HTMLFormElement>) {
|
|
e.preventDefault();
|
|
if (tagsInputRef.current)
|
|
tagsInputRef.current.setCustomValidity(form.tags.length ? "" : "Please fill out this field");
|
|
if (!e.currentTarget.reportValidity()) {
|
|
if (!form.amountDollars) amountInputRef.current?.focus();
|
|
else if (!form.tags.length) tagsInputRef.current?.focus();
|
|
return;
|
|
}
|
|
const amountDollars = Number(form.amountDollars || 0);
|
|
if (!Number.isFinite(amountDollars) || amountDollars <= 0) {
|
|
return;
|
|
}
|
|
if (!form.occurredAt) {
|
|
return;
|
|
}
|
|
if (!form.tags.length) {
|
|
return;
|
|
}
|
|
|
|
const purchaseType = form.tags.join(", ") || "General";
|
|
|
|
const nextRunAt = form.isRecurring ? form.occurredAt : null;
|
|
const createdEntry = await createEntry({
|
|
entryType: form.entryType,
|
|
amountDollars,
|
|
occurredAt: form.occurredAt,
|
|
necessity: form.necessity as "NECESSARY" | "BOTH" | "UNNECESSARY",
|
|
purchaseType,
|
|
notes: form.notes.trim() || undefined,
|
|
tags: form.tags,
|
|
isRecurring: form.isRecurring,
|
|
frequency: form.isRecurring ? form.frequency : null,
|
|
intervalCount: form.intervalCount,
|
|
endCondition: form.isRecurring ? form.endCondition : null,
|
|
endCount: form.endCondition === "AFTER_COUNT" ? Number(form.endCount || 0) || null : null,
|
|
endDate: form.endCondition === "BY_DATE" ? form.endDate || null : null,
|
|
nextRunAt
|
|
});
|
|
if (createdEntry) {
|
|
setForm({
|
|
amountDollars: "",
|
|
occurredAt: today,
|
|
necessity: "NECESSARY",
|
|
notes: "",
|
|
tags: [],
|
|
entryType: "SPENDING",
|
|
isRecurring: false,
|
|
frequency: "MONTHLY",
|
|
intervalCount: 1,
|
|
endCondition: "NEVER",
|
|
endCount: "",
|
|
endDate: ""
|
|
});
|
|
setIsModalOpen(false);
|
|
notify({
|
|
title: "Entry added",
|
|
message: `${form.tags.join(", ")} - $${amountDollars.toFixed(2)}`,
|
|
tone: "success"
|
|
});
|
|
notifyEntryMutation();
|
|
}
|
|
}
|
|
|
|
function setDetailsFromEntry(entry: typeof entries[number], index: number) {
|
|
const nextId = Number(entry.id);
|
|
if (!Number.isFinite(nextId) || nextId <= 0) {
|
|
alert("Invalid entry id");
|
|
return;
|
|
}
|
|
const nextForm = {
|
|
amountDollars: String(entry.amountDollars),
|
|
occurredAt: new Date(entry.occurredAt).toISOString().slice(0, 10),
|
|
necessity: entry.necessity,
|
|
notes: entry.notes || "",
|
|
tags: entry.tags || [],
|
|
entryType: entry.entryType,
|
|
isRecurring: entry.isRecurring,
|
|
frequency: entry.frequency || "MONTHLY",
|
|
intervalCount: entry.intervalCount || 1,
|
|
endCondition: entry.endCondition || "NEVER",
|
|
endCount: entry.endCount ? String(entry.endCount) : "",
|
|
endDate: entry.endDate || ""
|
|
};
|
|
setSelectedId(nextId);
|
|
setSelectedIndex(index);
|
|
setRemovedTags([]);
|
|
setDetailsForm(nextForm);
|
|
setDetailsOriginal(nextForm);
|
|
setDetailsOpen(true);
|
|
}
|
|
|
|
function handleOpenDetails(entry: typeof entries[number], index: number) {
|
|
requestDiscard({ type: "open", entry, index });
|
|
}
|
|
|
|
function normalizeTags(tags: string[]) {
|
|
return tags.map(tag => tag.toLowerCase()).sort();
|
|
}
|
|
|
|
function handleFilterAddTag(tag: string) {
|
|
setFilters(prev => (prev.tags.includes(tag) ? prev : { ...prev, tags: [...prev.tags, tag] }));
|
|
}
|
|
|
|
function handleFilterToggleTag(tag: string) {
|
|
setFilters(prev => ({ ...prev, tags: prev.tags.filter(item => item !== tag) }));
|
|
}
|
|
|
|
function handleClearFilters() {
|
|
setFilters(emptyFilters);
|
|
}
|
|
function hasDetailsChanges() {
|
|
if (!detailsOriginal) return false;
|
|
const currentTags = detailsForm.tags.filter(tag => !removedTags.includes(tag));
|
|
const currentTagsKey = normalizeTags(currentTags).join("|");
|
|
const originalTagsKey = normalizeTags(detailsOriginal.tags).join("|");
|
|
return (
|
|
detailsForm.amountDollars !== detailsOriginal.amountDollars ||
|
|
detailsForm.occurredAt !== detailsOriginal.occurredAt ||
|
|
detailsForm.necessity !== detailsOriginal.necessity ||
|
|
detailsForm.notes !== detailsOriginal.notes ||
|
|
detailsForm.entryType !== detailsOriginal.entryType ||
|
|
detailsForm.isRecurring !== detailsOriginal.isRecurring ||
|
|
detailsForm.frequency !== detailsOriginal.frequency ||
|
|
detailsForm.intervalCount !== detailsOriginal.intervalCount ||
|
|
detailsForm.endCondition !== detailsOriginal.endCondition ||
|
|
detailsForm.endCount !== detailsOriginal.endCount ||
|
|
detailsForm.endDate !== detailsOriginal.endDate ||
|
|
currentTagsKey !== originalTagsKey
|
|
);
|
|
}
|
|
|
|
function requestDiscard(action: NonNullable<typeof pendingDiscardRef.current>) {
|
|
if (!detailsOpen || !hasDetailsChanges()) {
|
|
runDiscardAction(action);
|
|
return;
|
|
}
|
|
pendingDiscardRef.current = action;
|
|
setDiscardOpen(true);
|
|
}
|
|
|
|
function runDiscardAction(action: NonNullable<typeof pendingDiscardRef.current>) {
|
|
if (action.type === "close") {
|
|
setDetailsOpen(false);
|
|
setDetailsOriginal(null);
|
|
setRemovedTags([]);
|
|
return;
|
|
}
|
|
if (action.type === "open") {
|
|
setDetailsFromEntry(action.entry, action.index);
|
|
return;
|
|
}
|
|
if (!totalEntries) return;
|
|
const current = selectedIndex ?? 0;
|
|
if (action.type === "prev") {
|
|
const nextIndex = current === 0 ? totalEntries - 1 : current - 1;
|
|
setDetailsFromEntry(visibleEntries[nextIndex], nextIndex);
|
|
}
|
|
if (action.type === "next") {
|
|
const nextIndex = current === totalEntries - 1 ? 0 : current + 1;
|
|
setDetailsFromEntry(visibleEntries[nextIndex], nextIndex);
|
|
}
|
|
}
|
|
|
|
async function handleUpdate(e: React.FormEvent<HTMLFormElement>) {
|
|
e.preventDefault();
|
|
if (!selectedId) return;
|
|
if (!hasDetailsChanges()) return;
|
|
|
|
const amountDollars = Number(detailsForm.amountDollars || 0);
|
|
if (!Number.isFinite(amountDollars) || amountDollars <= 0) {
|
|
alert("Enter a valid amount");
|
|
return;
|
|
}
|
|
if (!detailsForm.occurredAt) {
|
|
alert("Select a date");
|
|
return;
|
|
}
|
|
const nextTags = detailsForm.tags.filter(tag => !removedTags.includes(tag));
|
|
if (!nextTags.length) {
|
|
alert("Add at least one tag");
|
|
return;
|
|
}
|
|
|
|
const purchaseType = nextTags.join(", ") || "General";
|
|
|
|
const nextRunAt = detailsForm.isRecurring ? detailsForm.occurredAt : null;
|
|
const beforeEntry = entries.find(entry => Number(entry.id) === Number(selectedId)) || null;
|
|
const updatedEntry = await updateEntry({
|
|
id: selectedId,
|
|
entryType: detailsForm.entryType,
|
|
amountDollars,
|
|
occurredAt: detailsForm.occurredAt,
|
|
necessity: detailsForm.necessity as "NECESSARY" | "BOTH" | "UNNECESSARY",
|
|
purchaseType,
|
|
notes: detailsForm.notes.trim() || undefined,
|
|
tags: nextTags,
|
|
isRecurring: detailsForm.isRecurring,
|
|
frequency: detailsForm.isRecurring ? detailsForm.frequency : null,
|
|
intervalCount: detailsForm.intervalCount,
|
|
endCondition: detailsForm.isRecurring ? detailsForm.endCondition : null,
|
|
endCount: detailsForm.endCondition === "AFTER_COUNT" ? Number(detailsForm.endCount || 0) || null : null,
|
|
endDate: detailsForm.endCondition === "BY_DATE" ? detailsForm.endDate || null : null,
|
|
nextRunAt
|
|
});
|
|
if (updatedEntry) {
|
|
setDetailsOpen(false);
|
|
setDetailsOriginal(null);
|
|
setRemovedTags([]);
|
|
notify({
|
|
title: "Entry updated",
|
|
message: `${nextTags.join(", ")} - $${amountDollars.toFixed(2)}`
|
|
});
|
|
notifyEntryMutation();
|
|
}
|
|
}
|
|
|
|
async function handleDelete() {
|
|
if (!selectedId || !Number.isFinite(selectedId)) return;
|
|
const beforeEntry = entries.find(entry => Number(entry.id) === Number(selectedId)) || null;
|
|
const deletedEntry = await deleteEntry(selectedId);
|
|
if (deletedEntry || beforeEntry) {
|
|
setDetailsOpen(false);
|
|
setDetailsOriginal(null);
|
|
setRemovedTags([]);
|
|
notify({
|
|
title: "Entry deleted",
|
|
message: detailsForm.tags.join(", ") || "Entry removed",
|
|
tone: "danger"
|
|
});
|
|
notifyEntryMutation();
|
|
}
|
|
}
|
|
|
|
function handleToggleTag(tag: string) {
|
|
setRemovedTags(prev => prev.includes(tag) ? prev.filter(item => item !== tag) : [...prev, tag]);
|
|
}
|
|
|
|
function handleAddTag(tag: string) {
|
|
setDetailsForm(prev => ({ ...prev, tags: prev.tags.includes(tag) ? prev.tags : [...prev.tags, tag] }));
|
|
setRemovedTags(prev => prev.filter(item => item !== tag));
|
|
}
|
|
|
|
function handlePrev() {
|
|
if (!totalEntries) return;
|
|
requestDiscard({ type: "prev" });
|
|
}
|
|
|
|
function handleNext() {
|
|
if (!totalEntries) return;
|
|
requestDiscard({ type: "next" });
|
|
}
|
|
|
|
function handleCloseDetails() {
|
|
requestDiscard({ type: "close" });
|
|
}
|
|
|
|
function handleConfirmDiscard() {
|
|
const action = pendingDiscardRef.current;
|
|
pendingDiscardRef.current = null;
|
|
setDiscardOpen(false);
|
|
if (action) runDiscardAction(action);
|
|
}
|
|
|
|
function handleCancelDiscard() {
|
|
pendingDiscardRef.current = null;
|
|
setDiscardOpen(false);
|
|
}
|
|
|
|
function handleRevertDetails() {
|
|
if (!detailsOriginal) return;
|
|
setDetailsForm(detailsOriginal);
|
|
setRemovedTags([]);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="space-y-4">
|
|
<div className="panel panel-accent p-4">
|
|
<div className="card-header">
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<h2 className="card-title text-lg">Entries</h2>
|
|
<ToggleButtonGroup
|
|
value={entryTab}
|
|
onChange={setEntryTab}
|
|
ariaLabel="Entries tab"
|
|
sizeClassName="px-3 py-2 text-xs font-semibold"
|
|
options={[
|
|
{ value: "SINGLE", label: "Existing", className: "mr-[-10px] w-20" },
|
|
{ value: "RECURRING", label: "Scheduled", className: "w-20" }
|
|
]}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => setFilterOpen(true)}
|
|
className="rounded-lg btn-outline-accent px-3 py-1 text-xs font-semibold disabled:opacity-50"
|
|
disabled={!activeGroupId}
|
|
>
|
|
Filters{activeFilterCount ? ` (${activeFilterCount})` : ""}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsModalOpen(true)}
|
|
className="flex h-8 w-8 -translate-y-0.5 items-center justify-center rounded-full border border-accent bg-panel text-lg font-semibold leading-none hover:border-accent-strong disabled:opacity-50"
|
|
disabled={!activeGroupId}
|
|
aria-label="Add entry"
|
|
>
|
|
+
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<EntriesList
|
|
activeGroupId={activeGroupId}
|
|
loading={loading}
|
|
entries={entries}
|
|
visibleEntries={visibleEntries}
|
|
activeFilterCount={activeFilterCount}
|
|
onOpenDetails={handleOpenDetails}
|
|
onClearFilters={handleClearFilters}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<NewEntryModal
|
|
isOpen={isModalOpen && Boolean(activeGroupId)}
|
|
form={form}
|
|
error={error}
|
|
onClose={() => setIsModalOpen(false)}
|
|
onSubmit={handleCreate}
|
|
onChange={next => setForm(prev => ({ ...prev, ...next }))}
|
|
tagSuggestions={tagSuggestions}
|
|
emptyTagActionLabel={emptyTagActionLabel}
|
|
emptyTagActionDisabled={!canManageTags}
|
|
onEmptyTagAction={handleEmptyTagAction}
|
|
amountInputRef={amountInputRef}
|
|
tagsInputRef={tagsInputRef}
|
|
/>
|
|
<EntryDetailsModal
|
|
isOpen={detailsOpen}
|
|
form={detailsForm}
|
|
originalForm={detailsOriginal}
|
|
isDirty={hasDetailsChanges()}
|
|
error={error}
|
|
onClose={handleCloseDetails}
|
|
onSubmit={handleUpdate}
|
|
onRequestDelete={() => setConfirmDeleteOpen(true)}
|
|
onRevert={handleRevertDetails}
|
|
onChange={next => setDetailsForm(prev => ({ ...prev, ...next }))}
|
|
onAddTag={handleAddTag}
|
|
onToggleTag={handleToggleTag}
|
|
removedTags={removedTags}
|
|
tagSuggestions={tagSuggestions}
|
|
emptyTagActionLabel={emptyTagActionLabel}
|
|
emptyTagActionDisabled={!canManageTags}
|
|
onEmptyTagAction={handleEmptyTagAction}
|
|
onPrev={handlePrev}
|
|
onNext={handleNext}
|
|
loopHintPrev={selectedIndex === 0 && totalEntries > 1 ? "Loop" : ""}
|
|
loopHintNext={selectedIndex === totalEntries - 1 && totalEntries > 1 ? "Loop" : ""}
|
|
canNavigate={totalEntries > 1}
|
|
/>
|
|
<EntriesFilterModal
|
|
isOpen={filterOpen}
|
|
filters={filters}
|
|
setFilters={setFilters}
|
|
activeFilterCount={activeFilterCount}
|
|
tagSuggestions={tagSuggestions}
|
|
canManageTags={canManageTags}
|
|
emptyTagActionLabel={emptyTagActionLabel}
|
|
onEmptyTagAction={handleEmptyTagAction}
|
|
onClearFilters={handleClearFilters}
|
|
onFilterAddTag={handleFilterAddTag}
|
|
onFilterToggleTag={handleFilterToggleTag}
|
|
onClose={() => setFilterOpen(false)}
|
|
/>
|
|
<EntriesDiscardModal
|
|
isOpen={discardOpen}
|
|
onCancel={handleCancelDiscard}
|
|
onConfirm={handleConfirmDiscard}
|
|
/>
|
|
<ConfirmSlideModal
|
|
isOpen={confirmDeleteOpen}
|
|
title="Delete entry"
|
|
description="This will permanently remove the entry and its tags."
|
|
confirmLabel="Delete entry"
|
|
onClose={() => setConfirmDeleteOpen(false)}
|
|
onConfirm={() => {
|
|
setConfirmDeleteOpen(false);
|
|
handleDelete();
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
|