Files
GerbilManager/gerbil-manager-web/src/pages/KontaktDetailPage.tsx

167 lines
5.3 KiB
TypeScript

/**
* FEAT-2: Kontakt-Detailseite — Stammdaten + verknüpfte Tiere
* (Herkunft von / Abnehmer von, mit Farbschlag + Link zur Rennmaus),
* Löschen mit Bestätigung; Referenz-Konflikt 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 { deleteContact, getContact } from '../api/contacts'
import { listGerbils } from '../api/gerbils'
import { listColorVarieties } from '../api/lookups'
import { condition } from '../api/gridify'
import { useApi, useMutation } from '../hooks/useApi'
export default function KontaktDetailPage() {
const t = de.pages.kontakte
const { id = '' } = useParams()
const navigate = useNavigate()
const [deleteError, setDeleteError] = useState<string | null>(null)
const contact = useApi(() => getContact(id), [id])
// Verknüpfte Tiere: Kontakt ist Herkunft ODER Abnehmer (Gridify-OR via |).
const linked = useApi(
() =>
listGerbils({
filter: [
condition({ field: 'originContactId', op: '==', value: id }),
condition({ field: 'receiverContactId', op: '==', value: id }),
].join('|'),
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(() => deleteContact(id))
async function onDelete() {
if (!window.confirm(t.delete.confirmMessage)) return
setDeleteError(null)
const result = await removal.run()
if (result.ok) {
navigate('/kontakte')
} else if (result.cause instanceof ApiError && result.cause.status === 409) {
// Backend meldet Konflikt, wenn der Kontakt noch referenziert wird.
setDeleteError(t.delete.conflict)
} else {
setDeleteError(result.error)
}
}
if (contact.loading) return <p className="muted">{de.common.loading}</p>
if (contact.error || !contact.data) {
return (
<section className="page">
<p className="muted">{contact.error ?? t.detail.notFound}</p>
<Link to="/kontakte" className="btn">
{t.detail.back}
</Link>
</section>
)
}
const c = contact.data
const animals = linked.data?.items ?? []
return (
<section className="page">
<header className="page-head">
<h2>{c.name}</h2>
<div className="head-actions">
<Link to={`/kontakte/${c.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="/kontakte" className="btn">
{t.detail.back}
</Link>
</div>
</header>
{deleteError && <div className="alert alert--error">{deleteError}</div>}
<dl className="def-list">
<div className="def-row">
<dt>Rolle</dt>
<dd>
{[
c.isBreeder ? 'Züchter' : null,
c.isReceiver ? 'Abnehmer' : null,
]
.filter(Boolean)
.join(' · ') || '—'}
</dd>
</div>
<div className="def-row">
<dt>{t.fields.email}</dt>
<dd>{c.email ?? '—'}</dd>
</div>
<div className="def-row">
<dt>{t.fields.phone}</dt>
<dd>{c.phone ?? '—'}</dd>
</div>
<div className="def-row">
<dt>{t.fields.address}</dt>
<dd>{c.address ?? '—'}</dd>
</div>
<div className="def-row">
<dt>{t.fields.notes}</dt>
<dd>{c.notes ?? '—'}</dd>
</div>
</dl>
<h3>{t.linked.title}</h3>
{linked.loading && <p className="muted">{de.common.loading}</p>}
{linked.error && (
<div className="alert alert--error">
<span>{linked.error}</span>
<button type="button" className="btn" onClick={linked.reload}>
{de.common.retry}
</button>
</div>
)}
{!linked.loading && !linked.error && animals.length === 0 && (
<p className="muted">{t.linked.empty}</p>
)}
{animals.length > 0 && (
<ul className="card-list">
{animals.map((g) => (
<li key={g.id}>
<Link to={`/rennmaeuse/${g.id}`} className="gerbil-card">
{/* z. B. "Herkunft von Flecki" bzw. "Herkunft von · Abnehmer von Flecki" */}
<span className="gerbil-card__name">
{[
g.originContactId === id ? t.linked.originRole : null,
g.receiverContactId === id ? t.linked.receiverRole : null,
]
.filter(Boolean)
.join(' · ')}{' '}
{g.name}
</span>
<span className="gerbil-card__meta">
{g.colorVarietyId ? (colorNameById.get(g.colorVarietyId) ?? '—') : '—'}
</span>
</Link>
</li>
))}
</ul>
)}
</section>
)
}