OncoCare
unknown
plain_text
9 months ago
49 kB
13
Indexable
import React, { useState, useEffect } from "react";
import { Patient, Appointment, ComplianceRecord } from "@/entities/all";
import { Users, Calendar, CheckCircle, AlertTriangle } from "lucide-react";
import { isFuture, isPast, startOfToday } from "date-fns";
import StatCard from "../components/dashboard/StatCard";
import UpcomingAppointments from "../components/dashboard/UpcomingAppointments";
import RecentActivity from "../components/dashboard/RecentActivity";
export default function Dashboard() {
const [stats, setStats] = useState({
totalPatients: 0,
upcomingAppointments: 0,
complianceRate: 0,
alertsCount: 0,
});
const [appointments, setAppointments] = useState([]);
const [recentActivity, setRecentActivity] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadDashboardData();
}, []);
const loadDashboardData = async () => {
setLoading(true);
const patients = await Patient.list();
const allAppointments = await Appointment.list("-scheduled_date");
const records = await ComplianceRecord.list("-created_date", 10);
const today = startOfToday();
const upcoming = allAppointments.filter(apt =>
isFuture(new Date(apt.scheduled_date)) && apt.status === "Scheduled"
);
const missed = allAppointments.filter(apt =>
isPast(new Date(apt.scheduled_date)) && apt.status === "Scheduled"
);
const compliantRecords = records.filter(r => r.status === "Compliant").length;
const complianceRate = records.length > 0
? Math.round((compliantRecords / records.length) * 100)
: 0;
setStats({
totalPatients: patients.length,
upcomingAppointments: upcoming.length,
complianceRate,
alertsCount: missed.length,
});
setAppointments(upcoming.slice(0, 5));
setRecentActivity(records);
setLoading(false);
};
return (
<div className="p-4 md:p-8">
<div className="max-w-7xl mx-auto">
<div className="mb-8">
<h1 className="text-3xl font-bold text-slate-900 mb-2">Dashboard</h1>
<p className="text-slate-600">Overview of patient compliance and appointments</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<StatCard
title="Total Patients"
value={stats.totalPatients}
icon={Users}
color="blue"
/>
<StatCard
title="Upcoming Appointments"
value={stats.upcomingAppointments}
icon={Calendar}
color="purple"
/>
<StatCard
title="Compliance Rate"
value={`${stats.complianceRate}%`}
icon={CheckCircle}
color="green"
/>
<StatCard
title="Alerts"
value={stats.alertsCount}
subtitle={stats.alertsCount > 0 ? "Requires attention" : "All clear"}
icon={AlertTriangle}
color="amber"
/>
</div>
<div className="grid lg:grid-cols-2 gap-6">
<UpcomingAppointments appointments={appointments} />
<RecentActivity records={recentActivity} />
</div>
</div>
</div>
);
}
import React, { useState, useEffect } from "react";
import { Patient } from "@/entities/Patient";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Plus, Search } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import PatientCard from "../components/patients/PatientCard";
import PatientForm from "../components/patients/PatientForm";
export default function Patients() {
const navigate = useNavigate();
const [patients, setPatients] = useState([]);
const [filteredPatients, setFilteredPatients] = useState([]);
const [searchQuery, setSearchQuery] = useState("");
const [showForm, setShowForm] = useState(false);
const [editingPatient, setEditingPatient] = useState(null);
const [isProcessing, setIsProcessing] = useState(false);
useEffect(() => {
loadPatients();
}, []);
useEffect(() => {
if (searchQuery) {
const filtered = patients.filter(patient =>
patient.full_name.toLowerCase().includes(searchQuery.toLowerCase()) ||
patient.cancer_type.toLowerCase().includes(searchQuery.toLowerCase()) ||
patient.medical_record_number?.toLowerCase().includes(searchQuery.toLowerCase())
);
setFilteredPatients(filtered);
} else {
setFilteredPatients(patients);
}
}, [searchQuery, patients]);
const loadPatients = async () => {
const data = await Patient.list("-created_date");
setPatients(data);
setFilteredPatients(data);
};
const handleSave = async (patientData) => {
setIsProcessing(true);
if (editingPatient) {
await Patient.update(editingPatient.id, patientData);
} else {
await Patient.create(patientData);
}
setShowForm(false);
setEditingPatient(null);
loadPatients();
setIsProcessing(false);
};
const handleEdit = (patient) => {
setEditingPatient(patient);
setShowForm(true);
};
return (
<div className="p-4 md:p-8">
<div className="max-w-7xl mx-auto">
<div className="mb-8">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-6">
<div>
<h1 className="text-3xl font-bold text-slate-900 mb-2">Patients</h1>
<p className="text-slate-600">Manage patient records and compliance</p>
</div>
<Button
onClick={() => setShowForm(true)}
className="bg-blue-600 hover:bg-blue-700"
>
<Plus className="w-4 h-4 mr-2" />
Add Patient
</Button>
</div>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" />
<Input
placeholder="Search patients by name, cancer type, or record number..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
</div>
{showForm ? (
<PatientForm
patient={editingPatient}
onSave={handleSave}
onCancel={() => {
setShowForm(false);
setEditingPatient(null);
}}
isProcessing={isProcessing}
/>
) : (
<>
{filteredPatients.length === 0 ? (
<div className="text-center py-16">
<div className="w-20 h-20 mx-auto mb-4 rounded-full bg-slate-100 flex items-center justify-center">
<Search className="w-10 h-10 text-slate-400" />
</div>
<h3 className="text-lg font-semibold text-slate-900 mb-2">
{searchQuery ? "No patients found" : "No patients yet"}
</h3>
<p className="text-slate-600 mb-6">
{searchQuery ? "Try adjusting your search" : "Add your first patient to get started"}
</p>
{!searchQuery && (
<Button onClick={() => setShowForm(true)} className="bg-blue-600 hover:bg-blue-700">
<Plus className="w-4 h-4 mr-2" />
Add Patient
</Button>
)}
</div>
) : (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredPatients.map((patient) => (
<PatientCard
key={patient.id}
patient={patient}
onClick={() => handleEdit(patient)}
/>
))}
</div>
)}
</>
)}
</div>
</div>
);
}
import React, { useState, useEffect } from "react";
import { Appointment } from "@/entities/Appointment";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Plus, Calendar, Clock, User, MapPin } from "lucide-react";
import { format, isFuture, isPast } from "date-fns";
import AppointmentForm from "../components/appointments/AppointmentForm";
export default function Appointments() {
const [appointments, setAppointments] = useState([]);
const [showForm, setShowForm] = useState(false);
const [editingAppointment, setEditingAppointment] = useState(null);
const [isProcessing, setIsProcessing] = useState(false);
useEffect(() => {
loadAppointments();
}, []);
const loadAppointments = async () => {
const data = await Appointment.list("-scheduled_date");
setAppointments(data);
};
const handleSave = async (appointmentData) => {
setIsProcessing(true);
if (editingAppointment) {
await Appointment.update(editingAppointment.id, appointmentData);
} else {
await Appointment.create(appointmentData);
}
setShowForm(false);
setEditingAppointment(null);
loadAppointments();
setIsProcessing(false);
};
const handleEdit = (appointment) => {
setEditingAppointment(appointment);
setShowForm(true);
};
const getStatusColor = (date, status) => {
if (status === "Completed") return "bg-emerald-100 text-emerald-800 border-emerald-200";
if (status === "Missed") return "bg-red-100 text-red-800 border-red-200";
if (status === "Cancelled") return "bg-slate-100 text-slate-800 border-slate-200";
if (isPast(new Date(date)) && status === "Scheduled") return "bg-amber-100 text-amber-800 border-amber-200";
return "bg-blue-100 text-blue-800 border-blue-200";
};
return (
<div className="p-4 md:p-8">
<div className="max-w-7xl mx-auto">
<div className="mb-8">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-6">
<div>
<h1 className="text-3xl font-bold text-slate-900 mb-2">Appointments</h1>
<p className="text-slate-600">Manage patient appointments and follow-ups</p>
</div>
<Button
onClick={() => setShowForm(true)}
className="bg-blue-600 hover:bg-blue-700"
>
<Plus className="w-4 h-4 mr-2" />
Add Appointment
</Button>
</div>
</div>
{showForm ? (
<AppointmentForm
appointment={editingAppointment}
onSave={handleSave}
onCancel={() => {
setShowForm(false);
setEditingAppointment(null);
}}
isProcessing={isProcessing}
/>
) : (
<>
{appointments.length === 0 ? (
<div className="text-center py-16">
<div className="w-20 h-20 mx-auto mb-4 rounded-full bg-slate-100 flex items-center justify-center">
<Calendar className="w-10 h-10 text-slate-400" />
</div>
<h3 className="text-lg font-semibold text-slate-900 mb-2">No appointments yet</h3>
<p className="text-slate-600 mb-6">Schedule your first appointment to get started</p>
<Button onClick={() => setShowForm(true)} className="bg-blue-600 hover:bg-blue-700">
<Plus className="w-4 h-4 mr-2" />
Add Appointment
</Button>
</div>
) : (
<div className="grid gap-6">
{appointments.map((appointment) => {
const appointmentDate = new Date(appointment.scheduled_date);
return (
<Card
key={appointment.id}
className="border-slate-200 hover:border-blue-300 hover:shadow-md transition-all duration-300 cursor-pointer"
onClick={() => handleEdit(appointment)}
>
<CardContent className="p-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex-1">
<div className="flex items-start justify-between mb-3">
<div>
<h3 className="font-semibold text-slate-900 text-lg mb-1">
{appointment.patient_name}
</h3>
<p className="text-slate-600">{appointment.appointment_type}</p>
</div>
<Badge className={`${getStatusColor(appointmentDate, appointment.status)} border text-sm`}>
{appointment.status}
</Badge>
</div>
<div className="grid md:grid-cols-2 gap-3 text-sm text-slate-600">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-slate-400" />
<span>{format(appointmentDate, "MMMM d, yyyy")}</span>
</div>
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-slate-400" />
<span>{format(appointmentDate, "h:mm a")}</span>
</div>
{appointment.provider && (
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-slate-400" />
<span>{appointment.provider}</span>
</div>
)}
{appointment.location && (
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4 text-slate-400" />
<span>{appointment.location}</span>
</div>
)}
</div>
{appointment.notes && (
<p className="mt-3 text-sm text-slate-600 border-t border-slate-100 pt-3">
{appointment.notes}
</p>
)}
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
)}
</>
)}
</div>
</div>
);
}
import React from 'react';
import { Card } from "@/components/ui/card";
import { motion } from "framer-motion";
export default function StatCard({ title, value, subtitle, icon: Icon, trend, color = "blue" }) {
const colorClasses = {
blue: "from-blue-500 to-blue-600",
green: "from-emerald-500 to-emerald-600",
amber: "from-amber-500 to-amber-600",
purple: "from-purple-500 to-purple-600",
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<Card className="relative overflow-hidden border-slate-200 shadow-sm hover:shadow-md transition-shadow duration-300">
<div className={`absolute top-0 right-0 w-32 h-32 bg-gradient-to-br ${colorClasses[color]} opacity-5 rounded-full transform translate-x-12 -translate-y-12`} />
<div className="p-6">
<div className="flex items-start justify-between mb-4">
<div className={`p-3 rounded-xl bg-gradient-to-br ${colorClasses[color]} bg-opacity-10`}>
<Icon className={`w-6 h-6 text-${color}-600`} />
</div>
{trend && (
<span className={`text-sm font-medium ${trend.positive ? 'text-emerald-600' : 'text-red-600'}`}>
{trend.value}
</span>
)}
</div>
<p className="text-sm font-medium text-slate-600 mb-1">{title}</p>
<h3 className="text-3xl font-bold text-slate-900 mb-1">{value}</h3>
{subtitle && <p className="text-xs text-slate-500">{subtitle}</p>}
</div>
</Card>
</motion.div>
);
}
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Calendar, Clock, User } from "lucide-react";
import { format, isToday, isTomorrow, isPast } from "date-fns";
export default function UpcomingAppointments({ appointments }) {
const getDateLabel = (date) => {
if (isToday(date)) return "Today";
if (isTomorrow(date)) return "Tomorrow";
return format(date, "MMM d");
};
const getStatusColor = (date, status) => {
if (status === "Completed") return "bg-emerald-100 text-emerald-800 border-emerald-200";
if (status === "Missed") return "bg-red-100 text-red-800 border-red-200";
if (isPast(date) && status === "Scheduled") return "bg-amber-100 text-amber-800 border-amber-200";
return "bg-blue-100 text-blue-800 border-blue-200";
};
return (
<Card className="border-slate-200 shadow-sm">
<CardHeader className="border-b border-slate-100 pb-4">
<CardTitle className="text-lg font-semibold text-slate-900 flex items-center gap-2">
<Calendar className="w-5 h-5" />
Upcoming Appointments
</CardTitle>
</CardHeader>
<CardContent className="p-0">
{appointments.length === 0 ? (
<div className="p-8 text-center text-slate-500">
<Calendar className="w-12 h-12 mx-auto mb-3 opacity-30" />
<p>No upcoming appointments</p>
</div>
) : (
<div className="divide-y divide-slate-100">
{appointments.map((appointment) => {
const appointmentDate = new Date(appointment.scheduled_date);
return (
<div key={appointment.id} className="p-4 hover:bg-slate-50 transition-colors duration-200">
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<User className="w-4 h-4 text-slate-400" />
<span className="font-medium text-slate-900">{appointment.patient_name}</span>
</div>
<p className="text-sm text-slate-600 mb-2">{appointment.appointment_type}</p>
<div className="flex items-center gap-3 text-xs text-slate-500">
<span className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
{getDateLabel(appointmentDate)}
</span>
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{format(appointmentDate, "h:mm a")}
</span>
</div>
</div>
<Badge className={`${getStatusColor(appointmentDate, appointment.status)} border text-xs`}>
{appointment.status}
</Badge>
</div>
</div>
);
})}
</div>
)}
</CardContent>
</Card>
);
}
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Activity, CheckCircle, XCircle, Clock } from "lucide-react";
import { format } from "date-fns";
export default function RecentActivity({ records }) {
const getStatusIcon = (status) => {
switch (status) {
case "Compliant":
return <CheckCircle className="w-4 h-4 text-emerald-600" />;
case "Non-Compliant":
return <XCircle className="w-4 h-4 text-red-600" />;
case "Partial":
return <Clock className="w-4 h-4 text-amber-600" />;
default:
return <Clock className="w-4 h-4 text-slate-400" />;
}
};
return (
<Card className="border-slate-200 shadow-sm">
<CardHeader className="border-b border-slate-100 pb-4">
<CardTitle className="text-lg font-semibold text-slate-900 flex items-center gap-2">
<Activity className="w-5 h-5" />
Recent Activity
</CardTitle>
</CardHeader>
<CardContent className="p-0">
{records.length === 0 ? (
<div className="p-8 text-center text-slate-500">
<Activity className="w-12 h-12 mx-auto mb-3 opacity-30" />
<p>No recent activity</p>
</div>
) : (
<div className="divide-y divide-slate-100">
{records.map((record) => (
<div key={record.id} className="p-4 hover:bg-slate-50 transition-colors duration-200">
<div className="flex items-start gap-3">
<div className="mt-0.5">{getStatusIcon(record.status)}</div>
<div className="flex-1">
<p className="font-medium text-slate-900 text-sm mb-1">{record.patient_name}</p>
<p className="text-sm text-slate-600 mb-1">{record.description}</p>
<div className="flex items-center gap-2 text-xs text-slate-500">
<span>{record.record_type}</span>
<span>•</span>
<span>{format(new Date(record.due_date), "MMM d, yyyy")}</span>
</div>
</div>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
);
}
import React from 'react';
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { User, Calendar, Activity, ChevronRight } from "lucide-react";
import { motion } from "framer-motion";
export default function PatientCard({ patient, onClick }) {
const getStatusColor = (status) => {
switch (status) {
case "Active Treatment":
return "bg-blue-100 text-blue-800 border-blue-200";
case "Follow-up":
return "bg-purple-100 text-purple-800 border-purple-200";
case "Remission":
return "bg-emerald-100 text-emerald-800 border-emerald-200";
case "Surveillance":
return "bg-slate-100 text-slate-800 border-slate-200";
default:
return "bg-slate-100 text-slate-800 border-slate-200";
}
};
const getComplianceColor = (score) => {
if (score >= 80) return "text-emerald-600";
if (score >= 60) return "text-amber-600";
return "text-red-600";
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<Card
className="border-slate-200 hover:border-blue-300 hover:shadow-md transition-all duration-300 cursor-pointer"
onClick={onClick}
>
<div className="p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-start gap-3">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white font-semibold text-lg shadow-md">
{patient.full_name.charAt(0)}
</div>
<div>
<h3 className="font-semibold text-slate-900 text-lg mb-1">{patient.full_name}</h3>
<p className="text-sm text-slate-600">{patient.cancer_type}</p>
</div>
</div>
<ChevronRight className="w-5 h-5 text-slate-400" />
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Badge className={`${getStatusColor(patient.status)} border text-xs`}>
{patient.status}
</Badge>
{patient.compliance_score !== undefined && (
<div className="flex items-center gap-2">
<Activity className="w-4 h-4 text-slate-400" />
<span className={`text-sm font-semibold ${getComplianceColor(patient.compliance_score)}`}>
{patient.compliance_score}% Compliant
</span>
</div>
)}
</div>
<div className="flex items-center gap-4 text-xs text-slate-500">
{patient.cancer_stage && (
<span className="flex items-center gap-1">
<Activity className="w-3 h-3" />
{patient.cancer_stage}
</span>
)}
{patient.primary_oncologist && (
<span className="flex items-center gap-1">
<User className="w-3 h-3" />
Dr. {patient.primary_oncologist}
</span>
)}
</div>
</div>
</div>
</Card>
</motion.div>
);
}
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { X } from "lucide-react";
export default function PatientForm({ patient, onSave, onCancel, isProcessing }) {
const [formData, setFormData] = useState(patient || {
full_name: "",
date_of_birth: "",
medical_record_number: "",
cancer_type: "",
cancer_stage: "",
diagnosis_date: "",
treatment_plan: "",
primary_oncologist: "",
phone: "",
email: "",
emergency_contact: "",
status: "Active Treatment",
compliance_score: 0,
notes: "",
});
const handleSubmit = (e) => {
e.preventDefault();
onSave(formData);
};
const handleChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
return (
<Card className="border-slate-200 shadow-lg">
<CardHeader className="border-b border-slate-100">
<div className="flex items-center justify-between">
<CardTitle className="text-xl font-semibold text-slate-900">
{patient ? "Edit Patient" : "New Patient"}
</CardTitle>
<Button variant="ghost" size="icon" onClick={onCancel}>
<X className="w-5 h-5" />
</Button>
</div>
</CardHeader>
<form onSubmit={handleSubmit}>
<CardContent className="p-6 space-y-6">
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="full_name">Full Name *</Label>
<Input
id="full_name"
value={formData.full_name}
onChange={(e) => handleChange("full_name", e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="date_of_birth">Date of Birth</Label>
<Input
id="date_of_birth"
type="date"
value={formData.date_of_birth}
onChange={(e) => handleChange("date_of_birth", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="medical_record_number">Medical Record #</Label>
<Input
id="medical_record_number"
value={formData.medical_record_number}
onChange={(e) => handleChange("medical_record_number", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="cancer_type">Cancer Type *</Label>
<Input
id="cancer_type"
value={formData.cancer_type}
onChange={(e) => handleChange("cancer_type", e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="cancer_stage">Cancer Stage</Label>
<Select
value={formData.cancer_stage}
onValueChange={(value) => handleChange("cancer_stage", value)}
>
<SelectTrigger>
<SelectValue placeholder="Select stage" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Stage I">Stage I</SelectItem>
<SelectItem value="Stage II">Stage II</SelectItem>
<SelectItem value="Stage III">Stage III</SelectItem>
<SelectItem value="Stage IV">Stage IV</SelectItem>
<SelectItem value="In Remission">In Remission</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="diagnosis_date">Diagnosis Date</Label>
<Input
id="diagnosis_date"
type="date"
value={formData.diagnosis_date}
onChange={(e) => handleChange("diagnosis_date", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="primary_oncologist">Primary Oncologist</Label>
<Input
id="primary_oncologist"
value={formData.primary_oncologist}
onChange={(e) => handleChange("primary_oncologist", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="status">Status</Label>
<Select
value={formData.status}
onValueChange={(value) => handleChange("status", value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Active Treatment">Active Treatment</SelectItem>
<SelectItem value="Follow-up">Follow-up</SelectItem>
<SelectItem value="Remission">Remission</SelectItem>
<SelectItem value="Surveillance">Surveillance</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="phone">Phone</Label>
<Input
id="phone"
type="tel"
value={formData.phone}
onChange={(e) => handleChange("phone", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleChange("email", e.target.value)}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="treatment_plan">Treatment Plan</Label>
<Textarea
id="treatment_plan"
value={formData.treatment_plan}
onChange={(e) => handleChange("treatment_plan", e.target.value)}
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="emergency_contact">Emergency Contact</Label>
<Input
id="emergency_contact"
value={formData.emergency_contact}
onChange={(e) => handleChange("emergency_contact", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="notes">Notes</Label>
<Textarea
id="notes"
value={formData.notes}
onChange={(e) => handleChange("notes", e.target.value)}
rows={3}
/>
</div>
</CardContent>
<CardFooter className="border-t border-slate-100 p-6 flex justify-end gap-3">
<Button type="button" variant="outline" onClick={onCancel} disabled={isProcessing}>
Cancel
</Button>
<Button type="submit" disabled={isProcessing} className="bg-blue-600 hover:bg-blue-700">
{isProcessing ? "Saving..." : "Save Patient"}
</Button>
</CardFooter>
</form>
</Card>
);
}
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { X } from "lucide-react";
import { Patient } from "@/entities/Patient";
export default function AppointmentForm({ appointment, onSave, onCancel, isProcessing }) {
const [patients, setPatients] = useState([]);
const [formData, setFormData] = useState(appointment || {
patient_id: "",
patient_name: "",
appointment_type: "",
scheduled_date: "",
status: "Scheduled",
provider: "",
location: "",
notes: "",
});
useEffect(() => {
loadPatients();
}, []);
const loadPatients = async () => {
const data = await Patient.list();
setPatients(data);
};
const handleSubmit = (e) => {
e.preventDefault();
onSave(formData);
};
const handleChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handlePatientChange = (patientId) => {
const patient = patients.find(p => p.id === patientId);
setFormData(prev => ({
...prev,
patient_id: patientId,
patient_name: patient?.full_name || ""
}));
};
return (
<Card className="border-slate-200 shadow-lg">
<CardHeader className="border-b border-slate-100">
<div className="flex items-center justify-between">
<CardTitle className="text-xl font-semibold text-slate-900">
{appointment ? "Edit Appointment" : "New Appointment"}
</CardTitle>
<Button variant="ghost" size="icon" onClick={onCancel}>
<X className="w-5 h-5" />
</Button>
</div>
</CardHeader>
<form onSubmit={handleSubmit}>
<CardContent className="p-6 space-y-6">
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="patient_id">Patient *</Label>
<Select
value={formData.patient_id}
onValueChange={handlePatientChange}
required
>
<SelectTrigger>
<SelectValue placeholder="Select patient" />
</SelectTrigger>
<SelectContent>
{patients.map(patient => (
<SelectItem key={patient.id} value={patient.id}>
{patient.full_name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="appointment_type">Appointment Type *</Label>
<Select
value={formData.appointment_type}
onValueChange={(value) => handleChange("appointment_type", value)}
required
>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Follow-up Visit">Follow-up Visit</SelectItem>
<SelectItem value="Imaging Scan">Imaging Scan</SelectItem>
<SelectItem value="Blood Work">Blood Work</SelectItem>
<SelectItem value="Chemotherapy">Chemotherapy</SelectItem>
<SelectItem value="Radiation">Radiation</SelectItem>
<SelectItem value="Consultation">Consultation</SelectItem>
<SelectItem value="Surgery">Surgery</SelectItem>
<SelectItem value="Other">Other</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="scheduled_date">Date & Time *</Label>
<Input
id="scheduled_date"
type="datetime-local"
value={formData.scheduled_date}
onChange={(e) => handleChange("scheduled_date", e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="status">Status</Label>
<Select
value={formData.status}
onValueChange={(value) => handleChange("status", value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Scheduled">Scheduled</SelectItem>
<SelectItem value="Completed">Completed</SelectItem>
<SelectItem value="Missed">Missed</SelectItem>
<SelectItem value="Cancelled">Cancelled</SelectItem>
<SelectItem value="Rescheduled">Rescheduled</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="provider">Provider</Label>
<Input
id="provider"
value={formData.provider}
onChange={(e) => handleChange("provider", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="location">Location</Label>
<Input
id="location"
value={formData.location}
onChange={(e) => handleChange("location", e.target.value)}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="notes">Notes</Label>
<Textarea
id="notes"
value={formData.notes}
onChange={(e) => handleChange("notes", e.target.value)}
rows={3}
/>
</div>
</CardContent>
<CardFooter className="border-t border-slate-100 p-6 flex justify-end gap-3">
<Button type="button" variant="outline" onClick={onCancel} disabled={isProcessing}>
Cancel
</Button>
<Button type="submit" disabled={isProcessing} className="bg-blue-600 hover:bg-blue-700">
{isProcessing ? "Saving..." : "Save Appointment"}
</Button>
</CardFooter>
</form>
</Card>
);
}
{
"name": "Patient",
"type": "object",
"properties": {
"full_name": {
"type": "string",
"description": "Patient's full name"
},
"date_of_birth": {
"type": "string",
"format": "date",
"description": "Patient's date of birth"
},
"medical_record_number": {
"type": "string",
"description": "Unique medical record identifier"
},
"cancer_type": {
"type": "string",
"description": "Type of cancer diagnosis"
},
"cancer_stage": {
"type": "string",
"enum": [
"Stage I",
"Stage II",
"Stage III",
"Stage IV",
"In Remission"
],
"description": "Cancer stage"
},
"diagnosis_date": {
"type": "string",
"format": "date",
"description": "Date of diagnosis"
},
"treatment_plan": {
"type": "string",
"description": "Current treatment plan"
},
"primary_oncologist": {
"type": "string",
"description": "Name of primary oncologist"
},
"phone": {
"type": "string",
"description": "Patient contact phone"
},
"email": {
"type": "string",
"format": "email",
"description": "Patient email"
},
"emergency_contact": {
"type": "string",
"description": "Emergency contact information"
},
"compliance_score": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "Overall compliance percentage"
},
"status": {
"type": "string",
"enum": [
"Active Treatment",
"Follow-up",
"Remission",
"Surveillance"
],
"default": "Active Treatment",
"description": "Current patient status"
},
"notes": {
"type": "string",
"description": "Additional notes"
}
},
"required": [
"full_name",
"cancer_type"
]
}
{
"name": "Appointment",
"type": "object",
"properties": {
"patient_id": {
"type": "string",
"description": "Reference to patient"
},
"patient_name": {
"type": "string",
"description": "Patient name for easy reference"
},
"appointment_type": {
"type": "string",
"enum": [
"Follow-up Visit",
"Imaging Scan",
"Blood Work",
"Chemotherapy",
"Radiation",
"Consultation",
"Surgery",
"Other"
],
"description": "Type of appointment"
},
"scheduled_date": {
"type": "string",
"format": "date-time",
"description": "Scheduled appointment date and time"
},
"status": {
"type": "string",
"enum": [
"Scheduled",
"Completed",
"Missed",
"Cancelled",
"Rescheduled"
],
"default": "Scheduled",
"description": "Appointment status"
},
"provider": {
"type": "string",
"description": "Healthcare provider name"
},
"location": {
"type": "string",
"description": "Appointment location"
},
"notes": {
"type": "string",
"description": "Appointment notes"
},
"reminder_sent": {
"type": "boolean",
"default": false,
"description": "Whether reminder was sent"
}
},
"required": [
"patient_id",
"patient_name",
"appointment_type",
"scheduled_date"
]
}
{
"name": "ComplianceRecord",
"type": "object",
"properties": {
"patient_id": {
"type": "string",
"description": "Reference to patient"
},
"patient_name": {
"type": "string",
"description": "Patient name for easy reference"
},
"record_type": {
"type": "string",
"enum": [
"Medication",
"Test",
"Appointment",
"Lifestyle",
"Other"
],
"description": "Type of compliance record"
},
"description": {
"type": "string",
"description": "What was being tracked"
},
"due_date": {
"type": "string",
"format": "date",
"description": "When this was due"
},
"completion_date": {
"type": "string",
"format": "date",
"description": "When this was completed"
},
"status": {
"type": "string",
"enum": [
"Compliant",
"Non-Compliant",
"Partial",
"Pending"
],
"default": "Pending",
"description": "Compliance status"
},
"notes": {
"type": "string",
"description": "Additional notes"
}
},
"required": [
"patient_id",
"patient_name",
"record_type",
"description",
"due_date"
]
}
import React from "react";
import { Link, useLocation } from "react-router-dom";
import { createPageUrl } from "@/utils";
import { LayoutDashboard, Users, Calendar, Activity } from "lucide-react";
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarHeader,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
const navigationItems = [
{
title: "Dashboard",
url: createPageUrl("Dashboard"),
icon: LayoutDashboard,
},
{
title: "Patients",
url: createPageUrl("Patients"),
icon: Users,
},
{
title: "Appointments",
url: createPageUrl("Appointments"),
icon: Calendar,
},
];
export default function Layout({ children, currentPageName }) {
const location = useLocation();
return (
<SidebarProvider>
<div className="min-h-screen flex w-full bg-gradient-to-br from-slate-50 to-blue-50">
<style>{`
:root {
--primary: 213 94% 45%;
--primary-foreground: 0 0% 100%;
--accent: 152 69% 47%;
--accent-foreground: 0 0% 100%;
--destructive: 0 84% 60%;
}
`}</style>
<Sidebar className="border-r border-slate-200 bg-white/80 backdrop-blur-sm">
<SidebarHeader className="border-b border-slate-200 p-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-blue-600 to-blue-700 rounded-xl flex items-center justify-center shadow-lg">
<Activity className="w-6 h-6 text-white" />
</div>
<div>
<h2 className="font-semibold text-slate-900 text-lg">OncoTrack</h2>
<p className="text-xs text-slate-500">Compliance Management</p>
</div>
</div>
</SidebarHeader>
<SidebarContent className="p-3">
<SidebarGroup>
<SidebarGroupLabel className="text-xs font-medium text-slate-500 uppercase tracking-wider px-3 py-2">
Navigation
</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{navigationItems.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
asChild
className={`hover:bg-blue-50 hover:text-blue-700 transition-all duration-200 rounded-lg mb-1 ${
location.pathname === item.url ? 'bg-blue-50 text-blue-700 shadow-sm' : ''
}`}
>
<Link to={item.url} className="flex items-center gap-3 px-3 py-2.5">
<item.icon className="w-5 h-5" />
<span className="font-medium">{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
<main className="flex-1 flex flex-col">
<header className="bg-white/80 backdrop-blur-sm border-b border-slate-200 px-6 py-4 md:hidden">
<div className="flex items-center gap-4">
<SidebarTrigger className="hover:bg-slate-100 p-2 rounded-lg transition-colors duration-200" />
<h1 className="text-xl font-semibold text-slate-900">OncoTrack</h1>
</div>
</header>
<div className="flex-1 overflow-auto">
{children}
</div>
</main>
</div>
</SidebarProvider>
);
}Editor is loading...
Leave a Comment