From d7be812ad0e7a8391913286fe2a0092aa67db5e5 Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 00:27:09 +0200 Subject: [PATCH] FEAT-6: Gesundheit/Gewicht/Fotos tab components + hand-rolled SVG LineChart (no chart dependency) --- .../src/components/GerbilHealthTab.tsx | 217 ++++++++++++++++++ .../src/components/GerbilPhotosTab.tsx | 118 ++++++++++ .../src/components/GerbilWeightTab.tsx | 148 ++++++++++++ .../src/components/LineChart.tsx | 121 ++++++++++ gerbil-manager-web/src/format/dates.ts | 9 + 5 files changed, 613 insertions(+) create mode 100644 gerbil-manager-web/src/components/GerbilHealthTab.tsx create mode 100644 gerbil-manager-web/src/components/GerbilPhotosTab.tsx create mode 100644 gerbil-manager-web/src/components/GerbilWeightTab.tsx create mode 100644 gerbil-manager-web/src/components/LineChart.tsx create mode 100644 gerbil-manager-web/src/format/dates.ts diff --git a/gerbil-manager-web/src/components/GerbilHealthTab.tsx b/gerbil-manager-web/src/components/GerbilHealthTab.tsx new file mode 100644 index 0000000..ef84026 --- /dev/null +++ b/gerbil-manager-web/src/components/GerbilHealthTab.tsx @@ -0,0 +1,217 @@ +/** + * 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(null) + const [form, setForm] = useState(emptyForm) + const [errors, setErrors] = useState>>({}) + + const save = useMutation((body: CreateHealthRecord) => + editing && editing !== 'new' + ? updateHealthRecord(editing, body) + : createHealthRecord(body), + ) + const removal = useMutation((recordId: string) => deleteHealthRecord(recordId)) + + const set = (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> = {} + 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 ( +
+ {editing === null && ( + + )} + + {editing !== null && ( +
+

{editing === 'new' ? t.createTitle : t.editTitle}

+ + + + + +