FEAT-3: Würfe list page (year filter, sort by date, paging, parent names)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,152 @@
|
|||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import { de } from '../strings/de'
|
import { de } from '../strings/de'
|
||||||
|
import { listLitters } from '../api/litters'
|
||||||
|
import { listGerbils } from '../api/gerbils'
|
||||||
|
import { andFilter, condition, type GridifyQuery } from '../api/gridify'
|
||||||
|
import { useApi } from '../hooks/useApi'
|
||||||
|
import { formatDate } from '../format/labels'
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
|
type SortKey = 'dateDesc' | 'dateAsc'
|
||||||
|
const SORT_ORDER_BY: Record<SortKey, string> = {
|
||||||
|
dateDesc: 'date desc',
|
||||||
|
dateAsc: 'date',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Years offered in the filter: current year back through 15 years. */
|
||||||
|
function yearOptions(): number[] {
|
||||||
|
const current = new Date().getFullYear()
|
||||||
|
return Array.from({ length: 16 }, (_, i) => current - i)
|
||||||
|
}
|
||||||
|
|
||||||
export default function WuerfeListPage() {
|
export default function WuerfeListPage() {
|
||||||
// Fleshed out in the next FEAT-3 increment (Gridify list, year filter, sort).
|
const t = de.pages.litters
|
||||||
|
const [year, setYear] = useState('')
|
||||||
|
const [sort, setSort] = useState<SortKey>('dateDesc')
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
|
||||||
|
// Resolve parent names for the list rows.
|
||||||
|
const gerbils = useApi(() => listGerbils({ page: 1, pageSize: 1000, orderBy: 'name' }), [])
|
||||||
|
const nameById = useMemo(() => {
|
||||||
|
const map = new Map<string, string>()
|
||||||
|
for (const g of gerbils.data?.items ?? []) map.set(g.id, g.name)
|
||||||
|
return map
|
||||||
|
}, [gerbils.data])
|
||||||
|
|
||||||
|
const filter = andFilter(
|
||||||
|
year && condition({ field: 'date', op: '>=', value: `${year}-01-01` }),
|
||||||
|
year && condition({ field: 'date', op: '<=', value: `${year}-12-31` }),
|
||||||
|
)
|
||||||
|
const orderBy = SORT_ORDER_BY[sort]
|
||||||
|
const query: GridifyQuery = { filter: filter || undefined, orderBy, page, pageSize: PAGE_SIZE }
|
||||||
|
const litters = useApi(() => listLitters(query), [filter, orderBy, page])
|
||||||
|
|
||||||
|
const total = litters.data?.totalCount ?? 0
|
||||||
|
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))
|
||||||
|
const items = litters.data?.items ?? []
|
||||||
|
const parentName = (id: string | null) => (id ? (nameById.get(id) ?? t.detail.unknownParent) : '—')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="page">
|
<section className="page">
|
||||||
<h2>{de.pages.litters.title}</h2>
|
<header className="page-head">
|
||||||
|
<div>
|
||||||
|
<h2>{t.title}</h2>
|
||||||
|
{litters.data && (
|
||||||
|
<p className="muted">
|
||||||
|
{total} {t.countLabel}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Link to="/wuerfe/neu" className="btn btn--primary">
|
||||||
|
+ {t.newButton}
|
||||||
|
</Link>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="filters">
|
||||||
|
<label className="field">
|
||||||
|
<span>{t.filterYear}</span>
|
||||||
|
<select
|
||||||
|
value={year}
|
||||||
|
onChange={(e) => {
|
||||||
|
setYear(e.target.value)
|
||||||
|
setPage(1)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">{t.allYears}</option>
|
||||||
|
{yearOptions().map((y) => (
|
||||||
|
<option key={y} value={y}>
|
||||||
|
{y}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className="field">
|
||||||
|
<span>{de.pages.gerbils.filters.sortBy}</span>
|
||||||
|
<select value={sort} onChange={(e) => setSort(e.target.value as SortKey)}>
|
||||||
|
<option value="dateDesc">{t.sort.dateDesc}</option>
|
||||||
|
<option value="dateAsc">{t.sort.dateAsc}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{litters.loading && <p className="muted">{de.common.loading}</p>}
|
||||||
|
{litters.error && (
|
||||||
|
<div className="alert alert--error">
|
||||||
|
<span>{litters.error}</span>
|
||||||
|
<button type="button" className="btn" onClick={litters.reload}>
|
||||||
|
{de.common.retry}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!litters.loading && !litters.error && items.length === 0 && (
|
||||||
|
<p className="muted">{t.empty}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{items.length > 0 && (
|
||||||
|
<ul className="card-list">
|
||||||
|
{items.map((l) => (
|
||||||
|
<li key={l.id}>
|
||||||
|
<Link to={`/wuerfe/${l.id}`} className="gerbil-card">
|
||||||
|
<span className="gerbil-card__name">{l.name}</span>
|
||||||
|
<span className="gerbil-card__meta">{formatDate(l.date)}</span>
|
||||||
|
<span className="gerbil-card__meta">
|
||||||
|
{parentName(l.fatherId)} × {parentName(l.motherId)}
|
||||||
|
</span>
|
||||||
|
<span className="gerbil-card__meta">
|
||||||
|
{l.totalBorn != null ? `${l.totalBorn} ${t.fields.totalBorn}` : '—'}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<nav className="pager" aria-label="Seitennavigation">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn"
|
||||||
|
disabled={page <= 1}
|
||||||
|
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||||
|
>
|
||||||
|
{de.common.previous}
|
||||||
|
</button>
|
||||||
|
<span className="muted">
|
||||||
|
{de.common.page} {page} {de.common.of} {totalPages}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn"
|
||||||
|
disabled={page >= totalPages}
|
||||||
|
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||||
|
>
|
||||||
|
{de.common.next}
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user