Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .env.example

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
CLAUDE.md
193 changes: 193 additions & 0 deletions app/api/functions/book_appointment/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { NextResponse } from "next/server";
import { parseDateTime } from "@/lib/dateUtils";

export async function POST(request: Request) {
try {
const {
start,
attendeeName,
attendeeEmail,
attendeeTimeZone,
eventTypeId,
eventTypeSlug,
username,
lengthInMinutes,
attendeePhoneNumber,
guests,
language = "en",
} = await request.json();

// Get Cal.com API key and default username from environment variables
const calApiKey = process.env.CAL_API_KEY;
const defaultUsername = process.env.CAL_USERNAME;

if (!calApiKey) {
return NextResponse.json(
{ error: "Cal.com API key not configured" },
{ status: 500 }
);
}

// Validate required parameters
if (!start || !attendeeName || !attendeeEmail || !attendeeTimeZone) {
return NextResponse.json(
{ error: "Missing required parameters: start, attendeeName, attendeeEmail, attendeeTimeZone" },
{ status: 400 }
);
}

// Use provided username or fall back to default
const targetUsername = username || defaultUsername;

if (!eventTypeId && (!eventTypeSlug || !targetUsername)) {
return NextResponse.json(
{ error: "Either eventTypeId or both eventTypeSlug and username must be provided" },
{ status: 400 }
);
}

// Parse and convert start time to UTC format (Cal.com requires UTC without timezone offset)
let utcStartTime;
try {
console.log("Original start time received:", start);

// Try to parse the start time - it might be in ISO format or need parsing
if (start.includes('T')) {
// If it's already in ISO format, parse it properly considering timezone
const inputDate = new Date(start);
console.log("Parsed input date:", inputDate);

// Extract date and time components
const year = inputDate.getFullYear();
const month = inputDate.getMonth();
const day = inputDate.getDate();
const hours = inputDate.getHours();
const minutes = inputDate.getMinutes();

console.log("Date components:", { year, month, day, hours, minutes });

const currentYear = new Date().getFullYear();
const currentDate = new Date();
console.log("Current year:", currentYear, "Current date:", currentDate);

// If the parsed date has wrong year or is in the past, correct it
if (year < currentYear || inputDate < currentDate) {
console.log("Date appears to be wrong/past, correcting...");

// Calculate tomorrow's date with the desired time
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(hours, minutes, 0, 0);

console.log("Corrected to tomorrow:", tomorrow);
utcStartTime = tomorrow.toISOString();
} else {
console.log("Using original date");
utcStartTime = inputDate.toISOString();
}
} else {
// Parse natural language (e.g., "tomorrow", "10 AM")
console.log("Parsing as natural language");
const parsed = parseDateTime("tomorrow", start, attendeeTimeZone);
utcStartTime = parsed.utcDateTime;
}

console.log("Final UTC start time:", utcStartTime);
} catch (error) {
console.error("Date parsing error:", error);
return NextResponse.json(
{ error: "Invalid start time format. Please provide a valid date/time." },
{ status: 400 }
);
}

// Prepare the booking request body
const bookingData: any = {
start: utcStartTime,
attendee: {
name: attendeeName,
email: attendeeEmail,
timeZone: attendeeTimeZone,
language: language,
},
metadata: {},
};

// Add optional attendee phone number
if (attendeePhoneNumber) {
bookingData.attendee.phoneNumber = attendeePhoneNumber;
}

// Add event type identification
if (eventTypeId) {
bookingData.eventTypeId = eventTypeId;
} else {
bookingData.eventTypeSlug = eventTypeSlug;
bookingData.username = targetUsername;
}

// Don't include lengthInMinutes - Cal.com rejects this for fixed-length events
// and most event types have fixed lengths

if (guests && guests.length > 0) {
bookingData.guests = guests;
}

console.log("Sending booking request to Cal.com:", bookingData);

// Make the request to Cal.com API
const response = await fetch("https://api.cal.com/v2/bookings", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${calApiKey}`,
"cal-api-version": "2024-08-13",
},
body: JSON.stringify(bookingData),
});

const result = await response.json();

if (!response.ok) {
console.error("Cal.com API error:", result);
return NextResponse.json(
{
error: "Failed to create booking",
details: result.error || result.message || "Unknown error",
status: response.status,
},
{ status: response.status }
);
}

console.log("Cal.com booking successful:", result);

// Return the booking details
return NextResponse.json({
success: true,
booking: {
id: result.data.id,
uid: result.data.uid,
title: result.data.title,
start: result.data.start,
end: result.data.end,
duration: result.data.duration,
status: result.data.status,
meetingUrl: result.data.meetingUrl || result.data.location,
attendees: result.data.attendees,
hosts: result.data.hosts,
},
message: "Appointment booked successfully!",
});

} catch (error) {
console.error("Error booking appointment:", error);
return NextResponse.json(
{
error: "Internal server error",
details: error instanceof Error ? error.message : "Unknown error",
},
{ status: 500 }
);
}
}
163 changes: 163 additions & 0 deletions app/api/functions/get_available_slots/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { NextResponse } from "next/server";

export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const eventTypeId = searchParams.get('eventTypeId');
const startDate = searchParams.get('startDate');
const endDate = searchParams.get('endDate');
const timeZone = searchParams.get('timeZone') || 'UTC';

console.log("get_available_slots called with parameters:", {
eventTypeId,
startDate,
endDate,
timeZone
});

// Get Cal.com API key from environment variables
const calApiKey = process.env.CAL_API_KEY;
if (!calApiKey) {
return NextResponse.json(
{ error: "Cal.com API key not configured. Please add CAL_API_KEY to your environment variables." },
{ status: 500 }
);
}

// Validate required parameters
if (!eventTypeId || !startDate) {
return NextResponse.json(
{ error: "Missing required parameters: eventTypeId and startDate are required" },
{ status: 400 }
);
}

// Validate date format (should be YYYY-MM-DD)
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(startDate)) {
console.error("Invalid startDate format:", startDate);
return NextResponse.json(
{ error: `Invalid startDate format. Expected YYYY-MM-DD, got: ${startDate}` },
{ status: 400 }
);
}

if (endDate && !dateRegex.test(endDate)) {
console.error("Invalid endDate format:", endDate);
return NextResponse.json(
{ error: `Invalid endDate format. Expected YYYY-MM-DD, got: ${endDate}` },
{ status: 400 }
);
}

// Check if the date is in the past
const requestedDate = new Date(startDate + 'T00:00:00Z');
const today = new Date();
today.setHours(0, 0, 0, 0); // Set to start of today

if (requestedDate < today) {
console.error("Date appears to be in the past:", startDate, "Today:", today.toISOString().split('T')[0]);
return NextResponse.json(
{ error: `Date appears to be in the past. Got: ${startDate}, today is: ${today.toISOString().split('T')[0]}` },
{ status: 400 }
);
}

// Calculate end date if not provided (default to 7 days from start)
let finalEndDate = endDate;
if (!finalEndDate) {
const start = new Date(startDate);
const end = new Date(start);
end.setDate(start.getDate() + 7);
finalEndDate = end.toISOString().split('T')[0]; // Format as YYYY-MM-DD
}

// Build the URL for Cal.com Slots API
const params = new URLSearchParams({
eventTypeId: eventTypeId,
start: startDate,
end: finalEndDate,
timeZone: timeZone,
format: 'range' // Get start and end times for each slot
});

const apiUrl = `https://api.cal.com/v2/slots?${params.toString()}`;

console.log("Fetching available slots from Cal.com:", apiUrl);

// Make the request to Cal.com API
const response = await fetch(apiUrl, {
method: "GET",
headers: {
"Authorization": `Bearer ${calApiKey}`,
"cal-api-version": "2024-09-04",
"Content-Type": "application/json",
},
});

const result = await response.json();

if (!response.ok) {
console.error("Cal.com API error:", result);
return NextResponse.json(
{
error: "Failed to fetch available slots",
details: result.error || result.message || "Unknown error",
status: response.status,
},
{ status: response.status }
);
}

console.log("Cal.com slots fetched successfully:", result);

// Format the slots for easier use
const formattedSlots: { [date: string]: any[] } = {};
let totalSlots = 0;

if (result.data) {
Object.keys(result.data).forEach(date => {
const daySlots = result.data[date];
if (daySlots && daySlots.length > 0) {
formattedSlots[date] = daySlots.map((slot: any) => ({
start: slot.start,
end: slot.end,
date: date,
timeDisplay: new Date(slot.start).toLocaleTimeString('en-US', {
timeZone: timeZone,
hour: 'numeric',
minute: '2-digit',
hour12: true
})
}));
totalSlots += daySlots.length;
}
});
}

// Return the formatted slots
return NextResponse.json({
success: true,
slots: formattedSlots,
totalSlots,
dateRange: {
start: startDate,
end: finalEndDate,
timeZone: timeZone
},
message: totalSlots > 0
? `Found ${totalSlots} available slot(s) between ${startDate} and ${finalEndDate}`
: `No available slots found between ${startDate} and ${finalEndDate}`,
});

} catch (error) {
console.error("Error fetching available slots:", error);
return NextResponse.json(
{
error: "Internal server error",
details: error instanceof Error ? error.message : "Unknown error",
},
{ status: 500 }
);
}
}
Loading