Import mockup

This commit is contained in:
kelinfoxy
2026-03-30 00:01:51 -04:00
parent 965b9171ed
commit bbe9a6b811
21 changed files with 2233 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
# GEMINI_API_KEY: Required for Gemini AI API calls.
# AI Studio automatically injects this at runtime from user secrets.
# Users configure this via the Secrets panel in the AI Studio UI.
GEMINI_API_KEY="MY_GEMINI_API_KEY"
# APP_URL: The URL where this applet is hosted.
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
# Used for self-referential links, OAuth callbacks, and API endpoints.
APP_URL="MY_APP_URL"

8
aistudio.google.project/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
node_modules/
build/
dist/
coverage/
.DS_Store
*.log
.env*
!.env.example

View File

@@ -0,0 +1,10 @@
This folder contains a project mockup created on aistudio.google.com
It was created before deciding to use HumHub instead of a completely custom solution.
It was designed by the UI/UX designer as a mockup for what the interface should look like, what info should be displayed and what the workflow would look & function.
It includes multiple signin portals, which is being consolendated into a single humhub account
It serves as reference for the desired apperance, style & workflow
It it not something we are developing further, but can pull from as needed.

View File

@@ -0,0 +1,20 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</div>
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
View your app in AI Studio: https://ai.studio/apps/d4aafa4b-ae59-48de-940c-56b6d37e5785
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Google AI Studio App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,5 @@
{
"name": "Rescue-to-Rehab Engine",
"description": "Matches high-needs rescue dogs with specialized rehabilitation centers using AI.",
"requestFramePermissions": []
}

View File

@@ -0,0 +1,34 @@
{
"name": "react-example",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port=3000 --host=0.0.0.0",
"build": "vite build",
"preview": "vite preview",
"clean": "rm -rf dist",
"lint": "tsc --noEmit"
},
"dependencies": {
"@google/genai": "^1.29.0",
"@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4",
"lucide-react": "^0.546.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"vite": "^6.2.0",
"express": "^4.21.2",
"dotenv": "^17.2.3",
"motion": "^12.23.24"
},
"devDependencies": {
"@types/node": "^22.14.0",
"autoprefixer": "^10.4.21",
"tailwindcss": "^4.1.14",
"tsx": "^4.21.0",
"typescript": "~5.8.2",
"vite": "^6.2.0",
"@types/express": "^4.17.21"
}
}

View File

@@ -0,0 +1,25 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useState } from 'react';
import LoginPortal, { UserRole } from './views/LoginPortal';
import RescueIntakeView from './views/RescueIntakeView';
import DonorView from './views/DonorView';
import CenterView from './views/CenterView';
import AdminView from './views/AdminView';
export default function App() {
const [role, setRole] = useState<UserRole>(null);
const handleLogout = () => setRole(null);
if (!role) return <LoginPortal onLogin={setRole} />;
if (role === 'uploader') return <RescueIntakeView onLogout={handleLogout} />;
if (role === 'donor') return <DonorView onLogout={handleLogout} />;
if (role === 'center') return <CenterView onLogout={handleLogout} />;
if (role === 'admin') return <AdminView onLogout={handleLogout} />;
return null;
}

View File

@@ -0,0 +1,177 @@
import React, { createContext, useContext, useState, ReactNode } from 'react';
import { DogWithMatches, InboxItem, CenterProfile } from '../types';
import { CENTERS } from '../data/centers';
interface AppContextType {
dogs: DogWithMatches[];
inboxItems: InboxItem[];
centers: CenterProfile[];
addDog: (dog: DogWithMatches) => void;
updateDogPlacement: (dogId: string, updates: Partial<DogWithMatches>) => void;
updateDog: (dogId: string, updates: Partial<DogWithMatches>) => void;
updateInboxStatus: (itemId: string, status: InboxItem['status']) => void;
addInboxItems: (items: InboxItem[]) => void;
}
const AppContext = createContext<AppContextType | undefined>(undefined);
// Initial Mock Data
const initialDogs: DogWithMatches[] = [
{
id: 'DOG-B8N2X9Y',
name: 'Barnaby',
breed: 'Pitbull Mix',
age: '4 years',
vetNotes: 'Found as a stray with severe bilateral cruciate ligament tears. Requires TPLO surgery on both hind legs. Currently non-weight bearing. Very sweet temperament, no bite history, but highly stressed in the shelter environment. Needs strict crate rest and underwater treadmill therapy post-op.',
status: 'analyzed',
dateAdded: '2026-03-15',
documents: ['Intake_Exam.pdf', 'X-Rays_Hind_Legs.jpg', 'Behavioral_Eval.pdf'],
placementStatus: 'unplaced',
matches: [
{
center_id: 'c1',
center_name: 'Paws in Motion Rehab',
decision: {
match_score: 95,
reasoning: 'This center is a perfect fit for bilateral TPLO recovery. The facility has the underwater treadmill and carts needed for his non-weight bearing status.',
status_options: {
accept_immediately: false,
conditional_needs: {
funding_required: 4500,
resources_required: ['Custom Sling', 'Orthopedic Bed'],
donor_pitch: 'Barnaby needs double knee surgery to walk again. Your donation secures his spot at a premier mobility rehab center.'
}
}
}
}
]
},
{
id: 'DOG-L4M9P2Q',
name: 'Luna',
breed: 'German Shepherd',
age: '2 years',
vetNotes: 'Hit by car. Pelvic fractures (healing), requires physical therapy to rebuild muscle mass in hindquarters. High energy, gets frustrated easily during confinement.',
status: 'analyzed',
dateAdded: '2026-03-16',
documents: ['Vet_Records.pdf'],
placementStatus: 'unplaced',
matches: [
{
center_id: 'c1',
center_name: 'Paws in Motion Rehab',
decision: {
match_score: 82,
reasoning: 'Good match for orthopedic rehab capabilities, though her high energy might require extra behavioral management during confinement.',
status_options: {
accept_immediately: true,
conditional_needs: {
funding_required: 1200,
resources_required: ['Enrichment Toys'],
donor_pitch: 'Luna survived a car accident and needs physical therapy. Help fund her recovery journey.'
}
}
}
}
]
},
{
id: 'DOG-M7X1V5Z',
name: 'Max',
breed: 'Rottweiler',
age: '3 years',
vetNotes: 'Severe resource guarding with food and high-value toys. Has bitten a volunteer when a bowl was removed. Otherwise affectionate. Needs an experienced behavioral rehabilitation center with strict protocols.',
status: 'analyzed',
dateAdded: '2026-03-17',
documents: ['Bite_Report.pdf', 'Behavioral_Assessment.pdf'],
placementStatus: 'conditional',
placedCenterId: 'c2',
placedCenterName: 'Behavioral Crossroads',
fundingGoal: 2500,
fundingRaised: 450,
resourcesNeeded: ['Heavy Duty Muzzle', 'Secure Kenneling'],
donorPitch: 'Max is a misunderstood boy who needs intensive behavioral training to become adoptable. Your support gives him a second chance.',
centerDescription: 'We are prepared to take Max into our intensive behavioral modification program. Our staff is trained in severe resource guarding protocols.'
},
{
id: 'DOG-B3K8J4W',
name: 'Bella',
breed: 'Golden Retriever',
age: '11 years',
vetNotes: 'Diagnosed with terminal osteosarcoma. Given 3-6 months. Needs palliative care, pain management, and a quiet hospice environment. Cannot handle stairs.',
status: 'analyzed',
dateAdded: '2026-03-18',
documents: ['Oncology_Report.pdf'],
placementStatus: 'conditional',
placedCenterId: 'c3',
placedCenterName: 'Serenity Senior Sanctuary',
fundingGoal: 1500,
fundingRaised: 1500,
resourcesNeeded: ['Pain Medication', 'Orthopedic Mattress'],
donorPitch: 'Bella deserves to spend her final months in comfort and peace. Help us provide palliative care for this sweet senior.',
centerDescription: 'We will provide Bella with a warm, loving hospice environment and manage her pain for the remainder of her days.'
}
];
const initialInbox: InboxItem[] = [
{
id: 'i1',
dogId: 'DOG-B8N2X9Y',
centerId: 'c1',
matchScore: 95,
reasoning: 'This center is a perfect fit for bilateral TPLO recovery. The facility has the underwater treadmill and carts needed for his non-weight bearing status.',
status: 'unreviewed',
dateAdded: '2026-03-18',
decisionOptions: initialDogs[0].matches![0].decision
},
{
id: 'i2',
dogId: 'DOG-L4M9P2Q',
centerId: 'c1',
matchScore: 82,
reasoning: 'Good match for orthopedic rehab capabilities, though her high energy might require extra behavioral management during confinement.',
status: 'saved_for_later',
dateAdded: '2026-03-17',
decisionOptions: initialDogs[1].matches![0].decision
}
];
export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [dogs, setDogs] = useState<DogWithMatches[]>(initialDogs);
const [inboxItems, setInboxItems] = useState<InboxItem[]>(initialInbox);
const [centers] = useState<CenterProfile[]>(CENTERS);
const addDog = (dog: DogWithMatches) => {
setDogs(prev => [dog, ...prev]);
};
const updateDogPlacement = (dogId: string, updates: Partial<DogWithMatches>) => {
setDogs(prev => prev.map(d => d.id === dogId ? { ...d, ...updates } : d));
};
const updateDog = (dogId: string, updates: Partial<DogWithMatches>) => {
setDogs(prev => prev.map(d => d.id === dogId ? { ...d, ...updates } : d));
};
const updateInboxStatus = (itemId: string, status: InboxItem['status']) => {
setInboxItems(prev => prev.map(i => i.id === itemId ? { ...i, status } : i));
};
const addInboxItems = (items: InboxItem[]) => {
setInboxItems(prev => [...items, ...prev]);
};
return (
<AppContext.Provider value={{ dogs, inboxItems, centers, addDog, updateDogPlacement, updateDog, updateInboxStatus, addInboxItems }}>
{children}
</AppContext.Provider>
);
};
export const useAppContext = () => {
const context = useContext(AppContext);
if (context === undefined) {
throw new Error('useAppContext must be used within an AppProvider');
}
return context;
};

View File

@@ -0,0 +1,36 @@
import { CenterProfile } from '../types';
export const CENTERS: CenterProfile[] = [
{
id: 'c1',
name: 'Paws in Motion Rehab',
specialties: ['Mobility', 'Post-Op'],
equipment: ['Underwater Treadmill', 'Laser Therapy', 'Carts'],
capacity: 'High (1:3 staff-to-animal ratio)',
description: 'Premier facility for dogs recovering from orthopedic surgeries or suffering from severe mobility issues.'
},
{
id: 'c2',
name: 'Serenity Sanctuary',
specialties: ['Hospice', 'Behavioral', 'Medical Stabilization'],
equipment: ['Isolation Ward', 'Quiet Rooms', 'Oxygen Cages'],
capacity: 'Medium (1:5 staff-to-animal ratio)',
description: 'A quiet, low-stress environment ideal for end-of-life care or dogs needing severe behavioral decompression.'
},
{
id: 'c3',
name: 'Second Chance Behavioral Center',
specialties: ['Behavioral', 'Reactivity'],
equipment: ['Secure Play Yards', 'Agility Equipment', 'Muzzle Training Gear'],
capacity: 'Low (1:8 staff-to-animal ratio)',
description: 'Specializes in rehabilitating dogs with bite histories or severe reactivity to humans/other dogs.'
},
{
id: 'c4',
name: 'Hope Medical Rescue',
specialties: ['Post-Op', 'Medical Stabilization', 'Infectious Disease'],
equipment: ['Isolation Ward', 'Surgical Suite', 'IV Pumps'],
capacity: 'High (1:2 staff-to-animal ratio)',
description: 'Intensive care unit for dogs coming straight from severe neglect or trauma situations.'
}
];

View File

@@ -0,0 +1 @@
@import "tailwindcss";

View File

@@ -0,0 +1,13 @@
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import App from './App.tsx';
import { AppProvider } from './context/AppContext';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<AppProvider>
<App />
</AppProvider>
</StrictMode>,
);

View File

@@ -0,0 +1,97 @@
import { GoogleGenAI, Type, Schema } from '@google/genai';
import { DogProfile, CenterProfile, MatchResult } from '../types';
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const decisionSchema: Schema = {
type: Type.OBJECT,
properties: {
matches: {
type: Type.ARRAY,
items: {
type: Type.OBJECT,
properties: {
center_id: { type: Type.STRING },
center_name: { type: Type.STRING },
decision: {
type: Type.OBJECT,
properties: {
match_score: { type: Type.NUMBER, description: "0-100" },
reasoning: { type: Type.STRING, description: "Brief explanation of why this center fits." },
status_options: {
type: Type.OBJECT,
properties: {
accept_immediately: { type: Type.BOOLEAN },
conditional_needs: {
type: Type.OBJECT,
properties: {
funding_required: { type: Type.NUMBER, description: "in USD" },
resources_required: {
type: Type.ARRAY,
items: { type: Type.STRING }
},
donor_pitch: { type: Type.STRING, description: "A 2-sentence emotional appeal for the donor view." }
}
}
},
required: ["accept_immediately"]
}
},
required: ["match_score", "reasoning", "status_options"]
}
},
required: ["center_id", "center_name", "decision"]
}
}
},
required: ["matches"]
};
export async function evaluateDogProfile(dog: DogProfile, centers: CenterProfile[]): Promise<MatchResult[]> {
const prompt = `
Please evaluate the following rescue dog profile against the available rehabilitation centers.
DOG PROFILE:
Name: ${dog.name}
Breed: ${dog.breed}
Age: ${dog.age}
Vet Notes / Description: ${dog.vetNotes}
AVAILABLE CENTERS:
${JSON.stringify(centers, null, 2)}
Provide a decision object for EACH center evaluating how well they match this dog's needs.
CRITICAL: Use neutral, third-person language for the "reasoning" field (e.g., "This center has...", "The facility is equipped with..."). DO NOT use first-person pronouns like "we", "our", or "us". The reasoning is a system analysis, not a statement from the center itself.
`;
const response = await ai.models.generateContent({
model: 'gemini-3.1-pro-preview',
contents: prompt,
config: {
systemInstruction: `You are the "Rescue-to-Rehab Intelligence Engine." Your goal is to facilitate the matching of high-needs rescue dogs with specialized rehabilitation centers.
OPERATIONAL LOGIC:
1. PROFILE ANALYSIS: When a dog profile is submitted, extract 'Care Intensity Level' (1-10) and 'Specialized Needs' (Mobility, Behavioral, Post-Op, Hospice).
2. MATCHING: Evaluate dogs against Center Profiles based on:
- Equipment Match (e.g., Underwater Treadmill, Isolation Ward).
- Capacity (Staff-to-animal ratio).
- Expertise (Specialization in specific breeds or conditions).
TONE & CONSTRAINTS:
- Professional, efficient, and empathetic.
- Prioritize animal welfare over "perfect matches" (if no perfect match exists, suggest the next best stabilization center).
- Do not provide medical advice; strictly categorize based on the provided vet data.
- Use neutral, third-person language. Never use "we" or "our".`,
responseMimeType: 'application/json',
responseSchema: decisionSchema,
temperature: 0.2,
}
});
const text = response.text;
if (!text) throw new Error("No response from AI");
const parsed = JSON.parse(text);
return parsed.matches;
}

View File

@@ -0,0 +1,63 @@
export interface CenterProfile {
id: string;
name: string;
specialties: string[];
equipment: string[];
capacity: string;
description: string;
}
export interface DogProfile {
id?: string;
name: string;
breed: string;
age: string;
vetNotes: string;
status?: 'pending_analysis' | 'analyzed' | 'placed' | 'deceased';
documents?: string[];
}
export interface DecisionObject {
match_score: number;
reasoning: string;
status_options: {
accept_immediately: boolean;
conditional_needs?: {
funding_required: number;
resources_required: string[];
donor_pitch: string;
};
};
}
export interface MatchResult {
center_id: string;
center_name: string;
decision: DecisionObject;
}
export interface DogWithMatches extends DogProfile {
id: string;
matches?: MatchResult[];
dateAdded: string;
placementStatus: 'unplaced' | 'conditional' | 'immediate';
placedCenterId?: string;
placedCenterName?: string;
fundingGoal?: number;
fundingRaised?: number;
resourcesNeeded?: string[];
donorPitch?: string;
centerDescription?: string;
}
export interface InboxItem {
id: string;
dogId: string;
centerId: string;
matchScore: number;
reasoning: string;
status: 'unreviewed' | 'contact_requested' | 'confirmed' | 'denied' | 'saved_for_later';
dateAdded: string;
decisionOptions: DecisionObject;
}

View File

@@ -0,0 +1,76 @@
import React from 'react';
import { LineChart, LogOut, Activity, Users, Building2, HeartPulse } from 'lucide-react';
interface AdminViewProps {
onLogout: () => void;
}
export default function AdminView({ onLogout }: AdminViewProps) {
const stats = [
{ label: 'Total Dogs Placed', value: '1,248', icon: HeartPulse, trend: '+12%' },
{ label: 'Active Centers', value: '42', icon: Building2, trend: '+3' },
{ label: 'Funding Raised', value: '$842k', icon: Activity, trend: '+24%' },
{ label: 'Active Donors', value: '3,190', icon: Users, trend: '+8%' },
];
return (
<div className="min-h-screen bg-slate-50 text-slate-900 font-sans">
<header className="bg-indigo-900 text-white shadow-md">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex items-center justify-between">
<div className="flex items-center space-x-3">
<LineChart className="w-8 h-8 text-indigo-300" />
<div>
<h1 className="text-xl font-bold tracking-tight">Rescue-to-Rehab Engine</h1>
<p className="text-indigo-200 text-xs uppercase tracking-wider font-semibold">Company Backend / Admin</p>
</div>
</div>
<button onClick={onLogout} className="flex items-center text-indigo-200 hover:text-white text-sm font-medium transition-colors">
<LogOut className="w-4 h-4 mr-2" /> Sign Out
</button>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8">
<h2 className="text-2xl font-bold text-slate-900">Network Overview</h2>
<p className="text-slate-600 mt-1">System insights, matching metrics, and network oversight.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{stats.map((stat, i) => {
const Icon = stat.icon;
return (
<div key={i} className="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
<div className="flex items-center justify-between mb-4">
<div className="p-2 bg-indigo-50 rounded-lg">
<Icon className="w-6 h-6 text-indigo-600" />
</div>
<span className="text-xs font-bold text-emerald-600 bg-emerald-50 px-2 py-1 rounded-full">
{stat.trend}
</span>
</div>
<h3 className="text-3xl font-black text-slate-800">{stat.value}</h3>
<p className="text-sm font-medium text-slate-500 mt-1">{stat.label}</p>
</div>
);
})}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 p-6 h-80 flex flex-col">
<h3 className="text-lg font-semibold text-slate-800 mb-4">Placement Success Rate</h3>
<div className="flex-grow flex items-center justify-center border-2 border-dashed border-slate-100 rounded-xl bg-slate-50">
<p className="text-slate-400 text-sm font-medium">[ Chart Visualization Placeholder ]</p>
</div>
</div>
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 p-6 h-80 flex flex-col">
<h3 className="text-lg font-semibold text-slate-800 mb-4">Center Capacity Heatmap</h3>
<div className="flex-grow flex items-center justify-center border-2 border-dashed border-slate-100 rounded-xl bg-slate-50">
<p className="text-slate-400 text-sm font-medium">[ Map/Heatmap Visualization Placeholder ]</p>
</div>
</div>
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,832 @@
import React, { useState, useMemo } from 'react';
import { Building2, LogOut, CheckCircle2, XCircle, MessageSquare, Clock, Save, ShieldAlert, HeartPulse, ChevronRight, Info, Search, Filter, X, Plus, ChevronLeft, UploadCloud, FileText, ImageIcon } from 'lucide-react';
import { InboxItem, DogWithMatches, DogProfile } from '../types';
import { useAppContext } from '../context/AppContext';
interface CenterViewProps {
onLogout: () => void;
}
type InboxTab = 'all' | 'unreviewed' | 'saved_for_later' | 'confirmed' | 'denied';
export default function CenterView({ onLogout }: CenterViewProps) {
const { inboxItems, dogs, updateInboxStatus, updateDogPlacement, updateDog, addDog } = useAppContext();
const [activeMainTab, setActiveMainTab] = useState<'inbox' | 'enrolled' | 'profile'>('inbox');
const [activeInboxTab, setActiveInboxTab] = useState<InboxTab>('unreviewed');
const [searchQuery, setSearchQuery] = useState('');
// Enrolled State
const [enrolledView, setEnrolledView] = useState<'list' | 'add' | 'detail' | 'edit'>('list');
const [editingDog, setEditingDog] = useState<DogWithMatches | null>(null);
const [enrolledForm, setEnrolledForm] = useState<Partial<DogProfile>>({ name: '', breed: '', age: '', vetNotes: '' });
// Modal State
const [isAcceptModalOpen, setIsAcceptModalOpen] = useState(false);
const [selectedInboxItem, setSelectedInboxItem] = useState<(InboxItem & { dog: DogWithMatches }) | null>(null);
const [acceptType, setAcceptType] = useState<'immediate' | 'conditional'>('immediate');
const [conditionalForm, setConditionalForm] = useState({
description: '',
fundingRequired: '',
resourcesRequired: ''
});
// Profile State
const [profile, setProfile] = useState({
name: 'Paws in Motion Rehab',
capacity: 'Medium (10-20 dogs)',
medicalCapabilities: ['Post-Op Orthopedic', 'Neurological', 'Amputee Care', 'Wound Management'],
behavioralCapabilities: ['Fearful/Shy', 'Resource Guarding (Mild)'],
equipment: ['Underwater Treadmill', 'Laser Therapy', 'Custom Slings/Carts', 'Isolation Ward'],
idealIntake: 'We excel with dogs needing intensive physical therapy post-surgery, especially TPLO or spinal surgeries. We prefer dogs under 80lbs due to lifting requirements.',
dealbreakers: 'Severe human aggression, active infectious diseases (parvo, distemper).'
});
const centerId = 'c1'; // Mock logged-in center
const enrolledDogs = useMemo(() => {
return dogs.filter(d => d.placedCenterId === centerId && d.status !== 'deceased');
}, [dogs]);
const handleAddEnrolled = (e: React.FormEvent) => {
e.preventDefault();
if (!enrolledForm.name) return;
const newDog: DogWithMatches = {
id: `DOG-${Math.random().toString(36).substr(2, 9).toUpperCase()}`,
name: enrolledForm.name,
breed: enrolledForm.breed || '',
age: enrolledForm.age || '',
vetNotes: enrolledForm.vetNotes || '',
status: 'placed',
dateAdded: new Date().toISOString().split('T')[0],
placementStatus: 'immediate',
placedCenterId: centerId,
placedCenterName: profile.name
};
addDog(newDog);
setEnrolledView('list');
setEnrolledForm({ name: '', breed: '', age: '', vetNotes: '' });
};
const handleEditEnrolled = (e: React.FormEvent) => {
e.preventDefault();
if (!editingDog) return;
updateDog(editingDog.id, enrolledForm);
setEnrolledView('list');
setEditingDog(null);
};
const myInbox = useMemo(() => {
return inboxItems
.filter(i => i.centerId === centerId)
.map(item => ({
...item,
dog: dogs.find(d => d.id === item.dogId)!
}))
.filter(item => item.dog); // Ensure dog exists
}, [inboxItems, dogs]);
const filteredInbox = useMemo(() => {
let filtered = myInbox;
if (activeInboxTab !== 'all') {
filtered = filtered.filter(i => i.status === activeInboxTab);
}
if (searchQuery) {
const q = searchQuery.toLowerCase();
filtered = filtered.filter(i =>
i.dog.name.toLowerCase().includes(q) ||
i.dog.breed.toLowerCase().includes(q)
);
}
return filtered;
}, [myInbox, activeInboxTab, searchQuery]);
const handleStatusChange = (id: string, newStatus: InboxItem['status']) => {
updateInboxStatus(id, newStatus);
};
const openAcceptModal = (item: InboxItem & { dog: DogWithMatches }) => {
setSelectedInboxItem(item);
setAcceptType(item.decisionOptions.status_options.accept_immediately ? 'immediate' : 'conditional');
setConditionalForm({
description: item.decisionOptions.status_options.conditional_needs?.donor_pitch || '',
fundingRequired: item.decisionOptions.status_options.conditional_needs?.funding_required?.toString() || '',
resourcesRequired: item.decisionOptions.status_options.conditional_needs?.resources_required?.join(', ') || ''
});
setIsAcceptModalOpen(true);
};
const submitAccept = (e: React.FormEvent) => {
e.preventDefault();
if (!selectedInboxItem) return;
updateInboxStatus(selectedInboxItem.id, 'confirmed');
if (acceptType === 'immediate') {
updateDogPlacement(selectedInboxItem.dogId, {
placementStatus: 'immediate',
placedCenterId: centerId,
placedCenterName: profile.name,
centerDescription: 'We are prepared to take this dog immediately and have all necessary resources.'
});
} else {
updateDogPlacement(selectedInboxItem.dogId, {
placementStatus: 'conditional',
placedCenterId: centerId,
placedCenterName: profile.name,
fundingGoal: Number(conditionalForm.fundingRequired) || 0,
fundingRaised: 0,
resourcesNeeded: conditionalForm.resourcesRequired.split(',').map(s => s.trim()).filter(Boolean),
donorPitch: conditionalForm.description,
centerDescription: conditionalForm.description
});
}
setIsAcceptModalOpen(false);
setSelectedInboxItem(null);
};
const handleProfileSave = (e: React.FormEvent) => {
e.preventDefault();
alert('Profile updated successfully. This will improve your future match accuracy.');
};
const toggleArrayItem = (arrayName: 'medicalCapabilities' | 'behavioralCapabilities' | 'equipment', item: string) => {
setProfile(prev => {
const array = prev[arrayName] as string[];
if (array.includes(item)) {
return { ...prev, [arrayName]: array.filter(i => i !== item) };
} else {
return { ...prev, [arrayName]: [...array, item] };
}
});
};
const unreviewedCount = myInbox.filter(i => i.status === 'unreviewed').length;
return (
<div className="min-h-screen bg-slate-50 text-slate-900 font-sans">
<header className="bg-amber-800 text-white shadow-md">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex items-center justify-between">
<div className="flex items-center space-x-3">
<Building2 className="w-8 h-8 text-amber-300" />
<div>
<h1 className="text-xl font-bold tracking-tight">{profile.name}</h1>
<p className="text-amber-200 text-xs uppercase tracking-wider font-semibold">Center Management Portal</p>
</div>
</div>
<button onClick={onLogout} className="flex items-center text-amber-200 hover:text-white text-sm font-medium transition-colors">
<LogOut className="w-4 h-4 mr-2" /> Sign Out
</button>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Navigation Tabs */}
<div className="flex space-x-1 bg-slate-200/50 p-1 rounded-xl w-fit mb-8">
<button
onClick={() => setActiveMainTab('inbox')}
className={`flex items-center px-6 py-2.5 rounded-lg text-sm font-bold transition-all ${
activeMainTab === 'inbox' ? 'bg-white text-amber-900 shadow-sm' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-200'
}`}
>
Intake Inbox
{unreviewedCount > 0 && (
<span className="ml-2 bg-amber-500 text-white text-[10px] px-2 py-0.5 rounded-full">{unreviewedCount}</span>
)}
</button>
<button
onClick={() => setActiveMainTab('enrolled')}
className={`flex items-center px-6 py-2.5 rounded-lg text-sm font-bold transition-all ${
activeMainTab === 'enrolled' ? 'bg-white text-amber-900 shadow-sm' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-200'
}`}
>
Enrolled Animals
</button>
<button
onClick={() => setActiveMainTab('profile')}
className={`flex items-center px-6 py-2.5 rounded-lg text-sm font-bold transition-all ${
activeMainTab === 'profile' ? 'bg-white text-amber-900 shadow-sm' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-200'
}`}
>
Facility Profile & Capabilities
</button>
</div>
{/* INBOX TAB */}
{activeMainTab === 'inbox' && (
<div className="space-y-6">
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-4 mb-6">
<div>
<h2 className="text-2xl font-bold text-slate-900">Intake Inbox</h2>
<p className="text-slate-600 mt-1">Review dogs where your center is a top 5 match.</p>
</div>
<div className="flex items-center bg-white rounded-xl border border-slate-300 px-3 py-2 w-full md:w-auto">
<Search className="w-5 h-5 text-slate-400 mr-2" />
<input
type="text"
placeholder="Search name or breed..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
className="outline-none bg-transparent text-sm w-full md:w-64"
/>
</div>
</div>
{/* Sub-tabs for Inbox */}
<div className="flex overflow-x-auto space-x-2 pb-2 mb-4 border-b border-slate-200">
{[
{ id: 'all', label: 'All' },
{ id: 'unreviewed', label: 'New' },
{ id: 'saved_for_later', label: 'Saved' },
{ id: 'confirmed', label: 'Accepted' },
{ id: 'denied', label: 'Denied' }
].map(tab => (
<button
key={tab.id}
onClick={() => setActiveInboxTab(tab.id as InboxTab)}
className={`px-4 py-2 rounded-lg text-sm font-bold whitespace-nowrap transition-colors ${
activeInboxTab === tab.id
? 'bg-amber-100 text-amber-900'
: 'text-slate-600 hover:bg-slate-100'
}`}
>
{tab.label}
</button>
))}
</div>
{filteredInbox.length === 0 ? (
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 p-12 text-center">
<p className="text-slate-500">No matches found in this category.</p>
</div>
) : (
<div className="grid grid-cols-1 gap-6">
{filteredInbox.map(item => (
<div key={item.id} className={`bg-white rounded-2xl shadow-sm border overflow-hidden transition-all ${item.status === 'unreviewed' ? 'border-amber-300 ring-1 ring-amber-300' : 'border-slate-200'}`}>
{item.status === 'unreviewed' && (
<div className="bg-amber-100 text-amber-800 text-xs font-bold uppercase tracking-wider py-1.5 px-6 flex items-center">
<ShieldAlert className="w-4 h-4 mr-2" /> Action Required: Unreviewed Match
</div>
)}
{item.status === 'confirmed' && (
<div className="bg-emerald-100 text-emerald-800 text-xs font-bold uppercase tracking-wider py-1.5 px-6 flex items-center">
<CheckCircle2 className="w-4 h-4 mr-2" /> Accepted Intake
</div>
)}
<div className="p-6 md:p-8 flex flex-col lg:flex-row gap-8">
{/* Dog Info */}
<div className="flex-1">
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="text-2xl font-bold text-slate-900">{item.dog.name}</h3>
<p className="text-sm text-slate-500 font-medium mt-1">{item.dog.breed} {item.dog.age}</p>
</div>
<div className="flex flex-col items-end text-right">
<div className="flex items-center text-amber-600">
<span className="text-3xl font-black">{item.matchScore}</span>
<span className="text-sm font-medium ml-1">/100</span>
</div>
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider">Match Score</span>
</div>
</div>
<div className="bg-slate-50 rounded-xl p-4 mb-6 border border-slate-100">
<p className="text-sm text-slate-700 leading-relaxed">
<span className="font-bold text-slate-900 mr-2">Why you matched:</span>
{item.reasoning}
</p>
</div>
<div>
<h4 className="text-sm font-bold text-slate-900 mb-2">Veterinary Notes</h4>
<p className="text-sm text-slate-600 leading-relaxed whitespace-pre-wrap">{item.dog.vetNotes}</p>
</div>
{item.dog.documents && item.dog.documents.length > 0 && (
<div className="mt-4 pt-4 border-t border-slate-100">
<h4 className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">Attached Documents</h4>
<div className="flex flex-wrap gap-2">
{item.dog.documents.map((doc, i) => (
<span key={i} className="inline-flex items-center px-3 py-1 rounded-lg bg-slate-100 text-slate-600 text-xs font-medium">
{doc}
</span>
))}
</div>
</div>
)}
</div>
{/* Actions */}
<div className="lg:w-72 flex flex-col justify-between bg-slate-50 rounded-xl p-6 border border-slate-100">
<div>
<h4 className="text-sm font-bold text-slate-900 mb-4 uppercase tracking-wider">Review Decision</h4>
<div className="space-y-3">
{item.status !== 'confirmed' && (
<button
onClick={() => openAcceptModal(item)}
className="w-full flex items-center justify-center px-4 py-3 rounded-xl text-sm font-bold transition-colors bg-white border border-emerald-200 text-emerald-700 hover:bg-emerald-50"
>
<CheckCircle2 className="w-5 h-5 mr-2" /> Accept Intake
</button>
)}
{item.status !== 'contact_requested' && item.status !== 'confirmed' && (
<button
onClick={() => handleStatusChange(item.id, 'contact_requested')}
className="w-full flex items-center justify-center px-4 py-3 rounded-xl text-sm font-bold transition-colors bg-white border border-blue-200 text-blue-700 hover:bg-blue-50"
>
<MessageSquare className="w-5 h-5 mr-2" /> Request Contact
</button>
)}
{item.status !== 'saved_for_later' && item.status !== 'confirmed' && (
<button
onClick={() => handleStatusChange(item.id, 'saved_for_later')}
className="w-full flex items-center justify-center px-4 py-3 rounded-xl text-sm font-bold transition-colors bg-white border border-slate-200 text-slate-700 hover:bg-slate-100"
>
<Clock className="w-5 h-5 mr-2" /> Save for Later
</button>
)}
{item.status !== 'denied' && item.status !== 'confirmed' && (
<button
onClick={() => handleStatusChange(item.id, 'denied')}
className="w-full flex items-center justify-center px-4 py-3 rounded-xl text-sm font-bold transition-colors bg-white border border-rose-200 text-rose-700 hover:bg-rose-50"
>
<XCircle className="w-5 h-5 mr-2" /> Deny Match
</button>
)}
</div>
</div>
<div className="mt-6 text-center">
<span className="text-xs text-slate-400 font-medium">Added {item.dateAdded}</span>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
{/* ENROLLED TAB */}
{activeMainTab === 'enrolled' && (
<div className="space-y-6">
{enrolledView === 'list' && (
<>
<div className="flex justify-between items-end mb-6">
<div>
<h2 className="text-2xl font-bold text-slate-900">Enrolled Animals</h2>
<p className="text-slate-600 mt-1">Manage dogs currently in your care.</p>
</div>
<button
onClick={() => { setEnrolledForm({name: '', breed: '', age: '', vetNotes: ''}); setEnrolledView('add'); }}
className="bg-amber-600 hover:bg-amber-700 text-white px-4 py-2 rounded-lg text-sm font-bold transition-colors flex items-center"
>
<Plus className="w-4 h-4 mr-2" /> Add Animal
</button>
</div>
{enrolledDogs.length === 0 ? (
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 p-12 text-center">
<p className="text-slate-500">You have no animals currently enrolled.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{enrolledDogs.map(dog => (
<div
key={dog.id}
onClick={() => { setEditingDog(dog); setEnrolledView('detail'); }}
className="bg-white rounded-2xl shadow-sm border border-slate-200 p-6 cursor-pointer hover:border-amber-300 hover:shadow-md transition-all group"
>
<h3 className="text-xl font-bold text-slate-900 group-hover:text-amber-700 transition-colors">{dog.name}</h3>
<p className="text-sm text-slate-500 mb-3">ID: {dog.id} {dog.breed} {dog.age}</p>
<p className="text-sm text-slate-600 line-clamp-3">{dog.vetNotes}</p>
</div>
))}
</div>
)}
</>
)}
{enrolledView === 'add' && (
<div className="max-w-3xl mx-auto">
<button onClick={() => setEnrolledView('list')} className="flex items-center text-sm font-medium text-slate-500 hover:text-slate-800 mb-6 transition-colors">
<ChevronLeft className="w-4 h-4 mr-1" /> Back to Enrolled Animals
</button>
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
<div className="bg-slate-50 px-6 py-4 border-b border-slate-200">
<h2 className="text-lg font-semibold text-slate-800">Add Enrolled Animal</h2>
</div>
<form onSubmit={handleAddEnrolled} className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="md:col-span-2">
<label className="block text-sm font-medium text-slate-700 mb-1">Dog Name *</label>
<input type="text" required value={enrolledForm.name || ''} onChange={e => setEnrolledForm({...enrolledForm, name: e.target.value})} className="w-full rounded-xl border-slate-300 border px-4 py-2.5 focus:ring-2 focus:ring-amber-500 outline-none" />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Breed</label>
<input type="text" value={enrolledForm.breed || ''} onChange={e => setEnrolledForm({...enrolledForm, breed: e.target.value})} className="w-full rounded-xl border-slate-300 border px-4 py-2.5 focus:ring-2 focus:ring-amber-500 outline-none" />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Age</label>
<input type="text" value={enrolledForm.age || ''} onChange={e => setEnrolledForm({...enrolledForm, age: e.target.value})} className="w-full rounded-xl border-slate-300 border px-4 py-2.5 focus:ring-2 focus:ring-amber-500 outline-none" />
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Vet Notes & Behavioral Assessment</label>
<textarea rows={5} value={enrolledForm.vetNotes || ''} onChange={e => setEnrolledForm({...enrolledForm, vetNotes: e.target.value})} className="w-full rounded-xl border-slate-300 border px-4 py-2.5 focus:ring-2 focus:ring-amber-500 outline-none text-sm" />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Upload Documents & Photos</label>
<div className="border-2 border-dashed border-slate-300 rounded-xl p-8 text-center hover:bg-slate-50 transition-colors cursor-pointer">
<div className="flex justify-center space-x-4 mb-3">
<FileText className="w-8 h-8 text-slate-400" />
<ImageIcon className="w-8 h-8 text-slate-400" />
</div>
<p className="text-sm font-medium text-slate-700">Click to upload or drag and drop</p>
<p className="text-xs text-slate-500 mt-1">PDF, JPG, PNG up to 10MB</p>
</div>
</div>
<div className="pt-4 border-t border-slate-100 flex justify-end space-x-3">
<button type="button" onClick={() => setEnrolledView('list')} className="px-5 py-2.5 rounded-xl text-sm font-medium text-slate-600 hover:bg-slate-100 transition-colors">Cancel</button>
<button type="submit" className="bg-amber-600 hover:bg-amber-700 text-white font-medium py-2.5 px-6 rounded-xl transition-colors">Add Animal</button>
</div>
</form>
</div>
</div>
)}
{enrolledView === 'detail' && editingDog && (
<div className="max-w-3xl mx-auto">
<button onClick={() => setEnrolledView('list')} className="flex items-center text-sm font-medium text-slate-500 hover:text-slate-800 mb-6 transition-colors">
<ChevronLeft className="w-4 h-4 mr-1" /> Back to Enrolled Animals
</button>
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden mb-6">
<div className="p-6 md:p-8 flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h2 className="text-3xl font-bold text-slate-900">{editingDog.name}</h2>
<p className="text-slate-500 mt-1 flex items-center text-sm">
ID: {editingDog.id} {editingDog.breed} {editingDog.age} Enrolled {editingDog.dateAdded}
</p>
</div>
<div className="flex space-x-3">
<button
onClick={() => { setEnrolledForm(editingDog); setEnrolledView('edit'); }}
className="px-4 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-lg text-sm font-medium transition-colors"
>
Edit Info
</button>
</div>
</div>
<div className="px-6 md:px-8 pb-8">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Veterinary & Behavioral Notes</h3>
<div className="prose prose-slate max-w-none">
<p className="whitespace-pre-wrap text-slate-700 leading-relaxed">{editingDog.vetNotes}</p>
</div>
</div>
</div>
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 p-6 md:p-8">
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-semibold text-slate-900">Documents</h3>
</div>
{editingDog.documents && editingDog.documents.length > 0 && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
{editingDog.documents.map((doc, i) => (
<div key={i} className="flex items-center p-4 border border-slate-200 rounded-xl hover:bg-slate-50 cursor-pointer transition-colors">
{doc.endsWith('.pdf') ? <FileText className="w-8 h-8 text-rose-400 mr-3" /> : <ImageIcon className="w-8 h-8 text-blue-400 mr-3" />}
<div className="overflow-hidden">
<p className="text-sm font-medium text-slate-900 truncate">{doc}</p>
<p className="text-xs text-slate-500">Uploaded {editingDog.dateAdded}</p>
</div>
</div>
))}
</div>
)}
<div className="border-2 border-dashed border-slate-300 rounded-xl p-8 text-center hover:bg-slate-50 transition-colors cursor-pointer">
<div className="flex justify-center space-x-4 mb-3">
<FileText className="w-8 h-8 text-slate-400" />
<ImageIcon className="w-8 h-8 text-slate-400" />
</div>
<p className="text-sm font-medium text-slate-700">Click to upload new documents</p>
<p className="text-xs text-slate-500 mt-1">PDF, JPG, PNG up to 10MB</p>
</div>
</div>
</div>
)}
{enrolledView === 'edit' && editingDog && (
<div className="max-w-3xl mx-auto">
<button onClick={() => setEnrolledView('detail')} className="flex items-center text-sm font-medium text-slate-500 hover:text-slate-800 mb-6 transition-colors">
<ChevronLeft className="w-4 h-4 mr-1" /> Back to Overview
</button>
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
<div className="bg-slate-50 px-6 py-4 border-b border-slate-200">
<h2 className="text-lg font-semibold text-slate-800">Edit Enrolled Animal: {editingDog.name}</h2>
</div>
<form onSubmit={handleEditEnrolled} className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="md:col-span-2">
<label className="block text-sm font-medium text-slate-700 mb-1">Dog Name *</label>
<input type="text" required value={enrolledForm.name || ''} onChange={e => setEnrolledForm({...enrolledForm, name: e.target.value})} className="w-full rounded-xl border-slate-300 border px-4 py-2.5 focus:ring-2 focus:ring-amber-500 outline-none" />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Breed</label>
<input type="text" value={enrolledForm.breed || ''} onChange={e => setEnrolledForm({...enrolledForm, breed: e.target.value})} className="w-full rounded-xl border-slate-300 border px-4 py-2.5 focus:ring-2 focus:ring-amber-500 outline-none" />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Age</label>
<input type="text" value={enrolledForm.age || ''} onChange={e => setEnrolledForm({...enrolledForm, age: e.target.value})} className="w-full rounded-xl border-slate-300 border px-4 py-2.5 focus:ring-2 focus:ring-amber-500 outline-none" />
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Vet Notes & Behavioral Assessment</label>
<textarea rows={5} value={enrolledForm.vetNotes || ''} onChange={e => setEnrolledForm({...enrolledForm, vetNotes: e.target.value})} className="w-full rounded-xl border-slate-300 border px-4 py-2.5 focus:ring-2 focus:ring-amber-500 outline-none text-sm" />
</div>
<div className="pt-4 border-t border-slate-100 flex justify-between items-center">
<button type="button" onClick={() => {
if (window.confirm('Are you sure you want to mark this dog as deceased?')) {
updateDog(editingDog.id, { status: 'deceased' });
setEnrolledView('list');
}
}} className="px-4 py-2 rounded-xl text-sm font-bold text-rose-600 hover:bg-rose-50 transition-colors">
Mark Deceased
</button>
<div className="flex space-x-3">
<button type="button" onClick={() => setEnrolledView('detail')} className="px-5 py-2.5 rounded-xl text-sm font-medium text-slate-600 hover:bg-slate-100 transition-colors">Cancel</button>
<button type="submit" className="bg-amber-600 hover:bg-amber-700 text-white font-medium py-2.5 px-6 rounded-xl transition-colors">Save Changes</button>
</div>
</div>
</form>
</div>
</div>
)}
</div>
)}
{/* PROFILE TAB */}
{activeMainTab === 'profile' && (
<div className="max-w-4xl">
<div className="mb-6">
<h2 className="text-2xl font-bold text-slate-900">Facility Profile & Capabilities</h2>
<p className="text-slate-600 mt-1">Keep this updated to ensure accurate matches from the intelligence engine.</p>
</div>
<form onSubmit={handleProfileSave} className="space-y-8">
{/* Basic Info */}
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 p-6 md:p-8">
<h3 className="text-lg font-bold text-slate-900 mb-6 flex items-center">
<Building2 className="w-5 h-5 mr-2 text-amber-600" /> Basic Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">Center Name</label>
<input
type="text" value={profile.name} onChange={e => setProfile({...profile, name: e.target.value})}
className="w-full rounded-xl border-slate-300 border px-4 py-3 focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none"
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">Current Capacity</label>
<select
value={profile.capacity} onChange={e => setProfile({...profile, capacity: e.target.value})}
className="w-full rounded-xl border-slate-300 border px-4 py-3 focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none bg-white"
>
<option>Low (0-5 dogs)</option>
<option>Medium (10-20 dogs)</option>
<option>High (20+ dogs)</option>
<option>At Capacity (Emergency Only)</option>
</select>
</div>
</div>
</div>
{/* Capabilities Checkboxes */}
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 p-6 md:p-8">
<h3 className="text-lg font-bold text-slate-900 mb-6 flex items-center">
<HeartPulse className="w-5 h-5 mr-2 text-amber-600" /> Capabilities & Equipment
</h3>
<div className="space-y-8">
<div>
<h4 className="text-sm font-bold text-slate-700 mb-3 uppercase tracking-wider">Medical Capabilities</h4>
<div className="flex flex-wrap gap-3">
{['Post-Op Orthopedic', 'Neurological', 'Amputee Care', 'Wound Management', 'Hospice/Palliative', 'Infectious Disease Isolation', 'Diabetic Management'].map(cap => (
<button
key={cap} type="button"
onClick={() => toggleArrayItem('medicalCapabilities', cap)}
className={`px-4 py-2 rounded-xl text-sm font-medium transition-colors border ${
profile.medicalCapabilities.includes(cap)
? 'bg-amber-100 border-amber-300 text-amber-900'
: 'bg-white border-slate-200 text-slate-600 hover:border-amber-300'
}`}
>
{cap}
</button>
))}
</div>
</div>
<div>
<h4 className="text-sm font-bold text-slate-700 mb-3 uppercase tracking-wider">Behavioral Capabilities</h4>
<div className="flex flex-wrap gap-3">
{['Fearful/Shy', 'Resource Guarding (Mild)', 'Resource Guarding (Severe)', 'Dog Reactive', 'Human Reactive', 'Bite History (Known)', 'Separation Anxiety'].map(cap => (
<button
key={cap} type="button"
onClick={() => toggleArrayItem('behavioralCapabilities', cap)}
className={`px-4 py-2 rounded-xl text-sm font-medium transition-colors border ${
profile.behavioralCapabilities.includes(cap)
? 'bg-amber-100 border-amber-300 text-amber-900'
: 'bg-white border-slate-200 text-slate-600 hover:border-amber-300'
}`}
>
{cap}
</button>
))}
</div>
</div>
<div>
<h4 className="text-sm font-bold text-slate-700 mb-3 uppercase tracking-wider">Specialized Equipment</h4>
<div className="flex flex-wrap gap-3">
{['Underwater Treadmill', 'Laser Therapy', 'Custom Slings/Carts', 'Isolation Ward', 'Acupuncture', 'Hyperbaric Chamber'].map(eq => (
<button
key={eq} type="button"
onClick={() => toggleArrayItem('equipment', eq)}
className={`px-4 py-2 rounded-xl text-sm font-medium transition-colors border ${
profile.equipment.includes(eq)
? 'bg-amber-100 border-amber-300 text-amber-900'
: 'bg-white border-slate-200 text-slate-600 hover:border-amber-300'
}`}
>
{eq}
</button>
))}
</div>
</div>
</div>
</div>
{/* Open Ended Responses */}
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 p-6 md:p-8">
<h3 className="text-lg font-bold text-slate-900 mb-6 flex items-center">
<Info className="w-5 h-5 mr-2 text-amber-600" /> Open-Ended Preferences
</h3>
<div className="space-y-6">
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">Describe your ideal intake scenario</label>
<p className="text-xs text-slate-500 mb-2">This helps the AI understand nuances beyond checkboxes.</p>
<textarea
rows={4} value={profile.idealIntake} onChange={e => setProfile({...profile, idealIntake: e.target.value})}
className="w-full rounded-xl border-slate-300 border px-4 py-3 focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-sm"
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">Strict Dealbreakers</label>
<p className="text-xs text-slate-500 mb-2">Conditions or behaviors you absolutely cannot accommodate.</p>
<textarea
rows={3} value={profile.dealbreakers} onChange={e => setProfile({...profile, dealbreakers: e.target.value})}
className="w-full rounded-xl border-slate-300 border px-4 py-3 focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-sm"
/>
</div>
</div>
</div>
<div className="flex justify-end">
<button
type="submit"
className="bg-amber-700 hover:bg-amber-800 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center shadow-sm"
>
<Save className="w-5 h-5 mr-2" /> Save Profile Updates
</button>
</div>
</form>
</div>
)}
</main>
{/* ACCEPT INTAKE MODAL */}
{isAcceptModalOpen && selectedInboxItem && (
<div className="fixed inset-0 bg-slate-900/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl shadow-xl max-w-2xl w-full overflow-hidden">
<div className="bg-emerald-600 px-6 py-4 flex justify-between items-center">
<h3 className="text-lg font-bold text-white flex items-center">
<CheckCircle2 className="w-5 h-5 mr-2" /> Accept Intake: {selectedInboxItem.dog.name}
</h3>
<button onClick={() => setIsAcceptModalOpen(false)} className="text-emerald-200 hover:text-white">
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={submitAccept} className="p-6 md:p-8">
<div className="mb-6">
<label className="block text-sm font-bold text-slate-700 mb-3">Intake Type</label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<button
type="button"
onClick={() => setAcceptType('immediate')}
className={`p-4 rounded-xl border-2 text-left transition-all ${
acceptType === 'immediate'
? 'border-emerald-500 bg-emerald-50'
: 'border-slate-200 hover:border-emerald-200'
}`}
>
<div className="font-bold text-slate-900 mb-1">Take Immediately</div>
<div className="text-xs text-slate-500">We have all the necessary resources and funds to accept this dog right now.</div>
</button>
<button
type="button"
onClick={() => setAcceptType('conditional')}
className={`p-4 rounded-xl border-2 text-left transition-all ${
acceptType === 'conditional'
? 'border-amber-500 bg-amber-50'
: 'border-slate-200 hover:border-amber-200'
}`}
>
<div className="font-bold text-slate-900 mb-1">Conditionally Accept</div>
<div className="text-xs text-slate-500">We can take this dog, but we need to raise funds or acquire specific resources first.</div>
</button>
</div>
</div>
{acceptType === 'conditional' && (
<div className="space-y-4 bg-slate-50 p-6 rounded-xl border border-slate-200 mb-6">
<div>
<label className="block text-sm font-bold text-slate-700 mb-1">Public Description / Donor Pitch</label>
<p className="text-xs text-slate-500 mb-2">This will be visible on the public donation portal.</p>
<textarea
required
rows={3}
value={conditionalForm.description}
onChange={e => setConditionalForm({...conditionalForm, description: e.target.value})}
className="w-full rounded-xl border-slate-300 border px-4 py-2 focus:ring-2 focus:ring-amber-500 outline-none text-sm"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-bold text-slate-700 mb-1">Funds Needed (USD)</label>
<input
type="number"
required
value={conditionalForm.fundingRequired}
onChange={e => setConditionalForm({...conditionalForm, fundingRequired: e.target.value})}
className="w-full rounded-xl border-slate-300 border px-4 py-2 focus:ring-2 focus:ring-amber-500 outline-none text-sm"
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-1">Resources Needed</label>
<input
type="text"
placeholder="e.g. Wheelchair, Special Diet"
value={conditionalForm.resourcesRequired}
onChange={e => setConditionalForm({...conditionalForm, resourcesRequired: e.target.value})}
className="w-full rounded-xl border-slate-300 border px-4 py-2 focus:ring-2 focus:ring-amber-500 outline-none text-sm"
/>
</div>
</div>
</div>
)}
{acceptType === 'immediate' && (
<div className="bg-emerald-50 border border-emerald-200 p-4 rounded-xl mb-6">
<p className="text-sm text-emerald-800 font-medium">
By confirming, you agree that your facility is prepared to accept {selectedInboxItem.dog.name} immediately without additional network funding.
</p>
</div>
)}
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={() => setIsAcceptModalOpen(false)}
className="px-5 py-2.5 rounded-xl text-sm font-bold text-slate-600 hover:bg-slate-100 transition-colors"
>
Cancel
</button>
<button
type="submit"
className="bg-emerald-600 hover:bg-emerald-700 text-white font-bold py-2.5 px-6 rounded-xl transition-colors"
>
Confirm Acceptance
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,107 @@
import React from 'react';
import { HeartHandshake, LogOut, DollarSign, Package, ArrowRight, HeartPulse } from 'lucide-react';
import { useAppContext } from '../context/AppContext';
interface DonorViewProps {
onLogout: () => void;
}
export default function DonorView({ onLogout }: DonorViewProps) {
const { dogs } = useAppContext();
// Only show dogs that have been conditionally accepted and need funding/resources
const cases = dogs.filter(d => d.placementStatus === 'conditional');
return (
<div className="min-h-screen bg-slate-50 text-slate-900 font-sans">
<header className="bg-rose-700 text-white shadow-md">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex items-center justify-between">
<div className="flex items-center space-x-3">
<HeartHandshake className="w-8 h-8 text-rose-300" />
<div>
<h1 className="text-xl font-bold tracking-tight">Rescue-to-Rehab Engine</h1>
<p className="text-rose-200 text-xs uppercase tracking-wider font-semibold">Donator Portal</p>
</div>
</div>
<button onClick={onLogout} className="flex items-center text-rose-200 hover:text-white text-sm font-medium transition-colors">
<LogOut className="w-4 h-4 mr-2" /> Sign Out
</button>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8">
<h2 className="text-2xl font-bold text-slate-900">Critical Cases Awaiting Placement</h2>
<p className="text-slate-600 mt-1">These dogs have been approved by specialized centers but require funding or resources to secure their spot.</p>
</div>
{cases.length === 0 ? (
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 p-12 text-center">
<p className="text-slate-500">There are currently no dogs awaiting conditional placement funding.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{cases.map(c => {
const goal = c.fundingGoal || 0;
const raised = c.fundingRaised || 0;
const percent = goal > 0 ? Math.round((raised / goal) * 100) : 100;
return (
<div key={c.id} className="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden flex flex-col">
<div className="p-6 flex-grow">
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="text-xl font-bold text-slate-900">{c.name}</h3>
<p className="text-sm text-slate-500">{c.breed} {c.age}</p>
</div>
<span className="bg-rose-100 text-rose-800 text-xs font-bold px-2.5 py-1 rounded-full uppercase tracking-wider">
Urgent
</span>
</div>
<div className="bg-rose-50 rounded-xl p-4 mb-6 border border-rose-100">
<p className="text-rose-900 text-sm italic leading-relaxed">
"{c.donorPitch || c.centerDescription || 'This dog needs your help to secure a spot at a specialized center.'}"
</p>
</div>
<div className="space-y-4">
{goal > 0 && (
<div>
<div className="flex justify-between text-sm font-medium mb-1">
<span className="text-slate-700">Funding Goal</span>
<span className="text-slate-900">${raised} / ${goal}</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2.5">
<div className="bg-rose-500 h-2.5 rounded-full" style={{ width: `${percent}%` }}></div>
</div>
</div>
)}
{c.resourcesNeeded && c.resourcesNeeded.length > 0 && (
<div className="flex items-start text-sm text-slate-700">
<Package className="w-4 h-4 mr-2 text-slate-400 mt-0.5" />
<div>
<span className="font-medium">Resources Needed:</span>
<span className="ml-1 text-slate-600">{c.resourcesNeeded.join(', ')}</span>
</div>
</div>
)}
</div>
</div>
<div className="bg-slate-50 px-6 py-4 border-t border-slate-100 flex justify-between items-center">
<span className="text-xs text-slate-500 font-medium uppercase tracking-wider">Matched: {c.placedCenterName}</span>
<button className="bg-rose-600 hover:bg-rose-700 text-white px-4 py-2 rounded-lg text-sm font-bold transition-colors flex items-center">
Fund Case <ArrowRight className="w-4 h-4 ml-1" />
</button>
</div>
</div>
);
})}
</div>
)}
</main>
</div>
);
}

View File

@@ -0,0 +1,197 @@
import React, { useState } from 'react';
import { HeartHandshake, FileText, Building2, LineChart, ArrowRight, Lock, Mail, ChevronLeft, HeartPulse } from 'lucide-react';
export type UserRole = 'donor' | 'uploader' | 'center' | 'admin' | null;
interface LoginPortalProps {
onLogin: (role: UserRole) => void;
}
export default function LoginPortal({ onLogin }: LoginPortalProps) {
const [selectedRole, setSelectedRole] = useState<UserRole>(null);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const roles = [
{
id: 'donor' as UserRole,
title: 'Donator Portal',
icon: HeartHandshake,
color: 'text-rose-600',
bgColor: 'bg-rose-50',
hoverBg: 'hover:bg-rose-50',
borderColor: 'border-rose-200',
ringColor: 'ring-rose-500',
description: 'Fund critical cases and provide required resources for dogs in need.'
},
{
id: 'uploader' as UserRole,
title: 'Rescue / Shelter Intake',
icon: FileText,
color: 'text-teal-600',
bgColor: 'bg-teal-50',
hoverBg: 'hover:bg-teal-50',
borderColor: 'border-teal-200',
ringColor: 'ring-teal-500',
description: 'Submit permanent medical and behavioral records for dogs in need.'
},
{
id: 'center' as UserRole,
title: 'Center Management',
icon: Building2,
color: 'text-amber-600',
bgColor: 'bg-amber-50',
hoverBg: 'hover:bg-amber-50',
borderColor: 'border-amber-200',
ringColor: 'ring-amber-500',
description: 'Manage your facility capacity, specializations, resources, and services.'
},
{
id: 'admin' as UserRole,
title: 'Company Backend',
icon: LineChart,
color: 'text-indigo-600',
bgColor: 'bg-indigo-50',
hoverBg: 'hover:bg-indigo-50',
borderColor: 'border-indigo-200',
ringColor: 'ring-indigo-500',
description: 'Network insights, matching metrics, and system-wide oversight.'
}
];
const handleLogin = (e: React.FormEvent) => {
e.preventDefault();
if (selectedRole) {
onLogin(selectedRole);
}
};
return (
<div className="min-h-screen bg-slate-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8 font-sans selection:bg-teal-100 selection:text-teal-900">
<div className="sm:mx-auto sm:w-full sm:max-w-3xl">
<div className="flex justify-center mb-6">
<div className="bg-teal-800 p-3 rounded-2xl shadow-lg">
<HeartPulse className="w-10 h-10 text-teal-300" />
</div>
</div>
<h2 className="text-center text-3xl font-extrabold text-slate-900 tracking-tight">
Rescue-to-Rehab Engine
</h2>
<p className="mt-2 text-center text-sm text-slate-600 max-w-xl mx-auto">
Welcome to the specialized placement network. Please select your portal to sign in and continue.
</p>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-4xl">
<div className="bg-white py-8 px-4 shadow-xl shadow-slate-200/50 sm:rounded-3xl sm:px-10 border border-slate-100 relative overflow-hidden">
{/* Role Selection View */}
<div className={`transition-all duration-500 ease-in-out ${selectedRole ? 'opacity-0 translate-x-[-100%] absolute inset-0 pointer-events-none' : 'opacity-100 translate-x-0 relative'}`}>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{roles.map((role) => {
const Icon = role.icon;
return (
<button
key={role.id}
onClick={() => setSelectedRole(role.id)}
className={`relative flex flex-col items-start p-6 rounded-2xl border-2 border-slate-100 bg-white hover:border-transparent hover:shadow-md transition-all text-left group ${role.hoverBg}`}
>
<div className={`p-3 rounded-xl ${role.bgColor} ${role.color} mb-4 group-hover:scale-110 transition-transform`}>
<Icon className="w-6 h-6" />
</div>
<h3 className="text-lg font-bold text-slate-900 mb-1">{role.title}</h3>
<p className="text-sm text-slate-500 leading-relaxed">{role.description}</p>
<div className="mt-4 flex items-center text-sm font-semibold text-slate-400 group-hover:text-slate-700 transition-colors">
Select Portal <ArrowRight className="w-4 h-4 ml-1 group-hover:translate-x-1 transition-transform" />
</div>
</button>
);
})}
</div>
</div>
{/* Login Form View */}
<div className={`transition-all duration-500 ease-in-out ${!selectedRole ? 'opacity-0 translate-x-[100%] absolute inset-0 pointer-events-none' : 'opacity-100 translate-x-0 relative'}`}>
{selectedRole && (() => {
const activeRole = roles.find(r => r.id === selectedRole)!;
const Icon = activeRole.icon;
return (
<div className="max-w-md mx-auto py-4">
<button
onClick={() => setSelectedRole(null)}
className="flex items-center text-sm font-medium text-slate-500 hover:text-slate-800 mb-8 transition-colors"
>
<ChevronLeft className="w-4 h-4 mr-1" /> Back to portals
</button>
<div className="text-center mb-8">
<div className={`inline-flex p-4 rounded-2xl ${activeRole.bgColor} ${activeRole.color} mb-4`}>
<Icon className="w-8 h-8" />
</div>
<h3 className="text-2xl font-bold text-slate-900">{activeRole.title} Sign In</h3>
<p className="text-sm text-slate-500 mt-2">{activeRole.description}</p>
</div>
<form onSubmit={handleLogin} className="space-y-5">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Email Address</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Mail className="h-5 w-5 text-slate-400" />
</div>
<input
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className={`block w-full pl-10 pr-3 py-2.5 border border-slate-300 rounded-xl focus:ring-2 focus:outline-none transition-shadow ${activeRole.ringColor} focus:border-transparent`}
placeholder="you@example.com"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Password</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Lock className="h-5 w-5 text-slate-400" />
</div>
<input
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className={`block w-full pl-10 pr-3 py-2.5 border border-slate-300 rounded-xl focus:ring-2 focus:outline-none transition-shadow ${activeRole.ringColor} focus:border-transparent`}
placeholder="••••••••"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<input id="remember-me" type="checkbox" className={`h-4 w-4 rounded border-slate-300 ${activeRole.color.replace('text-', 'text-')}`} />
<label htmlFor="remember-me" className="ml-2 block text-sm text-slate-700">Remember me</label>
</div>
<div className="text-sm">
<a href="#" className={`font-medium ${activeRole.color} hover:opacity-80`}>Forgot password?</a>
</div>
</div>
<button
type="submit"
className={`w-full flex justify-center py-3 px-4 border border-transparent rounded-xl shadow-sm text-sm font-bold text-white ${activeRole.color.replace('text-', 'bg-')} hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2 ${activeRole.ringColor} transition-all`}
>
Sign in to {activeRole.title.split(' ')[0]}
</button>
</form>
</div>
);
})()}
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,470 @@
import React, { useState } from 'react';
import { Activity, HeartPulse, ShieldAlert, Stethoscope, MapPin, DollarSign, Package, HeartHandshake, Loader2, Info, LogOut, Plus, FileText, Image as ImageIcon, ChevronLeft, CheckCircle2, Clock, CheckCircle } from 'lucide-react';
import { DogProfile, MatchResult, DogWithMatches } from '../types';
import { evaluateDogProfile } from '../services/gemini';
import { useAppContext } from '../context/AppContext';
interface RescueIntakeViewProps {
onLogout: () => void;
}
export default function RescueIntakeView({ onLogout }: RescueIntakeViewProps) {
const { dogs, addDog, centers, updateDog } = useAppContext();
const [view, setView] = useState<'dashboard' | 'add' | 'detail' | 'edit'>('dashboard');
const [selectedDog, setSelectedDog] = useState<DogWithMatches | null>(null);
const [activeTab, setActiveTab] = useState<'overview' | 'documents' | 'matches'>('overview');
const [editDogForm, setEditDogForm] = useState<Partial<DogProfile>>({});
const [newDog, setNewDog] = useState<Partial<DogProfile>>({ name: '', breed: '', age: '', vetNotes: '' });
const [isAnalyzing, setIsAnalyzing] = useState(false);
const handleAddDog = async (e: React.FormEvent) => {
e.preventDefault();
if (!newDog.name || !newDog.vetNotes) return;
setIsAnalyzing(true);
try {
const dogToAnalyze = newDog as DogProfile;
const matches = await evaluateDogProfile(dogToAnalyze, centers);
matches.sort((a, b) => b.decision.match_score - a.decision.match_score);
const addedDog: DogWithMatches = {
...dogToAnalyze,
id: `DOG-${Math.random().toString(36).substr(2, 9).toUpperCase()}`,
status: 'analyzed',
dateAdded: new Date().toISOString().split('T')[0],
matches,
placementStatus: 'unplaced',
documents: ['Initial_Intake.pdf'] // Mock document
};
addDog(addedDog);
setNewDog({ name: '', breed: '', age: '', vetNotes: '' });
setView('dashboard');
} catch (err) {
console.error(err);
alert("Failed to analyze profile. Please try again.");
} finally {
setIsAnalyzing(false);
}
};
const openDogDetail = (dog: DogWithMatches) => {
setSelectedDog(dog);
setActiveTab('overview');
setView('detail');
};
return (
<div className="min-h-screen bg-slate-50 text-slate-900 font-sans">
<header className="bg-teal-800 text-white shadow-md">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex items-center justify-between">
<div className="flex items-center space-x-3 cursor-pointer" onClick={() => setView('dashboard')}>
<HeartPulse className="w-8 h-8 text-teal-300" />
<div>
<h1 className="text-xl font-bold tracking-tight">Rescue-to-Rehab Engine</h1>
<p className="text-teal-200 text-xs uppercase tracking-wider font-semibold">Rescue / Shelter Intake Portal</p>
</div>
</div>
<button onClick={onLogout} className="flex items-center text-teal-200 hover:text-white text-sm font-medium transition-colors">
<LogOut className="w-4 h-4 mr-2" /> Sign Out
</button>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* DASHBOARD VIEW */}
{view === 'dashboard' && (
<div>
<div className="flex justify-between items-end mb-8">
<div>
<h2 className="text-2xl font-bold text-slate-900">My Uploaded Dogs</h2>
<p className="text-slate-600 mt-1">Manage your permanent medical and behavioral records.</p>
</div>
<button
onClick={() => setView('add')}
className="bg-teal-600 hover:bg-teal-700 text-white px-4 py-2 rounded-lg text-sm font-bold transition-colors flex items-center"
>
<Plus className="w-4 h-4 mr-2" /> Add New Dog
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{dogs.map(dog => (
<div
key={dog.id}
onClick={() => openDogDetail(dog)}
className="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden cursor-pointer hover:shadow-md hover:border-teal-300 transition-all group"
>
<div className="p-6">
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="text-xl font-bold text-slate-900 group-hover:text-teal-700 transition-colors">{dog.name}</h3>
<p className="text-sm text-slate-500">{dog.breed} {dog.age}</p>
</div>
<div className="flex flex-col items-end gap-2">
{dog.placementStatus !== 'unplaced' ? (
<span className="bg-emerald-100 text-emerald-800 text-xs font-bold px-2.5 py-1 rounded-full flex items-center">
<CheckCircle className="w-3 h-3 mr-1" /> Approved
</span>
) : dog.status === 'analyzed' && (
<span className="bg-teal-100 text-teal-800 text-xs font-bold px-2.5 py-1 rounded-full flex items-center">
<CheckCircle2 className="w-3 h-3 mr-1" /> Analyzed
</span>
)}
</div>
</div>
<p className="text-sm text-slate-600 line-clamp-3 mb-4">{dog.vetNotes}</p>
<div className="flex items-center justify-between text-xs text-slate-500 border-t border-slate-100 pt-4">
<span className="flex items-center"><Clock className="w-3 h-3 mr-1" /> Added {dog.dateAdded}</span>
<span className="font-medium text-teal-600">{dog.matches?.length || 0} Matches Found</span>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* ADD DOG VIEW */}
{view === 'add' && (
<div className="max-w-3xl mx-auto">
<button
onClick={() => setView('dashboard')}
className="flex items-center text-sm font-medium text-slate-500 hover:text-slate-800 mb-6 transition-colors"
>
<ChevronLeft className="w-4 h-4 mr-1" /> Back to Dashboard
</button>
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
<div className="bg-slate-50 px-6 py-4 border-b border-slate-200">
<h2 className="text-lg font-semibold text-slate-800 flex items-center">
<Stethoscope className="w-5 h-5 mr-2 text-teal-600" />
New Intake Profile
</h2>
<p className="text-sm text-slate-500 mt-1">Submit a new dog to the permanent record system. Analysis will run automatically.</p>
</div>
<form onSubmit={handleAddDog} className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="md:col-span-2">
<label className="block text-sm font-medium text-slate-700 mb-1">Dog Name *</label>
<input
type="text" required
value={newDog.name} onChange={e => setNewDog({...newDog, name: e.target.value})}
className="w-full rounded-xl border-slate-300 border px-4 py-2.5 focus:ring-2 focus:ring-teal-500 focus:border-transparent outline-none"
placeholder="e.g. Barnaby"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Breed</label>
<input
type="text"
value={newDog.breed} onChange={e => setNewDog({...newDog, breed: e.target.value})}
className="w-full rounded-xl border-slate-300 border px-4 py-2.5 focus:ring-2 focus:ring-teal-500 focus:border-transparent outline-none"
placeholder="e.g. Pitbull Mix"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Age</label>
<input
type="text"
value={newDog.age} onChange={e => setNewDog({...newDog, age: e.target.value})}
className="w-full rounded-xl border-slate-300 border px-4 py-2.5 focus:ring-2 focus:ring-teal-500 focus:border-transparent outline-none"
placeholder="e.g. 4 years"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Vet Notes & Behavioral Assessment *</label>
<textarea
required rows={5}
value={newDog.vetNotes} onChange={e => setNewDog({...newDog, vetNotes: e.target.value})}
className="w-full rounded-xl border-slate-300 border px-4 py-2.5 focus:ring-2 focus:ring-teal-500 focus:border-transparent outline-none text-sm"
placeholder="Include medical conditions, behavioral notes, required equipment, and urgency..."
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Upload Documents & Photos</label>
<div className="border-2 border-dashed border-slate-300 rounded-xl p-8 text-center hover:bg-slate-50 transition-colors cursor-pointer">
<div className="flex justify-center space-x-4 mb-3">
<FileText className="w-8 h-8 text-slate-400" />
<ImageIcon className="w-8 h-8 text-slate-400" />
</div>
<p className="text-sm font-medium text-slate-700">Click to upload or drag and drop</p>
<p className="text-xs text-slate-500 mt-1">PDF, JPG, PNG up to 10MB</p>
</div>
</div>
<div className="pt-4 border-t border-slate-100 flex justify-end space-x-3">
<button
type="button" onClick={() => setView('dashboard')}
className="px-5 py-2.5 rounded-xl text-sm font-medium text-slate-600 hover:bg-slate-100 transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={isAnalyzing || !newDog.name || !newDog.vetNotes}
className="bg-teal-700 hover:bg-teal-800 text-white font-medium py-2.5 px-6 rounded-xl transition-colors flex items-center disabled:opacity-50 disabled:cursor-not-allowed"
>
{isAnalyzing ? (
<><Loader2 className="w-4 h-4 mr-2 animate-spin" /> Analyzing...</>
) : (
<><Activity className="w-4 h-4 mr-2" /> Submit & Analyze</>
)}
</button>
</div>
</form>
</div>
</div>
)}
{/* DETAIL VIEW */}
{view === 'detail' && selectedDog && (
<div>
<button
onClick={() => setView('dashboard')}
className="flex items-center text-sm font-medium text-slate-500 hover:text-slate-800 mb-6 transition-colors"
>
<ChevronLeft className="w-4 h-4 mr-1" /> Back to Dashboard
</button>
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden mb-6">
<div className="p-6 md:p-8 flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<div className="flex items-center gap-3">
<h2 className="text-3xl font-bold text-slate-900">{selectedDog.name}</h2>
{selectedDog.placementStatus !== 'unplaced' && (
<span className="bg-emerald-100 text-emerald-800 text-xs font-bold px-3 py-1 rounded-full flex items-center">
<CheckCircle className="w-4 h-4 mr-1" /> Approved for {selectedDog.placedCenterName}
</span>
)}
</div>
<p className="text-slate-500 mt-1 flex items-center text-sm">
ID: {selectedDog.id} {selectedDog.breed} {selectedDog.age} Added {selectedDog.dateAdded}
</p>
</div>
<div className="flex space-x-3">
<button
onClick={() => { setEditDogForm(selectedDog); setView('edit'); }}
className="px-4 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-lg text-sm font-medium transition-colors"
>
Edit Info
</button>
</div>
</div>
<div className="border-t border-slate-200 flex overflow-x-auto">
<button
onClick={() => setActiveTab('overview')}
className={`px-6 py-4 text-sm font-semibold border-b-2 whitespace-nowrap transition-colors ${activeTab === 'overview' ? 'border-teal-600 text-teal-700' : 'border-transparent text-slate-500 hover:text-slate-700'}`}
>
Overview & Medical
</button>
<button
onClick={() => setActiveTab('documents')}
className={`px-6 py-4 text-sm font-semibold border-b-2 whitespace-nowrap transition-colors ${activeTab === 'documents' ? 'border-teal-600 text-teal-700' : 'border-transparent text-slate-500 hover:text-slate-700'}`}
>
Documents ({selectedDog.documents?.length || 0})
</button>
<button
onClick={() => setActiveTab('matches')}
className={`px-6 py-4 text-sm font-semibold border-b-2 whitespace-nowrap transition-colors ${activeTab === 'matches' ? 'border-teal-600 text-teal-700' : 'border-transparent text-slate-500 hover:text-slate-700'}`}
>
Best-Fit Centers ({selectedDog.matches?.length || 0})
</button>
</div>
</div>
{/* TAB CONTENT */}
<div className="space-y-6">
{activeTab === 'overview' && (
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 p-6 md:p-8">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Veterinary & Behavioral Notes</h3>
<div className="prose prose-slate max-w-none">
<p className="whitespace-pre-wrap text-slate-700 leading-relaxed">{selectedDog.vetNotes}</p>
</div>
</div>
)}
{activeTab === 'documents' && (
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 p-6 md:p-8">
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-semibold text-slate-900">Uploaded Files</h3>
<button className="text-sm font-medium text-teal-600 hover:text-teal-700 flex items-center">
<Plus className="w-4 h-4 mr-1" /> Add File
</button>
</div>
{selectedDog.documents && selectedDog.documents.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{selectedDog.documents.map((doc, i) => (
<div key={i} className="flex items-center p-4 border border-slate-200 rounded-xl hover:bg-slate-50 cursor-pointer transition-colors">
{doc.endsWith('.pdf') ? <FileText className="w-8 h-8 text-rose-400 mr-3" /> : <ImageIcon className="w-8 h-8 text-blue-400 mr-3" />}
<div className="overflow-hidden">
<p className="text-sm font-medium text-slate-900 truncate">{doc}</p>
<p className="text-xs text-slate-500">Uploaded {selectedDog.dateAdded}</p>
</div>
</div>
))}
</div>
) : (
<p className="text-slate-500 text-sm">No documents uploaded yet.</p>
)}
</div>
)}
{activeTab === 'matches' && (
<div className="space-y-6">
{selectedDog.matches && selectedDog.matches.length > 0 ? (
selectedDog.matches.map((result, index) => {
const isTopMatch = index === 0;
return (
<div key={result.center_id} className={`bg-white rounded-2xl shadow-sm border overflow-hidden transition-all ${isTopMatch ? 'border-teal-500 ring-1 ring-teal-500 shadow-md' : 'border-slate-200'}`}>
{isTopMatch && (
<div className="bg-teal-500 text-white text-xs font-bold uppercase tracking-wider py-1.5 px-6 flex items-center">
<HeartPulse className="w-4 h-4 mr-2" /> Primary Recommendation
</div>
)}
<div className="p-6">
<div className="flex justify-between items-start mb-4">
<h3 className="text-xl font-bold text-slate-900">{result.center_name}</h3>
<div className="flex flex-col items-end">
<div className="flex items-center">
<span className="text-3xl font-black text-slate-800">{result.decision.match_score}</span>
<span className="text-sm font-medium text-slate-400 ml-1">/100</span>
</div>
<span className="text-xs font-medium text-slate-500 uppercase tracking-wider mt-1">Match Score</span>
</div>
</div>
<div className="bg-slate-50 rounded-xl p-4 mb-6 border border-slate-100">
<p className="text-slate-700 text-sm leading-relaxed">
<span className="font-semibold text-slate-900 mr-2">Reasoning:</span>
{result.decision.reasoning}
</p>
</div>
{/* Status Options Summary */}
<div className="flex flex-wrap gap-3">
<span className={`px-3 py-1.5 rounded-lg text-xs font-bold ${result.decision.status_options.accept_immediately ? 'bg-emerald-100 text-emerald-800' : 'bg-slate-100 text-slate-600'}`}>
Immediate Accept: {result.decision.status_options.accept_immediately ? 'YES' : 'NO'}
</span>
{result.decision.status_options.conditional_needs?.funding_required && (
<span className="px-3 py-1.5 rounded-lg text-xs font-bold bg-amber-100 text-amber-800 flex items-center">
<DollarSign className="w-3 h-3 mr-1" /> {result.decision.status_options.conditional_needs.funding_required} Needed
</span>
)}
</div>
</div>
</div>
);
})
) : (
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 p-8 text-center">
<p className="text-slate-500">No matches found or analysis pending.</p>
</div>
)}
</div>
)}
</div>
</div>
)}
{/* EDIT VIEW */}
{view === 'edit' && selectedDog && (
<div className="max-w-3xl mx-auto">
<button
onClick={() => setView('detail')}
className="flex items-center text-sm font-medium text-slate-500 hover:text-slate-800 mb-6 transition-colors"
>
<ChevronLeft className="w-4 h-4 mr-1" /> Back to Profile
</button>
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
<div className="bg-slate-50 px-6 py-4 border-b border-slate-200">
<h2 className="text-lg font-semibold text-slate-800">Edit Profile: {selectedDog.name}</h2>
</div>
<form onSubmit={(e) => {
e.preventDefault();
updateDog(selectedDog.id, editDogForm);
setSelectedDog({ ...selectedDog, ...editDogForm } as DogWithMatches);
setView('detail');
}} className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="md:col-span-2">
<label className="block text-sm font-medium text-slate-700 mb-1">Dog Name *</label>
<input
type="text" required
value={editDogForm.name || ''} onChange={e => setEditDogForm({...editDogForm, name: e.target.value})}
className="w-full rounded-xl border-slate-300 border px-4 py-2.5 focus:ring-2 focus:ring-teal-500 focus:border-transparent outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Breed</label>
<input
type="text"
value={editDogForm.breed || ''} onChange={e => setEditDogForm({...editDogForm, breed: e.target.value})}
className="w-full rounded-xl border-slate-300 border px-4 py-2.5 focus:ring-2 focus:ring-teal-500 focus:border-transparent outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Age</label>
<input
type="text"
value={editDogForm.age || ''} onChange={e => setEditDogForm({...editDogForm, age: e.target.value})}
className="w-full rounded-xl border-slate-300 border px-4 py-2.5 focus:ring-2 focus:ring-teal-500 focus:border-transparent outline-none"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Vet Notes & Behavioral Assessment *</label>
<textarea
required rows={5}
value={editDogForm.vetNotes || ''} onChange={e => setEditDogForm({...editDogForm, vetNotes: e.target.value})}
className="w-full rounded-xl border-slate-300 border px-4 py-2.5 focus:ring-2 focus:ring-teal-500 focus:border-transparent outline-none text-sm"
/>
</div>
<div className="pt-6 border-t border-slate-100 flex justify-between items-center">
<button
type="button"
onClick={() => {
if (window.confirm('Are you sure you want to mark this dog as deceased? This action cannot be undone.')) {
updateDog(selectedDog.id, { status: 'deceased' });
setView('dashboard');
}
}}
className="px-4 py-2 rounded-xl text-sm font-bold text-rose-600 hover:bg-rose-50 transition-colors"
>
Mark Deceased
</button>
<div className="flex space-x-3">
<button
type="button" onClick={() => setView('detail')}
className="px-5 py-2.5 rounded-xl text-sm font-medium text-slate-600 hover:bg-slate-100 transition-colors"
>
Cancel
</button>
<button
type="submit"
className="bg-teal-700 hover:bg-teal-800 text-white font-medium py-2.5 px-6 rounded-xl transition-colors"
>
Save Changes
</button>
</div>
</div>
</form>
</div>
</div>
)}
</main>
</div>
);
}

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}

View File

@@ -0,0 +1,24 @@
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import {defineConfig, loadEnv} from 'vite';
export default defineConfig(({mode}) => {
const env = loadEnv(mode, '.', '');
return {
plugins: [react(), tailwindcss()],
define: {
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
},
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
},
},
server: {
// HMR is disabled in AI Studio via DISABLE_HMR env var.
// Do not modify—file watching is disabled to prevent flickering during agent edits.
hmr: process.env.DISABLE_HMR !== 'true',
},
};
});