214 lines
6.8 KiB
TypeScript
214 lines
6.8 KiB
TypeScript
/**
|
|
* FEAT-2: Becken-Detailseite — Stammdaten, BELEGUNG (welche Tiere wohnen
|
|
* aktuell hier, mit Farbschlag + Link zur Rennmaus), Löschen mit
|
|
* Bestätigung; Konflikt (Becken nicht leer) wird deutsch gemeldet.
|
|
*/
|
|
import { useMemo, useState } from 'react'
|
|
import { Link, useNavigate, useParams } from 'react-router-dom'
|
|
import { de } from '../strings/de'
|
|
import { ApiError } from '../api/client'
|
|
import { deleteEnclosure, getEnclosure, markEnclosureCleaned } from '../api/enclosures'
|
|
import { listGerbils } from '../api/gerbils'
|
|
import { listColorVarieties } from '../api/lookups'
|
|
import { condition } from '../api/gridify'
|
|
import { useApi, useMutation } from '../hooks/useApi'
|
|
import { formatDate } from '../format/labels'
|
|
import { useToast } from '../components/toast'
|
|
import EnclosurePhotosSection from '../components/EnclosurePhotosSection'
|
|
|
|
/** true, wenn die nächste fällige Reinigung am/vor heute liegt. */
|
|
function isCleaningDue(nextCleaningDate: string | null): boolean {
|
|
if (!nextCleaningDate) return false
|
|
const today = new Date().toISOString().slice(0, 10)
|
|
return nextCleaningDate <= today
|
|
}
|
|
|
|
export default function BeckenDetailPage() {
|
|
const t = de.pages.becken
|
|
const { id = '' } = useParams()
|
|
const navigate = useNavigate()
|
|
const toast = useToast()
|
|
const [deleteError, setDeleteError] = useState<string | null>(null)
|
|
|
|
const enclosure = useApi(() => getEnclosure(id), [id])
|
|
// Belegung: alle Tiere, deren aktuelles Becken dieses ist.
|
|
const occupants = useApi(
|
|
() =>
|
|
listGerbils({
|
|
filter: condition({ field: 'enclosureId', op: '==', value: id }),
|
|
orderBy: 'name',
|
|
page: 1,
|
|
pageSize: 500,
|
|
}),
|
|
[id],
|
|
)
|
|
const colorVarieties = useApi(() => listColorVarieties(), [])
|
|
const colorNameById = useMemo(
|
|
() => new Map((colorVarieties.data ?? []).map((c) => [c.id, c.name])),
|
|
[colorVarieties.data],
|
|
)
|
|
|
|
const removal = useMutation(() => deleteEnclosure(id))
|
|
const cleaning = useMutation(() => markEnclosureCleaned(id))
|
|
|
|
async function onMarkCleaned() {
|
|
const result = await cleaning.run()
|
|
if (result.ok) {
|
|
toast.success(t.cleaning.marked)
|
|
enclosure.reload()
|
|
} else {
|
|
toast.error(result.error)
|
|
}
|
|
}
|
|
|
|
async function onDelete() {
|
|
if (!window.confirm(t.delete.confirmMessage)) return
|
|
setDeleteError(null)
|
|
const result = await removal.run()
|
|
if (result.ok) {
|
|
navigate('/gehege')
|
|
} else if (result.cause instanceof ApiError && result.cause.status === 409) {
|
|
// Backend meldet Konflikt, wenn noch Tiere im Becken wohnen.
|
|
setDeleteError(t.delete.conflict)
|
|
} else {
|
|
setDeleteError(result.error)
|
|
}
|
|
}
|
|
|
|
if (enclosure.loading) return <p className="muted">{de.common.loading}</p>
|
|
if (enclosure.error || !enclosure.data) {
|
|
return (
|
|
<section className="page">
|
|
<p className="muted">{enclosure.error ?? t.detail.notFound}</p>
|
|
<Link to="/gehege" className="btn">
|
|
{t.detail.back}
|
|
</Link>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
const e = enclosure.data
|
|
const animals = occupants.data?.items ?? []
|
|
const count = occupants.data?.totalCount ?? 0
|
|
|
|
return (
|
|
<section className="page">
|
|
<header className="page-head">
|
|
<div>
|
|
<h2>{e.name}</h2>
|
|
{occupants.data && (
|
|
<p className="muted">
|
|
{count} {count === 1 ? t.occupancy.countOne : t.occupancy.countMany}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="head-actions">
|
|
<Link to={`/gehege/${e.id}/bearbeiten`} className="btn btn--primary">
|
|
{t.detail.edit}
|
|
</Link>
|
|
<button
|
|
type="button"
|
|
className="btn btn--danger"
|
|
onClick={onDelete}
|
|
disabled={removal.pending}
|
|
>
|
|
{t.delete.action}
|
|
</button>
|
|
<Link to="/gehege" className="btn">
|
|
{t.detail.back}
|
|
</Link>
|
|
</div>
|
|
</header>
|
|
|
|
{deleteError && <div className="alert alert--error">{deleteError}</div>}
|
|
|
|
{(e.notes || e.size || e.capacity != null) && (
|
|
<dl className="def-list">
|
|
{e.notes && (
|
|
<div className="def-row">
|
|
<dt>{t.fields.notes}</dt>
|
|
<dd>{e.notes}</dd>
|
|
</div>
|
|
)}
|
|
{e.size && (
|
|
<div className="def-row">
|
|
<dt>{t.fields.size}</dt>
|
|
<dd>{e.size}</dd>
|
|
</div>
|
|
)}
|
|
{e.capacity != null && (
|
|
<div className="def-row">
|
|
<dt>{t.fields.capacity}</dt>
|
|
<dd>{t.cleaning.capacityUnit(e.capacity)}</dd>
|
|
</div>
|
|
)}
|
|
</dl>
|
|
)}
|
|
|
|
<h3>{t.cleaning.title}</h3>
|
|
{isCleaningDue(e.nextCleaningDate) && (
|
|
<div className="alert alert--warning" role="status">
|
|
{e.nextCleaningDate
|
|
? t.cleaning.dueSince(formatDate(e.nextCleaningDate))
|
|
: t.cleaning.due}
|
|
</div>
|
|
)}
|
|
<dl className="def-list">
|
|
<div className="def-row">
|
|
<dt>{t.fields.lastCleanedDate}</dt>
|
|
<dd>{e.lastCleanedDate ? formatDate(e.lastCleanedDate) : t.cleaning.neverCleaned}</dd>
|
|
</div>
|
|
<div className="def-row">
|
|
<dt>{t.fields.cleaningCycleDays}</dt>
|
|
<dd>{e.cleaningCycleDays != null ? t.cleaning.cycleUnit(e.cleaningCycleDays) : t.cleaning.noCycle}</dd>
|
|
</div>
|
|
{e.nextCleaningDate && (
|
|
<div className="def-row">
|
|
<dt>{t.fields.nextCleaningDate}</dt>
|
|
<dd>{formatDate(e.nextCleaningDate)}</dd>
|
|
</div>
|
|
)}
|
|
</dl>
|
|
<button
|
|
type="button"
|
|
className="btn"
|
|
onClick={onMarkCleaned}
|
|
disabled={cleaning.pending}
|
|
>
|
|
{cleaning.pending ? t.cleaning.marking : t.cleaning.markCleaned}
|
|
</button>
|
|
|
|
<h3>{t.photosTitle}</h3>
|
|
<EnclosurePhotosSection enclosureId={e.id} />
|
|
|
|
<h3>{t.occupancy.title}</h3>
|
|
{occupants.loading && <p className="muted">{de.common.loading}</p>}
|
|
{occupants.error && (
|
|
<div className="alert alert--error">
|
|
<span>{occupants.error}</span>
|
|
<button type="button" className="btn" onClick={occupants.reload}>
|
|
{de.common.retry}
|
|
</button>
|
|
</div>
|
|
)}
|
|
{!occupants.loading && !occupants.error && animals.length === 0 && (
|
|
<p className="muted">{t.occupancy.empty}</p>
|
|
)}
|
|
{animals.length > 0 && (
|
|
<ul className="card-list">
|
|
{animals.map((g) => (
|
|
<li key={g.id}>
|
|
<Link to={`/rennmaeuse/${g.id}`} className="gerbil-card">
|
|
<span className="gerbil-card__name">{g.name}</span>
|
|
<span className="gerbil-card__meta">
|
|
{g.colorVarietyId ? (colorNameById.get(g.colorVarietyId) ?? '—') : '—'}
|
|
</span>
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
)
|
|
}
|