471 lines
25 KiB
TypeScript
471 lines
25 KiB
TypeScript
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>
|
|
);
|
|
}
|
|
|