Repository navigation
Implement required changes #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| "use client"; | ||
|
|
||
| import { useEffect, useState } from "react"; | ||
| import { useRouter } from "next/navigation"; | ||
| import Link from "next/link"; | ||
|
|
||
| type Appointment = { | ||
| id: string; | ||
| status: "active" | "done" | "cancelled"; | ||
| created_at: string; | ||
| patient: { name: string; email: string }; | ||
| doctor: { name: string; specialty: string }; | ||
| slot: { start_time: string; end_time: string }; | ||
| }; | ||
|
|
||
| function formatDateTime(iso: string) { | ||
| return new Date(iso).toLocaleString("en-IN", { | ||
| dateStyle: "medium", | ||
| timeStyle: "short", | ||
| }); | ||
| } | ||
|
|
||
| export default function AdminDashboard() { | ||
| const router = useRouter(); | ||
| const [appointments, setAppointments] = useState<Appointment[]>([]); | ||
| const [loading, setLoading] = useState(true); | ||
| const [admin, setAdmin] = useState<{ email: string } | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| const session = localStorage.getItem("admin_session"); | ||
| if (!session) { | ||
| router.push("/admin/login"); | ||
| return; | ||
| } | ||
| setAdmin(JSON.parse(session)); | ||
|
|
||
| async function fetchAppointments() { | ||
| const res = await fetch("/api/admin/appointments"); | ||
| const data = await res.json(); | ||
| if (res.ok) { | ||
| setAppointments(data.appointments); | ||
| } | ||
| setLoading(false); | ||
| } | ||
|
|
||
| fetchAppointments(); | ||
| }, [router]); | ||
|
|
||
| function handleLogout() { | ||
| localStorage.removeItem("admin_session"); | ||
| router.push("/admin/login"); | ||
| } | ||
|
|
||
| if (loading) { | ||
| return ( | ||
| <main className="flex min-h-screen items-center justify-center"> | ||
| <p className="text-gray-500">Loading appointments...</p> | ||
| </main> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <main className="min-h-screen bg-gray-50 p-6"> | ||
| <div className="mx-auto max-w-6xl"> | ||
| <div className="mb-6 flex items-center justify-between"> | ||
| <div> | ||
| <h1 className="text-2xl font-bold">Admin Dashboard</h1> | ||
| <p className="text-sm text-gray-500">Logged in as {admin?.email}</p> | ||
| </div> | ||
| <button | ||
| onClick={handleLogout} | ||
| className="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100" | ||
| > | ||
| Logout | ||
| </button> | ||
| </div> | ||
|
|
||
| <section className="rounded-xl border bg-white shadow-sm overflow-hidden"> | ||
| <div className="border-b bg-gray-50 px-6 py-4"> | ||
| <h2 className="text-lg font-semibold text-gray-800">All Appointments</h2> | ||
| </div> | ||
| <div className="overflow-x-auto"> | ||
| <table className="w-full text-left text-sm"> | ||
| <thead className="bg-gray-50 text-gray-600 uppercase text-xs font-bold"> | ||
| <tr> | ||
| <th className="px-6 py-3">Patient</th> | ||
| <th className="px-6 py-3">Doctor</th> | ||
| <th className="px-6 py-3">Slot Time</th> | ||
| <th className="px-6 py-3">Status</th> | ||
| <th className="px-6 py-3">Created At</th> | ||
| </tr> | ||
| </thead> | ||
| <tbody className="divide-y divide-gray-100"> | ||
| {appointments.length === 0 ? ( | ||
| <tr> | ||
| <td colSpan={5} className="px-6 py-10 text-center text-gray-500"> | ||
| No appointments found in the system. | ||
| </td> | ||
| </tr> | ||
| ) : ( | ||
| appointments.map((appt) => ( | ||
| <tr key={appt.id} className="hover:bg-gray-50 transition-colors"> | ||
| <td className="px-6 py-4"> | ||
| <div className="font-medium text-gray-900">{appt.patient?.name}</div> | ||
| <div className="text-xs text-gray-500">{appt.patient?.email}</div> | ||
| </td> | ||
| <td className="px-6 py-4"> | ||
| <div className="font-medium text-gray-900">{appt.doctor?.name}</div> | ||
| <div className="text-xs text-gray-500">{appt.doctor?.specialty}</div> | ||
| </td> | ||
| <td className="px-6 py-4 text-gray-600"> | ||
| {formatDateTime(appt.slot?.start_time)} | ||
| </td> | ||
| <td className="px-6 py-4"> | ||
| <span | ||
| className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${ | ||
| appt.status === "active" | ||
| ? "bg-blue-100 text-blue-700" | ||
| : appt.status === "done" | ||
| ? "bg-green-100 text-green-700" | ||
| : "bg-red-100 text-red-700" | ||
| }`} | ||
| > | ||
| {appt.status.charAt(0).toUpperCase() + appt.status.slice(1)} | ||
| </span> | ||
| </td> | ||
| <td className="px-6 py-4 text-gray-500"> | ||
| {new Date(appt.created_at).toLocaleDateString()} | ||
| </td> | ||
| </tr> | ||
| )) | ||
| )} | ||
| </tbody> | ||
| </table> | ||
| </div> | ||
| </section> | ||
| </div> | ||
| </main> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| "use client"; | ||
|
|
||
| import { useState } from "react"; | ||
| import { useRouter } from "next/navigation"; | ||
| import Link from "next/link"; | ||
|
|
||
| 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); | ||
|
|
||
| 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) { | ||
| // Basic session handling for admin | ||
| localStorage.setItem("admin_session", JSON.stringify(data.admin)); | ||
| router.push("/admin/dashboard"); | ||
| } else { | ||
| setError(data.error || "Login failed"); | ||
| setLoading(false); | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <main className="flex min-h-screen items-center justify-center bg-gray-50"> | ||
| <div className="w-full max-w-md rounded-xl border bg-white p-8 shadow-sm"> | ||
| <h1 className="mb-6 text-2xl font-bold">Admin Login</h1> | ||
| <form onSubmit={handleSubmit} className="flex flex-col gap-4"> | ||
| <div> | ||
| <label className="mb-1 block text-sm font-medium text-gray-700"> | ||
| </label> | ||
| <input | ||
| type="email" | ||
| value={email} | ||
| onChange={(e) => setEmail(e.target.value)} | ||
| required | ||
| placeholder="admin@test.com" | ||
| className="w-full rounded-lg border px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" | ||
| /> | ||
| </div> | ||
| <div> | ||
| <label className="mb-1 block text-sm font-medium text-gray-700"> | ||
| Password | ||
| </label> | ||
| <input | ||
| type="password" | ||
| value={password} | ||
| onChange={(e) => setPassword(e.target.value)} | ||
| required | ||
| className="w-full rounded-lg border px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" | ||
| /> | ||
| </div> | ||
| {error && <p className="text-sm text-red-600">{error}</p>} | ||
| <button | ||
| type="submit" | ||
| disabled={loading} | ||
| className="rounded-lg bg-blue-600 py-2 font-medium text-white hover:bg-blue-700 disabled:opacity-50" | ||
| > | ||
| {loading ? "Logging in..." : "Login"} | ||
| </button> | ||
| </form> | ||
| <p className="mt-4 text-center text-sm text-gray-500"> | ||
| <Link href="/" className="text-blue-600 hover:underline"> | ||
| ← Back to home | ||
| </Link> | ||
| </p> | ||
| </div> | ||
| </main> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { supabaseAdmin } from "@/lib/supabase-admin"; | ||
|
|
||
| export async function GET(_req: NextRequest) { | ||
| try { | ||
| // In a real app, we would verify the admin session/token here. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| const { data: appointments, error } = await supabaseAdmin | ||
| .from("appointments") | ||
| .select(` | ||
| id, | ||
| status, | ||
| created_at, | ||
| patient:patients(name, email), | ||
| doctor:doctors(name, specialty), | ||
| slot:slots(start_time, end_time) | ||
| `) | ||
| .order("created_at", { ascending: false }); | ||
|
|
||
| if (error) { | ||
| console.error("Admin fetch appointments error:", error); | ||
| return NextResponse.json({ error: "Failed to fetch appointments" }, { status: 500 }); | ||
| } | ||
|
|
||
| return NextResponse.json({ appointments }); | ||
| } catch (err) { | ||
| console.error("Admin appointments route error:", err); | ||
| return NextResponse.json({ error: "Internal server error" }, { status: 500 }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { supabaseAdmin } from "@/lib/supabase-admin"; | ||
|
|
||
| export async function POST(req: NextRequest) { | ||
| try { | ||
| const { email, password } = await req.json(); | ||
|
|
||
| const { data: admin, error } = await supabaseAdmin | ||
| .from("system_admins") | ||
| .select("*") | ||
| .eq("email", email) | ||
| .eq("password", password) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| .maybeSingle(); | ||
|
|
||
| if (error || !admin) { | ||
| return NextResponse.json({ error: "Invalid credentials" }, { status: 401 }); | ||
| } | ||
|
|
||
| // In a real app, we would issue a JWT here. | ||
| // For this implementation, we return success and the client handles a basic session. | ||
| return NextResponse.json({ message: "Login successful", admin: { email: admin.email } }); | ||
| } catch (err) { | ||
| console.error("Admin login error:", err); | ||
| return NextResponse.json({ error: "Internal server error" }, { status: 500 }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,53 @@ | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { supabase } from "@/lib/supabase"; | ||
| import { supabaseAdmin } from "@/lib/supabase-admin"; | ||
| import { validateBooking } from "@/lib/appointment-logic"; | ||
|
|
||
| export async function POST(_req: NextRequest) { | ||
| return NextResponse.json({ error: "Not implemented" }, { status: 501 }); | ||
| export async function POST(req: NextRequest) { | ||
| try { | ||
| const { slotId, doctorId } = await req.json(); | ||
|
|
||
| const authHeader = req.headers.get("Authorization"); | ||
| if (!authHeader) return NextResponse.json({ error: "Missing authorization header" }, { status: 401 }); | ||
|
|
||
| const token = authHeader.replace("Bearer ", ""); | ||
| const { data: { user }, error: userError } = await supabase.auth.getUser(token); | ||
| if (userError || !user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
|
|
||
| const patientId = user.id; | ||
|
|
||
| // Use extracted logic | ||
| const validation = await validateBooking(supabaseAdmin, patientId, doctorId, slotId); | ||
| if (!validation.valid) { | ||
| return NextResponse.json({ error: validation.error }, { status: 400 }); | ||
| } | ||
|
Comment on lines
+19
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reject past slots on the server as part of booking validation. Right now the “future slots only” rule lives only in 🤖 Prompt for AI Agents |
||
|
|
||
| // 4. Update slot and create appointment (Atomic update using admin client) | ||
| const { error: slotUpdateError } = await supabaseAdmin | ||
| .from("slots") | ||
| .update({ is_booked: true }) | ||
| .eq("id", slotId); | ||
|
|
||
| if (slotUpdateError) { | ||
| return NextResponse.json({ error: "Failed to update slot" }, { status: 500 }); | ||
| } | ||
|
|
||
| const { error: insertError } = await supabaseAdmin.from("appointments").insert({ | ||
| patient_id: patientId, | ||
| doctor_id: doctorId, | ||
| slot_id: slotId, | ||
| status: "active", | ||
| }); | ||
|
|
||
| if (insertError) { | ||
| // Rollback slot update | ||
| await supabaseAdmin.from("slots").update({ is_booked: false }).eq("id", slotId); | ||
|
Comment on lines
+47
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| return NextResponse.json({ error: "Failed to create appointment" }, { status: 500 }); | ||
| } | ||
|
|
||
| return NextResponse.json({ message: "Appointment booked successfully" }); | ||
| } catch (err) { | ||
| console.error("Booking error:", err); | ||
| return NextResponse.json({ error: "Internal server error" }, { status: 500 }); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reset
loadingon thrown fetch/JSON errors.If the request fails at the network layer or returns invalid JSON,
handleSubmitthrows beforesetLoading(false)runs, leaving the form stuck in its disabled state with no feedback.🛠️ Proposed fix
async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setError(""); setLoading(true); - - 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) { - // Basic session handling for admin - localStorage.setItem("admin_session", JSON.stringify(data.admin)); - router.push("/admin/dashboard"); - } else { - setError(data.error || "Login failed"); - setLoading(false); - } + try { + 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("admin_session", JSON.stringify(data.admin)); + router.push("/admin/dashboard"); + return; + } + + setError(data.error || "Login failed"); + } catch { + setError("Login failed. Please try again."); + } finally { + setLoading(false); + } }🤖 Prompt for AI Agents