From 13185a5281668f3530d1b0bb8b2ae7c9093cc2a5 Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 11 Mar 2026 23:48:35 -0500 Subject: [PATCH] Roadmap 2,3,4 --- client/src/components/GeneticPanelCard.jsx | 97 +++++++++++ client/src/components/GeneticTestForm.jsx | 157 +++++++++++++++++ client/src/components/PedigreeView.jsx | 23 ++- client/src/pages/DogDetail.jsx | 17 ++ client/src/pages/DogList.jsx | 13 ++ client/src/pages/PairingSimulator.jsx | 46 ++++- server/db/init.js | 23 +++ server/routes/health.js | 86 +++++++-- server/routes/pedigree.js | 192 +++++++++++++++------ 9 files changed, 575 insertions(+), 79 deletions(-) create mode 100644 client/src/components/GeneticPanelCard.jsx create mode 100644 client/src/components/GeneticTestForm.jsx diff --git a/client/src/components/GeneticPanelCard.jsx b/client/src/components/GeneticPanelCard.jsx new file mode 100644 index 0000000..4d93d61 --- /dev/null +++ b/client/src/components/GeneticPanelCard.jsx @@ -0,0 +1,97 @@ +import { useState, useEffect } from 'react' +import { Dna, Plus } from 'lucide-react' +import axios from 'axios' +import GeneticTestForm from './GeneticTestForm' + +const RESULT_STYLES = { + clear: { bg: 'rgba(52,199,89,0.15)', color: 'var(--success)' }, + carrier: { bg: 'rgba(255,159,10,0.15)', color: 'var(--warning)' }, + affected: { bg: 'rgba(255,59,48,0.15)', color: 'var(--danger)' }, + not_tested: { bg: 'var(--bg-tertiary)', color: 'var(--text-muted)' } +} + +export default function GeneticPanelCard({ dogId }) { + const [data, setData] = useState(null) + const [error, setError] = useState(false) + const [loading, setLoading] = useState(true) + const [showForm, setShowForm] = useState(false) + const [editingRecord, setEditingRecord] = useState(null) + + const fetchGenetics = () => { + setLoading(true) + axios.get(`/api/genetics/dog/${dogId}`) + .then(res => setData(res.data)) + .catch(() => setError(true)) + .finally(() => setLoading(false)) + } + + useEffect(() => { fetchGenetics() }, [dogId]) + + const openAdd = () => { setEditingRecord(null); setShowForm(true) } + const openEdit = (rec) => { setEditingRecord(rec); setShowForm(true) } + const handleSaved = () => { setShowForm(false); fetchGenetics() } + + if (error || (!loading && !data)) return null + + const panel = data?.panel || [] + + return ( +
+
+

+ DNA Genetics Panel +

+ +
+ + {loading ? ( +
Loading...
+ ) : ( +
+ {panel.map(item => { + const style = RESULT_STYLES[item.result] || RESULT_STYLES.not_tested + // Pass the whole test record if it exists so we can edit it + const record = item.id ? item : { marker: item.marker, result: 'not_tested' } + + return ( +
openEdit(record)} + style={{ + padding: '0.5rem 0.75rem', + background: style.bg, + border: `1px solid ${style.color}44`, + borderRadius: 'var(--radius-sm)', + cursor: 'pointer', + transition: 'transform 0.1s ease', + }} + onMouseEnter={e => e.currentTarget.style.transform = 'translateY(-2px)'} + onMouseLeave={e => e.currentTarget.style.transform = 'translateY(0)'} + > +
+ {item.marker} +
+
+ {item.result.replace('_', ' ')} +
+
+ ) + })} +
+ )} + + {showForm && ( + setShowForm(false)} + onSave={handleSaved} + /> + )} +
+ ) +} diff --git a/client/src/components/GeneticTestForm.jsx b/client/src/components/GeneticTestForm.jsx new file mode 100644 index 0000000..b99a4fc --- /dev/null +++ b/client/src/components/GeneticTestForm.jsx @@ -0,0 +1,157 @@ +import { useState } from 'react' +import { X } from 'lucide-react' +import axios from 'axios' + +const GR_MARKERS = [ + { value: 'PRA1', label: 'PRA1' }, + { value: 'PRA2', label: 'PRA2' }, + { value: 'prcd-PRA', label: 'prcd-PRA' }, + { value: 'GR-PRA1', label: 'GR-PRA1' }, + { value: 'GR-PRA2', label: 'GR-PRA2' }, + { value: 'ICH1', label: 'ICH1 (Ichthyosis 1)' }, + { value: 'ICH2', label: 'ICH2 (Ichthyosis 2)' }, + { value: 'NCL', label: 'Neuronal Ceroid Lipofuscinosis' }, + { value: 'DM', label: 'Degenerative Myelopathy' }, + { value: 'MD', label: 'Muscular Dystrophy' } +] + +const RESULTS = [ + { value: 'clear', label: 'Clear / Normal' }, + { value: 'carrier', label: 'Carrier (1 copy)' }, + { value: 'affected', label: 'Affected / At Risk (2 copies)' }, + { value: 'not_tested', label: 'Not Tested' } +] + +const EMPTY = { + test_provider: 'Embark', + marker: 'PRA1', + result: 'clear', + test_date: '', + document_url: '', + notes: '' +} + +export default function GeneticTestForm({ dogId, record, onClose, onSave }) { + const [form, setForm] = useState(record || { ...EMPTY, dog_id: dogId }) + const [saving, setSaving] = useState(false) + const [error, setError] = useState(null) + + const set = (k, v) => setForm(f => ({ ...f, [k]: v })) + + const handleSubmit = async (e) => { + e.preventDefault() + setSaving(true) + setError(null) + + // If not tested, don't save + if (form.result === 'not_tested' && !record) { + setError('Cannot save a "Not Tested" result. Please just delete the record if it exists.') + setSaving(false) + return + } + + try { + if (record && record.id) { + if (form.result === 'not_tested') { + // If changed to not_tested, just delete it + await axios.delete(`/api/genetics/${record.id}`) + } else { + await axios.put(`/api/genetics/${record.id}`, form) + } + } else { + await axios.post('/api/genetics', { ...form, dog_id: dogId }) + } + onSave() + } catch (err) { + setError(err.response?.data?.error || 'Failed to save genetic 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' } + + return ( +
+
+
+

{record && record.id ? 'Edit' : 'Add'} Genetic Result

+ +
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+ + set('test_provider', e.target.value)} /> +
+
+ + set('test_date', e.target.value)} /> +
+
+ +
+ + set('document_url', e.target.value)} /> +
+ +
+ +