diff --git a/GerbilManagerWebAPI/Import/IngestResolvedService.cs b/GerbilManagerWebAPI/Import/IngestResolvedService.cs index 9aec54f..604a7c8 100644 --- a/GerbilManagerWebAPI/Import/IngestResolvedService.cs +++ b/GerbilManagerWebAPI/Import/IngestResolvedService.cs @@ -63,6 +63,7 @@ namespace GerbilManagerWebAPI.Import _db.HealthRecords.RemoveRange(_db.HealthRecords); _db.Gerbils.RemoveRange(_db.Gerbils); _db.Litters.RemoveRange(_db.Litters); + _db.Contacts.RemoveRange(_db.Contacts); await _db.SaveChangesAsync(); } else @@ -80,6 +81,7 @@ namespace GerbilManagerWebAPI.Import await _db.HealthRecords.ExecuteDeleteAsync(); await _db.Gerbils.ExecuteDeleteAsync(); await _db.Litters.ExecuteDeleteAsync(); + await _db.Contacts.ExecuteDeleteAsync(); } // 2. Import Contacts (Add new ones, and update roles/details of existing ones) diff --git a/gerbil-manager-web/src/pages/KontaktePage.tsx b/gerbil-manager-web/src/pages/KontaktePage.tsx index 253240a..1f5d2e9 100644 --- a/gerbil-manager-web/src/pages/KontaktePage.tsx +++ b/gerbil-manager-web/src/pages/KontaktePage.tsx @@ -1,35 +1,48 @@ -/** FEAT-2: Kontakte-Liste — Gridify-Namenssuche + Paging, Link auf Detailseite. */ -import { useState } from 'react' +/** FEAT-2: Kontakte-Liste — Gridify-Namenssuche + Infinite Scroll, Link auf Detailseite. */ +import { useEffect, useRef, useState } from 'react' import { Link } from 'react-router-dom' import { de } from '../strings/de' import { listContactsPaged } from '../api/contacts' -import { condition, type GridifyQuery } from '../api/gridify' -import { useApi } from '../hooks/useApi' +import { condition } from '../api/gridify' +import { useInfiniteList } from '../hooks/useInfiniteList' const PAGE_SIZE = 20 export default function KontaktePage() { const t = de.pages.kontakte const [search, setSearch] = useState('') - const [page, setPage] = useState(1) const filter = search.trim() ? condition({ field: 'name', op: 'contains', value: search.trim() }) : undefined - const query: GridifyQuery = { filter, orderBy: 'name', page, pageSize: PAGE_SIZE } + // Infinite scroll: one page at a time, accumulated. The trailing `id` makes the + // ordering a total order so OFFSET pagination never repeats/omits a row. + const list = useInfiniteList( + (page) => listContactsPaged({ filter, orderBy: 'name,id', page, pageSize: PAGE_SIZE }), + `${filter ?? ''}`, + ) + const { items, total, hasMore, loading, loadingMore, loadMore } = list - const contacts = useApi(() => listContactsPaged(query), [filter, page]) - - const total = contacts.data?.totalCount ?? 0 - const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)) - const items = contacts.data?.items ?? [] + const sentinelRef = useRef(null) + useEffect(() => { + const el = sentinelRef.current + if (!el) return + const io = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMore && !loading && !loadingMore) loadMore() + }, + { rootMargin: '300px 0px' }, + ) + io.observe(el) + return () => io.disconnect() + }, [hasMore, loading, loadingMore, loadMore]) return (

{t.title}

- {contacts.data && ( + {!loading && (

{total} {t.countLabel}

@@ -46,27 +59,22 @@ export default function KontaktePage() { className="input" placeholder={t.searchPlaceholder} value={search} - onChange={(e) => { - setSearch(e.target.value) - setPage(1) - }} + onChange={(e) => setSearch(e.target.value)} aria-label={t.fields.name} />
- {contacts.loading &&

{de.common.loading}

} - {contacts.error && ( + {loading &&

{de.common.loading}

} + {list.error && (
- {contacts.error} -
)} - {!contacts.loading && !contacts.error && items.length === 0 && ( -

{t.empty}

- )} + {!loading && !list.error && items.length === 0 &&

{t.empty}

} {items.length > 0 && (
    @@ -115,28 +123,12 @@ export default function KontaktePage() {
)} - {totalPages > 1 && ( - + {/* Infinite-scroll sentinel + "loading more" indicator. */} + {hasMore &&
)