Compare commits
17 Commits
aa3b1b2404
...
main2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b941c9a9a | ||
|
|
055364f467 | ||
|
|
b8eadd9efa | ||
|
|
ff1eb455dc | ||
|
|
c22ebbe45c | ||
|
|
e5f7b2b053 | ||
|
|
c00b6191e7 | ||
|
|
0f9d3cf187 | ||
|
|
2daccf7d8c | ||
| 5c6068364b | |||
| 768e25183d | |||
| 78069f2880 | |||
| 2cfeaf667e | |||
| 5eaa6e566c | |||
| 80b497e902 | |||
| 8cb4c773fd | |||
| 22e85f0d7e |
@@ -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.
|
||||||
87
.zenflow/tasks/init-9c68/plan.md
Normal file
87
.zenflow/tasks/init-9c68/plan.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Full SDD workflow
|
||||||
|
|
||||||
|
## 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: Requirements
|
||||||
|
<!-- chat-id: c41b4352-a09d-4280-9aa2-cf29378edb68 -->
|
||||||
|
|
||||||
|
Create a Product Requirements Document (PRD) based on the feature description.
|
||||||
|
|
||||||
|
1. Review existing codebase to understand current architecture and patterns
|
||||||
|
2. Analyze the feature definition and identify unclear aspects
|
||||||
|
3. Ask the user for clarifications on aspects that significantly impact scope or user experience
|
||||||
|
4. Make reasonable decisions for minor details based on context and conventions
|
||||||
|
5. If user can't clarify, make a decision, state the assumption, and continue
|
||||||
|
|
||||||
|
Save the PRD to `{@artifacts_path}/requirements.md`.
|
||||||
|
|
||||||
|
### [x] Step: Technical Specification
|
||||||
|
<!-- chat-id: c8a2d821-c232-4b10-a4b9-471df9e53543 -->
|
||||||
|
|
||||||
|
Create a technical specification based on the PRD in `{@artifacts_path}/requirements.md`.
|
||||||
|
|
||||||
|
1. Review existing codebase architecture and identify reusable components
|
||||||
|
2. Define the implementation approach
|
||||||
|
|
||||||
|
Save to `{@artifacts_path}/spec.md` with:
|
||||||
|
- Technical context (language, dependencies)
|
||||||
|
- Implementation approach referencing existing code patterns
|
||||||
|
- Source code structure changes
|
||||||
|
- Data model / API / interface changes
|
||||||
|
- Delivery phases (incremental, testable milestones)
|
||||||
|
- Verification approach using project lint/test commands
|
||||||
|
|
||||||
|
### [x] Step: Planning
|
||||||
|
<!-- chat-id: 5128cbb3-0529-47f2-a7ac-9a978ea72267 -->
|
||||||
|
|
||||||
|
Create a detailed implementation plan based on `{@artifacts_path}/spec.md`.
|
||||||
|
|
||||||
|
1. Break down the work into concrete tasks
|
||||||
|
2. Each task should reference relevant contracts and include verification steps
|
||||||
|
3. Replace the Implementation step below with the planned tasks
|
||||||
|
|
||||||
|
Rule of thumb for step size: each step should represent a coherent unit of work (e.g., implement a component, add an API endpoint). Avoid steps that are too granular (single function) or too broad (entire feature).
|
||||||
|
|
||||||
|
Important: unit tests must be part of each implementation task, not separate tasks. Each task should implement the code and its tests together, if relevant.
|
||||||
|
|
||||||
|
If the feature is trivial and doesn't warrant full specification, update this workflow to remove unnecessary steps and explain the reasoning to the user.
|
||||||
|
|
||||||
|
Save to `{@artifacts_path}/plan.md`.
|
||||||
|
|
||||||
|
### [x] Phase 1: Create DEVELOPMENT.md
|
||||||
|
<!-- chat-id: 0da2c64e-4b20-423d-9049-46fc0467eaec -->
|
||||||
|
1. Research tech stack, monorepo structure, and database schemas in `server/db/`.
|
||||||
|
2. Document the "Parents Table" approach and database initialization/migration.
|
||||||
|
3. Add setup and development commands.
|
||||||
|
4. Verify correctness against `server/db/init.js` and `package.json`.
|
||||||
|
|
||||||
|
### [x] Phase 2: Create API.md
|
||||||
|
<!-- chat-id: bde368a7-164c-419e-b4b9-582e6ced4a45 -->
|
||||||
|
1. Research all routes in `server/routes/` for endpoints, methods, parameters, and responses.
|
||||||
|
2. Document endpoint groups: Dogs, Litters, Health, Genetics, Breeding, and Settings.
|
||||||
|
3. Provide JSON schema examples for key data models (Dog, Litter, etc.).
|
||||||
|
4. Verify endpoints against route handlers in `server/routes/`.
|
||||||
|
|
||||||
|
### [x] Phase 3: Create FRONTEND_GUIDE.md
|
||||||
|
<!-- chat-id: f0c53f46-3014-4030-9433-3df4d730fde7 -->
|
||||||
|
1. Research React patterns, hooks (`useSettings`), and `PedigreeTree` logic.
|
||||||
|
2. Document routing, state management, and key reusable components (`DogForm`, `PedigreeTree`, etc.).
|
||||||
|
3. Explain styling conventions and theme implementation using CSS variables.
|
||||||
|
4. Verify patterns against `client/src/App.jsx`, `client/src/hooks/`, and `client/src/components/`.
|
||||||
|
|
||||||
|
### [x] Phase 4: Final Review and Verification
|
||||||
|
<!-- chat-id: b87b4e6a-5d6a-4ad2-91dc-70916b830845 -->
|
||||||
|
1. Cross-reference all new documentation files with the current codebase (v0.6.1).
|
||||||
|
2. Ensure consistent formatting and clarity across all three files.
|
||||||
|
3. Verify that an agent can understand how to implement a new feature using only these documents.
|
||||||
44
.zenflow/tasks/init-9c68/requirements.md
Normal file
44
.zenflow/tasks/init-9c68/requirements.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Product Requirements Document (PRD) - INIT (Codebase Documentation)
|
||||||
|
|
||||||
|
## 1. Goal
|
||||||
|
The primary goal of this task is to perform a comprehensive scan of the Breedr codebase and create essential developer-focused documentation (`.md` files). This documentation will streamline the onboarding of new agents or developers and simplify the process of implementing new features and fixing bugs.
|
||||||
|
|
||||||
|
## 2. Target Audience
|
||||||
|
- AI Agents performing code modifications.
|
||||||
|
- Human developers contributing to the project.
|
||||||
|
|
||||||
|
## 3. Scope of Documentation
|
||||||
|
|
||||||
|
### 3.1 DEVELOPMENT.md (Architecture & General Guide)
|
||||||
|
This document will serve as the entry point for understanding the Breedr system.
|
||||||
|
- **Tech Stack Overview**: React, Express, SQLite (better-sqlite3).
|
||||||
|
- **Architecture**: Monorepo structure (`client/`, `server/`), data flow, and core principles.
|
||||||
|
- **Database System**: Detailed explanation of the "Parents Table" approach vs. traditional `sire_id`/`dam_id` columns, migration strategies, and schema initialization.
|
||||||
|
- **Project Structure**: High-level explanation of key directories and files.
|
||||||
|
- **Development Workflow**: How to run the app locally, common commands, and testing procedures (if any).
|
||||||
|
- **Coding Standards**: Patterns for backend routes and frontend components.
|
||||||
|
|
||||||
|
### 3.2 API.md (REST API Documentation)
|
||||||
|
A comprehensive list of all backend API endpoints.
|
||||||
|
- **Endpoint Definitions**: URL, method, and purpose.
|
||||||
|
- **Request Parameters**: Headers, query params, and body schemas.
|
||||||
|
- **Response Format**: Expected JSON structure and status codes.
|
||||||
|
- **Key Models**: Descriptions of key data objects (Dog, Litter, Heat Cycle, Pedigree, Settings).
|
||||||
|
|
||||||
|
### 3.3 FRONTEND_GUIDE.md (UI/UX & React Patterns)
|
||||||
|
A guide focusing on the client-side implementation.
|
||||||
|
- **Context & Hooks**: Documentation of `useSettings`, `SettingsProvider`, and any other shared state mechanisms.
|
||||||
|
- **Component Patterns**: Key reusable components (`DogForm`, `PedigreeTree`, etc.).
|
||||||
|
- **Styling**: Use of CSS custom properties (theming) and global styles.
|
||||||
|
- **Pedigree Visualization**: How `react-d3-tree` is integrated and used for genealogy mapping.
|
||||||
|
- **Routing**: Client-side navigation structure using `react-router-dom`.
|
||||||
|
|
||||||
|
## 4. Non-Functional Requirements
|
||||||
|
- **Consistency**: Documentation must match the current state (v0.6.1) of the codebase.
|
||||||
|
- **Clarity**: Use clear, concise language and code examples where appropriate.
|
||||||
|
- **Maintainability**: Organize documents so they are easy to update when new features are added.
|
||||||
|
|
||||||
|
## 5. Success Criteria
|
||||||
|
- The three proposed documentation files (`DEVELOPMENT.md`, `API.md`, `FRONTEND_GUIDE.md`) are created in the project root.
|
||||||
|
- The documentation accurately reflects the current codebase architecture, API, and frontend patterns.
|
||||||
|
- An agent can use these documents to understand how to implement a new feature (e.g., adding a new field to the Dog model) without scanning the entire codebase.
|
||||||
89
.zenflow/tasks/init-9c68/spec.md
Normal file
89
.zenflow/tasks/init-9c68/spec.md
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# Technical Specification - Codebase Documentation (INIT)
|
||||||
|
|
||||||
|
This specification outlines the plan for creating comprehensive developer documentation for the Breedr codebase.
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
- **Backend**: Node.js, Express, `better-sqlite3`.
|
||||||
|
- **Frontend**: React (Vite), `react-router-dom`, `axios`, `react-d3-tree`.
|
||||||
|
- **Database**: SQLite (managed via `server/db/init.js` and `server/db/migrations.js`).
|
||||||
|
- **Structure**: Monorepo-style with `client/` and `server/` directories.
|
||||||
|
|
||||||
|
## Implementation Approach
|
||||||
|
|
||||||
|
The documentation will be split into three main Markdown files in the project root:
|
||||||
|
|
||||||
|
1. **DEVELOPMENT.md**: Focuses on architecture, database design, and workflow.
|
||||||
|
2. **API.md**: Detailed documentation of all REST API endpoints.
|
||||||
|
3. **FRONTEND_GUIDE.md**: Focuses on React patterns, components, and styling.
|
||||||
|
|
||||||
|
### Research Methodology
|
||||||
|
- **Database**: Analyze `server/db/init.js` for table schemas and `parents` table logic.
|
||||||
|
- **API**: Scan `server/routes/*.js` for endpoints, middleware, and request/response structures.
|
||||||
|
- **Frontend**: Analyze `client/src/App.jsx` for routing, `client/src/hooks/` for state management, and `client/src/components/` for reusable patterns.
|
||||||
|
|
||||||
|
## Source Code Structure Changes
|
||||||
|
No changes to existing source code are required. Three new files will be created in the root directory:
|
||||||
|
- `/DEVELOPMENT.md`
|
||||||
|
- `/API.md`
|
||||||
|
- `/FRONTEND_GUIDE.md`
|
||||||
|
|
||||||
|
## Documentation Structure
|
||||||
|
|
||||||
|
### 1. DEVELOPMENT.md
|
||||||
|
- **Overview**: System purpose and high-level architecture.
|
||||||
|
- **Project Layout**: Description of key directories (`client`, `server`, `data`, `static`, `uploads`).
|
||||||
|
- **Database Design**:
|
||||||
|
- Explain the "Parents Table" approach (decoupling genealogy from the `dogs` table).
|
||||||
|
- Schema overview (Dogs, Litters, Health, Genetics, Settings).
|
||||||
|
- Initialization and migration process.
|
||||||
|
- **Getting Started**:
|
||||||
|
- `npm install` (root and client).
|
||||||
|
- `npm run dev` (concurrent execution).
|
||||||
|
- Database initialization (`npm run db:init`).
|
||||||
|
- **Coding Standards**: Backend route structure, async/await usage, error handling.
|
||||||
|
|
||||||
|
### 2. API.md
|
||||||
|
- **Base URL**: `/api`
|
||||||
|
- **Authentication**: (Note if any exists, currently seems open).
|
||||||
|
- **Endpoint Groups**:
|
||||||
|
- `Dogs`: CRUD operations, photo management, parent/offspring retrieval.
|
||||||
|
- `Litters`: Management of whelping records.
|
||||||
|
- `Health`: OFA records and test results.
|
||||||
|
- `Genetics`: DNA panel markers and results.
|
||||||
|
- `Breeding`: Breeding records and pairing simulations.
|
||||||
|
- `Settings`: Kennel profile management.
|
||||||
|
- **Data Models**: JSON schema examples for Dog, Litter, HealthRecord, etc.
|
||||||
|
|
||||||
|
### 3. FRONTEND_GUIDE.md
|
||||||
|
- **Tech Stack**: Vite, React, CSS Modules/Global CSS.
|
||||||
|
- **Routing**: `react-router-dom` configuration in `App.jsx`.
|
||||||
|
- **State Management**: `SettingsProvider` and `useSettings` hook.
|
||||||
|
- **Pedigree Engine**: Implementation of `react-d3-tree` and `pedigreeHelpers.js`.
|
||||||
|
- **Key Components**:
|
||||||
|
- `DogForm`: Complex form with parent selection.
|
||||||
|
- `PedigreeTree`: SVG-based genealogy visualization.
|
||||||
|
- `ClearanceSummaryCard`: Health status overview.
|
||||||
|
- **Styling**: Theming with CSS variables (found in `index.css` and `App.css`).
|
||||||
|
|
||||||
|
## Delivery Phases
|
||||||
|
|
||||||
|
### Phase 1: Core Architecture & Database (DEVELOPMENT.md)
|
||||||
|
- Document the tech stack and monorepo structure.
|
||||||
|
- Detail the SQLite schema and genealogy logic.
|
||||||
|
- Add setup and development commands.
|
||||||
|
|
||||||
|
### Phase 2: API Documentation (API.md)
|
||||||
|
- Document all routes in `server/routes/`.
|
||||||
|
- Provide request/response examples.
|
||||||
|
- Document the `parents` table integration in API responses.
|
||||||
|
|
||||||
|
### Phase 3: Frontend Guide (FRONTEND_GUIDE.md)
|
||||||
|
- Document React component patterns and hooks.
|
||||||
|
- Explain the pedigree visualization logic.
|
||||||
|
- Document routing and styling conventions.
|
||||||
|
|
||||||
|
## Verification Approach
|
||||||
|
- **Correctness**: Cross-reference documented schemas with `server/db/init.js`.
|
||||||
|
- **Accuracy**: Test documented API endpoints against the running server if possible, or verify via route handlers.
|
||||||
|
- **Completeness**: Ensure all components in `client/src/components` and routes in `server/routes` are mentioned or categorized.
|
||||||
|
- **Formatting**: Use `markdownlint` (if available) or manual review to ensure readability.
|
||||||
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`.
|
||||||
201
API.md
Normal file
201
API.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
# BREEDR API Documentation (v0.6.1)
|
||||||
|
|
||||||
|
Base URL: `/api`
|
||||||
|
|
||||||
|
All endpoints return JSON responses. Errors follow the format `{ "error": "message" }`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Dogs (`/api/dogs`)
|
||||||
|
|
||||||
|
Manage individual dogs in the kennel.
|
||||||
|
|
||||||
|
### `GET /`
|
||||||
|
Get active kennel dogs.
|
||||||
|
- **Query Params**:
|
||||||
|
- `include_external=1`: Include external dogs (studs/others)
|
||||||
|
- `external_only=1`: Only show external dogs
|
||||||
|
- **Response**: Array of [Dog](#dog-object) objects with `sire` and `dam` attached.
|
||||||
|
|
||||||
|
### `GET /:id`
|
||||||
|
Get single dog details.
|
||||||
|
- **Response**: [Dog](#dog-object) object including `sire`, `dam`, and `offspring` array.
|
||||||
|
|
||||||
|
### `POST /`
|
||||||
|
Create a new dog.
|
||||||
|
- **Body**: See [Dog](#dog-object) fields. `name`, `breed`, `sex` are required.
|
||||||
|
- **Response**: The created Dog object.
|
||||||
|
|
||||||
|
### `PUT /:id`
|
||||||
|
Update an existing dog.
|
||||||
|
- **Body**: Dog fields to update.
|
||||||
|
- **Response**: The updated Dog object.
|
||||||
|
|
||||||
|
### `DELETE /:id`
|
||||||
|
Permanently delete a dog and its related records (health, heat, parents).
|
||||||
|
- **Response**: `{ "success": true, "message": "..." }`
|
||||||
|
|
||||||
|
### `POST /:id/photos`
|
||||||
|
Upload a photo for a dog.
|
||||||
|
- **Form-Data**: `photo` (file)
|
||||||
|
- **Response**: `{ "url": "...", "photos": [...] }`
|
||||||
|
|
||||||
|
### `DELETE /:id/photos/:photoIndex`
|
||||||
|
Delete a specific photo from a dog's photo array.
|
||||||
|
- **Response**: `{ "photos": [...] }`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Litters (`/api/litters`)
|
||||||
|
|
||||||
|
Manage breeding litters and puppy logs.
|
||||||
|
|
||||||
|
### `GET /`
|
||||||
|
Get all litters.
|
||||||
|
- **Response**: Array of [Litter](#litter-object) objects with sire/dam names and puppies.
|
||||||
|
|
||||||
|
### `GET /:id`
|
||||||
|
Get single litter details.
|
||||||
|
- **Response**: [Litter](#litter-object) object.
|
||||||
|
|
||||||
|
### `POST /`
|
||||||
|
Create a new litter.
|
||||||
|
- **Body**: `sire_id`, `dam_id`, `breeding_date` (required), `whelping_date`, `puppy_count`, `notes`.
|
||||||
|
- **Response**: The created Litter object.
|
||||||
|
|
||||||
|
### `PUT /:id`
|
||||||
|
Update litter details.
|
||||||
|
|
||||||
|
### `POST /:id/puppies/:puppyId`
|
||||||
|
Link a dog to a litter as a puppy.
|
||||||
|
- **Side Effect**: Automatically sets the litter's sire and dam as the puppy's parents.
|
||||||
|
|
||||||
|
### `DELETE /:id/puppies/:puppyId`
|
||||||
|
Remove a puppy from a litter (sets `litter_id` to NULL).
|
||||||
|
|
||||||
|
### `GET /:litterId/puppies/:puppyId/logs`
|
||||||
|
Get weight and health logs for a puppy.
|
||||||
|
|
||||||
|
### `POST /:litterId/puppies/:puppyId/logs`
|
||||||
|
Add a weight/health log entry.
|
||||||
|
- **Body**: `record_date` (required), `weight_oz`, `weight_lbs`, `notes`, `record_type`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Health (`/api/health`)
|
||||||
|
|
||||||
|
Manage OFA clearances and veterinary records.
|
||||||
|
|
||||||
|
### `GET /dog/:dogId`
|
||||||
|
Get all health records for a dog.
|
||||||
|
|
||||||
|
### `GET /dog/:dogId/clearance-summary`
|
||||||
|
Get GRCA core clearance status (Hip, Elbow, Heart, Eyes).
|
||||||
|
- **Response**: `{ summary, grca_eligible, age_eligible, chic_number }`
|
||||||
|
|
||||||
|
### `GET /dog/:dogId/chic-eligible`
|
||||||
|
Check if a dog has all required CHIC tests.
|
||||||
|
|
||||||
|
### `POST /`
|
||||||
|
Create health record.
|
||||||
|
- **Body**: `dog_id`, `record_type`, `test_date` (required), `test_type`, `test_name`, `ofa_result`, `ofa_number`, etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Genetics (`/api/genetics`)
|
||||||
|
|
||||||
|
Manage DNA panel results and breeding risks.
|
||||||
|
|
||||||
|
### `GET /dog/:dogId`
|
||||||
|
Get the full genetic panel for a dog. Returns `tests` (actual records) and `panel` (full list including `not_tested` placeholders).
|
||||||
|
|
||||||
|
### `GET /pairing-risk?sireId=X&damId=Y`
|
||||||
|
Check genetic compatibility between two dogs.
|
||||||
|
- **Response**: `{ risks: [...], safe_to_pair: boolean }`
|
||||||
|
|
||||||
|
### `POST /`
|
||||||
|
Add a genetic test result.
|
||||||
|
- **Body**: `dog_id`, `marker`, `result` (clear|carrier|affected|not_tested).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Breeding (`/api/breeding`)
|
||||||
|
|
||||||
|
Track heat cycles and whelping projections.
|
||||||
|
|
||||||
|
### `GET /heat-cycles/active`
|
||||||
|
Get currently active heat cycles.
|
||||||
|
|
||||||
|
### `GET /heat-cycles/:id/suggestions`
|
||||||
|
Get optimal breeding window (days 9-15) and whelping projections.
|
||||||
|
|
||||||
|
### `POST /heat-cycles`
|
||||||
|
Log a new heat cycle. Dog must be female.
|
||||||
|
|
||||||
|
### `GET /whelping-calculator?breeding_date=YYYY-MM-DD`
|
||||||
|
Standalone tool to calculate expected whelping window.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Pedigree (`/api/pedigree`)
|
||||||
|
|
||||||
|
Advanced ancestry and COI calculations.
|
||||||
|
|
||||||
|
### `GET /:id?generations=N`
|
||||||
|
Get an interactive pedigree tree (ancestors). Default 5 generations.
|
||||||
|
|
||||||
|
### `GET /:id/descendants?generations=N`
|
||||||
|
Get a descendant tree. Default 3 generations.
|
||||||
|
|
||||||
|
### `POST /trial-pairing`
|
||||||
|
Calculate Coefficient of Inbreeding (COI) for a hypothetical mating.
|
||||||
|
- **Body**: `sire_id`, `dam_id`.
|
||||||
|
- **Response**: `{ coi, commonAncestors, directRelation, recommendation }`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Settings (`/api/settings`)
|
||||||
|
|
||||||
|
### `GET /`
|
||||||
|
Get kennel metadata (name, address, etc.).
|
||||||
|
|
||||||
|
### `PUT /`
|
||||||
|
Update kennel settings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Objects
|
||||||
|
|
||||||
|
### Dog Object
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "BREEDR Champion",
|
||||||
|
"registration_number": "AKC123456",
|
||||||
|
"breed": "Golden Retriever",
|
||||||
|
"sex": "male",
|
||||||
|
"birth_date": "2020-01-01",
|
||||||
|
"color": "Golden",
|
||||||
|
"microchip": "900123456789",
|
||||||
|
"is_active": 1,
|
||||||
|
"is_champion": 1,
|
||||||
|
"is_external": 0,
|
||||||
|
"photo_urls": ["/uploads/img.jpg"],
|
||||||
|
"notes": "Excellent temperament",
|
||||||
|
"sire": { "id": 10, "name": "Sire Name" },
|
||||||
|
"dam": { "id": 11, "name": "Dam Name" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Litter Object
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"sire_id": 1,
|
||||||
|
"dam_id": 2,
|
||||||
|
"breeding_date": "2023-01-01",
|
||||||
|
"whelp_date": "2023-03-05",
|
||||||
|
"total_count": 8,
|
||||||
|
"puppies": [ ... ]
|
||||||
|
}
|
||||||
|
```
|
||||||
118
DEVELOPMENT.md
Normal file
118
DEVELOPMENT.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# DEVELOPMENT.md
|
||||||
|
|
||||||
|
This document provides technical details and guidelines for developing and maintaining the BREEDR Genealogy Management System.
|
||||||
|
|
||||||
|
## Tech Stack Overview
|
||||||
|
|
||||||
|
- **Monorepo Structure**:
|
||||||
|
- `server/`: Express.js backend.
|
||||||
|
- `client/`: React/Vite frontend.
|
||||||
|
- `data/`: SQLite database storage.
|
||||||
|
- `uploads/`: Uploaded images and documents.
|
||||||
|
- `static/`: Static assets for the application.
|
||||||
|
|
||||||
|
- **Backend**: Node.js, Express, better-sqlite3, multer, bcrypt, jsonwebtoken.
|
||||||
|
- **Frontend**: React 18, Vite, React Router 6, Axios, Lucide React, D3 (for pedigree trees).
|
||||||
|
- **Database**: SQLite (managed with better-sqlite3).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Node.js (v18+ recommended)
|
||||||
|
- npm
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
1. Clone the repository.
|
||||||
|
2. Install root dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
3. Install client dependencies:
|
||||||
|
```bash
|
||||||
|
cd client && npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Commands
|
||||||
|
Run the following from the project root:
|
||||||
|
- **Run both client and server**: `npm run dev`
|
||||||
|
- **Run server only**: `npm run server`
|
||||||
|
- **Run client only**: `npm run client`
|
||||||
|
- **Initialize Database**: `npm run db:init`
|
||||||
|
- **Build for production**: `npm run build`
|
||||||
|
|
||||||
|
## Database Architecture
|
||||||
|
|
||||||
|
### Data Storage
|
||||||
|
The database is a single SQLite file located at `data/breedr.db`. This directory is automatically created on startup if it doesn't exist.
|
||||||
|
|
||||||
|
### Initialization & Schema
|
||||||
|
- **Initialization**: `server/db/init.js` defines the initial schema and creates tables if they don't exist.
|
||||||
|
- **Migrations**: `server/db/migrations.js` handles schema updates. Migrations run automatically on server startup.
|
||||||
|
|
||||||
|
### "Parents Table" Approach
|
||||||
|
Instead of storing parent IDs directly in the `dogs` table (which was the old approach), relationships are managed in a dedicated `parents` table:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE parents (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
dog_id INTEGER NOT NULL,
|
||||||
|
parent_id INTEGER NOT NULL,
|
||||||
|
parent_type TEXT NOT NULL CHECK(parent_type IN ('sire', 'dam')),
|
||||||
|
FOREIGN KEY (dog_id) REFERENCES dogs(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (parent_id) REFERENCES dogs(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE(dog_id, parent_type)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Benefits**:
|
||||||
|
- Avoids complex `ALTER TABLE` operations when changing pedigree logic.
|
||||||
|
- Cleanly separates dog attributes from lineage relationships.
|
||||||
|
- Supports indexing for fast recursive pedigree lookups.
|
||||||
|
|
||||||
|
### Key Tables
|
||||||
|
- **`dogs`**: Core dog data (name, breed, sex, microchip, etc.).
|
||||||
|
- **`parents`**: Lineage relationships (Sire/Dam).
|
||||||
|
- **`litters`**: Groups of dogs from a single breeding.
|
||||||
|
- **`breeding_records`**: Planned or completed breeding events.
|
||||||
|
- **`health_records`**: OFA results, vaccinations, and other health tests.
|
||||||
|
- **`genetic_tests`**: DNA panel results.
|
||||||
|
- **`settings`**: Kennel-wide configurations.
|
||||||
|
|
||||||
|
## Backend Development
|
||||||
|
|
||||||
|
### API Routes
|
||||||
|
Routes are modularized in `server/routes/`:
|
||||||
|
- `/api/dogs`: Dog management.
|
||||||
|
- `/api/litters`: Litter management.
|
||||||
|
- `/api/health`: Health record management.
|
||||||
|
- `/api/genetics`: Genetic testing management.
|
||||||
|
- `/api/pedigree`: Pedigree tree generation.
|
||||||
|
- `/api/breeding`: Breeding records.
|
||||||
|
- `/api/settings`: Application settings.
|
||||||
|
|
||||||
|
### File Uploads
|
||||||
|
Images and documents are stored in `uploads/`. The `multer` middleware handles file processing. File paths are stored in the database as relative URLs (e.g., `/uploads/image.jpg`).
|
||||||
|
|
||||||
|
## Frontend Development
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
- **Settings**: Managed globally via `SettingsProvider` in `client/src/hooks/useSettings.jsx`.
|
||||||
|
- **Component State**: Local `useState` and `useEffect` are preferred for feature-specific data.
|
||||||
|
|
||||||
|
### Styling
|
||||||
|
- CSS Variables are used for theming.
|
||||||
|
- The UI uses a modern, clean design with Lucide icons.
|
||||||
|
|
||||||
|
### Pedigree Trees
|
||||||
|
The pedigree tree visualization is powered by `react-d3-tree` and D3.js. Logic for building the tree structure is located in `server/routes/pedigree.js` and visualized in the `PedigreeTree` component.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `PORT` | Server port | `3000` |
|
||||||
|
| `NODE_ENV` | Environment mode | `development` |
|
||||||
|
| `DATA_DIR` | Path to DB storage | `../data` |
|
||||||
|
| `UPLOAD_PATH` | Path to uploads | `../uploads` |
|
||||||
|
| `STATIC_PATH` | Path to static assets | `../static` |
|
||||||
|
| `DB_PATH` | Full path to .db file | `../data/breedr.db` |
|
||||||
105
FRONTEND_GUIDE.md
Normal file
105
FRONTEND_GUIDE.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# Frontend Guide
|
||||||
|
|
||||||
|
This document provides an overview of the frontend architecture, technologies, and patterns used in the BREEDR application.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Framework**: [React](https://reactjs.org/) (bootstrapped with [Vite](https://vitejs.dev/))
|
||||||
|
- **Routing**: [react-router-dom](https://reactrouter.com/)
|
||||||
|
- **Icons**: [lucide-react](https://lucide.dev/)
|
||||||
|
- **Data Fetching**: [axios](https://axios-http.com/)
|
||||||
|
- **Visualizations**: [react-d3-tree](https://github.com/bkrem/react-d3-tree) (for pedigree rendering)
|
||||||
|
- **Styling**: Standard CSS with CSS Variables (Custom properties)
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
client/src/
|
||||||
|
├── components/ # Reusable UI components
|
||||||
|
├── hooks/ # Custom hooks and context providers
|
||||||
|
├── pages/ # Page-level components (routes)
|
||||||
|
├── utils/ # Helper functions and utilities
|
||||||
|
├── App.jsx # Root component with routing
|
||||||
|
├── App.css # Layout-specific styles
|
||||||
|
├── index.css # Global styles and CSS variables
|
||||||
|
└── main.jsx # Application entry point
|
||||||
|
```
|
||||||
|
|
||||||
|
## Routing and Layout
|
||||||
|
|
||||||
|
The application uses `react-router-dom` for navigation. The primary layout and routes are defined in `client/src/App.jsx`.
|
||||||
|
|
||||||
|
- **Navbar**: Contains links to Dashboard, Dogs, Litters, Breeding, Pairing, and Settings.
|
||||||
|
- **Main Content**: Renders the matched route element within a `.main-content` container.
|
||||||
|
|
||||||
|
### Key Routes
|
||||||
|
- `/`: Dashboard
|
||||||
|
- `/dogs`: Dog List
|
||||||
|
- `/dogs/:id`: Dog Detail
|
||||||
|
- `/pedigree/:id`: Pedigree View
|
||||||
|
- `/litters`: Litter List
|
||||||
|
- `/breeding`: Breeding Calendar
|
||||||
|
- `/pairing`: Pairing Simulator
|
||||||
|
- `/settings`: Settings Page
|
||||||
|
|
||||||
|
## State Management
|
||||||
|
|
||||||
|
### Settings Context (`useSettings`)
|
||||||
|
Global application settings (like kennel name) are managed via a React Context.
|
||||||
|
|
||||||
|
- **Provider**: `SettingsProvider` in `client/src/hooks/useSettings.jsx`.
|
||||||
|
- **Usage**:
|
||||||
|
```javascript
|
||||||
|
import { useSettings } from '../hooks/useSettings';
|
||||||
|
const { settings, saveSettings, loading } = useSettings();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local State
|
||||||
|
Most page-specific data is managed using standard `useState` and `useEffect` hooks, fetching data via `axios`.
|
||||||
|
|
||||||
|
## Styling Conventions
|
||||||
|
|
||||||
|
The application follows a dark-theme aesthetic using CSS variables for consistency.
|
||||||
|
|
||||||
|
### CSS Variables (`client/src/index.css`)
|
||||||
|
Key variables include:
|
||||||
|
- `--primary`: Main brand color (warm amber/copper).
|
||||||
|
- `--bg-primary`: Primary background.
|
||||||
|
- `--text-primary`: Primary text color.
|
||||||
|
- `--border`: Standard border color.
|
||||||
|
- `--radius`: Default border radius.
|
||||||
|
|
||||||
|
### Reusable UI Classes
|
||||||
|
- `.btn`, `.btn-primary`, `.btn-secondary`: Standard button styles.
|
||||||
|
- `.card`: Container for grouped content.
|
||||||
|
- `.grid`, `.grid-2`, `.grid-3`: Responsive grid layouts.
|
||||||
|
- `.modal-overlay`, `.modal-content`: Standard modal structure.
|
||||||
|
|
||||||
|
## Key Components
|
||||||
|
|
||||||
|
### PedigreeTree (`client/src/components/PedigreeTree.jsx`)
|
||||||
|
Uses `react-d3-tree` to render a horizontal, step-based pedigree.
|
||||||
|
- **Props**: `dogId`, `pedigreeData`, `coi`.
|
||||||
|
- **Features**: Custom node rendering (differentiating by sex and champion status), zoom/pan controls, and COI display.
|
||||||
|
|
||||||
|
### DogForm (`client/src/components/DogForm.jsx`)
|
||||||
|
Handles both creation and editing of dog records.
|
||||||
|
- **Logic**: Manages internal/external dog states, parent selection (manual or linked to litter), and champion status.
|
||||||
|
- **Validation**: Basic required field validation; errors are displayed at the top of the form.
|
||||||
|
|
||||||
|
## Data Fetching Patterns
|
||||||
|
|
||||||
|
Standard `axios` requests are used:
|
||||||
|
```javascript
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const res = await axios.get('/api/endpoint');
|
||||||
|
setData(res.data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.error || 'Error message');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Icons
|
||||||
|
Use `lucide-react` for all icons to ensure consistency across the UI.
|
||||||
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
@@ -1,8 +1,8 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { X, Award } from 'lucide-react'
|
import { X, Award, ExternalLink } from 'lucide-react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
function DogForm({ dog, onClose, onSave }) {
|
function DogForm({ dog, onClose, onSave, isExternal = false }) {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
registration_number: '',
|
registration_number: '',
|
||||||
@@ -16,6 +16,7 @@ function DogForm({ dog, onClose, onSave }) {
|
|||||||
dam_id: null,
|
dam_id: null,
|
||||||
litter_id: null,
|
litter_id: null,
|
||||||
is_champion: false,
|
is_champion: false,
|
||||||
|
is_external: isExternal ? 1 : 0,
|
||||||
})
|
})
|
||||||
const [dogs, setDogs] = useState([])
|
const [dogs, setDogs] = useState([])
|
||||||
const [litters, setLitters] = useState([])
|
const [litters, setLitters] = useState([])
|
||||||
@@ -24,9 +25,14 @@ function DogForm({ dog, onClose, onSave }) {
|
|||||||
const [useManualParents, setUseManualParents] = useState(true)
|
const [useManualParents, setUseManualParents] = useState(true)
|
||||||
const [littersAvailable, setLittersAvailable] = useState(false)
|
const [littersAvailable, setLittersAvailable] = useState(false)
|
||||||
|
|
||||||
|
// Derive effective external state (editing an existing external dog or explicitly flagged)
|
||||||
|
const effectiveExternal = isExternal || (dog && dog.is_external)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDogs()
|
if (!effectiveExternal) {
|
||||||
fetchLitters()
|
fetchDogs()
|
||||||
|
fetchLitters()
|
||||||
|
}
|
||||||
if (dog) {
|
if (dog) {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: dog.name || '',
|
name: dog.name || '',
|
||||||
@@ -41,6 +47,7 @@ function DogForm({ dog, onClose, onSave }) {
|
|||||||
dam_id: dog.dam?.id || null,
|
dam_id: dog.dam?.id || null,
|
||||||
litter_id: dog.litter_id || null,
|
litter_id: dog.litter_id || null,
|
||||||
is_champion: !!dog.is_champion,
|
is_champion: !!dog.is_champion,
|
||||||
|
is_external: dog.is_external ?? (isExternal ? 1 : 0),
|
||||||
})
|
})
|
||||||
setUseManualParents(!dog.litter_id)
|
setUseManualParents(!dog.litter_id)
|
||||||
}
|
}
|
||||||
@@ -104,9 +111,10 @@ function DogForm({ dog, onClose, onSave }) {
|
|||||||
const submitData = {
|
const submitData = {
|
||||||
...formData,
|
...formData,
|
||||||
is_champion: formData.is_champion ? 1 : 0,
|
is_champion: formData.is_champion ? 1 : 0,
|
||||||
sire_id: formData.sire_id || null,
|
is_external: effectiveExternal ? 1 : 0,
|
||||||
dam_id: formData.dam_id || null,
|
sire_id: effectiveExternal ? null : (formData.sire_id || null),
|
||||||
litter_id: useManualParents ? null : (formData.litter_id || null),
|
dam_id: effectiveExternal ? null : (formData.dam_id || null),
|
||||||
|
litter_id: (effectiveExternal || useManualParents) ? null : (formData.litter_id || null),
|
||||||
registration_number: formData.registration_number || null,
|
registration_number: formData.registration_number || null,
|
||||||
birth_date: formData.birth_date || null,
|
birth_date: formData.birth_date || null,
|
||||||
color: formData.color || null,
|
color: formData.color || null,
|
||||||
@@ -133,10 +141,31 @@ function DogForm({ dog, onClose, onSave }) {
|
|||||||
<div className="modal-overlay" onClick={onClose}>
|
<div className="modal-overlay" onClick={onClose}>
|
||||||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<h2>{dog ? 'Edit Dog' : 'Add New Dog'}</h2>
|
<h2>
|
||||||
|
{effectiveExternal && <ExternalLink size={18} style={{ marginRight: '0.4rem', verticalAlign: 'middle', color: 'var(--text-muted)' }} />}
|
||||||
|
{dog ? 'Edit Dog' : effectiveExternal ? 'Add External Dog' : 'Add New Dog'}
|
||||||
|
</h2>
|
||||||
<button className="btn-icon" onClick={onClose}><X size={24} /></button>
|
<button className="btn-icon" onClick={onClose}><X size={24} /></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{effectiveExternal && (
|
||||||
|
<div style={{
|
||||||
|
margin: '0 0 1rem',
|
||||||
|
padding: '0.6rem 1rem',
|
||||||
|
background: 'rgba(99,102,241,0.08)',
|
||||||
|
border: '1px solid rgba(99,102,241,0.25)',
|
||||||
|
borderRadius: 'var(--radius)',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.5rem',
|
||||||
|
}}>
|
||||||
|
<ExternalLink size={14} />
|
||||||
|
External dog — not part of your kennel roster. Litter and parent fields are not applicable.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="modal-body">
|
<form onSubmit={handleSubmit} className="modal-body">
|
||||||
{error && <div className="error">{error}</div>}
|
{error && <div className="error">{error}</div>}
|
||||||
|
|
||||||
@@ -221,69 +250,71 @@ function DogForm({ dog, onClose, onSave }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Parent Section */}
|
{/* Parent Section — hidden for external dogs */}
|
||||||
<div style={{
|
{!effectiveExternal && (
|
||||||
marginTop: '1.5rem', padding: '1rem',
|
<div style={{
|
||||||
background: 'rgba(194, 134, 42, 0.04)',
|
marginTop: '1.5rem', padding: '1rem',
|
||||||
borderRadius: '8px',
|
background: 'rgba(194, 134, 42, 0.04)',
|
||||||
border: '1px solid rgba(194, 134, 42, 0.15)'
|
borderRadius: '8px',
|
||||||
}}>
|
border: '1px solid rgba(194, 134, 42, 0.15)'
|
||||||
<label className="label" style={{ marginBottom: '0.75rem', display: 'block', fontWeight: '600' }}>Parent Information</label>
|
}}>
|
||||||
|
<label className="label" style={{ marginBottom: '0.75rem', display: 'block', fontWeight: '600' }}>Parent Information</label>
|
||||||
|
|
||||||
{littersAvailable && (
|
{littersAvailable && (
|
||||||
<div style={{ display: 'flex', gap: '1.5rem', marginBottom: '1rem', flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: '1.5rem', marginBottom: '1rem', flexWrap: 'wrap' }}>
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', fontSize: '0.95rem' }}>
|
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', fontSize: '0.95rem' }}>
|
||||||
<input type="radio" name="parentMode" checked={!useManualParents}
|
<input type="radio" name="parentMode" checked={!useManualParents}
|
||||||
onChange={() => setUseManualParents(false)} style={{ width: '16px', height: '16px' }} />
|
onChange={() => setUseManualParents(false)} style={{ width: '16px', height: '16px' }} />
|
||||||
<span>Link to Litter</span>
|
<span>Link to Litter</span>
|
||||||
</label>
|
</label>
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', fontSize: '0.95rem' }}>
|
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', fontSize: '0.95rem' }}>
|
||||||
<input type="radio" name="parentMode" checked={useManualParents}
|
<input type="radio" name="parentMode" checked={useManualParents}
|
||||||
onChange={() => setUseManualParents(true)} style={{ width: '16px', height: '16px' }} />
|
onChange={() => setUseManualParents(true)} style={{ width: '16px', height: '16px' }} />
|
||||||
<span>Manual Parent Selection</span>
|
<span>Manual Parent Selection</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!useManualParents && littersAvailable ? (
|
{!useManualParents && littersAvailable ? (
|
||||||
<div className="form-group" style={{ marginTop: '0.5rem' }}>
|
<div className="form-group" style={{ marginTop: '0.5rem' }}>
|
||||||
<label className="label">Select Litter</label>
|
<label className="label">Select Litter</label>
|
||||||
<select name="litter_id" className="input"
|
<select name="litter_id" className="input"
|
||||||
value={formData.litter_id || ''} onChange={handleChange}>
|
value={formData.litter_id || ''} onChange={handleChange}>
|
||||||
<option value="">No Litter</option>
|
<option value="">No Litter</option>
|
||||||
{litters.map(l => (
|
{litters.map(l => (
|
||||||
<option key={l.id} value={l.id}>
|
<option key={l.id} value={l.id}>
|
||||||
{l.sire_name} x {l.dam_name} - {new Date(l.breeding_date).toLocaleDateString()}
|
{l.sire_name} x {l.dam_name} - {new Date(l.breeding_date).toLocaleDateString()}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
{formData.litter_id && (
|
{formData.litter_id && (
|
||||||
<div style={{ marginTop: '0.5rem', fontSize: '0.875rem', color: 'var(--primary)', fontStyle: 'italic' }}>
|
<div style={{ marginTop: '0.5rem', fontSize: '0.875rem', color: 'var(--primary)', fontStyle: 'italic' }}>
|
||||||
✓ Parents will be automatically set from the selected litter
|
✓ Parents will be automatically set from the selected litter
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="form-grid" style={{ marginTop: '0.5rem' }}>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="label">Sire (Father)</label>
|
||||||
|
<select name="sire_id" className="input"
|
||||||
|
value={formData.sire_id || ''} onChange={handleChange}>
|
||||||
|
<option value="">Unknown</option>
|
||||||
|
{males.map(d => <option key={d.id} value={d.id}>{d.name}{d.is_champion ? ' ✪' : ''}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="label">Dam (Mother)</label>
|
||||||
|
<select name="dam_id" className="input"
|
||||||
|
value={formData.dam_id || ''} onChange={handleChange}>
|
||||||
|
<option value="">Unknown</option>
|
||||||
|
{females.map(d => <option key={d.id} value={d.id}>{d.name}{d.is_champion ? ' ✪' : ''}</option>)}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="form-grid" style={{ marginTop: '0.5rem' }}>
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="label">Sire (Father)</label>
|
|
||||||
<select name="sire_id" className="input"
|
|
||||||
value={formData.sire_id || ''} onChange={handleChange}>
|
|
||||||
<option value="">Unknown</option>
|
|
||||||
{males.map(d => <option key={d.id} value={d.id}>{d.name}{d.is_champion ? ' ✪' : ''}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
)}
|
||||||
<label className="label">Dam (Mother)</label>
|
</div>
|
||||||
<select name="dam_id" className="input"
|
)}
|
||||||
value={formData.dam_id || ''} onChange={handleChange}>
|
|
||||||
<option value="">Unknown</option>
|
|
||||||
{females.map(d => <option key={d.id} value={d.id}>{d.name}{d.is_champion ? ' ✪' : ''}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group" style={{ marginTop: '1rem' }}>
|
<div className="form-group" style={{ marginTop: '1rem' }}>
|
||||||
<label className="label">Notes</label>
|
<label className="label">Notes</label>
|
||||||
@@ -294,7 +325,7 @@ function DogForm({ dog, onClose, onSave }) {
|
|||||||
<div className="modal-footer">
|
<div className="modal-footer">
|
||||||
<button type="button" className="btn btn-secondary" onClick={onClose} disabled={loading}>Cancel</button>
|
<button type="button" className="btn btn-secondary" onClick={onClose} disabled={loading}>Cancel</button>
|
||||||
<button type="submit" className="btn btn-primary" disabled={loading}>
|
<button type="submit" className="btn btn-primary" disabled={loading}>
|
||||||
{loading ? 'Saving...' : dog ? 'Update Dog' : 'Add Dog'}
|
{loading ? 'Saving...' : dog ? 'Update Dog' : effectiveExternal ? 'Add External Dog' : 'Add Dog'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -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,143 +1,389 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { Link } from 'react-router-dom'
|
||||||
import { Users, Plus, Search, ExternalLink, Award, Filter } from 'lucide-react';
|
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 navigate = useNavigate();
|
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() }, [])
|
||||||
fetch('/api/dogs/external')
|
useEffect(() => { filterDogs() }, [dogs, search, sexFilter])
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => { setDogs(data); setLoading(false); })
|
|
||||||
.catch(() => setLoading(false));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const filtered = dogs.filter(d => {
|
const fetchDogs = async () => {
|
||||||
const matchSearch = d.name.toLowerCase().includes(search.toLowerCase()) ||
|
try {
|
||||||
(d.breed || '').toLowerCase().includes(search.toLowerCase());
|
const res = await axios.get('/api/dogs/external')
|
||||||
const matchSex = sexFilter === 'all' || d.sex === sexFilter;
|
setDogs(res.data)
|
||||||
return matchSearch && matchSex;
|
setLoading(false)
|
||||||
});
|
} catch (error) {
|
||||||
|
console.error('Error fetching external dogs:', error)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sires = filtered.filter(d => d.sex === 'male');
|
const filterDogs = () => {
|
||||||
const dams = filtered.filter(d => d.sex === 'female');
|
let filtered = dogs
|
||||||
|
if (search) {
|
||||||
|
filtered = filtered.filter(dog =>
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) return <div className="loading">Loading external dogs...</div>;
|
const handleDelete = async () => {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
<div className="page-header-left">
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.25rem' }}>
|
||||||
<ExternalLink size={28} className="page-icon" />
|
<ExternalLink size={28} style={{ color: 'var(--primary)' }} />
|
||||||
<div>
|
<h1 style={{ margin: 0 }}>External Dogs</h1>
|
||||||
<h1 className="page-title">External Dogs</h1>
|
|
||||||
<p className="page-subtitle">External sires, dams, and ancestors used in your breeding program</p>
|
|
||||||
</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={() => navigate('/dogs/new?external=1')}
|
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' }}>
|
||||||
<input
|
<Search size={18} style={{ position: 'absolute', left: '0.875rem', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-muted)' }} />
|
||||||
type="text"
|
<input
|
||||||
placeholder="Search by name or breed..."
|
type="text"
|
||||||
value={search}
|
className="input"
|
||||||
onChange={e => setSearch(e.target.value)}
|
placeholder="Search by name or breed..."
|
||||||
className="search-input"
|
value={search}
|
||||||
/>
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
</div>
|
style={{ paddingLeft: '2.75rem' }}
|
||||||
<div className="filter-group">
|
/>
|
||||||
<Filter size={14} />
|
</div>
|
||||||
<select value={sexFilter} onChange={e => setSexFilter(e.target.value)} className="filter-select">
|
<select className="input" value={sexFilter} onChange={(e) => setSexFilter(e.target.value)} style={{ width: '160px' }}>
|
||||||
<option value="all">All</option>
|
<option value="all">All Genders</option>
|
||||||
<option value="male">Sires (Male)</option>
|
<option value="male">Sires (Male) ♂</option>
|
||||||
<option value="female">Dams (Female)</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' }}>
|
||||||
<button className="btn btn-primary" onClick={() => navigate('/dogs/new?external=1')}>
|
{search || sexFilter !== 'all' ? 'No dogs found' : 'No external dogs yet'}
|
||||||
<Plus size={16} /> Add First External Dog
|
</h3>
|
||||||
</button>
|
<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)}>
|
||||||
|
<Plus size={18} />
|
||||||
|
Add Your First External Dog
|
||||||
|
</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} navigate={navigate} />)}
|
style={{
|
||||||
|
padding: '1rem',
|
||||||
|
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 }} />
|
||||||
|
)}
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
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>
|
||||||
|
</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>
|
||||||
</section>
|
</div>
|
||||||
)}
|
))}
|
||||||
{(sexFilter === 'all' || sexFilter === 'female') && dams.length > 0 && (
|
</div>
|
||||||
<section className="external-section">
|
)}
|
||||||
<h2 className="section-heading">♀ Dams ({dams.length})</h2>
|
|
||||||
<div className="dog-grid">
|
{/* Add Dog Modal */}
|
||||||
{dams.map(dog => <DogCard key={dog.id} dog={dog} navigate={navigate} />)}
|
{showAddModal && (
|
||||||
</div>
|
<DogForm
|
||||||
</section>
|
isExternal={true}
|
||||||
)}
|
onClose={() => setShowAddModal(false)}
|
||||||
|
onSave={() => { fetchDogs(); setShowAddModal(false); }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Delete Confirmation Modal */}
|
||||||
|
{deleteTarget && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed', inset: 0,
|
||||||
|
background: 'rgba(0,0,0,0.65)',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
zIndex: 1000,
|
||||||
|
backdropFilter: 'blur(4px)'
|
||||||
|
}}>
|
||||||
|
<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 }}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn"
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={deleting}
|
||||||
|
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DogCard({ dog, navigate }) {
|
export default ExternalDogs
|
||||||
const photo = dog.photo_urls?.[0];
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="dog-card dog-card--external"
|
|
||||||
onClick={() => navigate(`/dogs/${dog.id}`)}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyDown={e => e.key === 'Enter' && navigate(`/dogs/${dog.id}`)}
|
|
||||||
>
|
|
||||||
<div className="dog-card-photo">
|
|
||||||
{photo
|
|
||||||
? <img src={photo} alt={dog.name} />
|
|
||||||
: <div className="dog-card-photo-placeholder"><Users size={32} /></div>
|
|
||||||
}
|
|
||||||
{dog.is_champion === 1 && <span className="champion-badge" title="Champion">🏆</span>}
|
|
||||||
<span className="external-badge"><ExternalLink size={11} /> Ext</span>
|
|
||||||
</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 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ export default function PairingSimulator() {
|
|||||||
const [relationChecking, setRelationChecking] = useState(false)
|
const [relationChecking, setRelationChecking] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/dogs')
|
// include_external=1 ensures external sires/dams appear for pairing
|
||||||
|
fetch('/api/dogs?include_external=1')
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
setDogs(Array.isArray(data) ? data : (data.dogs || []))
|
setDogs(Array.isArray(data) ? data : (data.dogs || []))
|
||||||
@@ -54,9 +55,6 @@ export default function PairingSimulator() {
|
|||||||
checkRelation(sireId, val)
|
checkRelation(sireId, val)
|
||||||
}
|
}
|
||||||
|
|
||||||
const males = dogs.filter(d => d.sex === 'male')
|
|
||||||
const females = dogs.filter(d => d.sex === 'female')
|
|
||||||
|
|
||||||
async function handleSimulate(e) {
|
async function handleSimulate(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!sireId || !damId) return
|
if (!sireId || !damId) return
|
||||||
@@ -67,13 +65,11 @@ export default function PairingSimulator() {
|
|||||||
const res = await fetch('/api/pedigree/trial-pairing', {
|
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) }),
|
||||||
})
|
})
|
||||||
if (!res.ok) {
|
const data = await res.json()
|
||||||
const err = await res.json()
|
if (!res.ok) throw new Error(data.error || 'Simulation failed')
|
||||||
throw new Error(err.error || 'Failed to calculate')
|
setResult(data)
|
||||||
}
|
|
||||||
setResult(await res.json())
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -81,204 +77,164 @@ export default function PairingSimulator() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function RiskBadge({ coi, recommendation }) {
|
const males = dogs.filter(d => d.sex === 'male')
|
||||||
const isLow = coi < 5
|
const females = dogs.filter(d => d.sex === 'female')
|
||||||
const isMed = coi >= 5 && coi < 10
|
|
||||||
const isHigh = coi >= 10
|
const coiColor = (coi) => {
|
||||||
return (
|
if (coi < 0.0625) return 'var(--success)'
|
||||||
<div className={`risk-badge risk-${isLow ? 'low' : isMed ? 'med' : 'high'}`}>
|
if (coi < 0.125) return 'var(--warning)'
|
||||||
{isLow && <CheckCircle size={20} />}
|
return 'var(--danger)'
|
||||||
{isMed && <AlertTriangle size={20} />}
|
}
|
||||||
{isHigh && <XCircle size={20} />}
|
|
||||||
<span>{recommendation}</span>
|
const coiLabel = (coi) => {
|
||||||
</div>
|
if (coi < 0.0625) return 'Low'
|
||||||
)
|
if (coi < 0.125) return 'Moderate'
|
||||||
|
if (coi < 0.25) return 'High'
|
||||||
|
return 'Very High'
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container" style={{ paddingTop: '2rem', paddingBottom: '3rem' }}>
|
<div className="container" style={{ paddingTop: '2rem', paddingBottom: '3rem', maxWidth: '720px' }}>
|
||||||
{/* Header */}
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.5rem' }}>
|
||||||
<div style={{ marginBottom: '2rem' }}>
|
<FlaskConical size={28} style={{ color: 'var(--primary)' }} />
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.5rem' }}>
|
<h1 style={{ margin: 0 }}>Pairing Simulator</h1>
|
||||||
<div style={{ width: '2.5rem', height: '2.5rem', borderRadius: 'var(--radius)', background: 'rgba(139,92,246,0.2)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--accent)' }}>
|
|
||||||
<FlaskConical size={20} />
|
|
||||||
</div>
|
|
||||||
<h1 style={{ fontSize: '1.75rem', margin: 0 }}>Trial Pairing Simulator</h1>
|
|
||||||
</div>
|
|
||||||
<p style={{ color: 'var(--text-muted)', margin: 0 }}>
|
|
||||||
Select a sire and dam to calculate the estimated inbreeding coefficient (COI) and view common ancestors.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p style={{ color: 'var(--text-muted)', marginBottom: '2rem' }}>
|
||||||
|
Estimate the Coefficient of Inbreeding (COI) for a hypothetical pairing before breeding.
|
||||||
|
Includes both kennel and external dogs.
|
||||||
|
</p>
|
||||||
|
|
||||||
{/* Selector Card */}
|
<div className="card" style={{ marginBottom: '1.5rem' }}>
|
||||||
<div className="card" style={{ marginBottom: '1.5rem', maxWidth: '720px' }}>
|
|
||||||
<form onSubmit={handleSimulate}>
|
<form onSubmit={handleSimulate}>
|
||||||
<div className="form-grid" style={{ marginBottom: '1.25rem' }}>
|
<div className="form-grid" style={{ marginBottom: '1rem' }}>
|
||||||
<div className="form-group" style={{ margin: 0 }}>
|
<div className="form-group">
|
||||||
<label className="label">Sire (Male) ♂</label>
|
<label className="label">Sire (Male) *</label>
|
||||||
<select
|
{dogsLoading ? (
|
||||||
value={sireId}
|
<div className="input" style={{ color: 'var(--text-muted)' }}>Loading dogs...</div>
|
||||||
onChange={handleSireChange}
|
) : (
|
||||||
required
|
<select className="input" value={sireId} onChange={handleSireChange} required>
|
||||||
disabled={dogsLoading}
|
<option value="">Select sire...</option>
|
||||||
>
|
{males.map(d => (
|
||||||
<option value="">— Select Sire —</option>
|
<option key={d.id} value={d.id}>
|
||||||
{males.map(d => (
|
{d.name}{d.is_champion ? ' ✪' : ''}{d.is_external ? ' [Ext]' : ''}
|
||||||
<option key={d.id} value={d.id}>
|
</option>
|
||||||
{d.name}{d.breed ? ` · ${d.breed}` : ''}
|
))}
|
||||||
</option>
|
</select>
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{!dogsLoading && males.length === 0 && (
|
|
||||||
<p style={{ color: 'var(--text-muted)', fontSize: '0.8rem', marginTop: '0.4rem' }}>No male dogs registered.</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group" style={{ margin: 0 }}>
|
<div className="form-group">
|
||||||
<label className="label">Dam (Female) ♀</label>
|
<label className="label">Dam (Female) *</label>
|
||||||
<select
|
{dogsLoading ? (
|
||||||
value={damId}
|
<div className="input" style={{ color: 'var(--text-muted)' }}>Loading dogs...</div>
|
||||||
onChange={handleDamChange}
|
) : (
|
||||||
required
|
<select className="input" value={damId} onChange={handleDamChange} required>
|
||||||
disabled={dogsLoading}
|
<option value="">Select dam...</option>
|
||||||
>
|
{females.map(d => (
|
||||||
<option value="">— Select Dam —</option>
|
<option key={d.id} value={d.id}>
|
||||||
{females.map(d => (
|
{d.name}{d.is_champion ? ' ✪' : ''}{d.is_external ? ' [Ext]' : ''}
|
||||||
<option key={d.id} value={d.id}>
|
</option>
|
||||||
{d.name}{d.breed ? ` · ${d.breed}` : ''}
|
))}
|
||||||
</option>
|
</select>
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{!dogsLoading && females.length === 0 && (
|
|
||||||
<p style={{ color: 'var(--text-muted)', fontSize: '0.8rem', marginTop: '0.4rem' }}>No female dogs registered.</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Direct-relation warning banner */}
|
|
||||||
{relationChecking && (
|
{relationChecking && (
|
||||||
<p style={{ fontSize: '0.8125rem', color: 'var(--text-muted)', marginBottom: '1rem' }}>Checking relationship…</p>
|
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginBottom: '0.75rem' }}>
|
||||||
|
Checking relationship...
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{!relationChecking && relationWarning && (
|
|
||||||
|
{relationWarning && !relationChecking && (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', alignItems: 'flex-start', gap: '0.6rem',
|
display: 'flex', alignItems: 'center', gap: '0.5rem',
|
||||||
background: 'rgba(234,179,8,0.12)', border: '1px solid rgba(234,179,8,0.4)',
|
padding: '0.6rem 1rem', marginBottom: '0.75rem',
|
||||||
borderRadius: 'var(--radius-sm)', padding: '0.75rem 1rem', marginBottom: '1rem'
|
background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.3)',
|
||||||
|
borderRadius: 'var(--radius)', fontSize: '0.875rem', color: 'var(--danger)',
|
||||||
}}>
|
}}>
|
||||||
<ShieldAlert size={18} style={{ color: '#eab308', flexShrink: 0, marginTop: '0.1rem' }} />
|
<ShieldAlert size={16} />
|
||||||
<div>
|
<strong>Related:</strong> {relationWarning}
|
||||||
<p style={{ margin: 0, fontWeight: 600, color: '#eab308', fontSize: '0.875rem' }}>Direct Relation Detected</p>
|
|
||||||
<p style={{ margin: '0.2rem 0 0', fontSize: '0.8125rem', color: 'var(--text-secondary)' }}>
|
|
||||||
{relationWarning}. COI will reflect the high inbreeding coefficient for this pairing.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
disabled={!sireId || !damId || loading || relationChecking}
|
disabled={loading || dogsLoading || !sireId || !damId}
|
||||||
style={{ minWidth: '160px' }}
|
style={{ width: '100%' }}
|
||||||
>
|
>
|
||||||
{loading ? 'Calculating…' : <><FlaskConical size={16} /> Simulate Pairing</>}
|
{loading ? 'Simulating...' : <><GitMerge size={16} style={{ marginRight: '0.4rem' }} />Simulate Pairing</>}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error */}
|
{error && (
|
||||||
{error && <div className="error" style={{ maxWidth: '720px' }}>{error}</div>}
|
<div className="card" style={{ borderColor: 'var(--danger)', marginBottom: '1.5rem' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', color: 'var(--danger)' }}>
|
||||||
|
<XCircle size={18} />
|
||||||
|
<strong>Error:</strong> {error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Results */}
|
|
||||||
{result && (
|
{result && (
|
||||||
<div style={{ maxWidth: '720px' }}>
|
<div className="card">
|
||||||
{/* Direct-relation alert in results */}
|
<h2 style={{ fontSize: '1rem', marginBottom: '1.25rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
||||||
{result.directRelation && (
|
Simulation Result
|
||||||
<div style={{
|
</h2>
|
||||||
display: 'flex', alignItems: 'flex-start', gap: '0.6rem',
|
|
||||||
background: 'rgba(239,68,68,0.1)', border: '1px solid rgba(239,68,68,0.35)',
|
<div style={{
|
||||||
borderRadius: 'var(--radius-sm)', padding: '0.75rem 1rem', marginBottom: '1.25rem'
|
display: 'flex', alignItems: 'center', gap: '1rem',
|
||||||
}}>
|
padding: '1.25rem', marginBottom: '1rem',
|
||||||
<ShieldAlert size={18} style={{ color: 'var(--danger)', flexShrink: 0, marginTop: '0.1rem' }} />
|
background: 'var(--bg-primary)', borderRadius: 'var(--radius)',
|
||||||
<div>
|
border: `2px solid ${coiColor(result.coi)}`,
|
||||||
<p style={{ margin: 0, fontWeight: 600, color: 'var(--danger)', fontSize: '0.875rem' }}>Direct Relation — High Inbreeding Risk</p>
|
}}>
|
||||||
<p style={{ margin: '0.2rem 0 0', fontSize: '0.8125rem', color: 'var(--text-secondary)' }}>{result.directRelation}</p>
|
{result.coi < 0.0625
|
||||||
|
? <CheckCircle size={32} style={{ color: coiColor(result.coi), flexShrink: 0 }} />
|
||||||
|
: <AlertTriangle size={32} style={{ color: coiColor(result.coi), flexShrink: 0 }} />
|
||||||
|
}
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '2rem', fontWeight: 700, color: coiColor(result.coi), lineHeight: 1 }}>
|
||||||
|
{(result.coi * 100).toFixed(2)}%
|
||||||
|
</div>
|
||||||
|
<div style={{ color: 'var(--text-muted)', fontSize: '0.875rem' }}>
|
||||||
|
COI — <strong style={{ color: coiColor(result.coi) }}>{coiLabel(result.coi)}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{result.commonAncestors && result.commonAncestors.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 style={{ fontSize: '0.875rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '0.5rem' }}>
|
||||||
|
Common Ancestors ({result.commonAncestors.length})
|
||||||
|
</h3>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.4rem' }}>
|
||||||
|
{result.commonAncestors.map((a, i) => (
|
||||||
|
<span key={i} style={{
|
||||||
|
padding: '0.2rem 0.6rem',
|
||||||
|
background: 'var(--bg-tertiary)',
|
||||||
|
borderRadius: 'var(--radius-sm)',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
}}>{a.name}</span>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* COI Summary */}
|
{result.recommendation && (
|
||||||
<div className="card" style={{ marginBottom: '1.25rem' }}>
|
<div style={{
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', flexWrap: 'wrap', gap: '1rem' }}>
|
marginTop: '1rem', padding: '0.75rem 1rem',
|
||||||
<div>
|
background: result.coi < 0.0625 ? 'rgba(34,197,94,0.08)' : 'rgba(239,68,68,0.08)',
|
||||||
<p style={{ color: 'var(--text-muted)', fontSize: '0.8125rem', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500, marginBottom: '0.25rem' }}>Pairing</p>
|
borderRadius: 'var(--radius)',
|
||||||
<p style={{ fontSize: '1.125rem', fontWeight: 600, margin: 0 }}>
|
border: `1px solid ${result.coi < 0.0625 ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'}`,
|
||||||
<span style={{ color: '#60a5fa' }}>{result.sire.name}</span>
|
fontSize: '0.875rem',
|
||||||
<span style={{ color: 'var(--text-muted)', margin: '0 0.5rem' }}>×</span>
|
color: 'var(--text-secondary)',
|
||||||
<span style={{ color: '#f472b6' }}>{result.dam.name}</span>
|
}}>
|
||||||
</p>
|
{result.recommendation}
|
||||||
</div>
|
|
||||||
<div style={{ textAlign: 'right' }}>
|
|
||||||
<p style={{ color: 'var(--text-muted)', fontSize: '0.8125rem', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500, marginBottom: '0.25rem' }}>COI</p>
|
|
||||||
<p style={{
|
|
||||||
fontSize: '2rem', fontWeight: 700, lineHeight: 1,
|
|
||||||
color: result.coi < 5 ? 'var(--success)' : result.coi < 10 ? 'var(--warning)' : 'var(--danger)'
|
|
||||||
}}>
|
|
||||||
{result.coi.toFixed(2)}%
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div style={{ marginTop: '1.25rem' }}>
|
|
||||||
<RiskBadge coi={result.coi} recommendation={result.recommendation} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginTop: '1rem', padding: '0.75rem', background: 'var(--bg-tertiary)', borderRadius: 'var(--radius-sm)', fontSize: '0.8125rem', color: 'var(--text-secondary)' }}>
|
|
||||||
<strong>COI Guide:</strong> <5% Low risk · 5–10% Moderate risk · >10% High risk
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Common Ancestors */}
|
|
||||||
<div className="card">
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '1rem' }}>
|
|
||||||
<GitMerge size={18} style={{ color: 'var(--accent)' }} />
|
|
||||||
<h3 style={{ margin: 0, fontSize: '1rem' }}>Common Ancestors</h3>
|
|
||||||
<span className="badge badge-primary" style={{ marginLeft: 'auto' }}>
|
|
||||||
{result.commonAncestors.length} found
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{result.commonAncestors.length === 0 ? (
|
|
||||||
<p style={{ color: 'var(--text-muted)', textAlign: 'center', padding: '1.5rem 0', margin: 0 }}>
|
|
||||||
No common ancestors found within 6 generations. This pairing has excellent genetic diversity.
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<div style={{ overflowX: 'auto' }}>
|
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.875rem' }}>
|
|
||||||
<thead>
|
|
||||||
<tr style={{ borderBottom: '1px solid var(--border)' }}>
|
|
||||||
<th style={{ textAlign: 'left', padding: '0.625rem 0.75rem', color: 'var(--text-muted)', fontWeight: 500, fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Ancestor</th>
|
|
||||||
<th style={{ textAlign: 'center', padding: '0.625rem 0.75rem', color: 'var(--text-muted)', fontWeight: 500, fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Sire Gen</th>
|
|
||||||
<th style={{ textAlign: 'center', padding: '0.625rem 0.75rem', color: 'var(--text-muted)', fontWeight: 500, fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Dam Gen</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{result.commonAncestors.map((anc, i) => (
|
|
||||||
<tr key={i} style={{ borderBottom: '1px solid var(--border)' }}>
|
|
||||||
<td style={{ padding: '0.625rem 0.75rem', fontWeight: 500 }}>{anc.name}</td>
|
|
||||||
<td style={{ padding: '0.625rem 0.75rem', textAlign: 'center' }}>
|
|
||||||
<span className="badge badge-primary">Gen {anc.sireGen}</span>
|
|
||||||
</td>
|
|
||||||
<td style={{ padding: '0.625rem 0.75rem', textAlign: 'center' }}>
|
|
||||||
<span className="badge" style={{ background: 'rgba(244,114,182,0.15)', color: '#f472b6' }}>Gen {anc.damGen}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</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',
|
||||||
|
|||||||
@@ -55,16 +55,33 @@ function attachParents(db, dogs) {
|
|||||||
return dogs;
|
return dogs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── GET all kennel dogs (is_external = 0) ───────────────────────────────────
|
// ── GET dogs
|
||||||
|
// Default: kennel dogs only (is_external = 0)
|
||||||
|
// ?include_external=1 : all active dogs (kennel + external)
|
||||||
|
// ?external_only=1 : external dogs only
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
router.get('/', (req, res) => {
|
router.get('/', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const db = getDatabase();
|
const db = getDatabase();
|
||||||
|
const includeExternal = req.query.include_external === '1' || req.query.include_external === 'true';
|
||||||
|
const externalOnly = req.query.external_only === '1' || req.query.external_only === 'true';
|
||||||
|
|
||||||
|
let whereClause;
|
||||||
|
if (externalOnly) {
|
||||||
|
whereClause = 'WHERE is_active = 1 AND is_external = 1';
|
||||||
|
} else if (includeExternal) {
|
||||||
|
whereClause = 'WHERE is_active = 1';
|
||||||
|
} else {
|
||||||
|
whereClause = 'WHERE is_active = 1 AND is_external = 0';
|
||||||
|
}
|
||||||
|
|
||||||
const dogs = db.prepare(`
|
const dogs = db.prepare(`
|
||||||
SELECT ${DOG_COLS}
|
SELECT ${DOG_COLS}
|
||||||
FROM dogs
|
FROM dogs
|
||||||
WHERE is_active = 1 AND is_external = 0
|
${whereClause}
|
||||||
ORDER BY name
|
ORDER BY name
|
||||||
`).all();
|
`).all();
|
||||||
|
|
||||||
res.json(attachParents(db, dogs));
|
res.json(attachParents(db, dogs));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching dogs:', error);
|
console.error('Error fetching dogs:', error);
|
||||||
@@ -73,6 +90,7 @@ router.get('/', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── GET all dogs (kennel + external) for dropdowns/pairing/pedigree ──────────
|
// ── GET all dogs (kennel + external) for dropdowns/pairing/pedigree ──────────
|
||||||
|
// Kept for backwards-compat; equivalent to GET /?include_external=1
|
||||||
router.get('/all', (req, res) => {
|
router.get('/all', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const db = getDatabase();
|
const db = getDatabase();
|
||||||
@@ -90,6 +108,7 @@ router.get('/all', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── GET external dogs only (is_external = 1) ──────────────────────────────
|
// ── GET external dogs only (is_external = 1) ──────────────────────────────
|
||||||
|
// Kept for backwards-compat; equivalent to GET /?external_only=1
|
||||||
router.get('/external', (req, res) => {
|
router.get('/external', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const db = getDatabase();
|
const db = getDatabase();
|
||||||
|
|||||||
@@ -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