Import mockup
This commit is contained in:
9
aistudio.google.project/.env.example
Normal file
9
aistudio.google.project/.env.example
Normal 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
8
aistudio.google.project/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
coverage/
|
||||
.DS_Store
|
||||
*.log
|
||||
.env*
|
||||
!.env.example
|
||||
10
aistudio.google.project/README-FIRST.md
Normal file
10
aistudio.google.project/README-FIRST.md
Normal 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.
|
||||
|
||||
20
aistudio.google.project/README.md
Normal file
20
aistudio.google.project/README.md
Normal 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`
|
||||
13
aistudio.google.project/index.html
Normal file
13
aistudio.google.project/index.html
Normal 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>
|
||||
|
||||
5
aistudio.google.project/metadata.json
Normal file
5
aistudio.google.project/metadata.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Rescue-to-Rehab Engine",
|
||||
"description": "Matches high-needs rescue dogs with specialized rehabilitation centers using AI.",
|
||||
"requestFramePermissions": []
|
||||
}
|
||||
34
aistudio.google.project/package.json
Normal file
34
aistudio.google.project/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
25
aistudio.google.project/src/App.tsx
Normal file
25
aistudio.google.project/src/App.tsx
Normal 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;
|
||||
}
|
||||
177
aistudio.google.project/src/context/AppContext.tsx
Normal file
177
aistudio.google.project/src/context/AppContext.tsx
Normal 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;
|
||||
};
|
||||
36
aistudio.google.project/src/data/centers.ts
Normal file
36
aistudio.google.project/src/data/centers.ts
Normal 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.'
|
||||
}
|
||||
];
|
||||
1
aistudio.google.project/src/index.css
Normal file
1
aistudio.google.project/src/index.css
Normal file
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
13
aistudio.google.project/src/main.tsx
Normal file
13
aistudio.google.project/src/main.tsx
Normal 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>,
|
||||
);
|
||||
97
aistudio.google.project/src/services/gemini.ts
Normal file
97
aistudio.google.project/src/services/gemini.ts
Normal 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;
|
||||
}
|
||||
63
aistudio.google.project/src/types.ts
Normal file
63
aistudio.google.project/src/types.ts
Normal 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;
|
||||
}
|
||||
|
||||
76
aistudio.google.project/src/views/AdminView.tsx
Normal file
76
aistudio.google.project/src/views/AdminView.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
832
aistudio.google.project/src/views/CenterView.tsx
Normal file
832
aistudio.google.project/src/views/CenterView.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
107
aistudio.google.project/src/views/DonorView.tsx
Normal file
107
aistudio.google.project/src/views/DonorView.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
197
aistudio.google.project/src/views/LoginPortal.tsx
Normal file
197
aistudio.google.project/src/views/LoginPortal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
470
aistudio.google.project/src/views/RescueIntakeView.tsx
Normal file
470
aistudio.google.project/src/views/RescueIntakeView.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
26
aistudio.google.project/tsconfig.json
Normal file
26
aistudio.google.project/tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
24
aistudio.google.project/vite.config.ts
Normal file
24
aistudio.google.project/vite.config.ts
Normal 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',
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user