feat/phase-4b-health-genetics #36

Merged
jason merged 8 commits from feat/phase-4b-health-genetics into master 2026-03-09 23:38:19 -05:00
Showing only changes of commit 56458340ea - Show all commits

View File

@@ -0,0 +1,194 @@
import { useState } from 'react'
import { X } from 'lucide-react'
import axios from 'axios'
const RECORD_TYPES = ['ofa_clearance', 'vaccination', 'exam', 'surgery', 'medication', 'other']
const OFA_TEST_TYPES = [
{ value: 'hip_ofa', label: 'Hip - OFA' },
{ value: 'hip_pennhip', label: 'Hip - PennHIP' },
{ value: 'elbow_ofa', label: 'Elbow - OFA' },
{ value: 'heart_ofa', label: 'Heart - OFA' },
{ value: 'heart_echo', label: 'Heart - Echo' },
{ value: 'eye_caer', label: 'Eyes - CAER' },
]
const OFA_RESULTS = ['Excellent', 'Good', 'Fair', 'Mild', 'Moderate', 'Severe', 'Normal', 'Abnormal', 'Pass', 'Fail']
const EMPTY = {
record_type: 'ofa_clearance', test_type: 'hip_ofa', test_name: '',
test_date: '', ofa_result: 'Good', ofa_number: '', performed_by: '',
expires_at: '', result: '', vet_name: '', next_due: '', notes: '', document_url: '',
}
export default function HealthRecordForm({ dogId, record, onClose, onSave }) {
const [form, setForm] = useState(record || { ...EMPTY, dog_id: dogId })
const [saving, setSaving] = useState(false)
const [error, setError] = useState(null)
const isOFA = form.record_type === 'ofa_clearance'
const set = (k, v) => setForm(f => ({ ...f, [k]: v }))
const handleSubmit = async (e) => {
e.preventDefault()
setSaving(true)
setError(null)
try {
if (record && record.id) {
await axios.put(`/api/health/${record.id}`, form)
} else {
await axios.post('/api/health', { ...form, dog_id: dogId })
}
onSave()
} catch (err) {
setError(err.response?.data?.error || 'Failed to save record')
} finally {
setSaving(false)
}
}
const labelStyle = {
fontSize: '0.8rem', color: 'var(--text-muted)',
marginBottom: '0.25rem', display: 'block',
}
const inputStyle = {
width: '100%', background: 'var(--bg-primary)',
border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)',
padding: '0.5rem 0.75rem', color: 'var(--text-primary)', fontSize: '0.9rem',
boxSizing: 'border-box',
}
const fw = { display: 'flex', flexDirection: 'column', gap: '0.25rem' }
const grid2 = { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }
return (
<div style={{
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)',
backdropFilter: 'blur(4px)', display: 'flex', alignItems: 'center',
justifyContent: 'center', zIndex: 1000, padding: '1rem',
}}>
<div className="card" style={{
width: '100%', maxWidth: '560px', maxHeight: '90vh',
overflowY: 'auto', position: 'relative',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' }}>
<h2 style={{ margin: 0 }}>{record && record.id ? 'Edit' : 'Add'} Health Record</h2>
<button className="btn-icon" onClick={onClose}><X size={20} /></button>
</div>
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{/* Record type */}
<div style={fw}>
<label style={labelStyle}>Record Type</label>
<select style={inputStyle} value={form.record_type} onChange={e => set('record_type', e.target.value)}>
{RECORD_TYPES.map(t => (
<option key={t} value={t}>
{t.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}
</option>
))}
</select>
</div>
{isOFA ? (
<>
<div style={grid2}>
<div style={fw}>
<label style={labelStyle}>OFA Test Type</label>
<select style={inputStyle} value={form.test_type} onChange={e => set('test_type', e.target.value)}>
{OFA_TEST_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
</select>
</div>
<div style={fw}>
<label style={labelStyle}>OFA Result</label>
<select style={inputStyle} value={form.ofa_result} onChange={e => set('ofa_result', e.target.value)}>
{OFA_RESULTS.map(r => <option key={r} value={r}>{r}</option>)}
</select>
</div>
</div>
<div style={grid2}>
<div style={fw}>
<label style={labelStyle}>OFA Number</label>
<input style={inputStyle} placeholder="GR-12345E24M-VPI" value={form.ofa_number}
onChange={e => set('ofa_number', e.target.value)} />
</div>
<div style={fw}>
<label style={labelStyle}>Performed By</label>
<input style={inputStyle} placeholder="Radiologist / cardiologist" value={form.performed_by}
onChange={e => set('performed_by', e.target.value)} />
</div>
</div>
<div style={grid2}>
<div style={fw}>
<label style={labelStyle}>Test Date *</label>
<input style={inputStyle} type="date" required value={form.test_date}
onChange={e => set('test_date', e.target.value)} />
</div>
<div style={fw}>
<label style={labelStyle}>Expires At</label>
<input style={inputStyle} type="date" value={form.expires_at}
onChange={e => set('expires_at', e.target.value)} />
</div>
</div>
</>
) : (
<>
<div style={fw}>
<label style={labelStyle}>Test / Procedure Name</label>
<input style={inputStyle} placeholder="e.g. Rabies, Bordetella..." value={form.test_name}
onChange={e => set('test_name', e.target.value)} />
</div>
<div style={grid2}>
<div style={fw}>
<label style={labelStyle}>Date *</label>
<input style={inputStyle} type="date" required value={form.test_date}
onChange={e => set('test_date', e.target.value)} />
</div>
<div style={fw}>
<label style={labelStyle}>Next Due</label>
<input style={inputStyle} type="date" value={form.next_due}
onChange={e => set('next_due', e.target.value)} />
</div>
</div>
<div style={grid2}>
<div style={fw}>
<label style={labelStyle}>Result</label>
<input style={inputStyle} placeholder="Normal, Pass, etc." value={form.result}
onChange={e => set('result', e.target.value)} />
</div>
<div style={fw}>
<label style={labelStyle}>Vet Name</label>
<input style={inputStyle} placeholder="Dr. Smith" value={form.vet_name}
onChange={e => set('vet_name', e.target.value)} />
</div>
</div>
</>
)}
<div style={fw}>
<label style={labelStyle}>Document URL (optional)</label>
<input style={inputStyle} type="url" placeholder="https://ofa.org/..." value={form.document_url}
onChange={e => set('document_url', e.target.value)} />
</div>
<div style={fw}>
<label style={labelStyle}>Notes</label>
<textarea style={{ ...inputStyle, minHeight: '70px', resize: 'vertical' }}
value={form.notes} onChange={e => set('notes', e.target.value)} />
</div>
{error && (
<div style={{
color: 'var(--danger)', fontSize: '0.85rem', padding: '0.5rem 0.75rem',
background: 'rgba(255,59,48,0.1)', borderRadius: 'var(--radius-sm)',
}}>{error}</div>
)}
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'flex-end' }}>
<button type="button" className="btn btn-ghost" onClick={onClose}>Cancel</button>
<button type="submit" className="btn btn-primary" disabled={saving}>
{saving ? 'Saving...' : record && record.id ? 'Save Changes' : 'Add Record'}
</button>
</div>
</form>
</div>
</div>
)
}