+
@@ -283,4 +258,4 @@ function DogList() {
)
}
-export default DogList
\ No newline at end of file
+export default DogList
diff --git a/client/src/pages/SettingsPage.jsx b/client/src/pages/SettingsPage.jsx
new file mode 100644
index 0000000..9fbcc0f
--- /dev/null
+++ b/client/src/pages/SettingsPage.jsx
@@ -0,0 +1,160 @@
+import { useState, useEffect } from 'react'
+import { Settings, Save, CheckCircle } from 'lucide-react'
+import { useSettings } from '../hooks/useSettings'
+
+const FIELDS = [
+ { key: 'kennel_name', label: 'Kennel / App Name', placeholder: 'BREEDR', type: 'text', required: true },
+ { key: 'kennel_tagline', label: 'Tagline', placeholder: 'Raising champions since...', type: 'text' },
+ { key: 'kennel_address', label: 'Address', placeholder: '123 Main St, City, ST', type: 'text' },
+ { key: 'kennel_phone', label: 'Phone', placeholder: '(555) 000-0000', type: 'tel' },
+ { key: 'kennel_email', label: 'Email', placeholder: 'kennel@example.com', type: 'email'},
+ { key: 'kennel_website', label: 'Website', placeholder: 'https://yourdomain.com', type: 'url' },
+ { key: 'kennel_akc_id', label: 'AKC Kennel ID', placeholder: 'Optional', type: 'text' },
+ { key: 'kennel_breed', label: 'Primary Breed', placeholder: 'e.g. Labrador Retriever', type: 'text' },
+]
+
+export default function SettingsPage() {
+ const { settings, saveSettings } = useSettings()
+ const [form, setForm] = useState({})
+ const [saving, setSaving] = useState(false)
+ const [saved, setSaved] = useState(false)
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ setForm({
+ kennel_name: settings.kennel_name || '',
+ kennel_tagline: settings.kennel_tagline || '',
+ kennel_address: settings.kennel_address || '',
+ kennel_phone: settings.kennel_phone || '',
+ kennel_email: settings.kennel_email || '',
+ kennel_website: settings.kennel_website || '',
+ kennel_akc_id: settings.kennel_akc_id || '',
+ kennel_breed: settings.kennel_breed || '',
+ })
+ }, [settings])
+
+ const handleChange = (key, value) => {
+ setForm(prev => ({ ...prev, [key]: value }))
+ setSaved(false)
+ }
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+ if (!form.kennel_name?.trim()) {
+ setError('Kennel name is required.')
+ return
+ }
+ setSaving(true)
+ setError(null)
+ try {
+ await saveSettings(form)
+ setSaved(true)
+ setTimeout(() => setSaved(false), 3000)
+ } catch (err) {
+ setError('Failed to save settings. Please try again.')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
Settings
+
+ Kennel profile & app configuration
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/server/db/init.js b/server/db/init.js
index 7b74afc..bc7d937 100644
--- a/server/db/init.js
+++ b/server/db/init.js
@@ -2,174 +2,155 @@ const Database = require('better-sqlite3');
const path = require('path');
const fs = require('fs');
-function initDatabase(dbPath) {
- // Ensure data directory exists
- const dir = path.dirname(dbPath);
- if (!fs.existsSync(dir)) {
- fs.mkdirSync(dir, { recursive: true });
- }
-
- const db = new Database(dbPath);
-
- // Enable foreign keys
- db.pragma('foreign_keys = ON');
-
- console.log('Initializing database schema...');
-
- // Dogs table - NO sire/dam columns, only litter_id
- db.exec(`
- CREATE TABLE IF NOT EXISTS dogs (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT NOT NULL,
- registration_number TEXT UNIQUE,
- breed TEXT NOT NULL,
- sex TEXT NOT NULL CHECK(sex IN ('male', 'female')),
- birth_date DATE,
- color TEXT,
- microchip TEXT,
- photo_urls TEXT, -- JSON array of photo URLs
- notes TEXT,
- litter_id INTEGER,
- is_active INTEGER DEFAULT 1,
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (litter_id) REFERENCES litters(id) ON DELETE SET NULL
- )
- `);
-
- // Create unique index for microchip that allows NULL values
- db.exec(`
- CREATE UNIQUE INDEX IF NOT EXISTS idx_dogs_microchip
- ON dogs(microchip)
- WHERE microchip IS NOT NULL
- `);
-
- // Parents table - Stores sire/dam relationships
- db.exec(`
- CREATE TABLE IF NOT EXISTS 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)
- )
- `);
-
- // Litters table - Breeding records
- db.exec(`
- CREATE TABLE IF NOT EXISTS litters (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- sire_id INTEGER NOT NULL,
- dam_id INTEGER NOT NULL,
- breeding_date DATE NOT NULL,
- whelping_date DATE,
- puppy_count INTEGER DEFAULT 0,
- notes TEXT,
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (sire_id) REFERENCES dogs(id) ON DELETE CASCADE,
- FOREIGN KEY (dam_id) REFERENCES dogs(id) ON DELETE CASCADE
- )
- `);
-
- // Health records table
- db.exec(`
- CREATE TABLE IF NOT EXISTS health_records (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- dog_id INTEGER NOT NULL,
- record_type TEXT NOT NULL CHECK(record_type IN ('test', 'vaccination', 'exam', 'treatment', 'certification')),
- test_name TEXT,
- test_date DATE NOT NULL,
- result TEXT,
- document_url TEXT,
- notes TEXT,
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (dog_id) REFERENCES dogs(id) ON DELETE CASCADE
- )
- `);
-
- // Heat cycles table
- db.exec(`
- CREATE TABLE IF NOT EXISTS heat_cycles (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- dog_id INTEGER NOT NULL,
- start_date DATE NOT NULL,
- end_date DATE,
- progesterone_peak_date DATE,
- breeding_date DATE,
- breeding_successful INTEGER DEFAULT 0,
- notes TEXT,
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (dog_id) REFERENCES dogs(id) ON DELETE CASCADE
- )
- `);
-
- // Traits table - Genetic trait tracking
- db.exec(`
- CREATE TABLE IF NOT EXISTS traits (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- dog_id INTEGER NOT NULL,
- trait_category TEXT NOT NULL,
- trait_name TEXT NOT NULL,
- trait_value TEXT NOT NULL,
- inherited_from INTEGER,
- notes TEXT,
- FOREIGN KEY (dog_id) REFERENCES dogs(id) ON DELETE CASCADE,
- FOREIGN KEY (inherited_from) REFERENCES dogs(id) ON DELETE SET NULL
- )
- `);
-
- // Create indexes for performance
- db.exec(`
- CREATE INDEX IF NOT EXISTS idx_dogs_name ON dogs(name);
- CREATE INDEX IF NOT EXISTS idx_dogs_registration ON dogs(registration_number);
- CREATE INDEX IF NOT EXISTS idx_dogs_litter ON dogs(litter_id);
- CREATE INDEX IF NOT EXISTS idx_parents_dog ON parents(dog_id);
- CREATE INDEX IF NOT EXISTS idx_parents_parent ON parents(parent_id);
- CREATE INDEX IF NOT EXISTS idx_litters_sire ON litters(sire_id);
- CREATE INDEX IF NOT EXISTS idx_litters_dam ON litters(dam_id);
- CREATE INDEX IF NOT EXISTS idx_health_dog ON health_records(dog_id);
- CREATE INDEX IF NOT EXISTS idx_heat_dog ON heat_cycles(dog_id);
- CREATE INDEX IF NOT EXISTS idx_traits_dog ON traits(dog_id);
- `);
-
- // Create trigger for updated_at
- db.exec(`
- CREATE TRIGGER IF NOT EXISTS update_dogs_timestamp
- AFTER UPDATE ON dogs
- FOR EACH ROW
- BEGIN
- UPDATE dogs SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
- END;
- `);
-
- console.log('✓ Database schema initialized successfully!');
- console.log('✓ Dogs table: NO sire/dam columns, uses parents table');
- console.log('✓ Parents table: Stores sire/dam relationships');
- console.log('✓ Litters table: Links puppies via litter_id');
-
- db.close();
- return true;
-}
+const dbPath = path.join(__dirname, '../../data');
+const db = new Database(path.join(dbPath, 'breedr.db'));
function getDatabase() {
- const dbPath = process.env.DB_PATH || path.join(__dirname, '../../data/breedr.db');
- const db = new Database(dbPath);
- db.pragma('foreign_keys = ON');
return db;
}
-module.exports = { initDatabase, getDatabase };
+function initDatabase() {
+ db.pragma('foreign_keys = ON');
+ db.pragma('journal_mode = WAL');
-// Run initialization if called directly
-if (require.main === module) {
- const dbPath = process.env.DB_PATH || path.join(__dirname, '../../data/breedr.db');
- console.log('\n==========================================');
- console.log('BREEDR Database Initialization');
- console.log('==========================================');
- console.log(`Database: ${dbPath}`);
- console.log('==========================================\n');
- initDatabase(dbPath);
- console.log('\n✓ Database ready!\n');
+ // ── Dogs ────────────────────────────────────────────────────────────
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS dogs (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ registration_number TEXT,
+ breed TEXT NOT NULL,
+ sex TEXT NOT NULL CHECK(sex IN ('male', 'female')),
+ birth_date TEXT,
+ color TEXT,
+ microchip TEXT,
+ litter_id INTEGER,
+ is_active INTEGER DEFAULT 1,
+ is_champion INTEGER DEFAULT 0,
+ photo_urls TEXT DEFAULT '[]',
+ notes TEXT,
+ created_at TEXT DEFAULT (datetime('now')),
+ updated_at TEXT DEFAULT (datetime('now'))
+ )
+ `);
+
+ // migrate: add is_champion if missing (safe on existing DBs)
+ try {
+ db.exec(`ALTER TABLE dogs ADD COLUMN is_champion INTEGER DEFAULT 0`);
+ } catch (_) { /* column already exists */ }
+
+ // ── Parents ─────────────────────────────────────────────────────────
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS 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),
+ FOREIGN KEY (parent_id) REFERENCES dogs(id)
+ )
+ `);
+
+ // ── Breeding Records ────────────────────────────────────────────────
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS breeding_records (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ sire_id INTEGER NOT NULL,
+ dam_id INTEGER NOT NULL,
+ breeding_date TEXT,
+ due_date TEXT,
+ conception_method TEXT CHECK(conception_method IN ('natural', 'ai', 'frozen', 'surgical')),
+ notes TEXT,
+ created_at TEXT DEFAULT (datetime('now')),
+ updated_at TEXT DEFAULT (datetime('now')),
+ FOREIGN KEY (sire_id) REFERENCES dogs(id),
+ FOREIGN KEY (dam_id) REFERENCES dogs(id)
+ )
+ `);
+
+ // ── Litters ─────────────────────────────────────────────────────────
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS litters (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ breeding_id INTEGER,
+ sire_id INTEGER NOT NULL,
+ dam_id INTEGER NOT NULL,
+ whelp_date TEXT,
+ total_count INTEGER DEFAULT 0,
+ male_count INTEGER DEFAULT 0,
+ female_count INTEGER DEFAULT 0,
+ stillborn_count INTEGER DEFAULT 0,
+ notes TEXT,
+ created_at TEXT DEFAULT (datetime('now')),
+ updated_at TEXT DEFAULT (datetime('now')),
+ FOREIGN KEY (breeding_id) REFERENCES breeding_records(id),
+ FOREIGN KEY (sire_id) REFERENCES dogs(id),
+ FOREIGN KEY (dam_id) REFERENCES dogs(id)
+ )
+ `);
+
+ // ── Health Records ──────────────────────────────────────────────────
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS health_records (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ dog_id INTEGER NOT NULL,
+ record_type TEXT NOT NULL,
+ date TEXT NOT NULL,
+ title TEXT NOT NULL,
+ description TEXT,
+ vet_name TEXT,
+ notes TEXT,
+ result TEXT,
+ next_due TEXT,
+ created_at TEXT DEFAULT (datetime('now')),
+ updated_at TEXT DEFAULT (datetime('now')),
+ FOREIGN KEY (dog_id) REFERENCES dogs(id)
+ )
+ `);
+
+ // ── Settings ─────────────────────────────────────────────────────────
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS settings (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ kennel_name TEXT DEFAULT 'BREEDR',
+ kennel_tagline TEXT,
+ kennel_address TEXT,
+ kennel_phone TEXT,
+ kennel_email TEXT,
+ kennel_website TEXT,
+ kennel_akc_id TEXT,
+ kennel_breed TEXT,
+ owner_name TEXT,
+ created_at TEXT DEFAULT (datetime('now')),
+ updated_at TEXT DEFAULT (datetime('now'))
+ )
+ `);
+
+ // migrate: add new kennel columns if missing (safe on existing DBs)
+ const kennelCols = [
+ ['kennel_name', "TEXT DEFAULT 'BREEDR'"],
+ ['kennel_tagline', 'TEXT'],
+ ['kennel_address', 'TEXT'],
+ ['kennel_phone', 'TEXT'],
+ ['kennel_email', 'TEXT'],
+ ['kennel_website', 'TEXT'],
+ ['kennel_akc_id', 'TEXT'],
+ ['kennel_breed', 'TEXT'],
+ ['owner_name', 'TEXT'],
+ ];
+ for (const [col, def] of kennelCols) {
+ try {
+ db.exec(`ALTER TABLE settings ADD COLUMN ${col} ${def}`);
+ } catch (_) { /* already exists */ }
+ }
+
+ // Seed a default settings row if none exists
+ const existing = db.prepare('SELECT id FROM settings LIMIT 1').get();
+ if (!existing) {
+ db.prepare(`INSERT INTO settings (kennel_name) VALUES (?)`).run('BREEDR');
+ }
+
+ console.log('✓ Database initialized successfully');
}
+
+module.exports = { getDatabase, initDatabase };
diff --git a/server/index.js b/server/index.js
index bba3f59..b595d4c 100644
--- a/server/index.js
+++ b/server/index.js
@@ -1,96 +1 @@
-const express = require('express');
-const cors = require('cors');
-const helmet = require('helmet');
-const path = require('path');
-const fs = require('fs');
-const { initDatabase } = require('./db/init');
-
-const app = express();
-const PORT = process.env.PORT || 3000;
-const DB_PATH = process.env.DB_PATH || path.join(__dirname, '../data/breedr.db');
-const UPLOAD_PATH = process.env.UPLOAD_PATH || path.join(__dirname, '../uploads');
-const STATIC_PATH = process.env.STATIC_PATH || path.join(__dirname, '../static');
-
-// Ensure directories exist
-const dataDir = path.dirname(DB_PATH);
-if (!fs.existsSync(dataDir)) {
- fs.mkdirSync(dataDir, { recursive: true });
-}
-if (!fs.existsSync(UPLOAD_PATH)) {
- fs.mkdirSync(UPLOAD_PATH, { recursive: true });
-}
-if (!fs.existsSync(STATIC_PATH)) {
- fs.mkdirSync(STATIC_PATH, { recursive: true });
-}
-
-// Initialize database schema (creates tables if they don't exist)
-console.log('Initializing database...');
-initDatabase(DB_PATH);
-console.log('✓ Database ready!\n');
-
-// Middleware
-app.use(helmet({
- contentSecurityPolicy: false, // Allow inline scripts for React
-}));
-app.use(cors());
-app.use(express.json());
-app.use(express.urlencoded({ extended: true }));
-
-// Static asset routes — registered BEFORE React catch-all so they are
-// resolved directly and never fall through to index.html
-app.use('/uploads', express.static(UPLOAD_PATH));
-app.use('/static', express.static(STATIC_PATH));
-
-// Explicit 404 for missing asset files so the catch-all never intercepts them
-app.use('/uploads', (req, res) => res.status(404).json({ error: 'Upload not found' }));
-app.use('/static', (req, res) => res.status(404).json({ error: 'Static asset not found' }));
-
-// API Routes
-app.use('/api/dogs', require('./routes/dogs'));
-app.use('/api/litters', require('./routes/litters'));
-app.use('/api/health', require('./routes/health'));
-app.use('/api/pedigree', require('./routes/pedigree'));
-app.use('/api/breeding', require('./routes/breeding'));
-
-// Health check endpoint
-app.get('/api/health', (req, res) => {
- res.json({ status: 'ok', timestamp: new Date().toISOString() });
-});
-
-// Serve React frontend in production
-// The catch-all is intentionally placed AFTER all asset/API routes above.
-// express.static(clientBuildPath) handles real build assets (JS/CSS chunks).
-// The scoped '*' only fires for HTML5 client-side routes (e.g. /dogs, /litters).
-if (process.env.NODE_ENV === 'production') {
- const clientBuildPath = path.join(__dirname, '../client/dist');
- app.use(express.static(clientBuildPath));
-
- // Only send index.html for non-asset, non-api paths
- app.get(/^(?!\/(api|static|uploads)\/).*$/, (req, res) => {
- res.sendFile(path.join(clientBuildPath, 'index.html'));
- });
-}
-
-// Error handling middleware
-app.use((err, req, res, next) => {
- console.error('Error:', err);
- res.status(err.status || 500).json({
- error: err.message || 'Internal server error',
- ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
- });
-});
-
-// Start server
-app.listen(PORT, '0.0.0.0', () => {
- console.log(`\n🐕 BREEDR Server Running`);
- console.log(`==============================`);
- console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
- console.log(`Port: ${PORT}`);
- console.log(`Database: ${DB_PATH}`);
- console.log(`Uploads: ${UPLOAD_PATH}`);
- console.log(`Static: ${STATIC_PATH}`);
- console.log(`Access: http://localhost:${PORT}`);
- console.log(`==============================\n`);
-});
-
-module.exports = app;
+Y29uc3QgZXhwcmVzcyA9IHJlcXVpcmUoJ2V4cHJlc3MnKTsKY29uc3QgY29ycyA9IHJlcXVpcmUoJ2NvcnMnKTsKY29uc3QgaGVsbWV0ID0gcmVxdWlyZSgnaGVsbWV0Jyk7CmNvbnN0IHBhdGggPSByZXF1aXJlKCdwYXRoJyk7CmNvbnN0IGZzID0gcmVxdWlyZSgnZnMnKTsKY29uc3QgeyBpbml0RGF0YWJhc2UgfSA9IHJlcXVpcmUoJy4vZGIvaW5pdCcpOwoKY29uc3QgYXBwID0gZXhwcmVzcygpOwpjb25zdCBQT1JUID0gcHJvY2Vzcy5lbnYuUE9SVCB8fCAzMDAwOwpjb25zdCBEQl9QQVRIID0gcHJvY2Vzcy5lbnYuREJfUEFUSCB8fCBwYXRoLmpvaW4oX19kaXJuYW1lLCAnLi4vZGF0YS9icmVlZHIuZGInKTsKY29uc3QgVVBMT0FEX1BBVEggPSBwcm9jZXNzLmVudi5VUExPQURfUEFUSCB8fCBwYXRoLmpvaW4oX19kaXJuYW1lLCAnLi4vdXBsb2FkcycpOwpjb25zdCBTVEFUSUNfUEFUSCA9IHByb2Nlc3MuZW52LlNUQVRJQ19QQVRIIHx8IHBhdGguam9pbihfX2Rpcm5hbWUsICcuLi9zdGF0aWMnKTsKCmNvbnN0IGRhdGFEaXIgPSBwYXRoLmRpcm5hbWUoREJfUEFUSCk7CmlmICghZnMuZXhpc3RzU3luYyhkYXRhRGlyKSkgZnMubWtkaXJTeW5jKGRhdGFEaXIsIHsgcmVjdXJzaXZlOiB0cnVlIH0pOwppZiAoIWZzLmV4aXN0c1N5bmMoVVBMT0FEX1BBVEgpKSBmcy5ta2RpclN5bmMoVVBMT0FEX1BBVEgsIHsgcmVjdXJzaXZlOiB0cnVlIH0pOwppZiAoIWZzLmV4aXN0c1N5bmMoU1RBVElDX1BBVEgpKSBmcy5ta2RpclN5bmMoU1RBVElDX1BBVEgsIHsgcmVjdXJzaXZlOiB0cnVlIH0pOwoKY29uc29sZS5sb2coJ0luaXRpYWxpemluZyBkYXRhYmFzZS4uLicpOwppbml0RGF0YWJhc2UoREJfUEFUSCk7CmNvbnNvbGUubG9nKCfinJMgRGF0YWJhc2UgcmVhZHkhXG4nKTsKCmFwcC51c2UoaGVsbWV0KHsgY29udGVudFNlY3VyaXR5UG9saWN5OiBmYWxzZSB9KSk7CmFwcC51c2UoY29ycygpKTsKYXBwLnVzZShleHByZXNzLmpzb24oKSk7CmFwcC51c2UoZXhwcmVzcy51cmxlbmNvZGVkKHsgZXh0ZW5kZWQ6IHRydWUgfSkpOwoKYXBwLnVzZSgnL3VwbG9hZHMnLCBleHByZXNzLnN0YXRpYyhVUExPQURfUEFUSCkpOwphcHAudXNlKCcvc3RhdGljJywgZXhwcmVzcy5zdGF0aWMoU1RBVElDX1BBVEgpKTsKYXBwLnVzZSgnL3VwbG9hZHMnLCAocmVxLCByZXMpID0+IHJlcy5zdGF0dXMoNDA0KS5qc29uKHsgZXJyb3I6ICdVcGxvYWQgbm90IGZvdW5kJyB9KSk7CmFwcC51c2UoJy9zdGF0aWMnLCAocmVxLCByZXMpID0+IHJlcy5zdGF0dXMoNDA0KS5qc29uKHsgZXJyb3I6ICdTdGF0aWMgYXNzZXQgbm90IGZvdW5kJyB9KSk7CgphcHAudXNlKCcvYXBpL2RvZ3MnLCByZXF1aXJlKCcuL3JvdXRlcy9kb2dzJykpOwphcHAudXNlKCcvYXBpL2xpdHRlcnMnLCByZXF1aXJlKCcuL3JvdXRlcy9saXR0ZXJzJykpOwphcHAudXNlKCcvYXBpL2hlYWx0aCcsIHJlcXVpcmUoJy4vcm91dGVzL2hlYWx0aCcpKTsKYXBwLnVzZSgnL2FwaS9wZWRpZ3JlZScsIHJlcXVpcmUoJy4vcm91dGVzL3BlZGlncmVlJykpOwphcHAudXNlKCcvYXBpL2JyZWVkaW5nJywgcmVxdWlyZSgnLi9yb3V0ZXMvYnJlZWRpbmcnKSk7CmFwcC51c2UoJy9hcGkvc2V0dGluZ3MnLCByZXF1aXJlKCcuL3JvdXRlcy9zZXR0aW5ncycpKTsKCmFwcC5nZXQoJy9hcGkvaGVhbHRoJywgKHJlcSwgcmVzKSA9PiB7CiAgcmVzLmpzb24oeyBzdGF0dXM6ICdvaycsIHRpbWVzdGFtcDogbmV3IERhdGUoKS50b0lTT1N0cmluZygpIH0pOwp9KTsKCmlmIChwcm9jZXNzLmVudi5OT0RFX0VOViA9PT0gJ3Byb2R1Y3Rpb24nKSB7CiAgY29uc3QgY2xpZW50QnVpbGRQYXRoID0gcGF0aC5qb2luKF9fZGlybmFtZSwgJy4uL2NsaWVudC9kaXN0Jyk7CiAgYXBwLnVzZShleHByZXNzLnN0YXRpYyhjbGllbnRCdWlsZFBhdGgpKTsKICBhcHAuZ2V0KC9eKD8hXC8oYXBpfHN0YXRpY3x1cGxvYWRzKVwvKS4qJC8sIChyZXEsIHJlcykgPT4gewogICAgcmVzLnNlbmRGaWxlKHBhdGguam9pbihjbGllbnRCdWlsZFBhdGgsICdpbmRleC5odG1sJykpOwogIH0pOwp9CgphcHAudXNlKChlcnIsIHJlcSwgcmVzLCBuZXh0KSA9PiB7CiAgY29uc29sZS5lcnJvcignRXJyb3I6JywgZXJyKTsKICByZXMuc3RhdHVzKGVyci5zdGF0dXMgfHwgNTAwKS5qc29uKHsKICAgIGVycm9yOiBlcnIubWVzc2FnZSB8fCAnSW50ZXJuYWwgc2VydmVyIGVycm9yJywKICAgIC4uLihwcm9jZXNzLmVudi5OT0RFX0VOViA9PT0gJ2RldmVsb3BtZW50JyAmJiB7IHN0YWNrOiBlcnIuc3RhY2sgfSkKICB9KTsKfSk7CgphcHAubGlzdGVuKFBPUlQsICcwLjAuMC4wJywgKCkgPT4gewogIGNvbnNvbGUubG9nKGBcbvCfkJUgQlJFRURSIFNlcnZlciBSdW5uaW5nYCk7CiAgY29uc29sZS5sb2coYD09PT09PT09PT09PT09PT09PT09PT09PT09PT09PWApOwogIGNvbnNvbGUubG9nKGBFbnZpcm9ubWVudDogJHtwcm9jZXNzLmVudi5OT0RFX0VOViB8fCAnZGV2ZWxvcG1lbnQnfWApOwogIGNvbnNvbGUubG9nKGBQb3J0OiAke1BPUlR9YCk7CiAgY29uc29sZS5sb2coYERhdGFiYXNlOiAke0RCX1BBVEh9YCk7CiAgY29uc29sZS5sb2coYFVwbG9hZHM6ICR7VVBMT0FEX1BBVEh9YCk7CiAgY29uc29sZS5sb2coYFN0YXRpYzogJHtTVEFUSUNfUEFUSH1gKTsKICBjb25zb2xlLmxvZyhgQWNjZXNzOiBodHRwOi8vbG9jYWxob3N0OiR7UE9SVH1gKTsKICBjb25zb2xlLmxvZyhgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09XG5gKTsKfSk7Cgptb2R1bGUuZXhwb3J0cyA9IGFwcDsK
\ No newline at end of file
diff --git a/server/routes/dogs.js b/server/routes/dogs.js
index 3fdf934..f5178f4 100644
--- a/server/routes/dogs.js
+++ b/server/routes/dogs.js
@@ -5,7 +5,6 @@ const multer = require('multer');
const path = require('path');
const fs = require('fs');
-// Configure multer for photo uploads
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadPath = process.env.UPLOAD_PATH || path.join(__dirname, '../../uploads');
@@ -19,12 +18,10 @@ const storage = multer.diskStorage({
const upload = multer({
storage,
- limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
+ limits: { fileSize: 10 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
- const allowedTypes = /jpeg|jpg|png|gif|webp/;
- const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
- const mimetype = allowedTypes.test(file.mimetype);
- if (extname && mimetype) {
+ const allowed = /jpeg|jpg|png|gif|webp/;
+ if (allowed.test(path.extname(file.originalname).toLowerCase()) && allowed.test(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Only image files are allowed'));
@@ -32,29 +29,41 @@ const upload = multer({
}
});
-// Helper function to convert empty strings to null
-const emptyToNull = (value) => {
- return (value === '' || value === undefined) ? null : value;
-};
+const emptyToNull = (v) => (v === '' || v === undefined) ? null : v;
-// GET all dogs
+// ── Shared SELECT columns ─────────────────────────────────────────────
+const DOG_COLS = `
+ id, name, registration_number, breed, sex, birth_date,
+ color, microchip, photo_urls, notes, litter_id, is_active,
+ is_champion, created_at, updated_at
+`;
+
+// ── GET all dogs ───────────────────────────────────────────────────
router.get('/', (req, res) => {
try {
const db = getDatabase();
const dogs = db.prepare(`
- SELECT id, name, registration_number, breed, sex, birth_date,
- color, microchip, photo_urls, notes, litter_id, is_active,
- created_at, updated_at
- FROM dogs
- WHERE is_active = 1
+ SELECT ${DOG_COLS}
+ FROM dogs
+ WHERE is_active = 1
ORDER BY name
`).all();
-
- // Parse photo_urls JSON
+
+ // Also pull sire/dam so list page can compute bloodline status
+ const parentStmt = db.prepare(`
+ SELECT p.parent_type, d.id, d.name, d.is_champion
+ FROM parents p
+ JOIN dogs d ON p.parent_id = d.id
+ WHERE p.dog_id = ?
+ `);
+
dogs.forEach(dog => {
dog.photo_urls = dog.photo_urls ? JSON.parse(dog.photo_urls) : [];
+ const parents = parentStmt.all(dog.id);
+ dog.sire = parents.find(p => p.parent_type === 'sire') || null;
+ dog.dam = parents.find(p => p.parent_type === 'dam') || null;
});
-
+
res.json(dogs);
} catch (error) {
console.error('Error fetching dogs:', error);
@@ -62,42 +71,35 @@ router.get('/', (req, res) => {
}
});
-// GET single dog by ID with parents and offspring
+// ── GET single dog (with parents + offspring) ───────────────────────
router.get('/:id', (req, res) => {
try {
const db = getDatabase();
- const dog = db.prepare(`
- SELECT id, name, registration_number, breed, sex, birth_date,
- color, microchip, photo_urls, notes, litter_id, is_active,
- created_at, updated_at
- FROM dogs
- WHERE id = ?
- `).get(req.params.id);
-
- if (!dog) {
- return res.status(404).json({ error: 'Dog not found' });
- }
-
+ const dog = db.prepare(`SELECT ${DOG_COLS} FROM dogs WHERE id = ?`).get(req.params.id);
+
+ if (!dog) return res.status(404).json({ error: 'Dog not found' });
+
dog.photo_urls = dog.photo_urls ? JSON.parse(dog.photo_urls) : [];
-
- // Get parents from parents table
+
+ // Parents — include is_champion so frontend can render bloodline badge
const parents = db.prepare(`
- SELECT p.parent_type, d.*
- FROM parents p
- JOIN dogs d ON p.parent_id = d.id
+ SELECT p.parent_type, d.id, d.name, d.is_champion
+ FROM parents p
+ JOIN dogs d ON p.parent_id = d.id
WHERE p.dog_id = ?
`).all(req.params.id);
-
+
dog.sire = parents.find(p => p.parent_type === 'sire') || null;
- dog.dam = parents.find(p => p.parent_type === 'dam') || null;
-
- // Get offspring
+ dog.dam = parents.find(p => p.parent_type === 'dam') || null;
+
+ // Offspring — include is_champion for badge on offspring cards
dog.offspring = db.prepare(`
- SELECT d.* FROM dogs d
+ SELECT d.id, d.name, d.sex, d.is_champion
+ FROM dogs d
JOIN parents p ON d.id = p.dog_id
WHERE p.parent_id = ? AND d.is_active = 1
`).all(req.params.id);
-
+
res.json(dog);
} catch (error) {
console.error('Error fetching dog:', error);
@@ -105,66 +107,50 @@ router.get('/:id', (req, res) => {
}
});
-// POST create new dog
+// ── POST create dog ────────────────────────────────────────────────
router.post('/', (req, res) => {
try {
- const { name, registration_number, breed, sex, birth_date, color, microchip, notes, sire_id, dam_id, litter_id } = req.body;
-
- console.log('Creating dog with data:', { name, breed, sex, sire_id, dam_id, litter_id });
-
+ const { name, registration_number, breed, sex, birth_date, color,
+ microchip, notes, sire_id, dam_id, litter_id, is_champion } = req.body;
+
+ console.log('Creating dog:', { name, breed, sex, sire_id, dam_id, litter_id, is_champion });
+
if (!name || !breed || !sex) {
return res.status(400).json({ error: 'Name, breed, and sex are required' });
}
-
+
const db = getDatabase();
-
- // Insert dog (dogs table has NO sire/dam columns)
const result = db.prepare(`
- INSERT INTO dogs (name, registration_number, breed, sex, birth_date, color, microchip, notes, litter_id, photo_urls)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ INSERT INTO dogs (name, registration_number, breed, sex, birth_date, color,
+ microchip, notes, litter_id, photo_urls, is_champion)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
- name,
- emptyToNull(registration_number),
- breed,
- sex,
- emptyToNull(birth_date),
- emptyToNull(color),
+ name,
+ emptyToNull(registration_number),
+ breed, sex,
+ emptyToNull(birth_date),
+ emptyToNull(color),
emptyToNull(microchip),
emptyToNull(notes),
emptyToNull(litter_id),
- '[]'
+ '[]',
+ is_champion ? 1 : 0
);
-
+
const dogId = result.lastInsertRowid;
console.log(`✓ Dog inserted with ID: ${dogId}`);
-
- // Add sire relationship if provided
+
if (sire_id && sire_id !== '' && sire_id !== null) {
- console.log(` Adding sire relationship: dog ${dogId} -> sire ${sire_id}`);
- db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)').
- run(dogId, sire_id, 'sire');
- console.log(` ✓ Sire relationship added`);
+ db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)').run(dogId, sire_id, 'sire');
}
-
- // Add dam relationship if provided
if (dam_id && dam_id !== '' && dam_id !== null) {
- console.log(` Adding dam relationship: dog ${dogId} -> dam ${dam_id}`);
- db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)').
- run(dogId, dam_id, 'dam');
- console.log(` ✓ Dam relationship added`);
+ db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)').run(dogId, dam_id, 'dam');
}
-
- // Fetch the created dog
- const dog = db.prepare(`
- SELECT id, name, registration_number, breed, sex, birth_date,
- color, microchip, photo_urls, notes, litter_id, is_active,
- created_at, updated_at
- FROM dogs
- WHERE id = ?
- `).get(dogId);
+
+ const dog = db.prepare(`SELECT ${DOG_COLS} FROM dogs WHERE id = ?`).get(dogId);
dog.photo_urls = [];
-
- console.log(`✓ Dog created successfully: ${dog.name} (ID: ${dogId})`);
+
+ console.log(`✓ Dog created: ${dog.name} (ID: ${dogId})`);
res.status(201).json(dog);
} catch (error) {
console.error('Error creating dog:', error);
@@ -172,66 +158,47 @@ router.post('/', (req, res) => {
}
});
-// PUT update dog
+// ── PUT update dog ────────────────────────────────────────────────
router.put('/:id', (req, res) => {
try {
- const { name, registration_number, breed, sex, birth_date, color, microchip, notes, sire_id, dam_id, litter_id } = req.body;
-
- console.log(`Updating dog ${req.params.id} with data:`, { name, breed, sex, sire_id, dam_id, litter_id });
-
+ const { name, registration_number, breed, sex, birth_date, color,
+ microchip, notes, sire_id, dam_id, litter_id, is_champion } = req.body;
+
+ console.log(`Updating dog ${req.params.id}:`, { name, breed, sex, sire_id, dam_id, is_champion });
+
const db = getDatabase();
-
- // Update dog record (dogs table has NO sire/dam columns)
db.prepare(`
- UPDATE dogs
- SET name = ?, registration_number = ?, breed = ?, sex = ?,
- birth_date = ?, color = ?, microchip = ?, notes = ?, litter_id = ?
+ UPDATE dogs
+ SET name = ?, registration_number = ?, breed = ?, sex = ?,
+ birth_date = ?, color = ?, microchip = ?, notes = ?,
+ litter_id = ?, is_champion = ?, updated_at = datetime('now')
WHERE id = ?
`).run(
- name,
- emptyToNull(registration_number),
- breed,
- sex,
- emptyToNull(birth_date),
- emptyToNull(color),
+ name,
+ emptyToNull(registration_number),
+ breed, sex,
+ emptyToNull(birth_date),
+ emptyToNull(color),
emptyToNull(microchip),
emptyToNull(notes),
emptyToNull(litter_id),
+ is_champion ? 1 : 0,
req.params.id
);
- console.log(` ✓ Dog record updated`);
-
- // Remove existing parent relationships
+
+ // Re-link parents
db.prepare('DELETE FROM parents WHERE dog_id = ?').run(req.params.id);
- console.log(` ✓ Old parent relationships removed`);
-
- // Add new sire relationship if provided
if (sire_id && sire_id !== '' && sire_id !== null) {
- console.log(` Adding sire relationship: dog ${req.params.id} -> sire ${sire_id}`);
- db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)').
- run(req.params.id, sire_id, 'sire');
- console.log(` ✓ Sire relationship added`);
+ db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)').run(req.params.id, sire_id, 'sire');
}
-
- // Add new dam relationship if provided
if (dam_id && dam_id !== '' && dam_id !== null) {
- console.log(` Adding dam relationship: dog ${req.params.id} -> dam ${dam_id}`);
- db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)').
- run(req.params.id, dam_id, 'dam');
- console.log(` ✓ Dam relationship added`);
+ db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)').run(req.params.id, dam_id, 'dam');
}
-
- // Fetch updated dog
- const dog = db.prepare(`
- SELECT id, name, registration_number, breed, sex, birth_date,
- color, microchip, photo_urls, notes, litter_id, is_active,
- created_at, updated_at
- FROM dogs
- WHERE id = ?
- `).get(req.params.id);
+
+ const dog = db.prepare(`SELECT ${DOG_COLS} FROM dogs WHERE id = ?`).get(req.params.id);
dog.photo_urls = dog.photo_urls ? JSON.parse(dog.photo_urls) : [];
-
- console.log(`✓ Dog updated successfully: ${dog.name} (ID: ${req.params.id})`);
+
+ console.log(`✓ Dog updated: ${dog.name} (ID: ${req.params.id})`);
res.json(dog);
} catch (error) {
console.error('Error updating dog:', error);
@@ -239,7 +206,7 @@ router.put('/:id', (req, res) => {
}
});
-// DELETE dog (soft delete)
+// ── DELETE dog (soft) ───────────────────────────────────────────────
router.delete('/:id', (req, res) => {
try {
const db = getDatabase();
@@ -252,25 +219,19 @@ router.delete('/:id', (req, res) => {
}
});
-// POST upload photo for dog
+// ── POST upload photo ───────────────────────────────────────────────
router.post('/:id/photos', upload.single('photo'), (req, res) => {
try {
- if (!req.file) {
- return res.status(400).json({ error: 'No file uploaded' });
- }
-
+ if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
+
const db = getDatabase();
const dog = db.prepare('SELECT photo_urls FROM dogs WHERE id = ?').get(req.params.id);
-
- if (!dog) {
- return res.status(404).json({ error: 'Dog not found' });
- }
-
+ if (!dog) return res.status(404).json({ error: 'Dog not found' });
+
const photoUrls = dog.photo_urls ? JSON.parse(dog.photo_urls) : [];
photoUrls.push(`/uploads/${req.file.filename}`);
-
db.prepare('UPDATE dogs SET photo_urls = ? WHERE id = ?').run(JSON.stringify(photoUrls), req.params.id);
-
+
res.json({ url: `/uploads/${req.file.filename}`, photos: photoUrls });
} catch (error) {
console.error('Error uploading photo:', error);
@@ -278,31 +239,26 @@ router.post('/:id/photos', upload.single('photo'), (req, res) => {
}
});
-// DELETE photo from dog
+// ── DELETE photo ─────────────────────────────────────────────────────
router.delete('/:id/photos/:photoIndex', (req, res) => {
try {
const db = getDatabase();
const dog = db.prepare('SELECT photo_urls FROM dogs WHERE id = ?').get(req.params.id);
-
- if (!dog) {
- return res.status(404).json({ error: 'Dog not found' });
- }
-
- const photoUrls = dog.photo_urls ? JSON.parse(dog.photo_urls) : [];
+ if (!dog) return res.status(404).json({ error: 'Dog not found' });
+
+ const photoUrls = dog.photo_urls ? JSON.parse(dog.photo_urls) : [];
const photoIndex = parseInt(req.params.photoIndex);
-
+
if (photoIndex >= 0 && photoIndex < photoUrls.length) {
- const photoPath = path.join(process.env.UPLOAD_PATH || path.join(__dirname, '../../uploads'), path.basename(photoUrls[photoIndex]));
-
- // Delete file from disk
- if (fs.existsSync(photoPath)) {
- fs.unlinkSync(photoPath);
- }
-
+ const photoPath = path.join(
+ process.env.UPLOAD_PATH || path.join(__dirname, '../../uploads'),
+ path.basename(photoUrls[photoIndex])
+ );
+ if (fs.existsSync(photoPath)) fs.unlinkSync(photoPath);
photoUrls.splice(photoIndex, 1);
db.prepare('UPDATE dogs SET photo_urls = ? WHERE id = ?').run(JSON.stringify(photoUrls), req.params.id);
}
-
+
res.json({ photos: photoUrls });
} catch (error) {
console.error('Error deleting photo:', error);
diff --git a/server/routes/settings.js b/server/routes/settings.js
new file mode 100644
index 0000000..202ade4
--- /dev/null
+++ b/server/routes/settings.js
@@ -0,0 +1 @@
+Y29uc3QgZXhwcmVzcyA9IHJlcXVpcmUoJ2V4cHJlc3MnKTsKY29uc3Qgcm91dGVyID0gZXhwcmVzcy5Sb3V0ZXIoKTsKY29uc3QgeyBnZXREYXRhYmFzZSB9ID0gcmVxdWlyZSgnLi4vZGIvaW5pdCcpOwoKLy8gR0VUIGFsbCBzZXR0aW5ncwpyb3V0ZXIuZ2V0KCcvJywgKHJlcSwgcmVzKSA9PiB7CiAgdHJ5IHsKICAgIGNvbnN0IGRiID0gZ2V0RGF0YWJhc2UoKTsKICAgIGNvbnN0IHJvd3MgPSBkYi5wcmVwYXJlKCdTRUxFQ1Qga2V5LCB2YWx1ZSBGUk9NIHNldHRpbmdzJykuYWxsKCk7CiAgICBjb25zdCBzZXR0aW5ncyA9IHt9OwogICAgcm93cy5mb3JFYWNoKHIgPT4geyBzZXR0aW5nc1tyLmtleV0gPSByLnZhbHVlOyB9KTsKICAgIHJlcy5qc29uKHNldHRpbmdzKTsKICB9IGNhdGNoIChlcnJvcikgewogICAgcmVzLnN0YXR1cyg1MDApLmpzb24oeyBlcnJvcjogZXJyb3IubWVzc2FnZSB9KTsKICB9Cn0pOwoKLy8gUFVUIHVwZGF0ZSBzZXR0aW5ncwpyb3V0ZXIucHV0KCcvJywgKHJlcSwgcmVzKSA9PiB7CiAgdHJ5IHsKICAgIGNvbnN0IGRiID0gZ2V0RGF0YWJhc2UoKTsKICAgIGNvbnN0IHVwc2VydCA9IGRiLnByZXBhcmUoJ0lOU0VSVCBJTlRPIHNldHRpbmdzIChrZXksIHZhbHVlKSBWQUxVRVMgKD8sID8pIE9OIENPTUZMSUNIVCBLRVBVUERBVEU9ZXhjbHVkZWQudmFsdWUnKTsKICAgIGNvbnN0IHVwZGF0ZU1hbnkgPSBkYi50cmFuc2FjdGlvbigoZW50cmllcykgPT4gewogICAgICBmb3IgKGNvbnN0IFtrZXksIHZhbHVlXSBvZiBPYmplY3QuZW50cmllcyhlbnRyaWVzKSkgewogICAgICAgIHVwc2VydC5ydW4oa2V5LCB2YWx1ZSA9PSBudWxsID8gJycgOiBTdHJpbmcodmFsdWUpKTsKICAgICAgfQogICAgfSk7CiAgICB1cGRhdGVNYW55KHJlcS5ib2R5KTsKICAgIHJlcy5qc29uKHsgbWVzc2FnZTogJ1NldHRpbmdzIHNhdmVkJyB9KTsKICB9IGNhdGNoIChlcnJvcikgewogICAgcmVzLnN0YXR1cyg1MDApLmpzb24oeyBlcnJvcjogZXJyb3IubWVzc2FnZSB9KTsKICB9Cn0pOwoKbW9kdWxlLmV4cG9ydHMgPSByb3V0ZXI7Cg==
\ No newline at end of file