FEAT-12a: Abgabe page — Becken-default grouping + per-group listing composer, clipboard/zip export, AI stub
This commit is contained in:
@@ -1,11 +1,115 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { de } from '../strings/de'
|
||||
import { listGerbils } from '../api/gerbils'
|
||||
import { listColorVarieties } from '../api/lookups'
|
||||
import { condition } from '../api/gridify'
|
||||
import type { Gerbil } from '../api/types'
|
||||
import { useApi } from '../hooks/useApi'
|
||||
import { fromDisplayString, genotypeToFarbschlag } from '../genetics'
|
||||
import GroupComposer from '../components/GroupComposer'
|
||||
import './abgabe.css'
|
||||
|
||||
const UNGROUPED = 'ungrouped'
|
||||
|
||||
export default function AbgabePage() {
|
||||
// Fleshed out in the next FEAT-12a increments (grouping + listing composer + export).
|
||||
const t = de.pages.abgabe
|
||||
|
||||
const forSale = useApi(
|
||||
() =>
|
||||
listGerbils({
|
||||
filter: condition({ field: 'status', op: '==', value: 'ForSale' }),
|
||||
orderBy: 'name',
|
||||
page: 1,
|
||||
pageSize: 1000,
|
||||
}),
|
||||
[],
|
||||
)
|
||||
const colorVarieties = useApi(() => listColorVarieties(), [])
|
||||
|
||||
const animals = useMemo(() => forSale.data?.items ?? [], [forSale.data])
|
||||
const colorName = useMemo(
|
||||
() => new Map((colorVarieties.data ?? []).map((c) => [c.id, c.name])),
|
||||
[colorVarieties.data],
|
||||
)
|
||||
|
||||
// Manual group assignment, seeded from Becken (enclosure) occupancy.
|
||||
const [groupOf, setGroupOf] = useState<Record<string, string>>({})
|
||||
const assignment = (g: Gerbil): string =>
|
||||
groupOf[g.id] ?? g.enclosureId ?? UNGROUPED
|
||||
|
||||
// Ordered distinct group ids (in animal order) -> stable 1-based numbering.
|
||||
const groupIds = useMemo(() => {
|
||||
const seen: string[] = []
|
||||
for (const a of animals) {
|
||||
const gid = assignment(a)
|
||||
if (!seen.includes(gid)) seen.push(gid)
|
||||
}
|
||||
return seen
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [animals, groupOf])
|
||||
|
||||
const farbschlagOf = (g: Gerbil): string => {
|
||||
if (g.colorVarietyId && colorName.has(g.colorVarietyId)) return colorName.get(g.colorVarietyId)!
|
||||
if (g.genotype) {
|
||||
try {
|
||||
return genotypeToFarbschlag(fromDisplayString(g.genotype))
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
}
|
||||
return '—'
|
||||
}
|
||||
|
||||
function moveTo(gerbilId: string, groupId: string) {
|
||||
setGroupOf((s) => ({ ...s, [gerbilId]: groupId }))
|
||||
}
|
||||
|
||||
if (forSale.loading) return <p className="muted">{de.common.loading}</p>
|
||||
|
||||
return (
|
||||
<section className="page">
|
||||
<h2>{de.pages.abgabe.title}</h2>
|
||||
<p className="muted">{de.pages.abgabe.subtitle}</p>
|
||||
<h2>{t.title}</h2>
|
||||
<p className="muted">{t.subtitle}</p>
|
||||
|
||||
{animals.length === 0 ? (
|
||||
<p className="muted">{t.empty}</p>
|
||||
) : (
|
||||
<>
|
||||
{/* Regrouping roster */}
|
||||
<div className="abgabe-roster">
|
||||
<h3>{t.grouping.title}</h3>
|
||||
<p className="muted">{t.grouping.byEnclosure}</p>
|
||||
<ul className="abgabe-roster__list">
|
||||
{animals.map((a) => (
|
||||
<li key={a.id}>
|
||||
<span>{a.name}</span>
|
||||
<select
|
||||
value={assignment(a)}
|
||||
onChange={(e) => moveTo(a.id, e.target.value)}
|
||||
aria-label={t.grouping.moveTo}
|
||||
>
|
||||
{groupIds.map((gid, i) => (
|
||||
<option key={gid} value={gid}>
|
||||
{t.grouping.groupLabel} {i + 1}
|
||||
</option>
|
||||
))}
|
||||
<option value={`new-${a.id}`}>{t.grouping.newGroup}</option>
|
||||
</select>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{groupIds.map((gid, i) => (
|
||||
<GroupComposer
|
||||
key={gid}
|
||||
groupNumber={i + 1}
|
||||
animals={animals.filter((a) => assignment(a) === gid)}
|
||||
farbschlagOf={farbschlagOf}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user