diff --git a/scripts/seed-demo.ts b/scripts/seed-demo.ts new file mode 100644 index 0000000..0ca888f --- /dev/null +++ b/scripts/seed-demo.ts @@ -0,0 +1,1716 @@ +import { drizzle } from 'drizzle-orm/mysql2'; +import mysql from 'mysql2/promise'; +import { hash } from '@node-rs/argon2'; +import * as schema from '../src/lib/server/schema'; + +// Simple slugify +function slugify(text: string): string { + return text + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); +} + +// Date helpers +function daysAgo(n: number): Date { + const d = new Date(); + d.setDate(d.getDate() - n); + return d; +} + +function daysFromNow(n: number): Date { + const d = new Date(); + d.setDate(d.getDate() + n); + return d; +} + +function randomBetween(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +function pick(arr: T[]): T { + return arr[Math.floor(Math.random() * arr.length)]; +} + +async function seedDemo() { + const pool = mysql.createPool({ uri: process.env.DATABASE_URL }); + const db = drizzle(pool, { schema, mode: 'default' }); + + console.log('🐾 Seeding FosterFlow demo data for Sunny Paws Rescue...\n'); + + // ── 1. Org Config ────────────────────────────────────────────────────────── + console.log('Creating org config...'); + const orgConfigRows = [ + { + key: 'branding', + value: { + primaryColor: '#ea580c', + primaryColorLight: '#f97316', + primaryColorDark: '#c2410c', + accentColor: '#0d9488', + accentColorLight: '#14b8a6', + accentColorDark: '#0f766e', + logoUrl: '' + } + }, + { + key: 'org_info', + value: { + name: 'Sunny Paws Rescue', + tagline: 'Giving every paw a second chance', + city: 'Austin', + state: 'TX', + phone: '(512) 555-0147', + email: 'demo@fosterflow.app', + facebookUrl: 'https://facebook.com/sunnypawsrescue', + instagramUrl: 'https://instagram.com/sunnypawsrescue' + } + }, + { key: 'locale', value: { language: 'en', currency: 'USD', dateFormat: 'MM/DD/YYYY' } }, + { + key: 'applications', + value: { + adoptionEnabled: true, + fosterEnabled: true, + volunteerEnabled: true, + surrenderEnabled: true, + adoptionFee: 250 + } + }, + { key: 'setup_complete', value: true } + ]; + for (const row of orgConfigRows) { + await db.insert(schema.orgConfig).values({ key: row.key, value: row.value }); + } + console.log(' āœ“ Org config'); + + // ── 2. Users ─────────────────────────────────────────────────────────────── + console.log('Creating users...'); + const adminHash = await hash('demodemo'); + const [adminResult] = await db.insert(schema.users).values({ + email: 'demo@fosterflow.app', + passwordHash: adminHash, + name: 'Demo Admin', + role: 'sysadmin', + active: true, + mustChangePassword: false + }); + const adminId = adminResult.insertId; + + const fosterUsers = [ + { email: 'foster@fosterflow.app', name: 'Foster User' }, + { email: 'sarah.mitchell@gmail.com', name: 'Sarah Mitchell' }, + { email: 'mike.torres@gmail.com', name: 'Mike Torres' }, + { email: 'jennifer.hayes@outlook.com', name: 'Jennifer Hayes' }, + { email: 'carlos.ruiz@gmail.com', name: 'Carlos Ruiz' }, + { email: 'amanda.chen@yahoo.com', name: 'Amanda Chen' } + ]; + + const fosterHash = await hash('demodemo'); + const fosterIds: number[] = []; + for (const u of fosterUsers) { + const [res] = await db.insert(schema.users).values({ + email: u.email, + passwordHash: fosterHash, + name: u.name, + role: 'foster', + active: true, + mustChangePassword: false + }); + fosterIds.push(res.insertId); + } + console.log(` āœ“ Admin + ${fosterUsers.length} foster users`); + + // ── 3. Intake Sources ────────────────────────────────────────────────────── + console.log('Creating intake sources...'); + const intakeSourceNames = ['Owner Surrender', 'Stray', 'Transfer', 'Born in Care', 'Other']; + const intakeSourceIds: number[] = []; + for (const name of intakeSourceNames) { + const [res] = await db.insert(schema.intakeSources).values({ name }); + intakeSourceIds.push(res.insertId); + } + // Map: 0=Owner Surrender, 1=Stray, 2=Transfer, 3=Born in Care, 4=Other + console.log(' āœ“ Intake sources'); + + // ── 4. Vets ──────────────────────────────────────────────────────────────── + console.log('Creating vets...'); + const vetData = [ + { + name: 'Dr. Patricia Nguyen', + clinic: 'Austin Animal Clinic', + phone: '(512) 555-0201', + email: 'info@austinaclinic.com', + address: '2450 South Lamar Blvd, Austin, TX 78704' + }, + { + name: 'Dr. Robert Kim', + clinic: 'Lone Star Veterinary', + phone: '(512) 555-0312', + email: 'care@lonestarvet.com', + address: '1820 Airport Blvd, Austin, TX 78702' + }, + { + name: 'Dr. Susan Flores', + clinic: 'Hill Country Vet', + phone: '(512) 555-0428', + email: 'hello@hillcountryvet.com', + address: '5501 N Lamar Blvd Suite 102, Austin, TX 78751' + }, + { + name: 'Dr. James Okafor', + clinic: 'South Austin Pet Hospital', + phone: '(512) 555-0519', + email: 'appointments@saph.com', + address: '4001 South Congress Ave, Austin, TX 78745' + } + ]; + const vetIds: number[] = []; + for (const v of vetData) { + const [res] = await db.insert(schema.vets).values({ ...v, active: true }); + vetIds.push(res.insertId); + } + console.log(' āœ“ 4 vets'); + + // ── 5. Pets ──────────────────────────────────────────────────────────────── + console.log('Creating pets...'); + + interface PetSeed { + name: string; + species: 'dog' | 'cat' | 'other'; + breed: string; + color: string; + sex: 'male' | 'female' | 'unknown'; + weight: string; + age: string; + status: 'available' | 'adopted' | 'foster' | 'medical_hold' | 'behavioral_hold' | 'pending' | 'trial' | 'hospice' | 'nursing' | 'whelping' | 'deceased' | 'transferred'; + description: string; + medicalNotes?: string; + behavioralNotes?: string; + goodWithDogs: boolean; + goodWithCats: boolean; + goodWithKids: boolean; + houseTrained: boolean; + spayedNeutered: boolean; + microchipped: boolean; + adoptionFee: number; + intakeDaysAgo: number; + intakeSourceIdx: number; + featured: boolean; + adopterName?: string; + adopterEmail?: string; + adoptedDaysAgo?: number; + } + + const petSeeds: PetSeed[] = [ + // Dogs - available + { + name: 'Bella', + species: 'dog', + breed: 'Golden Retriever Mix', + color: 'Golden', + sex: 'female', + weight: '48 lbs', + age: '2 years', + status: 'available', + description: + "Bella is a sweet and energetic Golden Retriever mix who loves nothing more than a good game of fetch at the park. She came to us after her previous family had to relocate internationally, and despite the upheaval she's remained cheerful and loving. Bella is fully housebroken, knows basic commands, and is an absolute dream on a leash.", + behavioralNotes: 'Excellent recall. Loves water. Gets overstimulated with too many dogs at once.', + goodWithDogs: true, + goodWithCats: false, + goodWithKids: true, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 30000, + intakeDaysAgo: 45, + intakeSourceIdx: 0, + featured: true + }, + { + name: 'Ranger', + species: 'dog', + breed: 'Australian Shepherd Mix', + color: 'Blue Merle', + sex: 'male', + weight: '52 lbs', + age: '3 years', + status: 'available', + description: + "Ranger is a breathtakingly handsome blue merle Aussie mix with one blue eye and one brown eye. He's incredibly intelligent and picks up new tricks in minutes — he already knows sit, stay, down, shake, and rollover. Ranger needs an active home with a yard where he can channel his boundless energy.", + behavioralNotes: 'High energy. Needs at least 2 hours of exercise daily. Herding instinct present.', + goodWithDogs: true, + goodWithCats: false, + goodWithKids: false, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 32500, + intakeDaysAgo: 30, + intakeSourceIdx: 1, + featured: true + }, + { + name: 'Daisy', + species: 'dog', + breed: 'Beagle Mix', + color: 'Tricolor', + sex: 'female', + weight: '24 lbs', + age: '4 years', + status: 'available', + description: + "Daisy is a classic tricolor Beagle mix with the soulful eyes that the breed is famous for. She loves sniffing everything on walks — every blade of grass is a new adventure to her. Daisy is mellow indoors and would do great in an apartment as long as she gets her daily walks.", + medicalNotes: 'Seasonal allergies managed with Benadryl as needed.', + goodWithDogs: true, + goodWithCats: true, + goodWithKids: true, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 27500, + intakeDaysAgo: 22, + intakeSourceIdx: 0, + featured: false + }, + { + name: 'Duke', + species: 'dog', + breed: 'Labrador Retriever Mix', + color: 'Black', + sex: 'male', + weight: '68 lbs', + age: '5 years', + status: 'available', + description: + "Duke is a big, friendly black Lab mix who is the definition of a velcro dog — he wants to be next to you at all times. He's great on the couch for movie nights and equally happy charging through Barton Creek Greenbelt. Duke is well-mannered, patient, and would thrive in almost any loving home.", + goodWithDogs: true, + goodWithCats: true, + goodWithKids: true, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 25000, + intakeDaysAgo: 60, + intakeSourceIdx: 2, + featured: false + }, + { + name: 'Rosie', + species: 'dog', + breed: 'Chihuahua Mix', + color: 'Fawn', + sex: 'female', + weight: '9 lbs', + age: '6 years', + status: 'available', + description: + "Don't let her tiny size fool you — Rosie has the personality of a dog ten times her size. She's sassy, opinionated, and deeply devoted to her favorite people. Rosie would do best as the only pet in the home, where she can be the undisputed queen of her kingdom.", + behavioralNotes: 'Resource guards food bowl. No other pets. Fine with older, calm children.', + goodWithDogs: false, + goodWithCats: false, + goodWithKids: false, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 20000, + intakeDaysAgo: 15, + intakeSourceIdx: 0, + featured: false + }, + { + name: 'Biscuit', + species: 'dog', + breed: 'Corgi Mix', + color: 'Sable and White', + sex: 'male', + weight: '28 lbs', + age: '1 year', + status: 'available', + description: + "Biscuit came to us as part of a litter surrender and has blossomed into the most charming little character. He has the classic Corgi butt wiggle and stubby legs that make everyone stop and laugh. Biscuit is still learning the ropes but is incredibly food-motivated and responds beautifully to positive reinforcement.", + goodWithDogs: true, + goodWithCats: true, + goodWithKids: true, + houseTrained: false, + spayedNeutered: true, + microchipped: true, + adoptionFee: 35000, + intakeDaysAgo: 10, + intakeSourceIdx: 3, + featured: true + }, + // Dogs - foster + { + name: 'Max', + species: 'dog', + breed: 'Pit Bull Terrier Mix', + color: 'Brindle', + sex: 'male', + weight: '55 lbs', + age: '3 years', + status: 'foster', + description: + "Max is a stunning brindle Pit Bull mix who is as gentle as a lamb with his foster family. He loves cuddling on the couch, going to the dog park, and meeting new people. Max has passed his Canine Good Citizen test and is one of our most adoptable dogs — he just needs the right person to find him.", + goodWithDogs: true, + goodWithCats: false, + goodWithKids: true, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 30000, + intakeDaysAgo: 90, + intakeSourceIdx: 1, + featured: false + }, + { + name: 'Penny', + species: 'dog', + breed: 'Dachshund Mix', + color: 'Chocolate and Tan', + sex: 'female', + weight: '14 lbs', + age: '4 years', + status: 'foster', + description: + "Penny is a spunky little Dachshund mix who came to us after being surrendered by a family that was moving. She took a few days to warm up but is now her foster family's shadow. Penny enjoys long naps in sunbeams, car rides, and bossing around her foster siblings.", + goodWithDogs: true, + goodWithCats: false, + goodWithKids: false, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 22500, + intakeDaysAgo: 50, + intakeSourceIdx: 0, + featured: false + }, + { + name: 'Bruno', + species: 'dog', + breed: 'German Shepherd Mix', + color: 'Black and Tan', + sex: 'male', + weight: '75 lbs', + age: '2 years', + status: 'foster', + description: + "Bruno is a magnificent German Shepherd mix who came to us as a stray and took a little time to trust people. With patience and consistency, he has become a loyal and devoted companion. He is protective of his people but not aggressive, and adores his foster mom.", + behavioralNotes: 'Slow intro to new people recommended. No cats. Good with confident dogs.', + goodWithDogs: true, + goodWithCats: false, + goodWithKids: false, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 32500, + intakeDaysAgo: 75, + intakeSourceIdx: 1, + featured: false + }, + { + name: 'Hazel', + species: 'dog', + breed: 'Boxer Mix', + color: 'Fawn', + sex: 'female', + weight: '49 lbs', + age: '2 years', + status: 'foster', + description: + "Hazel is a playful, goofy Boxer mix who thinks she's a lap dog despite her size. She gives the best Boxer kidney-bean wiggle greetings and hasn't met a human she doesn't love. Hazel is great with kids and other dogs but can be a little much for cats.", + goodWithDogs: true, + goodWithCats: false, + goodWithKids: true, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 27500, + intakeDaysAgo: 35, + intakeSourceIdx: 2, + featured: false + }, + { + name: 'Milo', + species: 'dog', + breed: 'Maltese Mix', + color: 'White', + sex: 'male', + weight: '11 lbs', + age: '7 years', + status: 'foster', + description: + "Milo is a fluffy white senior Maltese mix who deserves all the retirement luxury a dog could want. He enjoys slow walks, belly rubs, and sleeping in the sun. Milo is perfectly housebroken, doesn't chew anything, and is an absolute dream to have around.", + medicalNotes: 'Dental disease — had 4 teeth extracted last month. On soft diet.', + goodWithDogs: true, + goodWithCats: true, + goodWithKids: true, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 18000, + intakeDaysAgo: 40, + intakeSourceIdx: 0, + featured: false + }, + { + name: 'Atlas', + species: 'dog', + breed: 'Husky Mix', + color: 'Gray and White', + sex: 'male', + weight: '58 lbs', + age: '1 year', + status: 'foster', + description: + "Atlas is a stunning gray and white Husky mix with piercing ice-blue eyes. He came to us as a 6-month-old puppy who was growing too fast for his original family. He's still in the teenage phase — full of energy and mischief — but with the right home and training he will be an incredible dog.", + behavioralNotes: 'Counter surfs. Needs a secure 6-foot fence. Knows sit, down, and is working on stay.', + goodWithDogs: true, + goodWithCats: false, + goodWithKids: true, + houseTrained: false, + spayedNeutered: true, + microchipped: true, + adoptionFee: 30000, + intakeDaysAgo: 55, + intakeSourceIdx: 0, + featured: false + }, + // Dogs - medical hold + { + name: 'Cooper', + species: 'dog', + breed: 'Beagle Mix', + color: 'Lemon and White', + sex: 'male', + weight: '22 lbs', + age: '5 years', + status: 'medical_hold', + description: + "Cooper is a lemon and white Beagle mix who arrived as a stray with a leg injury. He had surgery two weeks ago and is recovering beautifully. Once he gets the all-clear from his vet, he'll be ready for adoption. Cooper has the most endearing howl and is incredibly food-motivated.", + medicalNotes: 'Left rear leg fracture, surgically repaired. 4-week recovery period. No running or jumping.', + goodWithDogs: true, + goodWithCats: true, + goodWithKids: true, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 25000, + intakeDaysAgo: 18, + intakeSourceIdx: 1, + featured: false + }, + // Dogs - adopted + { + name: 'Luna', + species: 'dog', + breed: 'Siberian Husky', + color: 'Black and White', + sex: 'female', + weight: '45 lbs', + age: '2 years', + status: 'adopted', + description: + "Luna is a gorgeous Siberian Husky who found her forever home with the Williams family. She loves running the trails at Bull Creek and has her own Instagram account with 2,000 followers. Her adopters sent us the sweetest update at Christmas.", + goodWithDogs: true, + goodWithCats: false, + goodWithKids: true, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 35000, + intakeDaysAgo: 120, + intakeSourceIdx: 2, + featured: false, + adopterName: 'Jason Williams', + adopterEmail: 'jwilliams@gmail.com', + adoptedDaysAgo: 60 + }, + { + name: 'Buddy', + species: 'dog', + breed: 'Golden Retriever', + color: 'Golden', + sex: 'male', + weight: '62 lbs', + age: '3 years', + status: 'adopted', + description: + "Buddy was our most popular dog of the year. He was adopted within 48 hours of going live on the website by a family with three kids who had been searching for months for the perfect family dog. Buddy fit the bill perfectly.", + goodWithDogs: true, + goodWithCats: true, + goodWithKids: true, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 35000, + intakeDaysAgo: 150, + intakeSourceIdx: 0, + featured: false, + adopterName: 'The Henderson Family', + adopterEmail: 'henderson.family@outlook.com', + adoptedDaysAgo: 90 + }, + { + name: 'Stella', + species: 'dog', + breed: 'Border Collie Mix', + color: 'Black and White', + sex: 'female', + weight: '38 lbs', + age: '4 years', + status: 'adopted', + description: + "Stella found her forever home with a software engineer who works from home and takes her to the office twice a week. She's been promoted to Chief Morale Officer and is thriving in her new role.", + goodWithDogs: true, + goodWithCats: false, + goodWithKids: false, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 30000, + intakeDaysAgo: 160, + intakeSourceIdx: 1, + featured: false, + adopterName: 'Kevin Park', + adopterEmail: 'kpark@email.com', + adoptedDaysAgo: 110 + }, + { + name: 'Chester', + species: 'dog', + breed: 'Basset Hound Mix', + color: 'Tricolor', + sex: 'male', + weight: '44 lbs', + age: '6 years', + status: 'adopted', + description: + "Chester's droopy ears and soulful eyes won over the Ramirez family before they even finished reading his profile. He now spends his days lounging on the patio, supervising the backyard birds, and collecting as many ear scratches as possible.", + goodWithDogs: true, + goodWithCats: true, + goodWithKids: true, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 22500, + intakeDaysAgo: 140, + intakeSourceIdx: 0, + featured: false, + adopterName: 'Maria Ramirez', + adopterEmail: 'maria.ramirez@gmail.com', + adoptedDaysAgo: 80 + }, + { + name: 'Nala', + species: 'dog', + breed: 'Labrador Mix', + color: 'Yellow', + sex: 'female', + weight: '55 lbs', + age: '1 year', + status: 'adopted', + description: + "Nala was adopted by a young couple who fell in love with her at one of our Zilker Park adoption events. She's been busy learning to swim in Lake Travis and chasing squirrels in her new backyard.", + goodWithDogs: true, + goodWithCats: true, + goodWithKids: true, + houseTrained: false, + spayedNeutered: true, + microchipped: true, + adoptionFee: 32500, + intakeDaysAgo: 130, + intakeSourceIdx: 3, + featured: false, + adopterName: 'Tyler and Ashley Brooks', + adopterEmail: 'tbrooks@email.com', + adoptedDaysAgo: 70 + }, + { + name: 'Finn', + species: 'dog', + breed: 'Irish Setter Mix', + color: 'Mahogany', + sex: 'male', + weight: '51 lbs', + age: '2 years', + status: 'adopted', + description: + "Finn's gorgeous mahogany coat was hard to miss. He was adopted by an active family of runners who bring him along for their morning 5Ks. He's been completing them in excellent time.", + goodWithDogs: true, + goodWithCats: false, + goodWithKids: true, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 30000, + intakeDaysAgo: 170, + intakeSourceIdx: 2, + featured: false, + adopterName: 'The Okafor Family', + adopterEmail: 'okafor@gmail.com', + adoptedDaysAgo: 100 + }, + { + name: 'Zoey', + species: 'dog', + breed: 'Poodle Mix', + color: 'Apricot', + sex: 'female', + weight: '18 lbs', + age: '3 years', + status: 'adopted', + description: + "Zoey is a curly apricot Poodle mix who turned out to be a therapy dog in the making. Her adopter is a school counselor who has begun the process of having Zoey certified as a school therapy dog.", + goodWithDogs: true, + goodWithCats: true, + goodWithKids: true, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 32500, + intakeDaysAgo: 180, + intakeSourceIdx: 0, + featured: false, + adopterName: 'Claire Hoffman', + adopterEmail: 'choffman@austinisd.edu', + adoptedDaysAgo: 120 + }, + // Cats - available + { + name: 'Whiskers', + species: 'cat', + breed: 'Domestic Shorthair (Tabby)', + color: 'Brown Tabby', + sex: 'male', + weight: '11 lbs', + age: '3 years', + status: 'available', + description: + "Whiskers is a classic brown tabby with a bold, confident personality. He came to us as a stray but adjusted to indoor life almost immediately. Whiskers loves to supervise cooking from his perch on the kitchen counter and will politely — and insistently — remind you when it's dinner time.", + goodWithDogs: false, + goodWithCats: true, + goodWithKids: true, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 15000, + intakeDaysAgo: 25, + intakeSourceIdx: 1, + featured: true + }, + { + name: 'Cleo', + species: 'cat', + breed: 'Domestic Shorthair (Tuxedo)', + color: 'Black and White', + sex: 'female', + weight: '8 lbs', + age: '2 years', + status: 'available', + description: + "Cleo carries herself with the elegance befitting her name. This tuxedo beauty is playful in the evenings but a serene lap cat by day. She gets along beautifully with other cats and would love a feline companion to groom.", + goodWithDogs: false, + goodWithCats: true, + goodWithKids: true, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 15000, + intakeDaysAgo: 18, + intakeSourceIdx: 0, + featured: false + }, + { + name: 'Oliver', + species: 'cat', + breed: 'Maine Coon Mix', + color: 'Orange Tabby', + sex: 'male', + weight: '14 lbs', + age: '4 years', + status: 'available', + description: + "Oliver is a big, fluffy orange Maine Coon mix who thinks he's a dog. He'll follow you from room to room, come when called, and even fetch crinkle balls. Oliver gets along with everyone and would fit seamlessly into most households.", + goodWithDogs: true, + goodWithCats: true, + goodWithKids: true, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 20000, + intakeDaysAgo: 30, + intakeSourceIdx: 2, + featured: true + }, + // Cats - foster + { + name: 'Shadow', + species: 'cat', + breed: 'Domestic Shorthair', + color: 'Black', + sex: 'male', + weight: '10 lbs', + age: '5 years', + status: 'foster', + description: + "Shadow is a sleek, all-black cat who came to us extremely shy. With patient foster care, he's blossomed into a sweet and affectionate boy who loves lap time. He's still timid around strangers and would do best in a quiet home with patient adopters.", + behavioralNotes: 'Shy with new people — takes 1-2 weeks to warm up. Do not approach forcefully.', + goodWithDogs: false, + goodWithCats: true, + goodWithKids: false, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 15000, + intakeDaysAgo: 65, + intakeSourceIdx: 1, + featured: false + }, + { + name: 'Pearl', + species: 'cat', + breed: 'Domestic Longhair', + color: 'White', + sex: 'female', + weight: '9 lbs', + age: '6 years', + status: 'foster', + description: + "Pearl is a strikingly beautiful long-haired white cat with pale blue eyes. She's a quiet, dignified lady who prefers calm environments. Pearl loves sitting by windows and watching birds. She bonds deeply with her person and becomes very affectionate once trust is established.", + goodWithDogs: false, + goodWithCats: false, + goodWithKids: false, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 17500, + intakeDaysAgo: 42, + intakeSourceIdx: 0, + featured: false + }, + // Cats - adopted + { + name: 'Mittens', + species: 'cat', + breed: 'Calico', + color: 'Calico', + sex: 'female', + weight: '9 lbs', + age: '3 years', + status: 'adopted', + description: + "Mittens was adopted by a retired teacher who wanted a gentle, sweet companion. The two are inseparable — Mittens supervises the crossword puzzles every morning.", + goodWithDogs: false, + goodWithCats: true, + goodWithKids: true, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 15000, + intakeDaysAgo: 110, + intakeSourceIdx: 0, + featured: false, + adopterName: 'Evelyn Marsh', + adopterEmail: 'emarsh@gmail.com', + adoptedDaysAgo: 65 + }, + { + name: 'Tiger', + species: 'cat', + breed: 'Domestic Shorthair (Tabby)', + color: 'Orange Tabby', + sex: 'male', + weight: '13 lbs', + age: '2 years', + status: 'adopted', + description: + "Tiger found his forever home with a family who had recently lost their elderly cat. He has helped heal their hearts and is now the undisputed ruler of the household.", + goodWithDogs: false, + goodWithCats: false, + goodWithKids: true, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 15000, + intakeDaysAgo: 125, + intakeSourceIdx: 1, + featured: false, + adopterName: 'The Patterson Family', + adopterEmail: 'patterrons@gmail.com', + adoptedDaysAgo: 85 + }, + { + name: 'Luna', + species: 'cat', + breed: 'Russian Blue Mix', + color: 'Blue-Gray', + sex: 'female', + weight: '8 lbs', + age: '1 year', + status: 'adopted', + description: + "Luna the cat (not to be confused with Luna the Husky!) was adopted by a graduate student who needed a calm study companion. Luna takes her role as study supervisor very seriously.", + goodWithDogs: false, + goodWithCats: true, + goodWithKids: true, + houseTrained: true, + spayedNeutered: true, + microchipped: true, + adoptionFee: 17500, + intakeDaysAgo: 135, + intakeSourceIdx: 3, + featured: false, + adopterName: 'Priya Sharma', + adopterEmail: 'psharma@utexas.edu', + adoptedDaysAgo: 95 + } + ]; + + const petIds: number[] = []; + const petSlugs: string[] = []; + + for (const pet of petSeeds) { + // Generate unique slug (add suffix if needed) + let slug = slugify(pet.name); + const speciesSlug = pet.species === 'cat' ? '-cat' : ''; + if (petSlugs.includes(slug)) { + slug = slug + speciesSlug + '-' + pet.species; + } + petSlugs.push(slug); + + const [res] = await db.insert(schema.pets).values({ + name: pet.name, + slug, + species: pet.species, + breed: pet.breed, + color: pet.color, + sex: pet.sex, + weight: pet.weight, + age: pet.age, + status: pet.status, + description: pet.description, + medicalNotes: pet.medicalNotes, + behavioralNotes: pet.behavioralNotes, + goodWithDogs: pet.goodWithDogs, + goodWithCats: pet.goodWithCats, + goodWithKids: pet.goodWithKids, + houseTrained: pet.houseTrained, + spayedNeutered: pet.spayedNeutered, + microchipped: pet.microchipped, + adoptionFee: pet.adoptionFee, + intakeDate: daysAgo(pet.intakeDaysAgo), + intakeSourceId: intakeSourceIds[pet.intakeSourceIdx], + adoptedDate: pet.adoptedDaysAgo ? daysAgo(pet.adoptedDaysAgo) : undefined, + adopterName: pet.adopterName, + adopterEmail: pet.adopterEmail, + vetId: pick(vetIds), + featured: pet.featured, + published: true + }); + petIds.push(res.insertId); + } + console.log(` āœ“ ${petSeeds.length} pets`); + + // ── 6. Pet Photos ────────────────────────────────────────────────────────── + console.log('Creating pet photos...'); + for (let i = 0; i < petIds.length; i++) { + const slug = petSlugs[i]; + await db.insert(schema.petPhotos).values({ + petId: petIds[i], + url: `/uploads/demo/pets/${slug}.jpg`, + alt: petSeeds[i].name, + isPrimary: true, + sortOrder: 0 + }); + } + console.log(' āœ“ Pet photos'); + + // ── 7. Medical Records ───────────────────────────────────────────────────── + console.log('Creating medical records...'); + type MedType = 'vaccine' | 'surgery' | 'checkup' | 'medication' | 'deworming' | 'flea_tick' | 'heartworm_test' | 'heartworm_treatment' | 'dental' | 'spay_neuter' | 'microchip' | 'other'; + + interface MedRecord { + type: MedType; + name: string; + daysAgoOffset: number; + cost?: number; + notes?: string; + } + + const commonMedRecords: MedRecord[] = [ + { type: 'vaccine', name: 'Rabies Vaccine', daysAgoOffset: 30, cost: 2500, notes: '3-year rabies' }, + { type: 'vaccine', name: 'DHPP Booster', daysAgoOffset: 30, cost: 3500, notes: 'Distemper/Hepatitis/Parvovirus/Parainfluenza combination' }, + { type: 'vaccine', name: 'Bordetella', daysAgoOffset: 45, cost: 2000, notes: 'Kennel cough prevention' }, + { type: 'microchip', name: 'Microchip Implant', daysAgoOffset: 60, cost: 4500 }, + { type: 'checkup', name: 'Intake Wellness Exam', daysAgoOffset: 65, cost: 5500 }, + { type: 'flea_tick', name: 'Flea & Tick Prevention (NexGard)', daysAgoOffset: 14, cost: 1800 }, + { type: 'heartworm_test', name: 'Heartworm Test', daysAgoOffset: 65, cost: 2800 } + ]; + + const catMedRecords: MedRecord[] = [ + { type: 'vaccine', name: 'Rabies Vaccine', daysAgoOffset: 30, cost: 2500 }, + { type: 'vaccine', name: 'FVRCP Vaccine', daysAgoOffset: 30, cost: 3000, notes: 'Feline Viral Rhinotracheitis, Calicivirus, Panleukopenia' }, + { type: 'microchip', name: 'Microchip Implant', daysAgoOffset: 60, cost: 4500 }, + { type: 'checkup', name: 'Intake Wellness Exam', daysAgoOffset: 65, cost: 5500 }, + { type: 'flea_tick', name: 'Flea Prevention (Revolution)', daysAgoOffset: 14, cost: 1500 } + ]; + + let medCount = 0; + for (let i = 0; i < Math.min(20, petIds.length); i++) { + const iscat = petSeeds[i].species === 'cat'; + const records = iscat ? catMedRecords : commonMedRecords; + for (const rec of records) { + await db.insert(schema.petMedical).values({ + petId: petIds[i], + type: rec.type, + name: rec.name, + date: daysAgo(rec.daysAgoOffset + randomBetween(0, 5)), + vetId: pick(vetIds), + cost: rec.cost, + notes: rec.notes + }); + medCount++; + } + // Spay/neuter for appropriate pets + if (petSeeds[i].spayedNeutered) { + await db.insert(schema.petMedical).values({ + petId: petIds[i], + type: 'spay_neuter', + name: petSeeds[i].sex === 'female' ? 'Spay Surgery' : 'Neuter Surgery', + date: daysAgo(randomBetween(60, 180)), + vetId: pick(vetIds), + cost: petSeeds[i].weight?.includes('lbs') && parseInt(petSeeds[i].weight) > 30 ? 35000 : 22000, + notes: 'Procedure completed without complications.' + }); + medCount++; + } + } + console.log(` āœ“ ${medCount} medical records`); + + // ── 8. Foster Assignments ────────────────────────────────────────────────── + console.log('Creating foster assignments...'); + // Pets with 'foster' status: indices in petSeeds + const fosterPetIndices = petSeeds + .map((p, i) => ({ p, i })) + .filter(({ p }) => p.status === 'foster') + .map(({ i }) => i); + + // 6 active assignments + for (let k = 0; k < Math.min(6, fosterPetIndices.length); k++) { + const petIdx = fosterPetIndices[k]; + await db.insert(schema.fosterAssignments).values({ + petId: petIds[petIdx], + userId: fosterIds[k], + startDate: daysAgo(petSeeds[petIdx].intakeDaysAgo - 3), + isCurrent: true, + notes: `Placed with foster on intake day +3. Going well.` + }); + } + + // 8 past assignments for adopted dogs + const adoptedDogIndices = petSeeds + .map((p, i) => ({ p, i })) + .filter(({ p }) => p.status === 'adopted' && p.species === 'dog') + .map(({ i }) => i); + + for (let k = 0; k < Math.min(8, adoptedDogIndices.length); k++) { + const petIdx = adoptedDogIndices[k]; + const endDate = petSeeds[petIdx].adoptedDaysAgo ? daysAgo(petSeeds[petIdx].adoptedDaysAgo! - 2) : daysAgo(50); + await db.insert(schema.fosterAssignments).values({ + petId: petIds[petIdx], + userId: fosterIds[k % fosterIds.length], + startDate: daysAgo(petSeeds[petIdx].intakeDaysAgo - 5), + endDate, + isCurrent: false, + notes: `Successfully fostered until adoption.` + }); + } + console.log(' āœ“ Foster assignments'); + + // ── 9. Applications ──────────────────────────────────────────────────────── + console.log('Creating applications...'); + + const adoptionFormData = (petName: string) => ({ + address: '1234 Bluebonnet Trail, Austin TX 78748', + hasYard: true, + yardFenced: true, + ownOrRent: 'own', + landlordAllows: null, + householdSize: 2, + hasChildren: false, + childrenAges: [], + currentPets: 'one cat, 8 years old', + petHistory: 'Previous dog owner for 10 years', + vetReference: 'Austin Animal Clinic, (512) 555-0201', + whyThisPet: `We fell in love with ${petName} immediately. We have a big yard and work from home so ${petName} will never be alone.`, + hoursAlonePerDay: 1, + exercisePlan: 'Morning walks and evening trips to Zilker dog park', + adoptionAgreement: true + }); + + const fosterFormData = (petName: string) => ({ + address: '5678 Mesquite Lane, Austin TX 78702', + hasYard: true, + yardFenced: true, + ownOrRent: 'rent', + landlordAllows: true, + currentPets: 'none', + petHistory: '5 years fostering experience, 12 fosters total', + vetReference: 'Hill Country Vet, (512) 555-0428', + whyFoster: `I want to help ${petName} find a great home and I have availability right now.`, + hoursAvailable: 20, + canTransport: true, + canAdminister: true, + fosterAgreement: true + }); + + const volunteerFormData = () => ({ + availability: ['Saturdays', 'Sunday afternoons'], + skills: ['dog walking', 'transport', 'social media'], + experience: 'Volunteer walker at Austin Humane Society for 2 years', + canDrive: true, + hasCar: true, + motivation: 'I want to give back to the community and help animals in need.', + volunteerAgreement: true + }); + + const surrenderFormData = (petName: string, breed: string) => ({ + petName, + species: 'dog', + breed, + age: '4 years', + sex: 'male', + reasonForSurrender: 'Allergies developed — my daughter has become allergic and we are heartbroken.', + vaccinated: true, + spayedNeutered: true, + vetName: 'Lone Star Veterinary', + behavioralIssues: 'none', + currentMedications: 'none', + surrenderAgreement: true + }); + + const applicationData = [ + // 5 pending + { type: 'adoption' as const, status: 'pending' as const, name: 'Robert Castillo', email: 'rcastillo@gmail.com', phone: '(512) 555-1001', petIdx: 0, formData: adoptionFormData('Bella') }, + { type: 'adoption' as const, status: 'pending' as const, name: 'Diana Nguyen', email: 'dnguyen@yahoo.com', phone: '(512) 555-1002', petIdx: 1, formData: adoptionFormData('Ranger') }, + { type: 'foster' as const, status: 'pending' as const, name: 'Marcus Webb', email: 'mwebb@outlook.com', phone: '(512) 555-1003', petIdx: null, formData: fosterFormData('any available dog') }, + { type: 'volunteer' as const, status: 'pending' as const, name: 'Stephanie Ross', email: 'sross@gmail.com', phone: '(512) 555-1004', petIdx: null, formData: volunteerFormData() }, + { type: 'adoption' as const, status: 'pending' as const, name: 'David Hernandez', email: 'dhernandez@gmail.com', phone: '(512) 555-1005', petIdx: 20, formData: adoptionFormData('Whiskers') }, + // 3 reviewing + { type: 'adoption' as const, status: 'reviewing' as const, name: 'Laura Kim', email: 'lkim@gmail.com', phone: '(512) 555-1006', petIdx: 2, formData: adoptionFormData('Daisy') }, + { type: 'adoption' as const, status: 'reviewing' as const, name: 'Paul Washington', email: 'pwashington@email.com', phone: '(512) 555-1007', petIdx: 3, formData: adoptionFormData('Duke') }, + { type: 'foster' as const, status: 'reviewing' as const, name: 'Nina Patel', email: 'npatel@gmail.com', phone: '(512) 555-1008', petIdx: null, formData: fosterFormData('small dog') }, + // 6 approved + { type: 'adoption' as const, status: 'approved' as const, name: 'Jason Williams', email: 'jwilliams@gmail.com', phone: '(512) 555-1009', petIdx: 13, formData: adoptionFormData('Luna') }, + { type: 'adoption' as const, status: 'approved' as const, name: 'Henderson Family', email: 'henderson.family@outlook.com', phone: '(512) 555-1010', petIdx: 14, formData: adoptionFormData('Buddy') }, + { type: 'adoption' as const, status: 'approved' as const, name: 'Kevin Park', email: 'kpark@email.com', phone: '(512) 555-1011', petIdx: 15, formData: adoptionFormData('Stella') }, + { type: 'adoption' as const, status: 'approved' as const, name: 'Maria Ramirez', email: 'maria.ramirez@gmail.com', phone: '(512) 555-1012', petIdx: 16, formData: adoptionFormData('Chester') }, + { type: 'volunteer' as const, status: 'approved' as const, name: 'Thomas Green', email: 'tgreen@gmail.com', phone: '(512) 555-1013', petIdx: null, formData: volunteerFormData() }, + { type: 'foster' as const, status: 'approved' as const, name: 'Sarah Mitchell', email: 'sarah.mitchell@gmail.com', phone: '(512) 555-2001', petIdx: null, formData: fosterFormData('any dog') }, + // 4 denied + { type: 'adoption' as const, status: 'denied' as const, name: 'Anonymous Applicant', email: 'denied1@email.com', phone: '(512) 555-1014', petIdx: 0, formData: { ...adoptionFormData('Bella'), hoursAlonePerDay: 12, hasYard: false } }, + { type: 'adoption' as const, status: 'denied' as const, name: 'Gary Peterson', email: 'gpeterson@email.com', phone: '(512) 555-1015', petIdx: 6, formData: { ...adoptionFormData('Max'), landlordAllows: false, ownOrRent: 'rent' } }, + { type: 'surrender' as const, status: 'denied' as const, name: 'Bill Thompson', email: 'bthompson@email.com', phone: '(512) 555-1016', petIdx: null, formData: surrenderFormData('Rocky', 'German Shepherd') }, + { type: 'foster' as const, status: 'denied' as const, name: 'Chris Evans', email: 'cevans@email.com', phone: '(512) 555-1017', petIdx: null, formData: { ...fosterFormData('any'), landlordAllows: false } } + ]; + + for (const app of applicationData) { + const petId = app.petIdx !== null ? petIds[app.petIdx] : undefined; + await db.insert(schema.applications).values({ + type: app.type, + status: app.status, + applicantName: app.name, + applicantEmail: app.email, + applicantPhone: app.phone, + petId, + formData: app.formData, + reviewedBy: ['approved', 'denied', 'reviewing'].includes(app.status) ? adminId : undefined, + reviewedAt: ['approved', 'denied'].includes(app.status) ? daysAgo(randomBetween(5, 30)) : undefined, + reviewNotes: app.status === 'denied' ? 'Application did not meet our requirements.' : app.status === 'approved' ? 'Excellent application. Background check passed.' : undefined, + createdAt: daysAgo(randomBetween(1, 90)) + }); + } + console.log(` āœ“ ${applicationData.length} applications`); + + // ── 10. Donations ────────────────────────────────────────────────────────── + console.log('Creating donations...'); + + const donorNames = [ + 'Patricia Moore', 'James Clark', 'Linda Johnson', 'Michael Davis', 'Susan Wilson', + 'Robert Anderson', 'Barbara Taylor', 'William Martinez', 'Elizabeth Thomas', 'Richard Jackson', + 'Jennifer White', 'Charles Harris', 'Mary Thompson', 'Joseph Garcia', 'Dorothy Brown', + 'Daniel Martinez', 'Betty Gonzalez', 'Steven Lewis', 'Nancy Walker', 'Edward Hall' + ]; + + const donationSources: Array<'stripe' | 'paypal' | 'cash' | 'check' | 'venmo'> = ['stripe', 'paypal', 'cash', 'check', 'venmo']; + const purposes = ['General Support', 'Medical Fund', 'Spay/Neuter Program', 'Food & Supplies', 'Event Sponsorship', null]; + + for (let i = 0; i < 35; i++) { + const donorName = donorNames[i % donorNames.length]; + const amount = pick([1000, 1500, 2000, 2500, 3000, 5000, 7500, 10000, 15000, 20000, 25000, 50000]); + const source = pick(donationSources); + const petId = Math.random() > 0.6 ? petIds[randomBetween(0, petIds.length - 1)] : undefined; + const purpose = pick(purposes) as string | null; + + await db.insert(schema.donations).values({ + donorName, + donorEmail: `${donorName.toLowerCase().replace(' ', '.')}@example.com`, + amount, + source, + petId: petId ?? undefined, + isRecurring: Math.random() > 0.8, + purpose: purpose ?? undefined, + date: daysAgo(randomBetween(0, 180)), + notes: source === 'check' ? `Check #${1000 + i}` : undefined + }); + } + console.log(' āœ“ 35 donations'); + + // ── 11. Expenses ─────────────────────────────────────────────────────────── + console.log('Creating expenses...'); + + const expenseTypes = [ + { description: 'Emergency vet visit', category: 'Veterinary', costRange: [8000, 25000] as [number, number] }, + { description: 'Spay/neuter surgery', category: 'Veterinary', costRange: [20000, 40000] as [number, number] }, + { description: 'Dental cleaning', category: 'Veterinary', costRange: [15000, 30000] as [number, number] }, + { description: 'Vaccines & wellness exam', category: 'Veterinary', costRange: [8000, 15000] as [number, number] }, + { description: 'Prescription medication', category: 'Medical Supplies', costRange: [2000, 8000] as [number, number] }, + { description: 'Dog food (monthly supply)', category: 'Food', costRange: [5000, 12000] as [number, number] }, + { description: 'Cat food (monthly supply)', category: 'Food', costRange: [3000, 7000] as [number, number] }, + { description: 'Crates and kennels', category: 'Equipment', costRange: [4000, 15000] as [number, number] }, + { description: 'Foster supply kit', category: 'Supplies', costRange: [2500, 6000] as [number, number] }, + { description: 'Microchipping event supplies', category: 'Medical Supplies', costRange: [5000, 10000] as [number, number] }, + { description: 'Transport fuel reimbursement', category: 'Transport', costRange: [1500, 4000] as [number, number] }, + { description: 'Event booth materials', category: 'Events', costRange: [3000, 8000] as [number, number] }, + { description: 'Orthopedic dog beds', category: 'Supplies', costRange: [2000, 6000] as [number, number] }, + { description: 'Parasite prevention medications', category: 'Medical Supplies', costRange: [4000, 10000] as [number, number] }, + { description: 'Boarding during transport', category: 'Boarding', costRange: [8000, 20000] as [number, number] } + ]; + + for (let i = 0; i < 15; i++) { + const exp = expenseTypes[i]; + const cost = randomBetween(exp.costRange[0], exp.costRange[1]); + const petId = Math.random() > 0.4 ? petIds[randomBetween(0, petIds.length - 1)] : undefined; + const vetId = exp.category === 'Veterinary' || exp.category === 'Medical Supplies' ? pick(vetIds) : undefined; + + await db.insert(schema.expenses).values({ + description: exp.description, + amount: cost, + category: exp.category, + petId: petId ?? undefined, + vetId: vetId ?? undefined, + date: daysAgo(randomBetween(0, 180)), + notes: `Paid via ${pick(['credit card', 'check', 'ACH transfer'])}` + }); + } + console.log(' āœ“ 15 expenses'); + + // ── 12. Sponsors ─────────────────────────────────────────────────────────── + console.log('Creating sponsors...'); + + await db.insert(schema.sponsors).values([ + { + name: 'Austin Pet Supply Co', + tier: 'top_dog', + websiteUrl: 'https://austinpetsupply.com', + description: + 'Austin Pet Supply Co has been a proud partner of Sunny Paws Rescue since 2021, donating over $15,000 in supplies annually.', + active: true, + sortOrder: 1 + }, + { + name: 'Bark & Beyond', + tier: 'wagging_tails', + websiteUrl: 'https://barkandbeyondaustin.com', + description: 'A local pet boutique dedicated to premium pet wellness and nutrition.', + active: true, + sortOrder: 2 + }, + { + name: 'Paws & Claws Grooming', + tier: 'wet_noses', + websiteUrl: 'https://pawsandclawsgrooming.com', + description: 'Providing complimentary grooming sessions for our rescue dogs before adoption events.', + active: true, + sortOrder: 3 + } + ]); + console.log(' āœ“ 3 sponsors'); + + // ── 13. Events ───────────────────────────────────────────────────────────── + console.log('Creating events...'); + + await db.insert(schema.events).values([ + { + title: 'Adoption Day at Zilker Park', + description: + 'Join us for our biggest adoption event of the year at beautiful Zilker Park! We\'ll have 20+ dogs and cats looking for their forever homes, plus food trucks, live music, and activities for the whole family. Bring your leash and an open heart.', + location: 'Zilker Park Picnic Area, 2100 Barton Springs Rd, Austin TX 78746', + startDate: daysFromNow(14), + endDate: daysFromNow(14), + published: true + }, + { + title: 'Puppy Yoga with Sunny Paws', + description: + 'Experience the joy of yoga while adorable puppies roam freely and shower you with kisses. All proceeds benefit our medical fund. Spots are extremely limited — reserve yours today!', + location: 'Black Swan Yoga, 4800 Burnet Rd, Austin TX 78756', + startDate: daysFromNow(21), + endDate: daysFromNow(21), + published: true + }, + { + title: 'Foster Orientation — August Session', + description: + 'Thinking about fostering but not sure where to start? Our friendly orientation will walk you through everything: what to expect, how to introduce a foster pet to your home, and the support we provide. Light refreshments will be served.', + location: 'Sunny Paws Rescue HQ, 3001 South First St, Austin TX 78704', + startDate: daysFromNow(7), + endDate: daysFromNow(7), + published: true + }, + { + title: 'Holiday Paws Fundraiser', + description: + 'Our annual holiday fundraiser raised over $12,000 to support our medical fund! Thank you to everyone who attended and donated. We are so grateful for this community.', + location: 'The Domain Whole Foods Market, 11920 Domain Dr, Austin TX 78758', + startDate: daysAgo(120), + endDate: daysAgo(120), + published: true + }, + { + title: 'Spring Fling Adoption Event', + description: + 'Our Spring Fling was a huge success — 14 animals found their forever homes in one day! A special thank you to Austin Pet Supply Co for sponsoring the event and to our amazing volunteers.', + location: 'Mueller Farmer\'s Market, 4209 Airport Blvd, Austin TX 78722', + startDate: daysAgo(75), + endDate: daysAgo(75), + published: true + } + ]); + console.log(' āœ“ 5 events'); + + // ── 14. Happy Tails ──────────────────────────────────────────────────────── + console.log('Creating happy tails...'); + + // Adopted pet indices + const lunaIdx = petSeeds.findIndex(p => p.name === 'Luna' && p.species === 'dog'); + const buddyIdx = petSeeds.findIndex(p => p.name === 'Buddy'); + const stellaIdx = petSeeds.findIndex(p => p.name === 'Stella'); + const chesterIdx = petSeeds.findIndex(p => p.name === 'Chester'); + const mittensIdx = petSeeds.findIndex(p => p.name === 'Mittens'); + const tigerIdx = petSeeds.findIndex(p => p.name === 'Tiger'); + + const happyTailsData = [ + { + petId: petIds[lunaIdx], + title: 'Luna is Living Her Best Life in Austin', + story: `We adopted Luna six months ago and she has completely transformed our lives. We'd been hesitant to adopt a Husky because of the reputation for being "difficult," but Luna has been nothing but an absolute joy. She goes on runs with my husband every morning, comes to the office on Thursdays (yes, our office allows dogs!), and has become best friends with our neighbor's Labrador. We started her Instagram account as a joke and she now has 2,400 followers. The team at Sunny Paws prepared us so well — the transition was seamless. Thank you from the bottom of our hearts.`, + imageUrl: '/uploads/demo/happy-tails/luna-williams.jpg', + adopterName: 'Jason & Kelsey Williams', + featured: true, + published: true, + submittedBy: 'Jason Williams', + submittedEmail: 'jwilliams@gmail.com' + }, + { + petId: petIds[buddyIdx], + title: "Buddy: The Dog Three Kids Have Been Dreaming Of", + story: `We have three kids aged 5, 8, and 11, and finding a dog that could handle the chaos of our household felt impossible. Then we met Buddy at a Sunny Paws event and within five minutes we knew he was ours. He's been with us for four months now and is the most patient, gentle, and loving dog imaginable. He lets our 5-year-old use him as a pillow during movie nights. He's become the center of our family in the best possible way.`, + imageUrl: '/uploads/demo/happy-tails/buddy-henderson.jpg', + adopterName: 'The Henderson Family', + featured: true, + published: true, + submittedBy: 'Anne Henderson', + submittedEmail: 'henderson.family@outlook.com' + }, + { + petId: petIds[stellaIdx], + title: 'Stella: Chief Morale Officer, Dog Division', + story: `I work from home as a software engineer and got Stella hoping she'd be a good companion. What I didn't expect was for her to become the most productive member of the team. She has an uncanny ability to sense when I'm in a meeting versus free, and she brings me her toy exactly at 5pm to signal that work time is over. I've had her three months and I genuinely can't remember life without her.`, + imageUrl: '/uploads/demo/happy-tails/stella-park.jpg', + adopterName: 'Kevin Park', + featured: false, + published: true, + submittedBy: 'Kevin Park', + submittedEmail: 'kpark@email.com' + }, + { + petId: petIds[chesterIdx], + title: 'Chester Stole Our Hearts (and the Sofa)', + story: `After 30 years of teaching, I retired last spring and decided it was finally time for a dog. Chester came into our lives and immediately knew exactly what his job was: to be the most loved dog in Central Texas. He has claimed the left half of the sofa as his, and honestly, I don't want it back. He's my walking buddy, my TV companion, and my excuse to talk to every neighbor on the block.`, + imageUrl: '/uploads/demo/happy-tails/chester-ramirez.jpg', + adopterName: 'Maria Ramirez', + featured: false, + published: true, + submittedBy: 'Maria Ramirez', + submittedEmail: 'maria.ramirez@gmail.com' + }, + { + petId: petIds[mittensIdx], + title: 'Mittens, the Perfect Retirement Companion', + story: `I adopted Mittens two months after retiring and she has been the perfect antidote to the quiet of an empty house. She sits with me over morning coffee, supervises my crossword puzzles with great authority, and purrs me to sleep every night. She is gentle, affectionate, and wonderfully undemanding. I wish I had adopted a cat years ago.`, + imageUrl: '/uploads/demo/happy-tails/mittens-marsh.jpg', + adopterName: 'Evelyn Marsh', + featured: false, + published: true, + submittedBy: 'Evelyn Marsh', + submittedEmail: 'emarsh@gmail.com' + }, + { + petId: petIds[tigerIdx], + title: "Tiger Helped Heal a Broken Heart", + story: `We lost our cat of 17 years in February and the house felt unbearably empty. A friend suggested looking at Sunny Paws, and Tiger's photo jumped off the screen. He's been with us for three months and he has filled our home with so much life and laughter. He's nothing like our previous cat, which is exactly what we needed. He's bold, opinionated, and utterly ridiculous. We love him fiercely.`, + imageUrl: '/uploads/demo/happy-tails/tiger-patterson.jpg', + adopterName: 'The Patterson Family', + featured: true, + published: true, + submittedBy: 'Greg Patterson', + submittedEmail: 'patterrons@gmail.com' + } + ]; + + for (const ht of happyTailsData) { + await db.insert(schema.happyTails).values(ht); + } + console.log(' āœ“ 6 happy tails'); + + // ── 15. Content Blocks ───────────────────────────────────────────────────── + console.log('Creating content blocks...'); + + const contentBlocksData = [ + { + page: 'home', + sectionKey: 'hero', + content: { + title: 'Every Paw Deserves a Second Chance', + subtitle: + "Sunny Paws Rescue has been finding loving homes for dogs and cats in Austin, TX since 2018. Browse our available pets and open your heart — your perfect companion might be waiting right now.", + ctaText: 'Meet Our Pets', + ctaUrl: '/pets' + } + }, + { + page: 'home', + sectionKey: 'mission', + content: { + title: 'Our Mission', + text: "Sunny Paws Rescue is a volunteer-driven, foster-based nonprofit dedicated to rescuing animals from high-kill shelters and unsafe situations across Central Texas. We believe every animal deserves medical care, safety, love, and a forever home. Since 2018, we've placed over 800 animals into loving families.", + stat1: { number: '800+', label: 'Animals Adopted' }, + stat2: { number: '120', label: 'Active Fosters' }, + stat3: { number: '6', label: 'Years Serving Austin' } + } + }, + { + page: 'about', + sectionKey: 'intro', + content: { + title: 'About Sunny Paws Rescue', + text: "Founded in 2018 by a group of Austin animal lovers, Sunny Paws Rescue began as a small operation fostering dogs from the Travis County Animal Shelter. Today, we're one of Central Texas's most trusted foster-based rescues, with a network of over 120 active foster homes, four partner veterinary clinics, and a passionate volunteer base of more than 200 people.", + imageUrl: '/uploads/demo/about/team.jpg' + } + }, + { + page: 'about', + sectionKey: 'values', + content: { + title: 'Our Values', + values: [ + { title: 'Compassion First', text: 'Every decision we make starts with the well-being of the animal.' }, + { title: 'Transparency', text: "We publish our financials annually and are open about the challenges of rescue work." }, + { title: 'Community', text: 'We couldn\'t do this work without Austin. We give back by hosting free microchipping events, low-cost spay/neuter clinics, and community education programs.' } + ] + } + }, + { + page: 'adoption-process', + sectionKey: 'intro', + content: { + title: 'How to Adopt from Sunny Paws', + text: "Adopting a pet is one of the most rewarding decisions you'll ever make. Our process is designed to make sure every animal goes to a home where they'll thrive — and that every adopter is set up for success.", + steps: [ + { step: 1, title: 'Browse Available Pets', text: 'Explore our available dogs and cats. Each profile includes photos, personality notes, and compatibility information.' }, + { step: 2, title: 'Submit an Application', text: "Fill out our adoption application — it takes about 10 minutes. We ask about your lifestyle, living situation, and what you're looking for in a pet." }, + { step: 3, title: 'Meet & Greet', text: "We'll arrange a meeting with the pet (and any current pets you have) to make sure it's a great fit." }, + { step: 4, title: 'Finalize the Adoption', text: "Sign the adoption agreement, pay the adoption fee, and take your new family member home!" } + ] + } + }, + { + page: 'become-a-foster', + sectionKey: 'intro', + content: { + title: 'Foster a Pet — Save a Life', + text: "Fostering is the lifeblood of Sunny Paws Rescue. Without foster homes, we simply cannot save more animals. When you foster, you provide a safe, loving temporary home for an animal in need while they wait for their forever family. We cover all food, supplies, and veterinary expenses — you just provide the love.", + benefits: [ + { title: 'We Cover All Costs', text: 'Food, supplies, vet bills — we cover everything. You just open your heart and home.' }, + { title: 'Choose Your Level', text: 'Foster a dog, a cat, a kitten, or a special needs animal — we match you with pets that fit your lifestyle.' }, + { title: '24/7 Support', text: 'Our foster coordinator is always a text away. You are never alone in this.' } + ] + } + }, + { + page: 'how-to-help', + sectionKey: 'intro', + content: { + title: 'There Are So Many Ways to Help', + text: "Whether you can donate your time, money, or supplies — every contribution directly saves lives. Here's how you can make a difference for the animals of Sunny Paws Rescue.", + ways: [ + { title: 'Foster', text: 'Open your home to an animal in need.' }, + { title: 'Donate', text: 'Financial contributions fund vet care, food, and operations.' }, + { title: 'Volunteer', text: 'Help at events, drive animals to vet appointments, or help with social media.' }, + { title: 'Adopt', text: "Give an animal their forever home." }, + { title: 'Share', text: 'Follow us on social media and share our pets with your network.' } + ] + } + }, + { + page: 'volunteer', + sectionKey: 'intro', + content: { + title: 'Volunteer with Sunny Paws', + text: "Our volunteers are the backbone of everything we do. From dog walkers and transport drivers to photographers and event helpers, there's a role for everyone. We ask for a minimum commitment of 4 hours per month.", + roles: [ + { title: 'Dog Walker', description: 'Walk fosters and shelter dogs to give them enrichment and socialization.' }, + { title: 'Transport Driver', description: 'Drive animals to and from vet appointments, foster homes, and adoption events.' }, + { title: 'Event Helper', description: 'Assist at adoption events, fundraisers, and community outreach activities.' }, + { title: 'Photographer', description: 'Take professional-quality photos of our animals to help them get adopted faster.' }, + { title: 'Social Media Manager', description: 'Help us tell our animals\' stories and grow our community online.' } + ] + } + }, + { + page: 'why-adopt', + sectionKey: 'intro', + content: { + title: 'Why Adopt Instead of Shop?', + text: "When you adopt from a rescue, you're not just getting a pet — you're saving a life. The dog or cat you bring home has often been through a difficult journey, and choosing them means the world. Rescue animals are often already housetrained, vaccinated, and spayed or neutered, making the transition to your home easier.", + points: [ + 'You save a life and make room for another animal in need.', + 'Rescue pets are often already vaccinated, spayed/neutered, and microchipped.', + 'Adult animals let you know exactly what you\'re getting — personality and energy level included.', + 'You support a local organization that gives back to the Austin community.', + "The bond with a rescue animal is unlike anything else." + ] + } + } + ]; + + for (const block of contentBlocksData) { + await db.insert(schema.contentBlocks).values(block); + } + console.log(` āœ“ ${contentBlocksData.length} content blocks`); + + // ── 16. Newsletter Subscribers ───────────────────────────────────────────── + console.log('Creating newsletter subscribers...'); + + const subscriberEmails = [ + 'ann.torres@gmail.com', 'brian.kelly@yahoo.com', 'carol.smith@outlook.com', + 'david.nguyen@gmail.com', 'emily.watson@gmail.com', 'frank.martin@hotmail.com', + 'grace.lee@gmail.com', 'henry.brown@yahoo.com', 'isabella.jones@gmail.com', + 'jack.wilson@outlook.com', 'karen.anderson@gmail.com', 'larry.johnson@gmail.com', + 'megan.taylor@yahoo.com', 'noah.miller@gmail.com', 'olivia.davis@outlook.com', + 'peter.garcia@gmail.com', 'quinn.martinez@gmail.com', 'rachel.robinson@yahoo.com', + 'sam.clark@gmail.com', 'tiffany.lewis@gmail.com' + ]; + + const subscriberNames = [ + 'Ann Torres', 'Brian Kelly', 'Carol Smith', 'David Nguyen', 'Emily Watson', + 'Frank Martin', 'Grace Lee', 'Henry Brown', 'Isabella Jones', 'Jack Wilson', + 'Karen Anderson', 'Larry Johnson', 'Megan Taylor', 'Noah Miller', 'Olivia Davis', + 'Peter Garcia', 'Quinn Martinez', 'Rachel Robinson', 'Sam Clark', 'Tiffany Lewis' + ]; + + for (let i = 0; i < subscriberEmails.length; i++) { + await db.insert(schema.newsletterSubscribers).values({ + email: subscriberEmails[i], + name: subscriberNames[i], + subscribedAt: daysAgo(randomBetween(1, 180)) + }); + } + console.log(' āœ“ 20 newsletter subscribers'); + + // ── 17. Contact Messages ─────────────────────────────────────────────────── + console.log('Creating contact messages...'); + + await db.insert(schema.contactMessages).values([ + { + name: 'Susan Garrett', + email: 'sgarrett@gmail.com', + subject: 'Question about adopting Bella', + message: "Hi! I submitted an adoption application for Bella last week and just wanted to check in on the timeline. We are so excited about her and have been preparing our yard. Please let me know if you need any additional information from us. Thank you for the wonderful work you do!", + read: false, + createdAt: daysAgo(2) + }, + { + name: 'Tom Bradley', + email: 'tombradley@outlook.com', + subject: 'Found a stray dog near South Congress', + message: "I found a medium-sized brown dog wandering near 1200 South Congress Ave this morning. She has no collar but seems friendly and well-fed. I've taken her inside temporarily — can someone help me figure out what to do? I can't keep her permanently but don't want to take her to the pound.", + read: false, + createdAt: daysAgo(1) + }, + { + name: 'Linda Cho', + email: 'lindacho@yahoo.com', + subject: 'Interested in volunteering as a photographer', + message: "I'm a professional photographer and have been following Sunny Paws on Instagram for years. I'd love to volunteer my skills to help take adoption photos. I have my own studio and could do individual shoots for your harder-to-adopt animals. Please let me know if this would be helpful!", + read: true, + createdAt: daysAgo(5) + }, + { + name: 'Carlos Mendez', + email: 'cmendez@gmail.com', + subject: 'Donation receipt request', + message: "Hello, I made a $250 cash donation at your Spring Fling event and was told I'd receive a receipt by email for tax purposes. I haven't received it yet. My name is Carlos Mendez. Could you please send the receipt to this email address? Thank you.", + read: true, + createdAt: daysAgo(10) + }, + { + name: 'Jessica Park', + email: 'jpark@gmail.com', + subject: 'Happy update on our adopted dog Nala!', + message: "We adopted Nala 2 months ago and just wanted to share an update — she's AMAZING. She passed her beginner obedience class last weekend and just learned to swim in Lake Travis. We are completely obsessed with her. Thank you for making us a family of three. Attaching a photo!", + read: true, + createdAt: daysAgo(15) + } + ]); + console.log(' āœ“ 5 contact messages'); + + // ── 18. Foster Submissions ───────────────────────────────────────────────── + console.log('Creating foster submissions...'); + + const fosterPetIdsForSubmissions = fosterPetIndices.slice(0, 6).map(i => petIds[i]); + + const submissionContents = [ + "Milo had a great week! He's been eating well after transitioning to his soft diet and his energy levels are improving. He went on three short walks and loved smelling every flower in the garden.", + "Atlas escaped the backyard twice this week — we've added a coyote roller to the fence and it seems to be working. Otherwise he's a joy. Slept through the night for the first time last night!", + "Max had his vet appointment on Tuesday for his annual checkup. Dr. Nguyen says he looks great. His weight is perfect and she cleared him for adoption. He's been SUCH a good boy.", + "Penny is warming up more every day. She cuddled with me on the couch last night for the first time! Still a little nervous around strangers but she's really blossoming.", + "Bruno went on a 3-mile hike at Barton Creek Greenbelt today and was absolutely in his element. He's much calmer in the evenings after big exercise. Starting to think he needs a running partner home.", + "Hazel learned 'sit' and 'shake' this week! She's so smart. She's also decided she belongs on the bed, which was not part of the plan, but here we are. She's 100% a lap dog.", + "Quick note: Shadow had a small UTI and we took him to Austin Animal Clinic. Dr. Nguyen prescribed antibiotics for 10 days. He's already feeling better and is very affectionate right now.", + "Pearl found a sunny spot on the windowsill and has barely moved from it in three days. She's happy, healthy, and very regal. Getting along fine with our senior cat now.", + "Update for Milo: dental wound sites are fully healed! He's been eating dry food with no issues as of this week. Very happy boy.", + "Atlas update: he met our neighbor's kids (ages 6 and 9) today and was fantastic. Gentle, curious, and let them pet him without any jumping. I think he's going to be great with the right family." + ]; + + for (let i = 0; i < 10; i++) { + const petId = fosterPetIdsForSubmissions[i % fosterPetIdsForSubmissions.length]; + const userId = fosterIds[i % fosterIds.length]; + const status = i < 7 ? 'approved' as const : 'pending' as const; + + await db.insert(schema.fosterSubmissions).values({ + userId, + petId, + type: 'update', + content: submissionContents[i], + status, + reviewedBy: status === 'approved' ? adminId : undefined, + reviewedAt: status === 'approved' ? daysAgo(randomBetween(1, 14)) : undefined, + createdAt: daysAgo(randomBetween(1, 45)) + }); + } + console.log(' āœ“ 10 foster submissions'); + + // ── 19. Supply Requests ──────────────────────────────────────────────────── + console.log('Creating supply requests...'); + + const supplyRequestData = [ + { items: 'Large crate (36"), 2 Kongs, 1 bag Blue Buffalo large breed food', urgency: 'high' as const, status: 'pending' as const, notes: 'Atlas has outgrown his current crate.' }, + { items: 'Soft food (Purina Pro Plan EN), pill pockets, enzymatic toothbrush kit', urgency: 'medium' as const, status: 'approved' as const, notes: 'For Milo\'s post-dental recovery.' }, + { items: 'Baby gate, slip lead, clicker training treats', urgency: 'low' as const, status: 'fulfilled' as const, notes: null }, + { items: 'Flea/tick prevention for 3 cats (Revolution), nail clippers', urgency: 'medium' as const, status: 'approved' as const, notes: null }, + { items: '1 orthopedic dog bed (XL), 2 fleece blankets, enzyme cleaner spray', urgency: 'low' as const, status: 'denied' as const, notes: 'We have orthopedic beds in stock, please pick up at HQ.' } + ]; + + for (let i = 0; i < supplyRequestData.length; i++) { + const req = supplyRequestData[i]; + const petId = fosterPetIdsForSubmissions[i % fosterPetIdsForSubmissions.length]; + await db.insert(schema.supplyRequests).values({ + userId: fosterIds[i % fosterIds.length], + petId, + items: req.items, + urgency: req.urgency, + status: req.status, + notes: req.notes ?? undefined, + reviewedBy: req.status !== 'pending' ? adminId : undefined, + reviewedAt: req.status !== 'pending' ? daysAgo(randomBetween(1, 10)) : undefined, + createdAt: daysAgo(randomBetween(1, 30)) + }); + } + console.log(' āœ“ 5 supply requests'); + + // ── 20. Audit Log ────────────────────────────────────────────────────────── + console.log('Creating audit log entries...'); + + const auditActions = [ + { action: 'pet.create', entity: 'pet', entityId: petIds[0], diff: { name: 'Bella', status: 'available' } }, + { action: 'pet.update', entity: 'pet', entityId: petIds[6], diff: { status: { from: 'available', to: 'foster' } } }, + { action: 'application.approve', entity: 'application', entityId: 9, diff: { status: { from: 'reviewing', to: 'approved' } } }, + { action: 'donation.create', entity: 'donation', entityId: 1, diff: { amount: 5000, source: 'stripe' } }, + { action: 'user.login', entity: 'user', entityId: adminId, diff: null }, + { action: 'pet.adopt', entity: 'pet', entityId: petIds[13], diff: { status: { from: 'foster', to: 'adopted' }, adopterName: 'Jason Williams' } }, + { action: 'event.create', entity: 'event', entityId: 1, diff: { title: 'Adoption Day at Zilker Park' } }, + { action: 'content.update', entity: 'content_block', entityId: 1, diff: { page: 'home', sectionKey: 'hero' } }, + { action: 'user.create', entity: 'user', entityId: fosterIds[0], diff: { role: 'foster', email: 'foster@fosterflow.app' } }, + { action: 'foster.assign', entity: 'foster_assignment', entityId: 1, diff: { petId: petIds[6], userId: fosterIds[0] } } + ]; + + for (const entry of auditActions) { + await db.insert(schema.auditLog).values({ + userId: adminId, + action: entry.action, + entity: entry.entity, + entityId: entry.entityId, + diff: entry.diff, + ipAddress: '127.0.0.1', + createdAt: daysAgo(randomBetween(0, 60)) + }); + } + console.log(' āœ“ 10 audit log entries'); + + // ── 21. Page Views ───────────────────────────────────────────────────────── + console.log('Creating page views...'); + + const commonPaths = [ + '/pets', '/pets?species=dog', '/pets?species=cat', '/adopt', '/foster', + '/about', '/how-to-help', '/volunteer', '/why-adopt', '/events', + '/happy-tails', '/donate', '/contact', '/become-a-foster', '/adoption-process' + ]; + + const petPaths = petSlugs.map(s => `/pets/${s}`); + const allPaths = [...commonPaths, ...petPaths]; + + const userAgents = [ + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/124.0 Safari/537.36' + ]; + + for (let i = 0; i < 200; i++) { + const path = pick(allPaths); + const petId = path.startsWith('/pets/') ? petIds[petSlugs.indexOf(path.replace('/pets/', ''))] : undefined; + + await db.insert(schema.pageViews).values({ + path, + petId: petId ?? undefined, + ipAddress: `192.168.${randomBetween(1, 255)}.${randomBetween(1, 255)}`, + userAgent: pick(userAgents), + referrer: pick(['https://google.com', 'https://instagram.com', 'https://facebook.com', null, null]) ?? undefined, + isBot: false, + createdAt: daysAgo(randomBetween(0, 30)) + }); + } + console.log(' āœ“ 200 page views'); + + console.log('\nāœ… Demo seeding complete!'); + console.log(' Org: Sunny Paws Rescue (demo.fosterflow.app)'); + console.log(' Admin login: demo@fosterflow.app / demodemo'); + console.log(' Foster login: foster@fosterflow.app / demodemo'); + console.log(` Pets: ${petSeeds.length} total (${petSeeds.filter(p => p.status === 'available').length} available, ${petSeeds.filter(p => p.status === 'foster').length} in foster, ${petSeeds.filter(p => p.status === 'adopted').length} adopted)`); + + await pool.end(); +} + +seedDemo().catch(console.error);