Files
GerbilManager/gerbil-manager-web/src/components/GerbilHealthTab.tsx

218 lines
6.8 KiB
TypeScript

/**
* FEAT-6: Gesundheit-Tab — Gesundheitseinträge eines Tieres:
* Liste (Datum absteigend) + Inline-Formular zum Anlegen/Bearbeiten,
* Löschen mit Bestätigung. Deutsch durchgehend.
*/
import { useState, type FormEvent } from 'react'
import { de } from '../strings/de'
import {
createHealthRecord,
deleteHealthRecord,
listHealthRecords,
updateHealthRecord,
HEALTH_RECORD_TYPES,
type CreateHealthRecord,
type HealthRecord,
type HealthRecordType,
} from '../api/healthRecords'
import { useApi, useMutation } from '../hooks/useApi'
import { formatDate } from '../format/labels'
import { todayISO } from '../format/dates'
interface FormState {
date: string
type: HealthRecordType
description: string
veterinarian: string
}
const emptyForm = (): FormState => ({
date: todayISO(),
type: 'Examination',
description: '',
veterinarian: '',
})
export default function GerbilHealthTab({ gerbilId }: { gerbilId: string }) {
const t = de.pages.tierTabs.health
const ta = de.pages.tierTabs.actions
const records = useApi(() => listHealthRecords(gerbilId), [gerbilId])
// editing: null = kein Formular, 'new' = anlegen, sonst Record-Id (bearbeiten)
const [editing, setEditing] = useState<string | null>(null)
const [form, setForm] = useState<FormState>(emptyForm)
const [errors, setErrors] = useState<Partial<Record<keyof FormState, string>>>({})
const save = useMutation((body: CreateHealthRecord) =>
editing && editing !== 'new'
? updateHealthRecord(editing, body)
: createHealthRecord(body),
)
const removal = useMutation((recordId: string) => deleteHealthRecord(recordId))
const set = <K extends keyof FormState>(key: K, value: FormState[K]) =>
setForm((f) => ({ ...f, [key]: value }))
function openCreate() {
setForm(emptyForm())
setErrors({})
setEditing('new')
}
function openEdit(r: HealthRecord) {
setForm({
date: r.date,
type: r.type,
description: r.description,
veterinarian: r.veterinarian ?? '',
})
setErrors({})
setEditing(r.id)
}
async function onSubmit(e: FormEvent) {
e.preventDefault()
const next: Partial<Record<keyof FormState, string>> = {}
if (!form.date) next.date = t.validation.dateRequired
if (form.description.trim() === '') next.description = t.validation.descriptionRequired
setErrors(next)
if (Object.keys(next).length > 0) return
await save.run({
gerbilId,
date: form.date,
type: form.type,
description: form.description.trim(),
veterinarian: form.veterinarian.trim() === '' ? null : form.veterinarian.trim(),
})
setEditing(null)
records.reload()
}
async function onDelete(recordId: string) {
if (!window.confirm(t.deleteConfirm)) return
await removal.run(recordId)
records.reload()
}
return (
<div>
{editing === null && (
<button type="button" className="btn btn--primary" onClick={openCreate}>
+ {t.newButton}
</button>
)}
{editing !== null && (
<form className="form" onSubmit={onSubmit} noValidate>
<h4>{editing === 'new' ? t.createTitle : t.editTitle}</h4>
<label className="field">
<span>{t.fields.date} *</span>
<input
type="date"
className="input"
value={form.date}
onChange={(e) => set('date', e.target.value)}
aria-invalid={Boolean(errors.date)}
/>
{errors.date && <small className="error-text">{errors.date}</small>}
</label>
<label className="field">
<span>{t.fields.type}</span>
<select
value={form.type}
onChange={(e) => set('type', e.target.value as HealthRecordType)}
>
{HEALTH_RECORD_TYPES.map((ty) => (
<option key={ty} value={ty}>
{t.typeLabels[ty]}
</option>
))}
</select>
</label>
<label className="field">
<span>{t.fields.description} *</span>
<textarea
value={form.description}
onChange={(e) => set('description', e.target.value)}
aria-invalid={Boolean(errors.description)}
/>
{errors.description && <small className="error-text">{errors.description}</small>}
</label>
<label className="field">
<span>{t.fields.veterinarian}</span>
<input
className="input"
value={form.veterinarian}
onChange={(e) => set('veterinarian', e.target.value)}
/>
</label>
{save.error && <div className="alert alert--error">{save.error}</div>}
<div className="form-actions">
<button type="submit" className="btn btn--primary" disabled={save.pending}>
{save.pending ? ta.saving : ta.save}
</button>
<button type="button" className="btn" onClick={() => setEditing(null)}>
{ta.cancel}
</button>
</div>
</form>
)}
{records.loading && <p className="muted">{de.common.loading}</p>}
{records.error && (
<div className="alert alert--error">
<span>{records.error}</span>
<button type="button" className="btn" onClick={records.reload}>
{de.common.retry}
</button>
</div>
)}
{removal.error && <div className="alert alert--error">{removal.error}</div>}
{!records.loading && !records.error && (records.data ?? []).length === 0 && (
<p className="muted">{t.empty}</p>
)}
{(records.data ?? []).length > 0 && (
<ul className="card-list" style={{ marginTop: '1rem' }}>
{(records.data ?? []).map((r) => (
<li key={r.id} className="record-card">
<div className="record-card__head">
<span className="record-card__date">{formatDate(r.date)}</span>
<span className="badge">{t.typeLabels[r.type]}</span>
</div>
<p className="record-card__body">{r.description}</p>
{r.veterinarian && (
<p className="muted">
{t.fields.veterinarian}: {r.veterinarian}
</p>
)}
<div className="record-card__actions">
<button type="button" className="btn" onClick={() => openEdit(r)}>
{ta.edit}
</button>
<button
type="button"
className="btn btn--danger"
onClick={() => onDelete(r.id)}
disabled={removal.pending}
>
{ta.delete}
</button>
</div>
</li>
))}
</ul>
)}
</div>
)
}