Repository navigation
Implemented appointment booking and cancellation #5
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 |
|---|---|---|
| @@ -1,5 +1,53 @@ | ||
| 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 }); | ||
| } | ||
| export async function POST(req: NextRequest) { | ||
| try { | ||
| const supabaseAdmin = createClient( | ||
| process.env.NEXT_PUBLIC_SUPABASE_URL!, | ||
| process.env.SUPABASE_SERVICE_ROLE_KEY! | ||
| ); | ||
|
|
||
| // Get user from cookie | ||
| const token = req.headers.get("authorization")?.replace("Bearer ", ""); | ||
| if (!token) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
|
|
||
| const { data: { user } } = await supabaseAdmin.auth.getUser(token); | ||
| if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
|
|
||
| const { slotId, doctorId } = await req.json(); | ||
| if (!slotId || !doctorId) return NextResponse.json({ error: "Missing fields" }, { status: 400 }); | ||
|
|
||
| // Check slot exists and is not already booked | ||
| const { data: existingAppt } = await supabaseAdmin | ||
| .from("appointments") | ||
| .select("id") | ||
| .eq("slot_id", slotId) | ||
| .eq("status", ["active", "done"]) | ||
| .maybeSingle(); | ||
|
|
||
| if (existingAppt) return NextResponse.json({ error: "Slot is already booked" }, { status: 409 }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| // Check patient doesn't already have active appointment with this doctor | ||
| const { data: duplicate } = await supabaseAdmin | ||
| .from("appointments") | ||
| .select("id") | ||
| .eq("patient_id", user.id) | ||
| .eq("doctor_id", doctorId) | ||
| .eq("status", "active") | ||
| .maybeSingle(); | ||
|
Comment on lines
+31
to
+38
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. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
sed -n '20,70p' app/api/appointments/book/route.tsRepository: botcode-com/task Length of output: 1825 Reorder the slot lookup before the duplicate check, and fix the property-name mismatch in the insert. Line 36 references Move the slot fetch (lines 44–48) before the duplicate check (lines 31–38) and use Fix+ const { data: slot } = await supabaseAdmin
+ .from("slots")
+ .select("start_time, doctor_id")
+ .eq("id", slotId)
+ .single();
+
+ if (!slot) {
+ return NextResponse.json({ error: "Slot not found" }, { status: 404 });
+ }
+
// Check slot exists and is not already booked
const { data: existingAppt } = await supabaseAdmin
.from("appointments")
.select("id")
.eq("slot_id", slotId)
.in("status", ["active", "done"])
.maybeSingle();
if (existingAppt) return NextResponse.json({ error: "Slot is already booked" }, { status: 409 });
// Check patient doesn't already have active appointment with this doctor
const { data: duplicate } = await supabaseAdmin
.from("appointments")
.select("id")
.eq("patient_id", user.id)
.eq("doctor_id", slot.doctor_id)
.eq("status", "active")
.maybeSingle();
if (duplicate) return NextResponse.json({ error: "You already have an active appointment with this doctor" }, { status: 409 });
const now = new Date().toISOString();
- const { data: slot } = await supabaseAdmin
- .from("slots")
- .select("start_time, doctor_id")
- .eq("id", slotId)
- .single();
-
- if (!slot) {
- return NextResponse.json({ error: "Slot not found" }, { status: 404 });
- }
-
if (new Date(slot.start_time) < new Date()) {
return NextResponse.json({ error: "Cannot book past slots" }, { status: 400 });
}
// Book it
const { error: insertError } = await supabaseAdmin
.from("appointments")
- .insert({ patient_id: user.id, doctor_id: slot.doctorId, slot_id: slotId, status: "active" });
+ .insert({ patient_id: user.id, doctor_id: slot.doctor_id, slot_id: slotId, status: "active" });🤖 Prompt for AI Agents |
||
|
|
||
| if (duplicate) return NextResponse.json({ error: "You already have an active appointment with this doctor" }, { status: 409 }); | ||
|
Comment on lines
+31
to
+40
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. 🧹 Nitpick | 🔵 Trivial Consider using the existing The duplicate appointment check duplicates logic already implemented in 🤖 Prompt for AI Agents |
||
|
|
||
| // Book it | ||
| const { error: insertError } = await supabaseAdmin | ||
| .from("appointments") | ||
| .insert({ patient_id: user.id, doctor_id: doctorId, slot_id: slotId, status: "active" }); | ||
|
|
||
| if (insertError) return NextResponse.json({ error: "Failed to book" }, { status: 500 }); | ||
|
|
||
| return NextResponse.json({ message: "Appointment booked successfully" }); | ||
| } catch { | ||
| 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 { createClient } from "@supabase/supabase-js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function POST(_req: NextRequest) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ error: "Not implemented" }, { status: 501 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function POST(req: NextRequest) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const supabaseAdmin = createClient( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.env.NEXT_PUBLIC_SUPABASE_URL!, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.env.SUPABASE_SERVICE_ROLE_KEY! | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const token = req.headers.get("authorization")?.replace("Bearer ", ""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!token) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { data: { user } } = await supabaseAdmin.auth.getUser(token); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { appointmentId, action } = await req.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!appointmentId || !action) return NextResponse.json({ error: "Missing fields" }, { status: 400 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!["done", "cancel"].includes(action)) return NextResponse.json({ error: "Invalid action" }, { status: 400 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+26
to
+31
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. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
rg -n 'action: "cancel"|const newStatus|includes\(action\)' app/patient/dashboard/page.tsx app/doctor/dashboard/page.tsx app/api/appointments/cancel/route.tsRepository: botcode-com/task Length of output: 495 🏁 Script executed: rg -n 'action: "' app/doctor/dashboard/page.tsxRepository: botcode-com/task Length of output: 145 🏁 Script executed: rg -n 'handleAction\(' app/doctor/dashboard/page.tsx -A 2Repository: botcode-com/task Length of output: 729 Accept Both dashboards send Fix const { appointmentId, action } = await req.json();
if (!appointmentId || !action) return NextResponse.json({ error: "Missing fields" }, { status: 400 });
- if (!["done", "cancelled"].includes(action)) return NextResponse.json({ error: "Invalid action" }, { status: 400 });
-
- const newStatus = action;
+ if (!["done", "cancel"].includes(action)) return NextResponse.json({ error: "Invalid action" }, { status: 400 });
+ const newStatus = action === "done" ? "done" : "cancelled";Then remove the redeclaration at Line 77 (the second 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { data: appointment } = await supabaseAdmin | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .from("appointments") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .select("id, status, patient_id, doctor_id, slot_id, slots(start_time)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .eq("id", appointmentId) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .single(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!appointment) return NextResponse.json({ error: "Appointment not found" }, { status: 404 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+32
to
+45
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. 🧩 Analysis chain🌐 Web query:
💡 Result: In Citations:
When Use Suggested fix const { data, error } = await supabaseAdmin
.from("appointments")
.select("id, status, patient_id, doctor_id, slot_id, slots(start_time)")
.eq("id", appointmentId)
- .single();
+ .maybeSingle();
if (error) {
return NextResponse.json({ error: "Failed to fetch appointment" }, { status: 500 });
}
const appointment = data as AppointmentWithSlot | null;
if (!appointment) return NextResponse.json({ error: "Appointment not found" }, { status: 404 });📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isDoctor = appointment.doctor_id === user.id; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isPatient = appointment.patient_id === user.id; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isDoctor && !isPatient) return NextResponse.json({ error: "Forbidden" }, { status: 403 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isPatient && action === "done") return NextResponse.json({ error: "Patients cannot mark as done" }, { status: 403 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (appointment.status !== "active") return NextResponse.json({ error: `Appointment is already ${appointment.status}` }, { status: 409 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isPatient && action === "cancel") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const startTime = new Date((appointment.slots as any).start_time).getTime(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (startTime - Date.now() < 60 * 60 * 1000) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ error: "Cannot cancel within 1 hour of appointment" }, { status: 409 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+60
to
+68
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. 🧹 Nitpick | 🔵 Trivial Consider using the existing The 1-hour cancellation restriction logic duplicates 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const newStatus = action === "done" ? "done" : "cancelled"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { error: updateError } = await supabaseAdmin | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .from("appointments").update({ status: newStatus }).eq("id", appointmentId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (updateError) return NextResponse.json({ error: "Failed to update" }, { status: 500 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ message: "Updated successfully" }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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.
The
.eq()filter in the Supabase client does not support arrays for 'IN' logic. This query will fail to find existing appointments correctly because it will look for a status that literally matches the string representation of the array. Use the.in()method instead.