fiddy/apps/web/features/entries/components/entries-panel.tsx

269 lines
11 KiB
TypeScript

"use client";
import { useMemo } from "react";
import EntriesList from "@/features/entries/components/entries-list";
import EntriesPanelHeader from "@/features/entries/components/entries-panel-header";
import EntriesPanelModals from "@/features/entries/components/entries-panel-modals";
import SchedulesList from "@/features/entries/components/schedules-list";
import useEntriesPanelCrud from "@/features/entries/components/use-entries-panel-crud";
import useEntriesPanelFilters from "@/features/entries/components/use-entries-panel-filters";
import useEntries from "@/features/entries/hooks/use-entries";
import useSchedules from "@/features/entries/hooks/use-schedules";
import useGroupSettings from "@/features/groups/hooks/use-group-settings";
import useTags from "@/features/tags/hooks/use-tags";
import { useEntryMutation } from "@/hooks/entry-mutation-context";
import { useGroupsContext } from "@/hooks/groups-context";
import { useNotificationsContext } from "@/hooks/notifications-context";
import useUserSettings from "@/hooks/use-user-settings";
type ListProgressSignalProps = {
hasMore: boolean;
shownCount: number;
totalCount: number;
noun: "entries" | "schedules";
};
function ListProgressSignal({ hasMore, shownCount, totalCount, noun }: ListProgressSignalProps) {
if (totalCount <= 0) return null;
return (
<div
className={`mt-3 flex items-center justify-center gap-2 rounded-lg border px-3 py-2 text-xs ${hasMore ? "border-accent-weak bg-accent-soft text-[color:var(--color-text)]" : "border-accent-weak bg-panel text-soft"}`}
aria-live="polite"
>
<span className={`inline-flex h-5 w-5 items-center justify-center rounded-full border ${hasMore ? "border-accent bg-panel text-[color:var(--color-accent)]" : "border-accent-weak text-soft"}`}>
{hasMore ? "\u21e3" : "\u2713"}
</span>
<span>
{hasMore
? `Keep scrolling for more ${noun} (${shownCount} of ${totalCount})`
: `You have reached the end of ${noun} (${totalCount} total)`}
</span>
</div>
);
}
export default function EntriesPanel() {
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 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,
filters,
setFilters,
filterOpen,
setFilterOpen,
activeFilterCount,
filteredEntries,
filteredSchedules,
visibleEntries,
visibleSchedules,
hasMoreEntries,
hasMoreSchedules,
entriesLoadSentinelRef,
schedulesLoadSentinelRef,
clearFilters,
onFilterAddTag,
onFilterToggleTag
} = useEntriesPanelFilters({ entries, schedules, pageSize });
const {
newEntryOpen,
setNewEntryOpen,
newScheduleOpen,
setNewScheduleOpen,
entryDetailsOpen,
setEntryDetailsOpen,
scheduleDetailsOpen,
setScheduleDetailsOpen,
confirmDeleteOpen,
setConfirmDeleteOpen,
deleteTarget,
setDeleteTarget,
selectedEntryIndex,
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
} = useEntriesPanelCrud({
filteredEntries,
schedules,
createEntry,
updateEntry,
deleteEntry,
createSchedule,
updateSchedule,
deleteSchedule,
notify,
notifyEntryMutation,
canManageTags
});
const listCounts = useMemo(() => {
return {
entriesShown: visibleEntries.length,
entriesTotal: filteredEntries.length,
schedulesShown: visibleSchedules.length,
schedulesTotal: filteredSchedules.length
};
}, [filteredEntries.length, filteredSchedules.length, visibleEntries.length, visibleSchedules.length]);
return (
<>
<div className="space-y-4">
<div className="panel panel-accent p-4">
<EntriesPanelHeader
entryTab={entryTab}
activeGroupId={activeGroupId}
activeFilterCount={activeFilterCount}
onTabChange={setEntryTab}
onOpenFilters={() => setFilterOpen(true)}
onOpenCreate={() => {
if (entryTab === "ENTRIES") {
setNewEntryOpen(true);
return;
}
setNewScheduleOpen(true);
}}
/>
{entryTab === "ENTRIES" ? (
<>
<EntriesList
activeGroupId={activeGroupId}
loading={entriesLoading}
entries={entries}
visibleEntries={visibleEntries}
activeFilterCount={activeFilterCount}
onOpenDetails={entry => openEntryDetails(entry.id)}
onClearFilters={clearFilters}
/>
<div ref={entriesLoadSentinelRef} className="h-1" aria-hidden="true" />
<ListProgressSignal
hasMore={hasMoreEntries}
shownCount={listCounts.entriesShown}
totalCount={listCounts.entriesTotal}
noun="entries"
/>
</>
) : (
<>
<SchedulesList
activeGroupId={activeGroupId}
loading={schedulesLoading}
schedules={schedules}
visibleSchedules={visibleSchedules}
activeFilterCount={activeFilterCount}
onOpenDetails={schedule => openScheduleDetails(schedule.id)}
onClearFilters={clearFilters}
/>
<div ref={schedulesLoadSentinelRef} className="h-1" aria-hidden="true" />
<ListProgressSignal
hasMore={hasMoreSchedules}
shownCount={listCounts.schedulesShown}
totalCount={listCounts.schedulesTotal}
noun="schedules"
/>
</>
)}
</div>
</div>
<EntriesPanelModals
activeGroupId={activeGroupId}
entriesError={entriesError}
schedulesError={schedulesError}
tagSuggestions={tagSuggestions}
canManageTags={canManageTags}
emptyTagActionLabel={emptyTagActionLabel}
handleEmptyTagAction={handleEmptyTagAction}
filterOpen={filterOpen}
setFilterOpen={setFilterOpen}
filters={filters}
setFilters={setFilters}
activeFilterCount={activeFilterCount}
clearFilters={clearFilters}
onFilterAddTag={onFilterAddTag}
onFilterToggleTag={onFilterToggleTag}
newEntryOpen={newEntryOpen}
setNewEntryOpen={setNewEntryOpen}
entryForm={entryForm}
setEntryForm={setEntryForm}
submitNewEntry={submitNewEntry}
amountInputRef={amountInputRef}
tagsInputRef={tagsInputRef}
newScheduleOpen={newScheduleOpen}
setNewScheduleOpen={setNewScheduleOpen}
scheduleForm={scheduleForm}
setScheduleForm={setScheduleForm}
submitNewSchedule={submitNewSchedule}
entryDetailsOpen={entryDetailsOpen}
setEntryDetailsOpen={setEntryDetailsOpen}
entryDetailsForm={entryDetailsForm}
setEntryDetailsForm={setEntryDetailsForm}
entryDetailsOriginal={entryDetailsOriginal}
hasEntryChanges={hasEntryChanges}
submitEntryUpdate={submitEntryUpdate}
entryRemovedTags={entryRemovedTags}
setEntryRemovedTags={setEntryRemovedTags}
prevEntry={prevEntry}
nextEntry={nextEntry}
selectedEntryIndex={selectedEntryIndex}
filteredEntriesCount={filteredEntries.length}
scheduleDetailsOpen={scheduleDetailsOpen}
setScheduleDetailsOpen={setScheduleDetailsOpen}
scheduleDetailsForm={scheduleDetailsForm}
setScheduleDetailsForm={setScheduleDetailsForm}
scheduleDetailsOriginal={scheduleDetailsOriginal}
hasScheduleChanges={hasScheduleChanges}
submitScheduleUpdate={submitScheduleUpdate}
scheduleRemovedTags={scheduleRemovedTags}
setScheduleRemovedTags={setScheduleRemovedTags}
confirmDeleteOpen={confirmDeleteOpen}
setConfirmDeleteOpen={setConfirmDeleteOpen}
deleteTarget={deleteTarget}
setDeleteTarget={setDeleteTarget}
confirmDelete={confirmDelete}
/>
</>
);
}