149 lines
4.9 KiB
TypeScript
149 lines
4.9 KiB
TypeScript
/**
|
|
* FEAT-6: Gewicht-Tab — Schnell-Erfassung (Datum vorbelegt mit heute,
|
|
* großes touch-freundliches Gramm-Feld), Wachstumskurve (eigenes SVG,
|
|
* keine Chart-Bibliothek) + Verlaufsliste mit Löschen.
|
|
*/
|
|
import { useState, type FormEvent } from 'react'
|
|
import { de } from '../strings/de'
|
|
import {
|
|
createWeightRecord,
|
|
deleteWeightRecord,
|
|
listWeightRecords,
|
|
} from '../api/weightRecords'
|
|
import { useApi, useMutation } from '../hooks/useApi'
|
|
import { formatDate } from '../format/labels'
|
|
import { todayISO } from '../format/dates'
|
|
import LineChart from './LineChart'
|
|
|
|
export default function GerbilWeightTab({ gerbilId }: { gerbilId: string }) {
|
|
const t = de.pages.tierTabs.weight
|
|
|
|
const records = useApi(() => listWeightRecords(gerbilId), [gerbilId])
|
|
|
|
const [date, setDate] = useState(todayISO())
|
|
const [grams, setGrams] = useState('')
|
|
const [errors, setErrors] = useState<{ date?: string; grams?: string }>({})
|
|
|
|
const add = useMutation((weightGrams: number) =>
|
|
createWeightRecord({ gerbilId, date, weightGrams }),
|
|
)
|
|
const removal = useMutation((recordId: string) => deleteWeightRecord(recordId))
|
|
|
|
async function onSubmit(e: FormEvent) {
|
|
e.preventDefault()
|
|
const value = Number(grams)
|
|
const next: { date?: string; grams?: string } = {}
|
|
if (!date) next.date = t.validation.dateRequired
|
|
if (grams.trim() === '' || Number.isNaN(value)) next.grams = t.validation.weightRequired
|
|
else if (value < 1 || value > 500 || !Number.isInteger(value))
|
|
next.grams = t.validation.weightRange
|
|
setErrors(next)
|
|
if (Object.keys(next).length > 0) return
|
|
|
|
await add.run(value)
|
|
setGrams('')
|
|
setDate(todayISO())
|
|
records.reload()
|
|
}
|
|
|
|
async function onDelete(recordId: string) {
|
|
if (!window.confirm(t.deleteConfirm)) return
|
|
await removal.run(recordId)
|
|
records.reload()
|
|
}
|
|
|
|
const items = records.data ?? []
|
|
|
|
return (
|
|
<div>
|
|
{/* Schnell-Erfassung: ein Daumen, eine Rennmaus in der anderen Hand. */}
|
|
<form className="weight-quick-add" onSubmit={onSubmit} noValidate>
|
|
<h4>{t.addTitle}</h4>
|
|
<div className="weight-quick-add__row">
|
|
<label className="field">
|
|
<span>{t.fields.date}</span>
|
|
<input
|
|
type="date"
|
|
className="input"
|
|
value={date}
|
|
onChange={(e) => setDate(e.target.value)}
|
|
aria-invalid={Boolean(errors.date)}
|
|
/>
|
|
</label>
|
|
<label className="field">
|
|
<span>{t.fields.weightGrams}</span>
|
|
<input
|
|
type="number"
|
|
inputMode="numeric"
|
|
min={1}
|
|
max={500}
|
|
step={1}
|
|
className="input input--xl"
|
|
value={grams}
|
|
placeholder="0"
|
|
onChange={(e) => setGrams(e.target.value)}
|
|
aria-invalid={Boolean(errors.grams)}
|
|
/>
|
|
</label>
|
|
<button type="submit" className="btn btn--primary" disabled={add.pending}>
|
|
{add.pending ? t.adding : t.addButton}
|
|
</button>
|
|
</div>
|
|
{errors.date && <small className="error-text">{errors.date}</small>}
|
|
{errors.grams && <small className="error-text">{errors.grams}</small>}
|
|
{add.error && <div className="alert alert--error">{add.error}</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 && items.length === 0 && (
|
|
<p className="muted">{t.empty}</p>
|
|
)}
|
|
|
|
{items.length > 0 && (
|
|
<>
|
|
<h4>{t.chartTitle}</h4>
|
|
{items.length >= 2 ? (
|
|
<LineChart
|
|
title={t.chartTitle}
|
|
unit={t.gramsSuffix}
|
|
points={items.map((r) => ({ date: r.date, value: r.weightGrams }))}
|
|
/>
|
|
) : (
|
|
<p className="muted">{t.chartNeedsTwo}</p>
|
|
)}
|
|
|
|
<h4>{t.historyTitle}</h4>
|
|
<ul className="card-list">
|
|
{items.map((r) => (
|
|
<li key={r.id} className="record-card record-card--row">
|
|
<span className="record-card__date">{formatDate(r.date)}</span>
|
|
<strong>
|
|
{r.weightGrams} {t.gramsSuffix}
|
|
</strong>
|
|
<button
|
|
type="button"
|
|
className="btn btn--danger"
|
|
onClick={() => onDelete(r.id)}
|
|
disabled={removal.pending}
|
|
>
|
|
{de.common.delete}
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|