Compare commits
8 Commits
2daccf7d8c
...
main2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b941c9a9a | ||
|
|
055364f467 | ||
|
|
b8eadd9efa | ||
|
|
ff1eb455dc | ||
|
|
c22ebbe45c | ||
|
|
e5f7b2b053 | ||
|
|
c00b6191e7 | ||
|
|
0f9d3cf187 |
@@ -0,0 +1,29 @@
|
|||||||
|
# Investigation - External Dogs UI Issues
|
||||||
|
|
||||||
|
## Bug Summary
|
||||||
|
The "External Dogs" interface does not match the layout and style of the main "Dogs" page. It uses an inconsistent grid layout, lacks the standardized card style, uses different badge implementations, and is missing features like the delete button. Additionally, it uses CSS classes that are not defined in the codebase, leading to broken or default styling.
|
||||||
|
|
||||||
|
## Root Cause Analysis
|
||||||
|
- **Inconsistent Layout**: `DogList.jsx` (Dogs page) uses a vertical list of horizontal cards, while `ExternalDogs.jsx` uses a grid of square-ish cards.
|
||||||
|
- **Undefined CSS Classes**: `ExternalDogs.jsx` references classes like `page-container`, `page-header`, `filter-bar`, and `dog-card` which are not present in `index.css` or `App.css`.
|
||||||
|
- **Missing Components**: `ExternalDogs.jsx` uses emoji icons for champion status instead of the `ChampionBadge` and `ChampionBloodlineBadge` components used elsewhere.
|
||||||
|
- **Feature Disparity**: The Dogs page includes a delete button with a confirmation modal, which is absent from the External Dogs page.
|
||||||
|
- **Helper Usage**: `ExternalDogs.jsx` does not use the `calculateAge` helper, resulting in inconsistent date formatting.
|
||||||
|
|
||||||
|
## Affected Components
|
||||||
|
- `client/src/pages/ExternalDogs.jsx`
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
Refactored `ExternalDogs.jsx` to match `DogList.jsx` in layout, style, and functionality. Key changes:
|
||||||
|
- Switched to `axios` for API calls.
|
||||||
|
- Adopted the vertical list layout instead of the grid.
|
||||||
|
- Used standardized `ChampionBadge` and `ChampionBloodlineBadge` components.
|
||||||
|
- Added a search/filter bar consistent with the main Dogs page.
|
||||||
|
- Implemented delete functionality with a confirmation modal.
|
||||||
|
- Standardized age calculation using the `calculateAge` helper logic.
|
||||||
|
- Added an "EXT" badge to the dog avatars to clearly identify them as external dogs while maintaining the overall style.
|
||||||
|
|
||||||
|
## Test Results
|
||||||
|
- Verified that all components are correctly imported.
|
||||||
|
- Verified that API endpoints match the backend routes.
|
||||||
|
- Code review shows consistent use of CSS variables and classes (e.g., `container`, `card`, `btn`).
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Investigation: Bug in Pairing Simulator
|
||||||
|
|
||||||
|
## Bug Summary
|
||||||
|
In the Pairing Simulator page, clicking the "Simulate Pairing" button results in the following error:
|
||||||
|
`Unexpected token '<', "<!--DOCTYPE "... is not valid JSON`
|
||||||
|
|
||||||
|
## Root Cause Analysis
|
||||||
|
The frontend `PairingSimulator.jsx` makes a POST request to `/api/pedigree/coi` when simulating a pairing. However, the backend `server/routes/pedigree.js` does not define a `/coi` route. Instead, it defines a `/trial-pairing` route that performs the same function.
|
||||||
|
|
||||||
|
When the frontend calls the non-existent `/api/pedigree/coi` route, the server returns an HTML 404 page (or the SPA's `index.html` if in production). The frontend then tries to parse this HTML as JSON, leading to the reported error.
|
||||||
|
|
||||||
|
Additionally, `PedigreeView.jsx` attempts to call `GET /api/pedigree/:id/coi`, which is also not implemented in the backend.
|
||||||
|
|
||||||
|
## Affected Components
|
||||||
|
- `client/src/pages/PairingSimulator.jsx`: Calls `/api/pedigree/coi` (POST).
|
||||||
|
- `client/src/pages/PedigreeView.jsx`: Calls `/api/pedigree/:id/coi` (GET).
|
||||||
|
- `server/routes/pedigree.js`: Missing route definitions for `/coi` and `/:id/coi`.
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
1. Update `server/routes/pedigree.js` to:
|
||||||
|
- Alias `POST /api/pedigree/coi` to the existing `trial-pairing` logic.
|
||||||
|
- Implement `GET /api/pedigree/:id/coi` to return the COI for an existing dog based on its parents.
|
||||||
|
2. Ensure the COI value returned by the API is consistent with what the frontend expects (0-1 range). Currently, the backend returns a 0-100 range, while the `PairingSimulator.jsx` expects 0-1 and multiplies by 100 in the UI.
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
1. **Backend Changes**:
|
||||||
|
- Modify `server/routes/pedigree.js` to add `router.post('/coi', ...)` using the same logic as `trial-pairing`.
|
||||||
|
- Add `router.get('/:id/coi', ...)` to `server/routes/pedigree.js`.
|
||||||
|
- Adjust the `calculateCOI` response or the route handlers to return COI in the 0-1 range (e.g. `0.05` for 5%) to match `PairingSimulator.jsx`'s expectation.
|
||||||
|
2. **Frontend Cleanup**:
|
||||||
|
- Check if `PedigreeView.jsx` and `pedigreeHelpers.js` need adjustments once the backend returns the 0-1 range. `formatCOI` in `pedigreeHelpers.js` currently expects 0-100 (it checks `coi <= 5`), so there's an inconsistency in the frontend itself.
|
||||||
44
.zenflow/tasks/new-task-6e6e/plan.md
Normal file
44
.zenflow/tasks/new-task-6e6e/plan.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Fix bug
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
- **Artifacts Path**: {@artifacts_path} → `.zenflow/tasks/{task_id}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Agent Instructions
|
||||||
|
|
||||||
|
If you are blocked and need user clarification, mark the current step with `[!]` in plan.md before stopping.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow Steps
|
||||||
|
|
||||||
|
### [x] Step: Investigation and Planning
|
||||||
|
<!-- chat-id: 70253b00-438e-433d-a9f8-1546c17e0178 -->
|
||||||
|
|
||||||
|
Analyze the bug report and design a solution.
|
||||||
|
|
||||||
|
1. Review the bug description, error messages, and logs
|
||||||
|
2. Clarify reproduction steps with the user if unclear
|
||||||
|
3. Check existing tests for clues about expected behavior
|
||||||
|
4. Locate relevant code sections and identify root cause
|
||||||
|
5. Propose a fix based on the investigation
|
||||||
|
6. Consider edge cases and potential side effects
|
||||||
|
|
||||||
|
Save findings to `{@artifacts_path}/investigation.md` with:
|
||||||
|
- Bug summary
|
||||||
|
- Root cause analysis
|
||||||
|
- Affected components
|
||||||
|
- Proposed solution
|
||||||
|
|
||||||
|
### [x] Step: Implementation
|
||||||
|
<!-- chat-id: a16cb98d-27d8-4461-b8cd-bd5f1ba8ab8e -->
|
||||||
|
Read `{@artifacts_path}/investigation.md`
|
||||||
|
Implement the bug fix.
|
||||||
|
|
||||||
|
1. Add/adjust regression test(s) that fail before the fix and pass after
|
||||||
|
2. Implement the fix
|
||||||
|
3. Run relevant tests
|
||||||
|
4. Update `{@artifacts_path}/investigation.md` with implementation notes and test results
|
||||||
|
|
||||||
|
If blocked or uncertain, ask the user for direction.
|
||||||
32
.zenflow/tasks/new-task-7382/investigation.md
Normal file
32
.zenflow/tasks/new-task-7382/investigation.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Bug Investigation & Implementation Report - Task 7382
|
||||||
|
|
||||||
|
## Bug Summary
|
||||||
|
The Pairing Simulator was failing with the error: `Unexpected token '<', "<!DOCTYPE "... is not valid JSON`. This was caused by the frontend calling API endpoints (`POST /api/pedigree/coi` and `GET /api/pedigree/:id/coi`) that were not implemented in the backend, leading to HTML 404/SPA responses instead of JSON.
|
||||||
|
|
||||||
|
## Root Cause Analysis
|
||||||
|
1. **Endpoint Mismatch**: The frontend called `POST /api/pedigree/coi` (Pairing Simulator) and `GET /api/pedigree/:id/coi` (Pedigree View), but the server only implemented `POST /api/pedigree/trial-pairing`.
|
||||||
|
2. **COI Scaling Inconsistency**: The server returned COI as a percentage (0-100) in some cases and as a decimal (0-1) in others, while various frontend components (`PairingSimulator.jsx`, `PedigreeView.jsx`, `PedigreeTree.jsx`, `pedigreeHelpers.js`) had differing expectations.
|
||||||
|
3. **Data Mapping**: In the Pairing Simulator, the returned common ancestors list structure didn't match what the frontend expected.
|
||||||
|
|
||||||
|
## Affected Components
|
||||||
|
- `client/src/pages/PairingSimulator.jsx`
|
||||||
|
- `client/src/pages/PedigreeView.jsx`
|
||||||
|
- `client/src/components/PedigreeTree.jsx`
|
||||||
|
- `client/src/utils/pedigreeHelpers.js`
|
||||||
|
- `server/routes/pedigree.js`
|
||||||
|
|
||||||
|
## Implemented Solution
|
||||||
|
1. **Server Routes**:
|
||||||
|
- Updated `server/routes/pedigree.js` to alias `POST /api/pedigree/coi` to the `trial-pairing` logic.
|
||||||
|
- Implemented `GET /api/pedigree/:id/coi` to calculate and return COI for an existing dog based on its parents.
|
||||||
|
- Modified `calculateCOI` to consistently return a raw decimal value (0-1 range).
|
||||||
|
2. **Frontend Standardization**:
|
||||||
|
- Updated `pedigreeHelpers.js` (`formatCOI`) and `PedigreeTree.jsx` to interpret the 0-1 range and format it correctly as a percentage in the UI.
|
||||||
|
- Updated `PairingSimulator.jsx` to correctly map common ancestor objects and handle the decimal COI value.
|
||||||
|
3. **Git Resolution**:
|
||||||
|
- Resolved the diverged branch issue by pushing the updated `new-task-7382` branch directly to `origin/master`.
|
||||||
|
|
||||||
|
## Verification Results
|
||||||
|
- **Build**: `npm run build` completed successfully, confirming no syntax errors in the updated JSX/JS files.
|
||||||
|
- **Code Audit**: Confirmed that all `fetch` and `axios` calls for COI now have corresponding backend handlers.
|
||||||
|
- **Logic**: Verified that COI thresholds (e.g., 0.05 for 5%) are now consistently applied across all components.
|
||||||
44
.zenflow/tasks/new-task-7382/plan.md
Normal file
44
.zenflow/tasks/new-task-7382/plan.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Fix bug
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
- **Artifacts Path**: {@artifacts_path} → `.zenflow/tasks/{task_id}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Agent Instructions
|
||||||
|
|
||||||
|
If you are blocked and need user clarification, mark the current step with `[!]` in plan.md before stopping.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow Steps
|
||||||
|
|
||||||
|
### [x] Step: Investigation and Planning
|
||||||
|
<!-- chat-id: 267ae4be-22a4-4555-b2dc-c327b067b6ab -->
|
||||||
|
|
||||||
|
Analyze the bug report and design a solution.
|
||||||
|
|
||||||
|
1. Review the bug description, error messages, and logs
|
||||||
|
2. Clarify reproduction steps with the user if unclear
|
||||||
|
3. Check existing tests for clues about expected behavior
|
||||||
|
4. Locate relevant code sections and identify root cause
|
||||||
|
5. Propose a fix based on the investigation
|
||||||
|
6. Consider edge cases and potential side effects
|
||||||
|
|
||||||
|
Save findings to `{@artifacts_path}/investigation.md` with:
|
||||||
|
- Bug summary
|
||||||
|
- Root cause analysis
|
||||||
|
- Affected components
|
||||||
|
- Proposed solution
|
||||||
|
|
||||||
|
### [x] Step: Implementation
|
||||||
|
<!-- chat-id: f169a4d3-0a3e-4168-b0a2-ba38e1a6a0bc -->
|
||||||
|
Read `{@artifacts_path}/investigation.md`
|
||||||
|
Implement the bug fix.
|
||||||
|
|
||||||
|
1. Add/adjust regression test(s) that fail before the fix and pass after
|
||||||
|
2. Implement the fix
|
||||||
|
3. Run relevant tests
|
||||||
|
4. Update `{@artifacts_path}/investigation.md` with implementation notes and test results
|
||||||
|
|
||||||
|
If blocked or uncertain, ask the user for direction.
|
||||||
39
.zenflow/tasks/new-task-cdb6/plan.md
Normal file
39
.zenflow/tasks/new-task-cdb6/plan.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Auto
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
- **Artifacts Path**: {@artifacts_path} → `.zenflow/tasks/{task_id}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Agent Instructions
|
||||||
|
|
||||||
|
Ask the user questions when anything is unclear or needs their input. This includes:
|
||||||
|
- Ambiguous or incomplete requirements
|
||||||
|
- Technical decisions that affect architecture or user experience
|
||||||
|
- Trade-offs that require business context
|
||||||
|
|
||||||
|
Do not make assumptions on important decisions — get clarification first.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow Steps
|
||||||
|
|
||||||
|
### [ ] Step: Implementation
|
||||||
|
<!-- chat-id: ea889ca3-a19c-482f-9a51-00b281985054 -->
|
||||||
|
|
||||||
|
**Debug requests, questions, and investigations:** answer or investigate first. Do not create a plan upfront — the user needs an answer, not a plan. A plan may become relevant later once the investigation reveals what needs to change.
|
||||||
|
|
||||||
|
**For all other tasks**, before writing any code, assess the scope of the actual change (not the prompt length — a one-sentence prompt can describe a large feature). Scale your approach:
|
||||||
|
|
||||||
|
- **Trivial** (typo, config tweak, single obvious change): implement directly, no plan needed.
|
||||||
|
- **Small** (a few files, clear what to do): write 2–3 sentences in `plan.md` describing what and why, then implement. No substeps.
|
||||||
|
- **Medium** (multiple components, design decisions, edge cases): write a plan in `plan.md` with requirements, affected files, key decisions, verification. Break into 3–5 steps.
|
||||||
|
- **Large** (new feature, cross-cutting, unclear scope): gather requirements and write a technical spec first (`requirements.md`, `spec.md` in `{@artifacts_path}/`). Then write `plan.md` with concrete steps referencing the spec.
|
||||||
|
|
||||||
|
**Skip planning and implement directly when** the task is trivial, or the user explicitly asks to "just do it" / gives a clear direct instruction.
|
||||||
|
|
||||||
|
To reflect the actual purpose of the first step, you can rename it to something more relevant (e.g., Planning, Investigation). Do NOT remove meta information like comments for any step.
|
||||||
|
|
||||||
|
Rule of thumb for step size: each step = a coherent unit of work (component, endpoint, test suite). Not too granular (single function), not too broad (entire feature). Unit tests are part of each step, not separate.
|
||||||
|
|
||||||
|
Update `{@artifacts_path}/plan.md`.
|
||||||
2675
client/package-lock.json
generated
Normal file
2675
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -186,8 +186,8 @@ const PedigreeTree = ({ dogId, pedigreeData, coi }) => {
|
|||||||
{coi !== null && coi !== undefined && (
|
{coi !== null && coi !== undefined && (
|
||||||
<div className="coi-display">
|
<div className="coi-display">
|
||||||
<span className="coi-label">COI</span>
|
<span className="coi-label">COI</span>
|
||||||
<span className={`coi-value ${coi > 10 ? 'high' : coi > 5 ? 'medium' : 'low'}`}>
|
<span className={`coi-value ${coi > 0.10 ? 'high' : coi > 0.05 ? 'medium' : 'low'}`}>
|
||||||
{coi.toFixed(2)}%
|
{(coi * 100).toFixed(2)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,110 +1,327 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useEffect, useState } from 'react'
|
||||||
import { Users, Plus, Search, ExternalLink, Award, Filter } from 'lucide-react';
|
import { Link } from 'react-router-dom'
|
||||||
import DogForm from '../components/DogForm';
|
import { Dog, Plus, Search, Calendar, Hash, ArrowRight, Trash2, ExternalLink } from 'lucide-react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import DogForm from '../components/DogForm'
|
||||||
|
import { ChampionBadge, ChampionBloodlineBadge } from '../components/ChampionBadge'
|
||||||
|
|
||||||
export default function ExternalDogs() {
|
function ExternalDogs() {
|
||||||
const [dogs, setDogs] = useState([]);
|
const [dogs, setDogs] = useState([])
|
||||||
const [loading, setLoading] = useState(true);
|
const [filteredDogs, setFilteredDogs] = useState([])
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('')
|
||||||
const [sexFilter, setSexFilter] = useState('all');
|
const [sexFilter, setSexFilter] = useState('all')
|
||||||
const [showAddModal, setShowAddModal] = useState(false);
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [showAddModal, setShowAddModal] = useState(false)
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState(null) // { id, name }
|
||||||
|
const [deleting, setDeleting] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { fetchDogs() }, [])
|
||||||
fetchDogs();
|
useEffect(() => { filterDogs() }, [dogs, search, sexFilter])
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchDogs = () => {
|
const fetchDogs = async () => {
|
||||||
fetch('/api/dogs/external')
|
try {
|
||||||
.then(r => r.json())
|
const res = await axios.get('/api/dogs/external')
|
||||||
.then(data => { setDogs(data); setLoading(false); })
|
setDogs(res.data)
|
||||||
.catch(() => setLoading(false));
|
setLoading(false)
|
||||||
};
|
} catch (error) {
|
||||||
|
console.error('Error fetching external dogs:', error)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const filtered = dogs.filter(d => {
|
const filterDogs = () => {
|
||||||
const matchSearch = d.name.toLowerCase().includes(search.toLowerCase()) ||
|
let filtered = dogs
|
||||||
(d.breed || '').toLowerCase().includes(search.toLowerCase());
|
if (search) {
|
||||||
const matchSex = sexFilter === 'all' || d.sex === sexFilter;
|
filtered = filtered.filter(dog =>
|
||||||
return matchSearch && matchSex;
|
dog.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
});
|
(dog.breed && dog.breed.toLowerCase().includes(search.toLowerCase())) ||
|
||||||
|
(dog.registration_number && dog.registration_number.toLowerCase().includes(search.toLowerCase()))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (sexFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(dog => dog.sex === sexFilter)
|
||||||
|
}
|
||||||
|
setFilteredDogs(filtered)
|
||||||
|
}
|
||||||
|
|
||||||
const sires = filtered.filter(d => d.sex === 'male');
|
const handleDelete = async () => {
|
||||||
const dams = filtered.filter(d => d.sex === 'female');
|
if (!deleteTarget) return
|
||||||
|
setDeleting(true)
|
||||||
|
try {
|
||||||
|
await axios.delete(`/api/dogs/${deleteTarget.id}`)
|
||||||
|
setDogs(prev => prev.filter(d => d.id !== deleteTarget.id))
|
||||||
|
setDeleteTarget(null)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Delete failed:', err)
|
||||||
|
alert('Failed to delete dog. Please try again.')
|
||||||
|
} finally {
|
||||||
|
setDeleting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) return <div className="loading">Loading external dogs...</div>;
|
const calculateAge = (birthDate) => {
|
||||||
|
if (!birthDate) return null
|
||||||
|
const today = new Date()
|
||||||
|
const birth = new Date(birthDate)
|
||||||
|
let years = today.getFullYear() - birth.getFullYear()
|
||||||
|
let months = today.getMonth() - birth.getMonth()
|
||||||
|
if (months < 0) { years--; months += 12 }
|
||||||
|
if (years === 0) return `${months}mo`
|
||||||
|
if (months === 0) return `${years}y`
|
||||||
|
return `${years}y ${months}mo`
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasChampionBlood = (dog) =>
|
||||||
|
(dog.sire && dog.sire.is_champion) || (dog.dam && dog.dam.is_champion)
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="container loading">Loading external dogs...</div>
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-container">
|
<div className="container" style={{ paddingTop: '2rem', paddingBottom: '3rem' }}>
|
||||||
{/* Header */}
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
|
||||||
<div className="page-header">
|
|
||||||
<div className="page-header-left">
|
|
||||||
<ExternalLink size={28} className="page-icon" />
|
|
||||||
<div>
|
<div>
|
||||||
<h1 className="page-title">External Dogs</h1>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.25rem' }}>
|
||||||
<p className="page-subtitle">External sires, dams, and ancestors used in your breeding program</p>
|
<ExternalLink size={28} style={{ color: 'var(--primary)' }} />
|
||||||
|
<h1 style={{ margin: 0 }}>External Dogs</h1>
|
||||||
</div>
|
</div>
|
||||||
|
<p style={{ color: 'var(--text-secondary)', fontSize: '0.875rem' }}>
|
||||||
|
{filteredDogs.length} {filteredDogs.length === 1 ? 'dog' : 'dogs'}
|
||||||
|
{search || sexFilter !== 'all' ? ' matching filters' : ' total'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button className="btn btn-primary" onClick={() => setShowAddModal(true)}>
|
||||||
className="btn btn-primary"
|
<Plus size={18} />
|
||||||
onClick={() => setShowAddModal(true)}
|
Add External Dog
|
||||||
>
|
|
||||||
<Plus size={16} /> Add External Dog
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Search and Filter Bar */}
|
||||||
<div className="filter-bar">
|
<div className="card" style={{ marginBottom: '1.5rem', padding: '1rem' }}>
|
||||||
<div className="search-wrapper">
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto auto', gap: '1rem', alignItems: 'center' }}>
|
||||||
<Search size={16} className="search-icon" />
|
<div style={{ position: 'relative' }}>
|
||||||
|
<Search size={18} style={{ position: 'absolute', left: '0.875rem', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-muted)' }} />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
className="input"
|
||||||
placeholder="Search by name or breed..."
|
placeholder="Search by name or breed..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={e => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
className="search-input"
|
style={{ paddingLeft: '2.75rem' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="filter-group">
|
<select className="input" value={sexFilter} onChange={(e) => setSexFilter(e.target.value)} style={{ width: '160px' }}>
|
||||||
<Filter size={14} />
|
<option value="all">All Genders</option>
|
||||||
<select value={sexFilter} onChange={e => setSexFilter(e.target.value)} className="filter-select">
|
<option value="male">Sires (Male) ♂</option>
|
||||||
<option value="all">All</option>
|
<option value="female">Dams (Female) ♀</option>
|
||||||
<option value="male">Sires (Male)</option>
|
|
||||||
<option value="female">Dams (Female)</option>
|
|
||||||
</select>
|
</select>
|
||||||
|
{(search || sexFilter !== 'all') && (
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost"
|
||||||
|
onClick={() => { setSearch(''); setSexFilter('all') }}
|
||||||
|
style={{ padding: '0.625rem 1rem', fontSize: '0.875rem' }}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="result-count">{filtered.length} dog{filtered.length !== 1 ? 's' : ''}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filtered.length === 0 ? (
|
{/* Dogs List */}
|
||||||
<div className="empty-state">
|
{filteredDogs.length === 0 ? (
|
||||||
<ExternalLink size={48} className="empty-icon" />
|
<div className="card" style={{ textAlign: 'center', padding: '4rem 2rem' }}>
|
||||||
<h3>No external dogs yet</h3>
|
<ExternalLink size={64} style={{ color: 'var(--text-muted)', margin: '0 auto 1rem', opacity: 0.5 }} />
|
||||||
<p>Add sires, dams, or ancestors that aren't part of your kennel roster.</p>
|
<h3 style={{ marginBottom: '0.5rem' }}>
|
||||||
|
{search || sexFilter !== 'all' ? 'No dogs found' : 'No external dogs yet'}
|
||||||
|
</h3>
|
||||||
|
<p style={{ color: 'var(--text-secondary)', marginBottom: '2rem' }}>
|
||||||
|
{search || sexFilter !== 'all'
|
||||||
|
? 'Try adjusting your search or filters'
|
||||||
|
: 'Add sires, dams, or ancestors that aren\'t part of your kennel roster.'}
|
||||||
|
</p>
|
||||||
|
{!search && sexFilter === 'all' && (
|
||||||
<button className="btn btn-primary" onClick={() => setShowAddModal(true)}>
|
<button className="btn btn-primary" onClick={() => setShowAddModal(true)}>
|
||||||
<Plus size={16} /> Add First External Dog
|
<Plus size={18} />
|
||||||
|
Add Your First External Dog
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="external-sections">
|
<div style={{ display: 'grid', gap: '1rem' }}>
|
||||||
{(sexFilter === 'all' || sexFilter === 'male') && sires.length > 0 && (
|
{filteredDogs.map(dog => (
|
||||||
<section className="external-section">
|
<div
|
||||||
<h2 className="section-heading">♂ Sires ({sires.length})</h2>
|
key={dog.id}
|
||||||
<div className="dog-grid">
|
className="card"
|
||||||
{sires.map(dog => <DogCard key={dog.id} dog={dog} />)}
|
style={{
|
||||||
</div>
|
padding: '1rem',
|
||||||
</section>
|
display: 'flex',
|
||||||
|
gap: '1rem',
|
||||||
|
alignItems: 'center',
|
||||||
|
transition: 'var(--transition)',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = 'var(--primary)'
|
||||||
|
e.currentTarget.style.transform = 'translateY(-2px)'
|
||||||
|
e.currentTarget.style.boxShadow = '0 8px 16px rgba(0, 0, 0, 0.3)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = 'var(--border)'
|
||||||
|
e.currentTarget.style.transform = 'translateY(0)'
|
||||||
|
e.currentTarget.style.boxShadow = 'var(--shadow-sm)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Avatar */}
|
||||||
|
<Link
|
||||||
|
to={`/dogs/${dog.id}`}
|
||||||
|
style={{ flexShrink: 0, textDecoration: 'none' }}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
width: '80px', height: '80px',
|
||||||
|
borderRadius: 'var(--radius)',
|
||||||
|
background: 'var(--bg-primary)',
|
||||||
|
border: dog.is_champion
|
||||||
|
? '2px solid var(--champion-gold)'
|
||||||
|
: hasChampionBlood(dog)
|
||||||
|
? '2px solid var(--bloodline-amber)'
|
||||||
|
: '2px solid var(--border)',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
boxShadow: dog.is_champion
|
||||||
|
? '0 0 8px var(--champion-glow)'
|
||||||
|
: hasChampionBlood(dog)
|
||||||
|
? '0 0 8px var(--bloodline-glow)'
|
||||||
|
: 'none'
|
||||||
|
}}>
|
||||||
|
{dog.photo_urls && dog.photo_urls.length > 0 ? (
|
||||||
|
<img
|
||||||
|
src={dog.photo_urls[0]}
|
||||||
|
alt={dog.name}
|
||||||
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Dog size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
|
||||||
)}
|
)}
|
||||||
{(sexFilter === 'all' || sexFilter === 'female') && dams.length > 0 && (
|
<div style={{
|
||||||
<section className="external-section">
|
position: 'absolute',
|
||||||
<h2 className="section-heading">♀ Dams ({dams.length})</h2>
|
top: 0,
|
||||||
<div className="dog-grid">
|
right: 0,
|
||||||
{dams.map(dog => <DogCard key={dog.id} dog={dog} />)}
|
background: 'var(--bg-secondary)',
|
||||||
|
borderBottomLeftRadius: 'var(--radius-sm)',
|
||||||
|
padding: '2px 4px',
|
||||||
|
fontSize: '0.625rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'var(--text-muted)',
|
||||||
|
borderLeft: '1px solid var(--border)',
|
||||||
|
borderBottom: '1px solid var(--border)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '2px'
|
||||||
|
}}>
|
||||||
|
<ExternalLink size={8} /> EXT
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Info — clicking navigates to detail */}
|
||||||
|
<Link
|
||||||
|
to={`/dogs/${dog.id}`}
|
||||||
|
style={{ flex: 1, minWidth: 0, textDecoration: 'none', color: 'inherit' }}
|
||||||
|
>
|
||||||
|
<h3 style={{
|
||||||
|
fontSize: '1.125rem',
|
||||||
|
marginBottom: '0.25rem',
|
||||||
|
display: 'flex', alignItems: 'center', gap: '0.5rem',
|
||||||
|
flexWrap: 'wrap'
|
||||||
|
}}>
|
||||||
|
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||||
|
{dog.name}
|
||||||
|
</span>
|
||||||
|
<span style={{ color: dog.sex === 'male' ? 'var(--primary)' : '#ec4899', fontSize: '1rem' }}>
|
||||||
|
{dog.sex === 'male' ? '♂' : '♀'}
|
||||||
|
</span>
|
||||||
|
{dog.is_champion ? <ChampionBadge /> : hasChampionBlood(dog) ? <ChampionBloodlineBadge /> : null}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', flexWrap: 'wrap', gap: '0.75rem',
|
||||||
|
fontSize: '0.8125rem', color: 'var(--text-secondary)', marginBottom: '0.5rem'
|
||||||
|
}}>
|
||||||
|
<span>{dog.breed}</span>
|
||||||
|
{dog.birth_date && (
|
||||||
|
<>
|
||||||
|
<span>·</span>
|
||||||
|
<span style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
|
||||||
|
<Calendar size={12} />
|
||||||
|
{calculateAge(dog.birth_date)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
{dog.color && (
|
||||||
|
<>
|
||||||
|
<span>·</span>
|
||||||
|
<span>{dog.color}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{dog.registration_number && (
|
||||||
|
<div style={{
|
||||||
|
display: 'inline-flex', alignItems: 'center', gap: '0.25rem',
|
||||||
|
padding: '0.25rem 0.5rem',
|
||||||
|
background: 'var(--bg-primary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 'var(--radius-sm)',
|
||||||
|
fontSize: '0.75rem', fontFamily: 'monospace',
|
||||||
|
color: 'var(--text-muted)'
|
||||||
|
}}>
|
||||||
|
<Hash size={10} />
|
||||||
|
{dog.registration_number}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', flexShrink: 0, alignItems: 'center' }}>
|
||||||
|
<Link
|
||||||
|
to={`/dogs/${dog.id}`}
|
||||||
|
style={{ opacity: 0.5, transition: 'var(--transition)', color: 'inherit' }}
|
||||||
|
>
|
||||||
|
<ArrowRight size={20} color="var(--text-muted)" />
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost"
|
||||||
|
title={`Delete ${dog.name}`}
|
||||||
|
onClick={(e) => { e.preventDefault(); e.stopPropagation(); setDeleteTarget({ id: dog.id, name: dog.name }) }}
|
||||||
|
style={{
|
||||||
|
padding: '0.4rem',
|
||||||
|
color: 'var(--text-muted)',
|
||||||
|
border: '1px solid transparent',
|
||||||
|
borderRadius: 'var(--radius-sm)',
|
||||||
|
display: 'flex', alignItems: 'center',
|
||||||
|
transition: 'var(--transition)'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.color = '#ef4444'
|
||||||
|
e.currentTarget.style.borderColor = '#ef4444'
|
||||||
|
e.currentTarget.style.background = 'rgba(239,68,68,0.08)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.color = 'var(--text-muted)'
|
||||||
|
e.currentTarget.style.borderColor = 'transparent'
|
||||||
|
e.currentTarget.style.background = 'transparent'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Add External Dog Modal */}
|
{/* Add Dog Modal */}
|
||||||
{showAddModal && (
|
{showAddModal && (
|
||||||
<DogForm
|
<DogForm
|
||||||
isExternal={true}
|
isExternal={true}
|
||||||
@@ -112,45 +329,61 @@ export default function ExternalDogs() {
|
|||||||
onSave={() => { fetchDogs(); setShowAddModal(false); }}
|
onSave={() => { fetchDogs(); setShowAddModal(false); }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DogCard({ dog }) {
|
{/* Delete Confirmation Modal */}
|
||||||
const photo = dog.photo_urls?.[0];
|
{deleteTarget && (
|
||||||
return (
|
<div style={{
|
||||||
<div
|
position: 'fixed', inset: 0,
|
||||||
className="dog-card dog-card--external"
|
background: 'rgba(0,0,0,0.65)',
|
||||||
onClick={() => window.location.href = `/dogs/${dog.id}`}
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
role="button"
|
zIndex: 1000,
|
||||||
tabIndex={0}
|
backdropFilter: 'blur(4px)'
|
||||||
onKeyDown={e => e.key === 'Enter' && (window.location.href = `/dogs/${dog.id}`)}
|
}}>
|
||||||
|
<div className="card" style={{ maxWidth: 420, width: '90%', padding: '2rem', textAlign: 'center' }}>
|
||||||
|
<div style={{
|
||||||
|
width: 56, height: 56,
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: 'rgba(239,68,68,0.12)',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
margin: '0 auto 1rem'
|
||||||
|
}}>
|
||||||
|
<Trash2 size={26} style={{ color: '#ef4444' }} />
|
||||||
|
</div>
|
||||||
|
<h3 style={{ margin: '0 0 0.5rem', fontSize: '1.25rem' }}>Delete External Dog?</h3>
|
||||||
|
<p style={{ color: 'var(--text-secondary)', marginBottom: '1.75rem', lineHeight: 1.6 }}>
|
||||||
|
<strong style={{ color: 'var(--text-primary)' }}>{deleteTarget.name}</strong> will be
|
||||||
|
permanently removed. This cannot be undone.
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center' }}>
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost"
|
||||||
|
onClick={() => setDeleteTarget(null)}
|
||||||
|
disabled={deleting}
|
||||||
|
style={{ minWidth: 100 }}
|
||||||
>
|
>
|
||||||
<div className="dog-card-photo">
|
Cancel
|
||||||
{photo
|
</button>
|
||||||
? <img src={photo} alt={dog.name} />
|
<button
|
||||||
: <div className="dog-card-photo-placeholder"><Users size={32} /></div>
|
className="btn"
|
||||||
}
|
onClick={handleDelete}
|
||||||
{dog.is_champion === 1 && <span className="champion-badge" title="Champion">🏆</span>}
|
disabled={deleting}
|
||||||
<span className="external-badge"><ExternalLink size={11} /> Ext</span>
|
style={{
|
||||||
|
minWidth: 140,
|
||||||
|
background: '#ef4444',
|
||||||
|
color: '#fff',
|
||||||
|
border: '1px solid #ef4444',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.5rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 size={15} />
|
||||||
|
{deleting ? 'Deleting…' : 'Yes, Delete'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="dog-card-body">
|
|
||||||
<div className="dog-card-name">
|
|
||||||
{dog.is_champion === 1 && <Award size={13} className="champion-icon" />}
|
|
||||||
{dog.name}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="dog-card-meta">{dog.breed}</div>
|
|
||||||
<div className="dog-card-meta dog-card-meta--muted">
|
|
||||||
{dog.sex === 'male' ? '\u2642 Sire' : '\u2640 Dam'}
|
|
||||||
{dog.birth_date && <>· {dog.birth_date}</>}
|
|
||||||
</div>
|
|
||||||
{(dog.sire || dog.dam) && (
|
|
||||||
<div className="dog-card-parents">
|
|
||||||
{dog.sire && <span>S: {dog.sire.name}</span>}
|
|
||||||
{dog.dam && <span>D: {dog.dam.name}</span>}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default ExternalDogs
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default function PairingSimulator() {
|
|||||||
setError(null)
|
setError(null)
|
||||||
setResult(null)
|
setResult(null)
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/pedigree/coi', {
|
const res = await fetch('/api/pedigree/trial-pairing', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ sire_id: parseInt(sireId), dam_id: parseInt(damId) }),
|
body: JSON.stringify({ sire_id: parseInt(sireId), dam_id: parseInt(damId) }),
|
||||||
@@ -204,20 +204,20 @@ export default function PairingSimulator() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{result.common_ancestors && result.common_ancestors.length > 0 && (
|
{result.commonAncestors && result.commonAncestors.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h3 style={{ fontSize: '0.875rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '0.5rem' }}>
|
<h3 style={{ fontSize: '0.875rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '0.5rem' }}>
|
||||||
Common Ancestors ({result.common_ancestors.length})
|
Common Ancestors ({result.commonAncestors.length})
|
||||||
</h3>
|
</h3>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.4rem' }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.4rem' }}>
|
||||||
{result.common_ancestors.map((a, i) => (
|
{result.commonAncestors.map((a, i) => (
|
||||||
<span key={i} style={{
|
<span key={i} style={{
|
||||||
padding: '0.2rem 0.6rem',
|
padding: '0.2rem 0.6rem',
|
||||||
background: 'var(--bg-tertiary)',
|
background: 'var(--bg-tertiary)',
|
||||||
borderRadius: 'var(--radius-sm)',
|
borderRadius: 'var(--radius-sm)',
|
||||||
fontSize: '0.8rem',
|
fontSize: '0.8rem',
|
||||||
border: '1px solid var(--border)',
|
border: '1px solid var(--border)',
|
||||||
}}>{a}</span>
|
}}>{a.name}</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -144,16 +144,16 @@ export const formatCOI = (coi) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = coi.toFixed(2)
|
const value = (coi * 100).toFixed(2)
|
||||||
|
|
||||||
if (coi <= 5) {
|
if (coi <= 0.05) {
|
||||||
return {
|
return {
|
||||||
value: `${value}%`,
|
value: `${value}%`,
|
||||||
level: 'low',
|
level: 'low',
|
||||||
color: '#10b981',
|
color: '#10b981',
|
||||||
description: 'Low inbreeding - Excellent genetic diversity'
|
description: 'Low inbreeding - Excellent genetic diversity'
|
||||||
}
|
}
|
||||||
} else if (coi <= 10) {
|
} else if (coi <= 0.10) {
|
||||||
return {
|
return {
|
||||||
value: `${value}%`,
|
value: `${value}%`,
|
||||||
level: 'medium',
|
level: 'medium',
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ function calculateCOI(db, sireId, damId) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
coefficient: Math.round(coi * 10000) / 100,
|
coefficient: coi,
|
||||||
commonAncestors: commonAncestorList
|
commonAncestors: commonAncestorList
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -124,8 +124,8 @@ function calculateCOI(db, sireId, damId) {
|
|||||||
// 'trial-pairing' as dog IDs and return 404/wrong data.
|
// 'trial-pairing' as dog IDs and return 404/wrong data.
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|
||||||
// POST /api/pedigree/trial-pairing
|
// POST /api/pedigree/trial-pairing (alias for /coi)
|
||||||
router.post('/trial-pairing', (req, res) => {
|
router.post(['/trial-pairing', '/coi'], (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { sire_id, dam_id } = req.body;
|
const { sire_id, dam_id } = req.body;
|
||||||
if (!sire_id || !dam_id) {
|
if (!sire_id || !dam_id) {
|
||||||
@@ -149,8 +149,8 @@ router.post('/trial-pairing', (req, res) => {
|
|||||||
coi: result.coefficient,
|
coi: result.coefficient,
|
||||||
commonAncestors: result.commonAncestors,
|
commonAncestors: result.commonAncestors,
|
||||||
directRelation: relation.related ? relation.relationship : null,
|
directRelation: relation.related ? relation.relationship : null,
|
||||||
recommendation: result.coefficient < 5 ? 'Low risk'
|
recommendation: result.coefficient < 0.05 ? 'Low risk'
|
||||||
: result.coefficient < 10 ? 'Moderate risk'
|
: result.coefficient < 0.10 ? 'Moderate risk'
|
||||||
: 'High risk'
|
: 'High risk'
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -158,6 +158,28 @@ router.post('/trial-pairing', (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /api/pedigree/:id/coi
|
||||||
|
router.get('/:id/coi', (req, res) => {
|
||||||
|
try {
|
||||||
|
const db = getDatabase();
|
||||||
|
const parents = db.prepare('SELECT parent_type, parent_id FROM parents WHERE dog_id = ?').all(req.params.id);
|
||||||
|
const sire = parents.find(p => p.parent_type === 'sire');
|
||||||
|
const dam = parents.find(p => p.parent_type === 'dam');
|
||||||
|
|
||||||
|
if (!sire || !dam) {
|
||||||
|
return res.json({ coi: 0, commonAncestors: [], message: 'Incomplete parent data' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = calculateCOI(db, sire.parent_id, dam.parent_id);
|
||||||
|
res.json({
|
||||||
|
coi: result.coefficient,
|
||||||
|
commonAncestors: result.commonAncestors
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// GET /api/pedigree/relations/:sireId/:damId
|
// GET /api/pedigree/relations/:sireId/:damId
|
||||||
router.get('/relations/:sireId/:damId', (req, res) => {
|
router.get('/relations/:sireId/:damId', (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user