diff --git a/app/admin/dashboard/page.tsx b/app/admin/dashboard/page.tsx new file mode 100644 index 0000000..73a84c1 --- /dev/null +++ b/app/admin/dashboard/page.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { useEffect, useState } from "react"; + +type AppointmentRow = { + id: string; + status: string; + patients?: { + name: string; + }; + doctors?: { + name: string; + }; + slots?: { + start_time: string; + }; +}; + +export default function AdminDashboard() { + const [rows, setRows] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const ok = localStorage.getItem("adminLoggedIn"); + + if (ok !== "true") { + window.location.href = "/admin/login"; + return; + } + + loadAppointments(); + }, []); + + async function loadAppointments() { + try { + const res = await fetch("/api/admin/appointments"); + const data = await res.json(); + + setRows(data || []); + } catch (error) { + console.log(error); + } finally { + setLoading(false); + } + } + + function handleLogout() { + localStorage.removeItem("adminLoggedIn"); + window.location.href = "/"; + } + + return ( +
+
+
+

All Appointments

+ + +
+ + {loading ? ( +

Loading...

+ ) : ( + + + + + + + + + + + + {rows.length === 0 ? ( + + + + ) : ( + rows.map((row) => ( + + + + + + + )) + )} + +
PatientDoctorSlotStatus
+ No appointments found +
{row.patients?.name}{row.doctors?.name}{row.slots?.start_time}{row.status}
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/app/admin/login/page.tsx b/app/admin/login/page.tsx new file mode 100644 index 0000000..cf83063 --- /dev/null +++ b/app/admin/login/page.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { useState } from "react"; + +export default function AdminLogin() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + + async function handleLogin(e: React.FormEvent) { + e.preventDefault(); + setError(""); + + const res = await fetch("/api/admin/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password }), + }); + + const data = await res.json(); + + if (res.ok) { + localStorage.setItem("adminLoggedIn", "true"); + window.location.href = "/admin/dashboard"; + } else { + setError(data.error || "Login failed"); + } + } + + return ( +
+
+

Admin Login

+ + setEmail(e.target.value)} + /> + + setPassword(e.target.value)} + /> + + {error && ( +

{error}

+ )} + + +
+
+ ); +} \ No newline at end of file diff --git a/app/api/admin/appointments/route.ts b/app/api/admin/appointments/route.ts new file mode 100644 index 0000000..c6c807b --- /dev/null +++ b/app/api/admin/appointments/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from "next/server"; +import { createClient } from "@supabase/supabase-js"; + +const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! +); + +export async function GET() { + try { + const { data, error } = await supabase + .from("appointments") + .select(` + id, + status, + created_at, + patients(name), + doctors(name), + slots(start_time) + `) + .order("created_at", { ascending: false }); + + if (error) { + return NextResponse.json([], { status: 200 }); + } + + return NextResponse.json(data || []); + } catch (error) { + return NextResponse.json([], { status: 200 }); + } +} \ No newline at end of file diff --git a/app/api/admin/login/route.ts b/app/api/admin/login/route.ts new file mode 100644 index 0000000..a0c5f8c --- /dev/null +++ b/app/api/admin/login/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from "next/server"; +import { createClient } from "@supabase/supabase-js"; + +const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! +); + +export async function POST(req: NextRequest) { + try { + const { email, password } = await req.json(); + + const { data, error } = await supabase + .from("system_admins") + .select("*") + .eq("email", email) + .eq("password", password) + .single(); + + if (error || !data) { + return NextResponse.json( + { error: "Invalid credentials" }, + { status: 401 } + ); + } + + return NextResponse.json({ success: true }); + } catch (error) { + return NextResponse.json( + { error: "Login failed" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/appointments/book/route.ts b/app/api/appointments/book/route.ts index 4fcd17b..a94eb80 100644 --- a/app/api/appointments/book/route.ts +++ b/app/api/appointments/book/route.ts @@ -1,5 +1,70 @@ import { NextRequest, NextResponse } from "next/server"; +import { createClient } from "@supabase/supabase-js"; -export async function POST(_req: NextRequest) { - return NextResponse.json({ error: "Not implemented" }, { status: 501 }); -} +const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! +); + +export async function POST(req: NextRequest) { + try { + const { slotId, doctorId, patientId } = await req.json(); + + if (!slotId || !doctorId || !patientId) { + return NextResponse.json( + { error: "Missing required fields" }, + { status: 400 } + ); + } + + const { data: slot } = await supabase + .from("slots") + .select("*") + .eq("id", slotId) + .single(); + + if (!slot || slot.is_booked) { + return NextResponse.json( + { error: "Slot already booked" }, + { status: 400 } + ); + } + + const { data: existing } = await supabase + .from("appointments") + .select("id") + .eq("patient_id", patientId) + .eq("doctor_id", doctorId) + .eq("status", "active"); + + if (existing && existing.length > 0) { + return NextResponse.json( + { error: "You already have an active appointment with this doctor" }, + { status: 400 } + ); + } + + const { error: insertError } = await supabase + .from("appointments") + .insert({ + patient_id: patientId, + doctor_id: doctorId, + slot_id: slotId, + status: "active", + }); + + if (insertError) throw insertError; + + await supabase + .from("slots") + .update({ is_booked: true }) + .eq("id", slotId); + + return NextResponse.json({ success: true }); + } catch (error) { + return NextResponse.json( + { error: "Booking failed" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/appointments/cancel/route.ts b/app/api/appointments/cancel/route.ts index 4fcd17b..126dd29 100644 --- a/app/api/appointments/cancel/route.ts +++ b/app/api/appointments/cancel/route.ts @@ -1,5 +1,93 @@ import { NextRequest, NextResponse } from "next/server"; +import { createClient } from "@supabase/supabase-js"; -export async function POST(_req: NextRequest) { - return NextResponse.json({ error: "Not implemented" }, { status: 501 }); -} +const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! +); + +export async function POST(req: NextRequest) { + try { + const { appointmentId, action, patientId, doctorId } = await req.json(); + + if (!appointmentId || !action) { + return NextResponse.json( + { error: "Missing required fields" }, + { status: 400 } + ); + } + + const { data: appt } = await supabase + .from("appointments") + .select("*, slots(start_time)") + .eq("id", appointmentId) + .single(); + + if (!appt) { + return NextResponse.json( + { error: "Appointment not found" }, + { status: 404 } + ); + } + + if (appt.status !== "active") { + return NextResponse.json( + { error: "Appointment already completed/cancelled" }, + { status: 400 } + ); + } + + if (action === "cancel") { + if (patientId) { + const start = new Date(appt.slots.start_time).getTime(); + const now = Date.now(); + const diffHours = (start - now) / (1000 * 60 * 60); + + if (diffHours < 1) { + return NextResponse.json( + { error: "Cannot cancel less than 1 hour before appointment" }, + { status: 400 } + ); + } + } + + await supabase + .from("appointments") + .update({ status: "cancelled" }) + .eq("id", appointmentId); + + await supabase + .from("slots") + .update({ is_booked: false }) + .eq("id", appt.slot_id); + + return NextResponse.json({ success: true }); + } + + if (action === "done") { + if (!doctorId) { + return NextResponse.json( + { error: "Doctor only action" }, + { status: 403 } + ); + } + + await supabase + .from("appointments") + .update({ status: "done" }) + .eq("id", appointmentId); + + return NextResponse.json({ success: true }); + } + + return NextResponse.json( + { error: "Invalid action" }, + { status: 400 } + ); + } catch (error) { + return NextResponse.json( + { error: "Request failed" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/doctor/dashboard/page.tsx b/app/doctor/dashboard/page.tsx index 3f89c4c..d53c5fd 100644 --- a/app/doctor/dashboard/page.tsx +++ b/app/doctor/dashboard/page.tsx @@ -94,7 +94,11 @@ export default function DoctorDashboard() { const res = await fetch("/api/appointments/cancel", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ appointmentId, action }), + body: JSON.stringify({ + appointmentId, + action, + doctorId: doctor?.id, +}), }); const data = await res.json(); if (res.ok) { diff --git a/app/patient/dashboard/page.tsx b/app/patient/dashboard/page.tsx index 443e06c..f11ba89 100644 --- a/app/patient/dashboard/page.tsx +++ b/app/patient/dashboard/page.tsx @@ -98,14 +98,19 @@ export default function PatientDashboard() { const res = await fetch("/api/appointments/book", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ slotId, doctorId }), + body: JSON.stringify({slotId,doctorId,patientId: patient?.id,}), }); const data = await res.json(); if (res.ok) { - setActionMsg("Appointment booked successfully!"); - setAvailableSlots((prev) => prev.filter((s) => s.id !== slotId)); - } else { + setActionMsg("Appointment booked successfully!"); + + setAvailableSlots((prev) => + prev.filter((s) => s.id !== slotId) + ); + + window.location.reload(); +} else { setActionMsg(data.error ?? "Booking failed."); } @@ -117,7 +122,11 @@ export default function PatientDashboard() { const res = await fetch("/api/appointments/cancel", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ appointmentId, action: "cancel" }), + body: JSON.stringify({ + appointmentId, + action: "cancel", + patientId: patient?.id, +}), }); const data = await res.json();