Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/app/api/analytics/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Analytics API module
44 changes: 44 additions & 0 deletions backend/app/api/analytics/analytics_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from fastapi import APIRouter
from app.services.analytics_service import (
get_top_doctors,
get_department_load,
get_monthly_visits,
get_doctor_workload,
get_kpis,
)


router = APIRouter(
prefix="/analytics",
tags=["Analytics"]
)


@router.get("/top-doctors")
def list_top_doctors():
"""Get the top 10 doctors by number of appointments"""
return get_top_doctors()


@router.get("/department-load")
def list_department_load():
"""Get total visits per department"""
return get_department_load()


@router.get("/monthly-visits")
def list_monthly_visits():
"""Get monthly patient visits over time"""
return get_monthly_visits()


@router.get("/doctor-workload")
def list_doctor_workload():
"""Get total visits per doctor"""
return get_doctor_workload()


@router.get("/kpis")
def list_kpis():
"""Get key performance indicators"""
return get_kpis()
178 changes: 178 additions & 0 deletions backend/app/services/analytics_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
from app.db_connection import get_connection


def get_top_doctors():
"""Get the top 10 doctors by number of appointments"""
try:
conn = get_connection()
cur = conn.cursor()

query = """
SELECT doctor_id,
COUNT(*) AS total_appointments
FROM Appointments
GROUP BY doctor_id
ORDER BY total_appointments DESC
LIMIT 10;
"""

cur.execute(query)
rows = cur.fetchall()

cur.close()
conn.close()

return [
{
"doctor_id": r[0],
"total_appointments": r[1],
}
for r in rows
]
except Exception as e:
print(f"Error in get_top_doctors: {str(e)}")
return []


def get_department_load():
"""Get total visits per department"""
try:
conn = get_connection()
cur = conn.cursor()

query = """
SELECT
d.department_id,
COUNT(v.visit_id) AS total_visits
FROM Departments d
LEFT JOIN Doctors doc ON d.department_id = doc.department_id
LEFT JOIN Visits v ON doc.doctor_id = v.doctor_id
GROUP BY d.department_id
ORDER BY COUNT(v.visit_id) DESC;
"""

cur.execute(query)
rows = cur.fetchall()

cur.close()
conn.close()

return [
{
"department_id": r[0],
"total_visits": int(r[1]) if r[1] else 0,
}
for r in rows
]
except Exception as e:
print(f"Error in get_department_load: {str(e)}")
return []


def get_monthly_visits():
"""Get monthly patient visits over time"""
try:
conn = get_connection()
cur = conn.cursor()

query = """
SELECT
EXTRACT(YEAR FROM v.visit_date)::INT AS year,
EXTRACT(MONTH FROM v.visit_date)::INT AS month,
COUNT(v.visit_id) AS total_visits
FROM Visits v
GROUP BY EXTRACT(YEAR FROM v.visit_date), EXTRACT(MONTH FROM v.visit_date)
ORDER BY year, month;
"""

cur.execute(query)
rows = cur.fetchall()

cur.close()
conn.close()

return [
{
"year": int(r[0]) if r[0] else 0,
"month": int(r[1]) if r[1] else 0,
"total_visits": int(r[2]) if r[2] else 0,
}
for r in rows
]
except Exception as e:
print(f"Error in get_monthly_visits: {str(e)}")
return []


def get_doctor_workload():
"""Get total visits per doctor"""
try:
conn = get_connection()
cur = conn.cursor()

query = """
SELECT
v.doctor_id,
COUNT(v.visit_id) AS total_visits
FROM Visits v
GROUP BY v.doctor_id
ORDER BY COUNT(v.visit_id) DESC;
"""

cur.execute(query)
rows = cur.fetchall()

cur.close()
conn.close()

return [
{
"doctor_id": r[0],
"total_visits": int(r[1]) if r[1] else 0,
}
for r in rows
]
except Exception as e:
print(f"Error in get_doctor_workload: {str(e)}")
return []


def get_kpis():
"""Get key performance indicators: total patients, doctors, appointments, visits"""
try:
conn = get_connection()
cur = conn.cursor()

# Get total patients
cur.execute("SELECT COUNT(*) FROM Patients;")
total_patients = cur.fetchone()[0] or 0

# Get total doctors
cur.execute("SELECT COUNT(*) FROM Doctors;")
total_doctors = cur.fetchone()[0] or 0

# Get total appointments
cur.execute("SELECT COUNT(*) FROM Appointments;")
total_appointments = cur.fetchone()[0] or 0

# Get total visits
cur.execute("SELECT COUNT(*) FROM Visits;")
total_visits = cur.fetchone()[0] or 0

cur.close()
conn.close()

return {
"total_patients": int(total_patients),
"total_doctors": int(total_doctors),
"total_appointments": int(total_appointments),
"total_visits": int(total_visits),
}
except Exception as e:
print(f"Error in get_kpis: {str(e)}")
return {
"total_patients": 0,
"total_doctors": 0,
"total_appointments": 0,
"total_visits": 0,
}
7 changes: 5 additions & 2 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from app.api.doctor.doctor_routes import router as doctor_router
from app.api.doctor.visit_routes import router as visit_router
from app.api.patient.department_routes import router as department_router
from app.api.analytics.analytics_routes import router as analytics_router

load_dotenv()

Expand All @@ -18,7 +19,8 @@
)

# CORS — allow configured origins (set ALLOWED_ORIGINS in env vars)
allowed_origins_raw = os.environ.get("ALLOWED_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000")
# In development, allow localhost on all ports; in production, restrict to specific origins
allowed_origins_raw = os.environ.get("ALLOWED_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000,http://localhost:3001,http://localhost:8080,http://localhost:5173")
allowed_origins = [o.strip() for o in allowed_origins_raw.split(",")]

app.add_middleware(
Expand All @@ -34,6 +36,7 @@
app.include_router(doctor_router)
app.include_router(visit_router)
app.include_router(department_router)
app.include_router(analytics_router)


@app.get("/")
Expand All @@ -42,4 +45,4 @@ def root():
"message": "MedInsight Healthcare API is running",
"docs": "/docs",
"version": "1.0.0"
}
}
72 changes: 72 additions & 0 deletions frontend/app/analytics/components/department-load-chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use client";

import {
PieChart,
Pie,
Cell,
Legend,
Tooltip,
ResponsiveContainer,
} from "recharts";
import { DepartmentLoad } from "@/lib/api";

interface DepartmentLoadChartProps {
data: DepartmentLoad[];
departments: Map<number, string>;
}

const COLORS = [
"hsl(var(--primary))",
"hsl(var(--accent))",
"#8b5cf6",
"#ec4899",
"#f59e0b",
"#10b981",
"#06b6d4",
];

export function DepartmentLoadChart({ data, departments }: DepartmentLoadChartProps) {
const totalVisits = data.reduce((sum, item) => sum + item.total_visits, 0);

const chartData = data.map((item) => ({
...item,
name: departments.get(item.department_id) || `Department ${item.department_id}`,
value: item.total_visits,
percentage: totalVisits > 0 ? ((item.total_visits / totalVisits) * 100).toFixed(1) : 0,
}));

return (
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
labelLine={false}
label={({ name, percentage }) => `${name}: ${percentage}%`}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
color: "hsl(var(--foreground))",
}}
formatter={(value, name, props) => [`${value} visits`, "Total"]}
/>
<Legend
wrapperStyle={{
color: "hsl(var(--foreground))",
}}
/>
</PieChart>
</ResponsiveContainer>
);
}
58 changes: 58 additions & 0 deletions frontend/app/analytics/components/doctor-workload-chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"use client";

import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
import { DoctorWorkload } from "@/lib/api";

interface DoctorWorkloadChartProps {
data: DoctorWorkload[];
doctors: Map<number, string>;
}

export function DoctorWorkloadChart({ data, doctors }: DoctorWorkloadChartProps) {
// Take only top 10 doctors and reverse for better visualization (highest at bottom)
const chartData = data
.slice(0, 10)
.map((item) => ({
...item,
doctor_name: doctors.get(item.doctor_id) || `Doctor ${item.doctor_id}`,
}))
.reverse();

return (
<ResponsiveContainer width="100%" height={350}>
<BarChart
data={chartData}
layout="vertical"
margin={{ top: 5, right: 30, left: 200, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<XAxis type="number" stroke="hsl(var(--muted-foreground))" />
<YAxis
dataKey="doctor_name"
type="category"
stroke="hsl(var(--muted-foreground))"
tick={{ fontSize: 12 }}
/>
<Tooltip
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
color: "hsl(var(--foreground))",
}}
cursor={{ fill: "hsl(var(--accent))", opacity: 0.1 }}
formatter={(value) => [`${value} visits`, "Total Visits"]}
/>
<Bar dataKey="total_visits" fill="hsl(var(--accent))" radius={[0, 8, 8, 0]} />
</BarChart>
</ResponsiveContainer>
);
}
Loading