-
Notifications
You must be signed in to change notification settings - Fork 12
Completed appointment booking system task #8
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 all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<AppointmentRow[]>([]); | ||
| 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); | ||
| } | ||
|
Comment on lines
+34
to
+44
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. Validate the response before storing it in
Suggested fix+ const [error, setError] = useState("");
+
async function loadAppointments() {
try {
const res = await fetch("/api/admin/appointments");
const data = await res.json();
-
- setRows(data || []);
+ if (!res.ok || !Array.isArray(data)) {
+ throw new Error("Failed to load appointments");
+ }
+ setRows(data);
} catch (error) {
- console.log(error);
+ setError("Failed to load appointments");
+ setRows([]);
} finally {
setLoading(false);
}
}🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| function handleLogout() { | ||
| localStorage.removeItem("adminLoggedIn"); | ||
| window.location.href = "/"; | ||
| } | ||
|
|
||
| return ( | ||
| <main className="min-h-screen bg-gray-50 p-8"> | ||
| <div className="mx-auto max-w-6xl"> | ||
| <div className="mb-6 flex items-center justify-between"> | ||
| <h1 className="text-2xl font-bold">All Appointments</h1> | ||
|
|
||
| <button | ||
| onClick={handleLogout} | ||
| className="rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700" | ||
| > | ||
| Logout | ||
| </button> | ||
| </div> | ||
|
|
||
| {loading ? ( | ||
| <p>Loading...</p> | ||
| ) : ( | ||
| <table className="w-full border bg-white text-sm"> | ||
| <thead> | ||
| <tr className="bg-gray-100"> | ||
| <th className="border p-2">Patient</th> | ||
| <th className="border p-2">Doctor</th> | ||
| <th className="border p-2">Slot</th> | ||
| <th className="border p-2">Status</th> | ||
| </tr> | ||
| </thead> | ||
|
|
||
| <tbody> | ||
| {rows.length === 0 ? ( | ||
| <tr> | ||
| <td colSpan={4} className="p-4 text-center"> | ||
| No appointments found | ||
| </td> | ||
| </tr> | ||
| ) : ( | ||
| rows.map((row) => ( | ||
| <tr key={row.id}> | ||
| <td className="border p-2">{row.patients?.name}</td> | ||
| <td className="border p-2">{row.doctors?.name}</td> | ||
| <td className="border p-2">{row.slots?.start_time}</td> | ||
| <td className="border p-2">{row.status}</td> | ||
| </tr> | ||
| )) | ||
| )} | ||
| </tbody> | ||
| </table> | ||
| )} | ||
| </div> | ||
| </main> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"); | ||
|
Comment on lines
+14
to
+28
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. Handle request and JSON parse failures in the submit path. If Suggested fix 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");
+ 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().catch(() => ({}));
+
+ if (res.ok) {
+ localStorage.setItem("adminLoggedIn", "true");
+ window.location.href = "/admin/dashboard";
+ } else {
+ setError(data.error || "Login failed");
+ }
+ } catch {
+ setError("Login failed");
}
}🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <main className="flex min-h-screen items-center justify-center bg-gray-50"> | ||
| <form | ||
| onSubmit={handleLogin} | ||
| className="w-full max-w-md rounded-xl border bg-white p-8" | ||
| > | ||
| <h1 className="mb-6 text-3xl font-bold">Admin Login</h1> | ||
|
|
||
| <input | ||
| type="email" | ||
| placeholder="Email" | ||
| className="mb-4 w-full rounded border px-4 py-3" | ||
| value={email} | ||
| onChange={(e) => setEmail(e.target.value)} | ||
| /> | ||
|
|
||
| <input | ||
| type="password" | ||
| placeholder="Password" | ||
| className="mb-4 w-full rounded border px-4 py-3" | ||
| value={password} | ||
| onChange={(e) => setPassword(e.target.value)} | ||
| /> | ||
|
|
||
| {error && ( | ||
| <p className="mb-4 text-sm text-red-600">{error}</p> | ||
| )} | ||
|
|
||
| <button | ||
| type="submit" | ||
| className="w-full rounded bg-black py-3 text-white" | ||
| > | ||
| Login | ||
| </button> | ||
| </form> | ||
| </main> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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() { | ||||||||||||||||||||||||||||||||||||||||||||||
|
DHIRAVIYASUNDARAM marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||
| 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 }); | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+23
to
+29
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. Do not convert database failures into Both failure paths currently masquerade as an empty dashboard, so a broken query or missing env var becomes “No appointments found” instead of an actionable server error. Return a 5xx here and let the client render an error state. Suggested fix if (error) {
- return NextResponse.json([], { status: 200 });
+ return NextResponse.json(
+ { error: "Failed to load appointments" },
+ { status: 500 }
+ );
}
return NextResponse.json(data || []);
} catch (error) {
- return NextResponse.json([], { status: 200 });
+ return NextResponse.json(
+ { error: "Failed to load appointments" },
+ { status: 500 }
+ );
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
|
DHIRAVIYASUNDARAM marked this conversation as resolved.
|
||
|
|
||
| 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 } | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) { | ||
|
DHIRAVIYASUNDARAM marked this conversation as resolved.
|
||
| 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(); | ||
|
DHIRAVIYASUNDARAM marked this conversation as resolved.
Comment on lines
+20
to
+24
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. Derive the appointment doctor from the slot, not from the request. The handler never checks that 🩹 Suggested fix- const { slotId, doctorId, patientId } = await req.json();
+ const { slotId, patientId } = await req.json();
@@
- if (!slotId || !doctorId || !patientId) {
+ if (!slotId || !patientId) {
return NextResponse.json(
{ error: "Missing required fields" },
{ status: 400 }
);
}
@@
- const { data: slot } = await supabase
+ const { data: slot } = await supabase
.from("slots")
- .select("*")
+ .select("id, doctor_id, is_booked")
.eq("id", slotId)
.single();
@@
+ const doctorId = slot.doctor_id;
+
const { data: existing } = await supabase
.from("appointments")
.select("id")
.eq("patient_id", patientId)
.eq("doctor_id", doctorId)
.eq("status", "active");Also applies to: 33-54, 58-61 🤖 Prompt for AI Agents |
||
|
|
||
| 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 } | ||
| ); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.