diff --git a/app/admin/dashboard/page.tsx b/app/admin/dashboard/page.tsx new file mode 100644 index 0000000..2dec62b --- /dev/null +++ b/app/admin/dashboard/page.tsx @@ -0,0 +1,124 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { supabase } from "@/lib/supabase"; + +type Appointment = { + id: string; + status: "active" | "done" | "cancelled"; + created_at: string; + doctors: { name: string; specialty: string }; + patients: { name: string }; + slots: { start_time: string; end_time: string }; +}; + +export default function AdminDashboard() { + const router = useRouter(); + + const [appointments, setAppointments] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // ✅ Check admin login + const isAdmin = localStorage.getItem("admin"); + if (!isAdmin) { + router.push("/admin/login"); + return; + } + + async function load() { + const { data } = await supabase + .from("appointments") + .select(` + id, + status, + created_at, + doctors(name, specialty), + patients(name), + slots(start_time, end_time) + `) + .order("created_at", { ascending: false }); + + setAppointments((data as any) ?? []); + setLoading(false); + } + + load(); + }, [router]); + + function formatDateTime(iso: string) { + return new Date(iso).toLocaleString("en-IN", { + dateStyle: "medium", + timeStyle: "short", + }); + } + + return ( +
+

Admin Dashboard

+ + + + {loading ? ( +

Loading...

+ ) : ( +
+ + + + + + + + + + + + {appointments.map((appt) => ( + + + + + + + + + + ))} + +
DoctorPatientSlotStatus
+ {appt.doctors?.name || "Unknown"} +
+ {appt.doctors?.specialty} +
+
+ {appt.patients?.name || "Unknown"} + + {formatDateTime(appt.slots?.start_time)} + + + {appt.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..5f04407 --- /dev/null +++ b/app/admin/login/page.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { supabase } from "@/lib/supabase"; + +export default function AdminLogin() { + const router = useRouter(); + + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(""); + setLoading(true); + + // ✅ Check admin in DB + const { data, error } = await supabase + .from("system_admins") + .select("*") + .eq("email", email) + .eq("password", password) + .single(); + + if (error || !data) { + setError("Invalid admin credentials"); + setLoading(false); + return; + } + + // ✅ Save session + localStorage.setItem("admin", "true"); + + router.push("/admin/dashboard"); + } + + return ( +
+
+

Admin Login

+ +
+ {/* Email */} + setEmail(e.target.value)} + required + className="border p-2 rounded" + /> + + {/* Password */} + setPassword(e.target.value)} + required + className="border p-2 rounded" + /> + + {/* Error */} + {error &&

{error}

} + + {/* Button */} + +
+
+
+ ); +} \ No newline at end of file diff --git a/app/api/appointments/book/route.ts b/app/api/appointments/book/route.ts index 4fcd17b..727a1cb 100644 --- a/app/api/appointments/book/route.ts +++ b/app/api/appointments/book/route.ts @@ -1,5 +1,104 @@ 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 body = await req.json(); + const { appointmentId, action } = body; + + if (!appointmentId || !action) { + return NextResponse.json( + { error: "Missing appointmentId or action" }, + { status: 400 } + ); + } + + // ✅ STEP 1: Get logged-in user + const { + data: { user }, + error: userError, + } = await supabase.auth.getUser(); + + if (userError || !user) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401 } + ); + } + + // ✅ STEP 2: Check if doctor + const { data: doctor } = await supabase + .from("doctors") + .select("id") + .eq("id", user.id) + .single(); + + const isDoctor = !!doctor; + + // ❌ Only doctor can mark "done" + if (action === "done" && !isDoctor) { + return NextResponse.json( + { error: "Only doctor can mark appointment as done" }, + { status: 403 } + ); + } + + // ✅ STEP 3: Get appointment + const { data: appt } = await supabase + .from("appointments") + .select("*") + .eq("id", appointmentId) + .single(); + + if (!appt) { + return NextResponse.json( + { error: "Appointment not found" }, + { status: 404 } + ); + } + + // ❌ Patient can only cancel their own appointment + if (!isDoctor && appt.patient_id !== user.id) { + return NextResponse.json( + { error: "Not allowed" }, + { status: 403 } + ); + } + + // ✅ STEP 4: Update status + const newStatus = action === "done" ? "done" : "cancelled"; + + const { error: updateError } = await supabase + .from("appointments") + .update({ status: newStatus }) + .eq("id", appointmentId); + + if (updateError) { + return NextResponse.json( + { error: updateError.message }, + { status: 500 } + ); + } + + // ✅ STEP 5: Free slot if cancelled + if (newStatus === "cancelled") { + await supabase + .from("slots") + .update({ is_booked: false }) + .eq("id", appt.slot_id); + } + + return NextResponse.json({ success: true }); + + } catch (err: any) { + return NextResponse.json( + { error: err.message }, + { 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..876ee01 100644 --- a/app/api/appointments/cancel/route.ts +++ b/app/api/appointments/cancel/route.ts @@ -1,5 +1,84 @@ import { NextRequest, NextResponse } from "next/server"; +import { createClient } from "@supabase/supabase-js"; +import { canCancelWithTime } from "@/lib/appointmentRules"; -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 } = await req.json(); + + if (!appointmentId || !action) { + return NextResponse.json( + { error: "Missing appointmentId or action" }, + { status: 400 } + ); + } + + // 1️⃣ Get appointment + const { data: appt, error: apptError } = await supabase + .from("appointments") + .select("*") + .eq("id", appointmentId) + .single(); + + if (apptError || !appt) { + return NextResponse.json( + { error: "Appointment not found" }, + { status: 404 } + ); + } + + // =============================== + // 🔥 ADD THIS BLOCK HERE + // =============================== + + // Get slot time + const { data: slot } = await supabase + .from("slots") + .select("start_time") + .eq("id", appt.slot_id) + .single(); + + // Apply rule ONLY for cancel (not for "done") + if (action === "cancel") { + const canCancel = canCancelWithTime(slot.start_time); + + if (!canCancel) { + return NextResponse.json( + { error: "Cannot cancel within 1 hour of appointment" }, + { status: 400 } + ); + } + } + + // =============================== + // 🔥 END OF NEW BLOCK + // =============================== + + // 2️⃣ Update status + const newStatus = action === "done" ? "done" : "cancelled"; + + await supabase + .from("appointments") + .update({ status: newStatus }) + .eq("id", appointmentId); + + // 3️⃣ Free slot + await supabase + .from("slots") + .update({ is_booked: false }) + .eq("id", appt.slot_id); + + return NextResponse.json({ success: true }); + + } catch (err: any) { + return NextResponse.json( + { error: err.message }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/doctor/dashboard/page.tsx b/app/doctor/dashboard/page.tsx index 3f89c4c..9096291 100644 --- a/app/doctor/dashboard/page.tsx +++ b/app/doctor/dashboard/page.tsx @@ -22,8 +22,8 @@ type Appointment = { id: string; status: "active" | "done" | "cancelled"; created_at: string; - patients: { name: string }; - slots: { start_time: string; end_time: string }; + patient: { name: string }; + slot: { start_time: string; end_time: string }; }; function formatDateTime(iso: string) { @@ -74,12 +74,34 @@ export default function DoctorDashboard() { setSlots(slotData ?? []); const { data: apptData } = await supabase - .from("appointments") - .select("id, status, created_at, patients(name), slots(start_time, end_time)") - .eq("doctor_id", user.id) - .order("created_at", { ascending: false }); + .from("appointments") + .select(` + id, + status, + created_at, + patient_id, + slot_id, + slot:slots(start_time, end_time) + `) + .eq("doctor_id", user.id) + .order("created_at", { ascending: false }); + + const enrichedAppointments = await Promise.all( + (apptData ?? []).map(async (appt: any) => { + const { data: patient } = await supabase + .from("patients") + .select("name") + .eq("id", appt.patient_id) + .single(); + + return { + ...appt, + patient: { name: patient?.name || "Unknown" }, + }; + }) +); - setAppointments((apptData as Appointment[]) ?? []); + setAppointments(enrichedAppointments); setLoading(false); } @@ -205,9 +227,9 @@ export default function DoctorDashboard() { {appointments.map((appt) => ( - {appt.patients?.name} + {appt.patient?.name} - {formatDateTime(appt.slots?.start_time)} + {formatDateTime(appt.slot?.start_time)} prev.filter((s) => s.id !== slotId)); - } else { - setActionMsg(data.error ?? "Booking failed."); - } + const { + data: { user }, + } = await supabase.auth.getUser(); + if (!user) { + setActionMsg("User not logged in"); setBookingSlotId(null); + return; } + console.log("Sending:", { + slot_id: slotId, + patient_id: user.id, + }); + + const res = await fetch("/api/appointments/book", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + slot_id: slotId, + patient_id: user.id, // ⭐ REQUIRED + }), +}); + + const data = await res.json(); + console.log("Response:", data); + + if (res.ok) { + setActionMsg("Appointment booked successfully!"); + setAvailableSlots((prev) => prev.filter((s) => s.id !== slotId)); + } else { + setActionMsg(data.error ?? "Booking failed."); + } + + setBookingSlotId(null); +} + + async function handleCancel(appointmentId: string) { setActionMsg(""); const res = await fetch("/api/appointments/cancel", { @@ -196,7 +219,7 @@ export default function PatientDashboard() {