diff --git a/ROADMAP.md b/ROADMAP.md index bc3f46b..d6c240c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -114,7 +114,7 @@ - [x] `GET /api/breeding/heat-cycles` endpoint - [x] `GET /api/breeding/heat-cycles/:id/suggestions` endpoint -- [x] **Projected Whelping Calendar Identifier** ✅ *(March 9, 2026 — v0.5.1)* +- [x] **Projected Whelping Calendar Identifier** ✅ *(March 9, 2026 − v0.5.1)* - [x] Gestation constants: earliest=58, expected=63, latest=65 days - [x] `getWwhelpDates(cycle)` client-side helper (no extra API call) - [x] Indigo whelp window cells (days 58–63) on calendar grid @@ -129,7 +129,7 @@ --- -## ✅ Phase 4a: Champion & Settings (COMPLETE — v0.6.0) +## ✅ Phase 4a: Champion & Settings (COMPLETE − v0.6.0) ### Champion Bloodline Tracking - [x] `is_champion INTEGER DEFAULT 0` column on `dogs` table @@ -145,48 +145,147 @@ - [x] `settings` table: `kennel_name`, `kennel_tagline`, `kennel_address`, `kennel_phone`, `kennel_email`, `kennel_website`, `kennel_akc_id`, `kennel_breed`, `owner_name` - [x] Safe `ALTER TABLE settings ADD COLUMN` migration loop for all kennel fields - [x] Auto-seed default row (`kennel_name = 'BREEDR'`) if table is empty -- [x] `GET /api/settings` — returns single-row as flat JSON object -- [x] `PUT /api/settings` — partial update via `ALLOWED_KEYS` whitelist +- [x] `GET /api/settings` − returns single-row as flat JSON object +- [x] `PUT /api/settings` − partial update via `ALLOWED_KEYS` whitelist - [x] `SettingsProvider` / `useSettings` React context hook - [x] Kennel name displayed in navbar from settings - [x] `SettingsPage` component for editing kennel info ### Build & Runtime Fixes (v0.6.0) -- [x] `useSettings.js` → `useSettings.jsx` — Vite build failed because JSX in `.js` file -- [x] `server/index.js` — `initDatabase()` called with no args (was passing `DB_PATH`, now path is internal) -- [x] `server/index.js` — removed duplicate `app.get('/api/health')` inline route -- [x] `server/index.js` — `DATA_DIR` env var replaces `path.dirname(DB_PATH)` for directory creation -- [x] `server/routes/settings.js` — rewrote from double-encoded base64 + old key/value schema to correct single-row column schema +- [x] `useSettings.js` → `useSettings.jsx` − Vite build failed because JSX in `.js` file +- [x] `server/index.js` − `initDatabase()` called with no args (was passing `DB_PATH`, now path is internal) +- [x] `server/index.js` − removed duplicate `app.get('/api/health')` inline route +- [x] `server/index.js` − `DATA_DIR` env var replaces `path.dirname(DB_PATH)` for directory creation +- [x] `server/routes/settings.js` − rewrote from double-encoded base64 + old key/value schema to correct single-row column schema --- -## 📋 Phase 4b: Health & Genetics (NEXT UP) +## 📋 Phase 4b: Health & Genetics (NEXT UP − v0.7.0) -### Health Records *(Priority 1)* 🚨 -- [ ] Health records list view per dog -- [ ] Add/edit health test results -- [ ] Vaccination tracking with expiry alerts -- [ ] Medical history timeline view -- [ ] Document uploads (PDFs, images) -- [ ] Health clearance status badges on dog cards +> **Context:** Golden Retriever health clearances follow GRCA Code of Ethics and OFA/CHIC standards. +> This phase builds a structured, breed-aware health tracking system aligned with those requirements. + +### Tier 1 — OFA Health Clearances *(Priority 1)* 🩺 + +The four GRCA-required clearances that must be on record in the public OFA database before breeding. + +**Database (schema additions to `health_records` table):** +- [ ] Add `test_type` ENUM-style field: `hip_ofa`, `hip_pennhip`, `elbow_ofa`, `heart_ofa`, `heart_echo`, `eye_caer`, `thyroid_ofa`, `dna_panel` +- [ ] Add `result` field: `pass`, `fail`, `carrier`, `clear`, `excellent`, `good`, `fair`, `borderline` +- [ ] Add `ofa_number` VARCHAR — official OFA certification number +- [ ] Add `chic_number` VARCHAR — CHIC certification number (dog-level field on `dogs` table) +- [ ] Add `performed_by` VARCHAR — vet or specialist name +- [ ] Add `expires_at` DATE — for annually-renewed tests (eyes, heart) +- [ ] Add `document_url` VARCHAR — path to uploaded PDF/image +- [ ] Safe ALTER TABLE migration guards for all new columns + +**API:** +- [ ] `GET /api/health/:dogId` — list all health records for a dog +- [ ] `POST /api/health` — create health record +- [ ] `PUT /api/health/:id` — update health record +- [ ] `DELETE /api/health/:id` — delete health record +- [ ] `GET /api/health/:dogId/clearance-summary` — returns pass/fail/missing for all 4 OFA tiers +- [ ] `GET /api/health/:dogId/chic-eligible` — returns boolean + missing tests + +**UI Components:** +- [ ] `HealthRecordForm` modal — test type dropdown, result, OFA#, date, performed-by, expiry, document upload +- [ ] `HealthTimeline` component — chronological list of all health events per dog on DogDetail page +- [ ] `ClearanceSummaryCard` — shows OFA Hip / Elbow / Heart / Eyes status in a 2x2 grid with color badges (green=pass, yellow=expiring, red=missing/fail) +- [ ] `ChicStatusBadge` — amber badge on dog cards and DogDetail if CHIC number is on file +- [ ] Expiry alert: yellow badge on dog card if any annual test expires within 90 days; red if expired +- [ ] Document upload support (PDF/image) tied to individual health records + +**Clearance Tiers Tracked:** +| Test | OFA Minimum Age | Renewal | Notes | +|---|---|---|---| +| Hip Dysplasia | 24 months | Once (final) | OFA eval or PennHIP | +| Elbow Dysplasia | 24 months | Once (final) | OFA eval | +| Cardiac (Heart) | 12 months | Annual recommended | Echo preferred over auscultation | +| Eyes (CAER) | 12 months | **Annual** | Board-certified ACVO ophthalmologist | +| Thyroid (OFA) | 12 months | Annual recommended | Bonus/Tier 2 | **Complexity:** Medium | **Impact:** High | **User Value:** Excellent +**Estimated Time:** 8–10 hours -**Why this is recommended:** -- Natural complement to existing dog profiles -- Vaccination expiry alerts are high day-to-day utility -- Clearance badges on dog cards improve trust at a glance -- Builds toward breeding decision support +--- -**Estimated Time:** 6-8 hours +### Tier 2 — DNA Genetic Panel *(Priority 2)* 🧬 -### Genetic Trait Tracking *(Priority 2)* -- [ ] Track inherited traits -- [ ] Color genetics calculator -- [ ] Health clearance status -- [ ] Link traits to ancestors +Embark or equivalent panel results per dog. Allows carrier × clear pairing without producing affected offspring. -**Estimated Time:** 5-7 hours +**Database:** +- [ ] `genetic_tests` table: `id`, `dog_id`, `test_provider` (embark/optigen/etc), `test_name`, `result` (clear/carrier/affected), `test_date`, `document_url`, `created_at` +- [ ] Safe `CREATE TABLE IF NOT EXISTS` guard + +**Golden Retriever Panel — Key Markers:** +- [ ] PRA1 (Progressive Retinal Atrophy type 1) +- [ ] PRA2 (Progressive Retinal Atrophy type 2) +- [ ] prcd-PRA (Progressive Rod-Cone Degeneration) +- [ ] ICH1 / ICH2 (Ichthyosis — very common in Goldens) +- [ ] NCL (Neuronal Ceroid Lipofuscinosis — fatal neurological) +- [ ] DM (Degenerative Myelopathy) +- [ ] MD (Muscular Dystrophy) +- [ ] GR-PRA1, GR-PRA2 (Golden-specific PRA variants) + +**API:** +- [ ] `GET /api/genetics/:dogId` — list all genetic test results +- [ ] `POST /api/genetics` — add genetic result +- [ ] `PUT /api/genetics/:id` — update +- [ ] `DELETE /api/genetics/:id` — delete +- [ ] `GET /api/genetics/pairing-risk?sireId=&damId=` — returns at-risk combinations for a trial pairing + +**UI Components:** +- [ ] `GeneticTestForm` modal — provider, marker, result (clear/carrier/affected), date, upload +- [ ] `GeneticPanelCard` on DogDetail — color-coded grid of all markers (green=clear, yellow=carrier, red=affected, gray=not tested) +- [ ] Pairing risk overlay on Trial Pairing Simulator — flag if sire+dam are both carriers for same marker +- [ ] "Not Tested" indicator on dog cards when no DNA panel on file + +**Complexity:** Medium | **Impact:** High | **User Value:** Excellent +**Estimated Time:** 6–8 hours + +--- + +### Tier 3 — Cancer Lineage & Longevity Tracking *(Priority 3)* 📊 + +Golden Retrievers have ~60% cancer mortality rate. Lineage-based cancer history is a major differentiator for responsible breeders. + +**Database:** +- [ ] `cancer_history` table: `id`, `dog_id`, `cancer_type`, `age_at_diagnosis`, `age_at_death`, `cause_of_death`, `notes`, `created_at` +- [ ] Add `age_at_death` and `cause_of_death` optional fields to `dogs` table + +**API:** +- [ ] `GET /api/health/:dogId/cancer-history` +- [ ] `POST /api/health/cancer-history` +- [ ] `GET /api/pedigree/:dogId/cancer-lineage` — walks ancestors and returns cancer incidence summary + +**UI:** +- [ ] Longevity section on DogDetail — age at death, cause of death +- [ ] Cancer lineage indicator on Trial Pairing Simulator — "X of 8 ancestors had cancer history" +- [ ] Optional cancer history entry on DogForm + +**Complexity:** Low-Medium | **Impact:** Medium | **User Value:** High (differentiator) +**Estimated Time:** 4–5 hours + +--- + +### Tier 4 — Breeding Eligibility Checker *(Priority 4)* ✅ + +Automatic litter eligibility gate based on health clearance status of sire and dam. + +**Logic:** +- [ ] Dog is "GRCA eligible" if: Hip OFA ✅ + Elbow OFA ✅ + Heart ✅ + Eyes (non-expired) ✅ + age ≥ 24 months +- [ ] Dog is "CHIC eligible" if all four tests are in OFA public database (CHIC number on file) +- [ ] Warning flags in Trial Pairing Simulator if sire or dam is missing required clearances +- [ ] Block litter creation (with override) if either parent fails eligibility check + +**UI:** +- [ ] Eligibility badge on dog cards: `GRCA Eligible` (green) / `Incomplete` (yellow) / `Not Eligible` (red) +- [ ] Eligibility breakdown tooltip on hover — shows which tests are missing +- [ ] Pre-litter warning modal when creating a litter with non-eligible parents +- [ ] CHIC number field + verification note on DogDetail + +**Complexity:** Low | **Impact:** High | **User Value:** Excellent +**Estimated Time:** 3–4 hours --- @@ -266,9 +365,10 @@ - [ ] Export to Excel/CSV - [ ] Integration with kennel clubs - [ ] Backup to cloud storage +- [ ] OFA database lookup by registration number ### Advanced Genetics -- [ ] DNA test result tracking +- [ ] DNA test result tracking (full Embark import) - [ ] Genetic diversity analysis - [ ] Breed-specific calculators - [ ] Health risk predictions @@ -281,48 +381,58 @@ --- -## 📅 Current Sprint: v0.7.0 +## 🏃 Current Sprint: v0.7.0 (Phase 4b) ### ✅ Completed This Sprint (v0.6.0) -- [x] `is_champion` flag — DB column, API, DogForm toggle, offspring badge, parent dropdown `✪` -- [x] Kennel Settings — `settings` table with all kennel fields, `GET/PUT /api/settings`, `SettingsProvider`, navbar kennel name +- [x] `is_champion` flag − DB column, API, DogForm toggle, offspring badge, parent dropdown `✪` +- [x] Kennel Settings − `settings` table with all kennel fields, `GET/PUT /api/settings`, `SettingsProvider`, navbar kennel name - [x] `useSettings.jsx` rename (Vite build fix) -- [x] `server/index.js` fix — `initDatabase()` no-arg call, duplicate health route removed -- [x] `server/routes/settings.js` rewrite — fixed double-encoded base64 + wrong key/value schema +- [x] `server/index.js` fix − `initDatabase()` no-arg, duplicate health route removed +- [x] `server/routes/settings.js` rewrite: double-encoded base64 + wrong schema fixed ### ✅ Previously Completed (v0.5.1) -- [x] Projected Whelping Calendar Identifier — indigo whelp window cells, due label, active card range, jump-to-month button +- [x] Projected Whelping Calendar Identifier − indigo whelp window cells, due label, active card range, jump-to-month button - [x] Live whelp preview in Cycle Detail modal (client-side, no save required) - [x] Full-width whelping banner for months with projected whelps - [x] "Projected Whelp" legend entry + updated page subtitle -### 🔜 Next Up (Priority Order) +### 🔜 Next Up — Phase 4b Build Order -#### Option 1: Health Records System (Recommended) 🚨 -**Complexity:** Medium | **Impact:** High | **User Value:** Excellent +#### Step 1: DB Schema Extensions +- [ ] Extend `health_records` table with OFA-specific columns (test_type, result, ofa_number, chic_number, expires_at, document_url) +- [ ] Create `genetic_tests` table (PRA, ICH, NCL, DM, MD, GR-PRA variants) +- [ ] Create `cancer_history` table +- [ ] Add `chic_number`, `age_at_death`, `cause_of_death` to `dogs` table +- [ ] All changes via safe ALTER TABLE / CREATE TABLE IF NOT EXISTS guards -**Tasks:** -- Create `HealthRecordForm` component -- Health records list/timeline per dog on DogDetail page -- Vaccination tracking with expiry date + alert badge -- Health clearance status badges (OFA, CERF, etc.) -- Optional document/PDF upload +#### Step 2: API Layer +- [ ] `GET|POST|PUT|DELETE /api/health/:dogId` (OFA records) +- [ ] `GET /api/health/:dogId/clearance-summary` +- [ ] `GET /api/health/:dogId/chic-eligible` +- [ ] `GET|POST|PUT|DELETE /api/genetics/:dogId` +- [ ] `GET /api/genetics/pairing-risk` (sire + dam carrier check) +- [ ] Cancer history endpoints -**Estimated Time:** 6-8 hours +#### Step 3: Core UI — Health Records +- [ ] `HealthRecordForm` modal (test type, result, OFA#, expiry, doc upload) +- [ ] `HealthTimeline` on DogDetail page +- [ ] `ClearanceSummaryCard` 2×2 grid (Hip / Elbow / Heart / Eyes) +- [ ] `ChicStatusBadge` on dog cards +- [ ] Expiry alert badges (90-day warning, expired) ---- +#### Step 4: Core UI — Genetics Panel +- [ ] `GeneticTestForm` modal +- [ ] `GeneticPanelCard` on DogDetail (color-coded markers) +- [ ] Pairing risk overlay on Trial Pairing Simulator -#### Option 2: Genetic Trait Tracking -**Complexity:** Medium | **Impact:** Medium | **User Value:** Good +#### Step 5: Eligibility Checker +- [ ] Eligibility logic (`grca_eligible`, `chic_eligible` computed fields) +- [ ] Eligibility badge on dog cards +- [ ] Pre-litter eligibility warning modal -**Tasks:** -- Trait entry form (coat color, pattern, carried traits) -- Display traits on dog detail page -- Predicted trait calculator for trial pairings - -**Estimated Time:** 5-7 hours - ---- +#### Step 6: Cancer / Longevity (Stretch) +- [ ] Cancer history form + lineage summary on Trial Pairing page +- [ ] Age at death / cause of death on DogDetail ### Testing Needed - [x] Add/edit dog forms with litter selection @@ -335,13 +445,15 @@ - [x] Brand logo display and sizing - [x] Gradient title rendering - [x] Static asset serving in prod and dev -- [ ] Champion toggle — DogForm save/load round-trip -- [ ] Champion badge — offspring card display -- [ ] Kennel settings — save + navbar name update +- [ ] Champion toggle − DogForm save/load round-trip +- [ ] Champion badge − offspring card display +- [ ] Kennel settings − save + navbar name update - [ ] Trial pairing simulator (end-to-end) - [ ] Heat cycle calendar (start cycle, detail modal, whelping) - [ ] Projected whelping calendar identifier (whelp cells, due label, banner) -- [ ] Health records +- [ ] Health records — OFA clearance CRUD +- [ ] Genetic panel — DNA marker entry and display +- [ ] Eligibility checker — badge and litter gate ### Known Issues - None currently @@ -358,6 +470,12 @@ ## Version History +- **v0.7.0** (In Progress) - Phase 4b: Health & Genetics + - OFA clearance tracking (Hip, Elbow, Heart, Eyes + CHIC number) + - DNA genetic panel (PRA, ICH, NCL, DM, MD variants) + - Cancer lineage & longevity tracking + - Breeding eligibility checker (GRCA + CHIC gates) + - **v0.6.0** (March 9, 2026) - Champion Bloodline, Settings, Build Fixes - `is_champion` flag on dogs table with ALTER TABLE migration guard - Champion toggle in DogForm; `✪` suffix in parent dropdowns; offspring badge diff --git a/client/src/components/ClearanceSummaryCard.jsx b/client/src/components/ClearanceSummaryCard.jsx new file mode 100644 index 0000000..708cfcf --- /dev/null +++ b/client/src/components/ClearanceSummaryCard.jsx @@ -0,0 +1,126 @@ +import { useEffect, useState } from 'react' +import { ShieldCheck, ShieldAlert, ShieldX, Clock, AlertTriangle, Plus } from 'lucide-react' +import axios from 'axios' + +const STATUS_CONFIG = { + pass: { icon: ShieldCheck, color: 'var(--success)', label: 'Clear', bg: 'rgba(52,199,89,0.1)' }, + expiring_soon: { icon: Clock, color: 'var(--warning)', label: 'Expiring Soon', bg: 'rgba(255,159,10,0.1)' }, + expired: { icon: ShieldX, color: 'var(--danger)', label: 'Expired', bg: 'rgba(255,59,48,0.1)' }, + missing: { icon: ShieldAlert, color: 'var(--text-muted)', label: 'Missing', bg: 'var(--bg-primary)' }, +} + +const GROUP_LABELS = { hip: 'Hips', elbow: 'Elbows', heart: 'Heart', eye: 'Eyes' } + +function ClearanceChip({ group, status, record }) { + const cfg = STATUS_CONFIG[status] || STATUS_CONFIG.missing + const Icon = cfg.icon + const tip = record + ? `OFA #${record.ofa_number || '-'} - ${record.ofa_result || record.result || ''}` + : 'No record on file' + return ( +
+ +
+
+ {GROUP_LABELS[group]} +
+
+ {cfg.label} +
+
+
+ ) +} + +export default function ClearanceSummaryCard({ dogId, onAddRecord }) { + const [data, setData] = useState(null) + const [error, setError] = useState(null) + + useEffect(() => { + axios.get(`/api/health/dog/${dogId}/clearance-summary`) + .then(r => setData(r.data)) + .catch(() => setError(true)) + }, [dogId]) + + if (error || !data) return null + + const { summary, grca_eligible, age_eligible, chic_number } = data + const hasMissing = Object.values(summary).some(s => s.status === 'missing') + const hasExpiring = Object.values(summary).some(s => s.status === 'expiring_soon') + + return ( +
+ {/* Header row */} +
+

+ OFA Clearances +

+
+ {grca_eligible && ( + GRCA Eligible + )} + {!age_eligible && ( + Under 24mo + )} + {chic_number && ( + CHIC #{chic_number} + )} +
+
+ + {/* Clearance chips */} +
+ {Object.entries(summary).map(([group, { status, record }]) => ( + + ))} +
+ + {/* Expiry warning */} + {hasExpiring && ( +
+ + One or more clearances expire within 90 days. Schedule re-testing. +
+ )} + + {/* CTA */} + {(hasMissing || onAddRecord) && ( + + )} +
+ ) +} diff --git a/client/src/components/HealthRecordForm.jsx b/client/src/components/HealthRecordForm.jsx new file mode 100644 index 0000000..6916974 --- /dev/null +++ b/client/src/components/HealthRecordForm.jsx @@ -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 ( +
+
+
+

{record && record.id ? 'Edit' : 'Add'} Health Record

+ +
+ +
+ + {/* Record type */} +
+ + +
+ + {isOFA ? ( + <> +
+
+ + +
+
+ + +
+
+
+
+ + set('ofa_number', e.target.value)} /> +
+
+ + set('performed_by', e.target.value)} /> +
+
+
+
+ + set('test_date', e.target.value)} /> +
+
+ + set('expires_at', e.target.value)} /> +
+
+ + ) : ( + <> +
+ + set('test_name', e.target.value)} /> +
+
+
+ + set('test_date', e.target.value)} /> +
+
+ + set('next_due', e.target.value)} /> +
+
+
+
+ + set('result', e.target.value)} /> +
+
+ + set('vet_name', e.target.value)} /> +
+
+ + )} + +
+ + set('document_url', e.target.value)} /> +
+ +
+ +