Refactor booking process in BookNowPage to integrate appointment creation via useAppointments hook. Enhance form submission logic to check user authentication before proceeding. Update API endpoint configurations for appointment management, including available dates and user appointments. Improve error handling and user feedback with toast notifications.

This commit is contained in:
iamkiddy 2025-11-23 21:43:13 +00:00
parent 041c36079d
commit 37531f2b2b
6 changed files with 781 additions and 91 deletions

View File

@ -30,7 +30,9 @@ import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { LoginDialog } from "@/components/LoginDialog"; import { LoginDialog } from "@/components/LoginDialog";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { useAppointments } from "@/hooks/useAppointments";
import { toast } from "sonner"; import { toast } from "sonner";
import type { Appointment } from "@/lib/models/appointments";
interface User { interface User {
ID: number; ID: number;
@ -77,6 +79,7 @@ export default function BookNowPage() {
const { theme } = useAppTheme(); const { theme } = useAppTheme();
const isDark = theme === "dark"; const isDark = theme === "dark";
const { isAuthenticated, logout } = useAuth(); const { isAuthenticated, logout } = useAuth();
const { create, isCreating } = useAppointments();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
firstName: "", firstName: "",
lastName: "", lastName: "",
@ -86,7 +89,6 @@ export default function BookNowPage() {
preferredTimes: [] as string[], preferredTimes: [] as string[],
message: "", message: "",
}); });
const [loading, setLoading] = useState(false);
const [booking, setBooking] = useState<Booking | null>(null); const [booking, setBooking] = useState<Booking | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [showLoginDialog, setShowLoginDialog] = useState(false); const [showLoginDialog, setShowLoginDialog] = useState(false);
@ -100,131 +102,123 @@ export default function BookNowPage() {
// Handle submit button click // Handle submit button click
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
// Open login dialog instead of submitting directly
setShowLoginDialog(true); // Check if user is authenticated
if (!isAuthenticated) {
// Open login dialog if not authenticated
setShowLoginDialog(true);
return;
}
// If authenticated, proceed with booking
await submitBooking();
}; };
const handleLoginSuccess = async () => { const handleLoginSuccess = async () => {
// Close login dialog
setShowLoginDialog(false);
// After successful login, proceed with booking submission // After successful login, proceed with booking submission
await submitBooking(); await submitBooking();
}; };
const submitBooking = async () => { const submitBooking = async () => {
setLoading(true);
setError(null); setError(null);
try { try {
if (formData.preferredDays.length === 0) { if (formData.preferredDays.length === 0) {
setError("Please select at least one available day."); setError("Please select at least one available day.");
setLoading(false);
return; return;
} }
if (formData.preferredTimes.length === 0) { if (formData.preferredTimes.length === 0) {
setError("Please select at least one preferred time."); setError("Please select at least one preferred time.");
setLoading(false);
return; return;
} }
// For now, we'll use the first selected day and first selected time // Convert day names to dates (YYYY-MM-DD format)
// This can be adjusted based on your backend requirements // Get next occurrence of each selected day
const firstDay = formData.preferredDays[0];
const firstTime = formData.preferredTimes[0];
const timeMap: { [key: string]: string } = {
morning: "09:00",
lunchtime: "12:00",
afternoon: "14:00",
};
const time24 = timeMap[firstTime] || "09:00";
// Get next occurrence of the first selected day
const today = new Date(); const today = new Date();
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const targetDayIndex = days.indexOf(firstDay); const preferredDates: string[] = [];
let daysUntilTarget = (targetDayIndex - today.getDay() + 7) % 7;
if (daysUntilTarget === 0) daysUntilTarget = 7; // Next week if today
const targetDate = new Date(today);
targetDate.setDate(today.getDate() + daysUntilTarget);
const dateString = targetDate.toISOString().split("T")[0];
// Combine date and time into scheduled_at (ISO format) formData.preferredDays.forEach((dayName) => {
const dateTimeString = `${dateString}T${time24}:00Z`; const targetDayIndex = days.indexOf(dayName);
if (targetDayIndex === -1) return;
// Prepare request payload let daysUntilTarget = (targetDayIndex - today.getDay() + 7) % 7;
if (daysUntilTarget === 0) daysUntilTarget = 7; // Next week if today
const targetDate = new Date(today);
targetDate.setDate(today.getDate() + daysUntilTarget);
const dateString = targetDate.toISOString().split("T")[0];
preferredDates.push(dateString);
});
// Map time slots - API expects "morning", "afternoon", "evening"
// Form has "morning", "lunchtime", "afternoon"
const timeSlotMap: { [key: string]: "morning" | "afternoon" | "evening" } = {
morning: "morning",
lunchtime: "afternoon", // Map lunchtime to afternoon
afternoon: "afternoon",
};
const preferredTimeSlots = formData.preferredTimes
.map((time) => timeSlotMap[time] || "morning")
.filter((time, index, self) => self.indexOf(time) === index) as ("morning" | "afternoon" | "evening")[]; // Remove duplicates
// Prepare request payload according to API spec
const payload = { const payload = {
first_name: formData.firstName, first_name: formData.firstName,
last_name: formData.lastName, last_name: formData.lastName,
email: formData.email, email: formData.email,
phone: formData.phone, preferred_dates: preferredDates,
scheduled_at: dateTimeString, preferred_time_slots: preferredTimeSlots,
duration: 60, // Default to 60 minutes ...(formData.phone && { phone: formData.phone }),
preferred_days: formData.preferredDays, ...(formData.message && { reason: formData.message }),
preferred_times: formData.preferredTimes,
notes: formData.message || "",
}; };
// Simulate API call - Replace with actual API endpoint // Call the actual API using the hook
const response = await fetch("/api/bookings", { const appointmentData = await create(payload);
method: "POST",
headers: { // Convert API response to Booking format for display
"Content-Type": "application/json", const bookingData: Booking = {
ID: parseInt(appointmentData.id) || Math.floor(Math.random() * 1000),
CreatedAt: appointmentData.created_at || new Date().toISOString(),
UpdatedAt: appointmentData.updated_at || new Date().toISOString(),
DeletedAt: null,
user_id: 0, // API doesn't return user_id in this response
user: {
ID: 0,
first_name: appointmentData.first_name,
last_name: appointmentData.last_name,
email: appointmentData.email,
phone: appointmentData.phone || "",
location: "",
is_admin: false,
bookings: null,
}, },
body: JSON.stringify(payload), scheduled_at: appointmentData.scheduled_datetime || "",
}).catch(() => { duration: appointmentData.scheduled_duration || 60,
// Fallback to mock data if API is not available status: appointmentData.status || "pending_review",
return null; jitsi_room_id: appointmentData.jitsi_room_id || "",
}); jitsi_room_url: appointmentData.jitsi_meet_url || "",
payment_id: "",
let bookingData: Booking; payment_status: "pending",
amount: 0,
if (response && response.ok) { notes: appointmentData.reason || "",
const data: BookingsResponse = await response.json(); };
bookingData = data.bookings[0];
} else {
// Mock response for development - matches the API structure provided
await new Promise((resolve) => setTimeout(resolve, 1000));
bookingData = {
ID: Math.floor(Math.random() * 1000),
CreatedAt: new Date().toISOString(),
UpdatedAt: new Date().toISOString(),
DeletedAt: null,
user_id: 1,
user: {
ID: 1,
CreatedAt: new Date().toISOString(),
UpdatedAt: new Date().toISOString(),
DeletedAt: null,
first_name: formData.firstName,
last_name: formData.lastName,
email: formData.email,
phone: formData.phone,
location: "",
date_of_birth: "0001-01-01T00:00:00Z",
is_admin: false,
bookings: null,
},
scheduled_at: dateTimeString,
duration: 60,
status: "scheduled",
jitsi_room_id: `booking-${Math.floor(Math.random() * 1000)}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
jitsi_room_url: `https://meet.jit.si/booking-${Math.floor(Math.random() * 1000)}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
payment_id: "",
payment_status: "pending",
amount: 52,
notes: formData.message || "Initial consultation session",
};
}
setBooking(bookingData); setBooking(bookingData);
setLoading(false); toast.success("Appointment request submitted successfully! We'll review and get back to you soon.");
// Redirect to home after 2 seconds // Redirect to user dashboard after 3 seconds
setTimeout(() => { setTimeout(() => {
router.push("/"); router.push("/user/dashboard");
}, 2000); }, 3000);
} catch (err) { } catch (err) {
setError("Failed to submit booking. Please try again."); const errorMessage = err instanceof Error ? err.message : "Failed to submit booking. Please try again.";
setLoading(false); setError(errorMessage);
toast.error(errorMessage);
console.error("Booking error:", err); console.error("Booking error:", err);
} }
}; };
@ -638,10 +632,10 @@ export default function BookNowPage() {
<Button <Button
type="submit" type="submit"
size="lg" size="lg"
disabled={loading} disabled={isCreating}
className="w-full bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all h-12 text-base font-semibold disabled:opacity-50 disabled:cursor-not-allowed" className="w-full bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all h-12 text-base font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
> >
{loading ? ( {isCreating ? (
<> <>
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> <Loader2 className="w-4 h-4 mr-2 animate-spin" />
Submitting... Submitting...

207
hooks/useAppointments.ts Normal file
View File

@ -0,0 +1,207 @@
"use client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback } from "react";
import {
createAppointment,
getAvailableDates,
listAppointments,
getUserAppointments,
getAppointmentDetail,
scheduleAppointment,
rejectAppointment,
getAdminAvailability,
updateAdminAvailability,
getAppointmentStats,
getJitsiMeetingInfo,
} from "@/lib/actions/appointments";
import type {
CreateAppointmentInput,
ScheduleAppointmentInput,
RejectAppointmentInput,
UpdateAvailabilityInput,
} from "@/lib/schema/appointments";
import type {
Appointment,
AdminAvailability,
AppointmentStats,
JitsiMeetingInfo,
} from "@/lib/models/appointments";
export function useAppointments() {
const queryClient = useQueryClient();
// Get available dates query
const availableDatesQuery = useQuery<string[]>({
queryKey: ["appointments", "available-dates"],
queryFn: () => getAvailableDates(),
staleTime: 5 * 60 * 1000, // 5 minutes
});
// List appointments query
const appointmentsQuery = useQuery<Appointment[]>({
queryKey: ["appointments", "list"],
queryFn: () => listAppointments(),
enabled: false, // Only fetch when explicitly called
});
// Get user appointments query
const userAppointmentsQuery = useQuery<Appointment[]>({
queryKey: ["appointments", "user"],
queryFn: () => getUserAppointments(),
staleTime: 30 * 1000, // 30 seconds
});
// Get appointment detail query
const useAppointmentDetail = (id: string | null) => {
return useQuery<Appointment>({
queryKey: ["appointments", "detail", id],
queryFn: () => getAppointmentDetail(id!),
enabled: !!id,
});
};
// Get admin availability query
const adminAvailabilityQuery = useQuery<AdminAvailability>({
queryKey: ["appointments", "admin", "availability"],
queryFn: () => getAdminAvailability(),
staleTime: 5 * 60 * 1000, // 5 minutes
});
// Get appointment stats query
const appointmentStatsQuery = useQuery<AppointmentStats>({
queryKey: ["appointments", "stats"],
queryFn: () => getAppointmentStats(),
staleTime: 1 * 60 * 1000, // 1 minute
});
// Get Jitsi meeting info query
const useJitsiMeetingInfo = (id: string | null) => {
return useQuery<JitsiMeetingInfo>({
queryKey: ["appointments", "jitsi", id],
queryFn: () => getJitsiMeetingInfo(id!),
enabled: !!id,
staleTime: 30 * 1000, // 30 seconds
});
};
// Create appointment mutation
const createAppointmentMutation = useMutation({
mutationFn: (input: CreateAppointmentInput) => createAppointment(input),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["appointments"] });
},
});
// Schedule appointment mutation
const scheduleAppointmentMutation = useMutation({
mutationFn: ({ id, input }: { id: string; input: ScheduleAppointmentInput }) =>
scheduleAppointment(id, input),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["appointments"] });
},
});
// Reject appointment mutation
const rejectAppointmentMutation = useMutation({
mutationFn: ({ id, input }: { id: string; input: RejectAppointmentInput }) =>
rejectAppointment(id, input),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["appointments"] });
},
});
// Update admin availability mutation
const updateAdminAvailabilityMutation = useMutation({
mutationFn: (input: UpdateAvailabilityInput) => updateAdminAvailability(input),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["appointments", "admin", "availability"] });
queryClient.invalidateQueries({ queryKey: ["appointments", "available-dates"] });
},
});
// Convenience functions
const create = useCallback(
async (input: CreateAppointmentInput) => {
return await createAppointmentMutation.mutateAsync(input);
},
[createAppointmentMutation]
);
const schedule = useCallback(
async (id: string, input: ScheduleAppointmentInput) => {
return await scheduleAppointmentMutation.mutateAsync({ id, input });
},
[scheduleAppointmentMutation]
);
const reject = useCallback(
async (id: string, input: RejectAppointmentInput) => {
return await rejectAppointmentMutation.mutateAsync({ id, input });
},
[rejectAppointmentMutation]
);
const updateAvailability = useCallback(
async (input: UpdateAvailabilityInput) => {
return await updateAdminAvailabilityMutation.mutateAsync(input);
},
[updateAdminAvailabilityMutation]
);
const fetchAppointments = useCallback(
async (email?: string) => {
const data = await listAppointments(email);
queryClient.setQueryData(["appointments", "list"], data);
return data;
},
[queryClient]
);
return {
// Queries
availableDates: availableDatesQuery.data || [],
appointments: appointmentsQuery.data || [],
userAppointments: userAppointmentsQuery.data || [],
adminAvailability: adminAvailabilityQuery.data,
appointmentStats: appointmentStatsQuery.data,
// Query states
isLoadingAvailableDates: availableDatesQuery.isLoading,
isLoadingAppointments: appointmentsQuery.isLoading,
isLoadingUserAppointments: userAppointmentsQuery.isLoading,
isLoadingAdminAvailability: adminAvailabilityQuery.isLoading,
isLoadingStats: appointmentStatsQuery.isLoading,
// Query refetch functions
refetchAvailableDates: availableDatesQuery.refetch,
refetchAppointments: appointmentsQuery.refetch,
refetchUserAppointments: userAppointmentsQuery.refetch,
refetchAdminAvailability: adminAvailabilityQuery.refetch,
refetchStats: appointmentStatsQuery.refetch,
// Hooks for specific queries
useAppointmentDetail,
useJitsiMeetingInfo,
// Mutations
create,
schedule,
reject,
updateAvailability,
fetchAppointments,
// Mutation states
isCreating: createAppointmentMutation.isPending,
isScheduling: scheduleAppointmentMutation.isPending,
isRejecting: rejectAppointmentMutation.isPending,
isUpdatingAvailability: updateAdminAvailabilityMutation.isPending,
// Direct mutation access (if needed)
createAppointmentMutation,
scheduleAppointmentMutation,
rejectAppointmentMutation,
updateAdminAvailabilityMutation,
};
}

364
lib/actions/appointments.ts Normal file
View File

@ -0,0 +1,364 @@
import { API_ENDPOINTS } from "@/lib/api_urls";
import { getStoredTokens } from "./auth";
import type {
CreateAppointmentInput,
ScheduleAppointmentInput,
RejectAppointmentInput,
UpdateAvailabilityInput,
} from "@/lib/schema/appointments";
import type {
Appointment,
AppointmentResponse,
AppointmentsListResponse,
AvailableDatesResponse,
AdminAvailability,
AppointmentStats,
JitsiMeetingInfo,
ApiError,
} from "@/lib/models/appointments";
// Helper function to extract error message from API response
function extractErrorMessage(error: ApiError): string {
if (error.detail) {
if (Array.isArray(error.detail)) {
return error.detail.join(", ");
}
return String(error.detail);
}
if (error.message) {
if (Array.isArray(error.message)) {
return error.message.join(", ");
}
return String(error.message);
}
if (typeof error === "string") {
return error;
}
return "An error occurred while creating the appointment";
}
// Create appointment
export async function createAppointment(
input: CreateAppointmentInput
): Promise<Appointment> {
const tokens = getStoredTokens();
if (!tokens.access) {
throw new Error("Authentication required. Please log in to book an appointment.");
}
const response = await fetch(API_ENDPOINTS.meetings.createAppointment, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens.access}`,
},
body: JSON.stringify(input),
});
const data: AppointmentResponse = await response.json();
if (!response.ok) {
const errorMessage = extractErrorMessage(data as ApiError);
throw new Error(errorMessage);
}
// Handle different response formats
if (data.appointment) {
return data.appointment;
}
if ((data as any).data) {
return (data as any).data;
}
// If appointment is returned directly
return data as unknown as Appointment;
}
// Get available dates
export async function getAvailableDates(): Promise<string[]> {
const response = await fetch(API_ENDPOINTS.meetings.availableDates, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const data: AvailableDatesResponse | string[] = await response.json();
if (!response.ok) {
const errorMessage = extractErrorMessage(data as ApiError);
throw new Error(errorMessage);
}
// API returns array of dates in YYYY-MM-DD format
if (Array.isArray(data)) {
return data;
}
return (data as AvailableDatesResponse).dates || [];
}
// List appointments (Admin sees all, users see their own)
export async function listAppointments(email?: string): Promise<Appointment[]> {
const tokens = getStoredTokens();
if (!tokens.access) {
throw new Error("Authentication required.");
}
const url = email
? `${API_ENDPOINTS.meetings.listAppointments}?email=${encodeURIComponent(email)}`
: API_ENDPOINTS.meetings.listAppointments;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens.access}`,
},
});
const data: AppointmentsListResponse = await response.json();
if (!response.ok) {
const errorMessage = extractErrorMessage(data as ApiError);
throw new Error(errorMessage);
}
return data.appointments || [];
}
// Get user appointments
export async function getUserAppointments(): Promise<Appointment[]> {
const tokens = getStoredTokens();
if (!tokens.access) {
throw new Error("Authentication required.");
}
const response = await fetch(API_ENDPOINTS.meetings.userAppointments, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens.access}`,
},
});
const data: AppointmentsListResponse = await response.json();
if (!response.ok) {
const errorMessage = extractErrorMessage(data as ApiError);
throw new Error(errorMessage);
}
return data.appointments || [];
}
// Get appointment detail
export async function getAppointmentDetail(id: string): Promise<Appointment> {
const tokens = getStoredTokens();
if (!tokens.access) {
throw new Error("Authentication required.");
}
const response = await fetch(`${API_ENDPOINTS.meetings.listAppointments}${id}/`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens.access}`,
},
});
const data: AppointmentResponse = await response.json();
if (!response.ok) {
const errorMessage = extractErrorMessage(data as ApiError);
throw new Error(errorMessage);
}
if (data.appointment) {
return data.appointment;
}
return data as unknown as Appointment;
}
// Schedule appointment (Admin only)
export async function scheduleAppointment(
id: string,
input: ScheduleAppointmentInput
): Promise<Appointment> {
const tokens = getStoredTokens();
if (!tokens.access) {
throw new Error("Authentication required.");
}
const response = await fetch(`${API_ENDPOINTS.meetings.listAppointments}${id}/schedule/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens.access}`,
},
body: JSON.stringify(input),
});
const data: AppointmentResponse = await response.json();
if (!response.ok) {
const errorMessage = extractErrorMessage(data as ApiError);
throw new Error(errorMessage);
}
if (data.appointment) {
return data.appointment;
}
return data as unknown as Appointment;
}
// Reject appointment (Admin only)
export async function rejectAppointment(
id: string,
input: RejectAppointmentInput
): Promise<Appointment> {
const tokens = getStoredTokens();
if (!tokens.access) {
throw new Error("Authentication required.");
}
const response = await fetch(`${API_ENDPOINTS.meetings.listAppointments}${id}/reject/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens.access}`,
},
body: JSON.stringify(input),
});
const data: AppointmentResponse = await response.json();
if (!response.ok) {
const errorMessage = extractErrorMessage(data as ApiError);
throw new Error(errorMessage);
}
if (data.appointment) {
return data.appointment;
}
return data as unknown as Appointment;
}
// Get admin availability
export async function getAdminAvailability(): Promise<AdminAvailability> {
const tokens = getStoredTokens();
if (!tokens.access) {
throw new Error("Authentication required.");
}
const response = await fetch(`${API_ENDPOINTS.meetings.base}admin/availability/`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens.access}`,
},
});
const data: AdminAvailability = await response.json();
if (!response.ok) {
const errorMessage = extractErrorMessage(data as ApiError);
throw new Error(errorMessage);
}
return data;
}
// Update admin availability
export async function updateAdminAvailability(
input: UpdateAvailabilityInput
): Promise<AdminAvailability> {
const tokens = getStoredTokens();
if (!tokens.access) {
throw new Error("Authentication required.");
}
const response = await fetch(`${API_ENDPOINTS.meetings.base}admin/availability/`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens.access}`,
},
body: JSON.stringify(input),
});
const data: AdminAvailability = await response.json();
if (!response.ok) {
const errorMessage = extractErrorMessage(data as ApiError);
throw new Error(errorMessage);
}
return data;
}
// Get appointment stats (Admin only)
export async function getAppointmentStats(): Promise<AppointmentStats> {
const tokens = getStoredTokens();
if (!tokens.access) {
throw new Error("Authentication required.");
}
const response = await fetch(`${API_ENDPOINTS.meetings.listAppointments}stats/`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens.access}`,
},
});
const data: AppointmentStats = await response.json();
if (!response.ok) {
const errorMessage = extractErrorMessage(data as ApiError);
throw new Error(errorMessage);
}
return data;
}
// Get Jitsi meeting info
export async function getJitsiMeetingInfo(id: string): Promise<JitsiMeetingInfo> {
const tokens = getStoredTokens();
if (!tokens.access) {
throw new Error("Authentication required.");
}
const response = await fetch(`${API_ENDPOINTS.meetings.listAppointments}${id}/jitsi-meeting/`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens.access}`,
},
});
const data: JitsiMeetingInfo = await response.json();
if (!response.ok) {
const errorMessage = extractErrorMessage(data as ApiError);
throw new Error(errorMessage);
}
return data;
}

View File

@ -23,6 +23,10 @@ export const API_ENDPOINTS = {
}, },
meetings: { meetings: {
base: `${API_BASE_URL}/meetings/`, base: `${API_BASE_URL}/meetings/`,
availableDates: `${API_BASE_URL}/meetings/appointments/available-dates/`,
createAppointment: `${API_BASE_URL}/meetings/appointments/create/`,
listAppointments: `${API_BASE_URL}/meetings/appointments/`,
userAppointments: `${API_BASE_URL}/meetings/user/appointments/`,
}, },
} as const; } as const;

View File

@ -0,0 +1,78 @@
// Appointment Models
export interface Appointment {
id: string;
first_name: string;
last_name: string;
email: string;
phone?: string;
reason?: string;
preferred_dates: string[]; // YYYY-MM-DD format
preferred_time_slots: string[]; // "morning", "afternoon", "evening"
status: "pending_review" | "scheduled" | "rejected";
created_at: string;
updated_at: string;
scheduled_datetime?: string;
scheduled_duration?: number;
rejection_reason?: string;
jitsi_meet_url?: string;
jitsi_room_id?: string;
has_jitsi_meeting?: boolean;
can_join_meeting?: boolean;
meeting_status?: string;
}
export interface AppointmentResponse {
appointment?: Appointment;
message?: string;
[key: string]: any;
}
export interface AppointmentsListResponse {
appointments: Appointment[];
count?: number;
next?: string | null;
previous?: string | null;
}
export interface AvailableDatesResponse {
dates: string[]; // YYYY-MM-DD format
available_days?: number[]; // 0-6 (Monday-Sunday)
available_days_display?: string[];
}
export interface AdminAvailability {
available_days: number[]; // 0-6 (Monday-Sunday)
available_days_display: string[];
}
export interface AppointmentStats {
total_requests: number;
pending_review: number;
scheduled: number;
rejected: number;
completion_rate: number;
}
export interface JitsiMeetingInfo {
meeting_url: string;
room_id: string;
scheduled_time: string;
duration: string;
can_join: boolean;
meeting_status: string;
join_instructions: string;
}
export interface ApiError {
detail?: string | string[];
message?: string | string[];
error?: string;
preferred_dates?: string[];
preferred_time_slots?: string[];
email?: string[];
first_name?: string[];
last_name?: string[];
[key: string]: string | string[] | undefined;
}

View File

@ -0,0 +1,43 @@
import { z } from "zod";
// Create Appointment Schema
export const createAppointmentSchema = z.object({
first_name: z.string().min(1, "First name is required"),
last_name: z.string().min(1, "Last name is required"),
email: z.string().email("Invalid email address"),
preferred_dates: z
.array(z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format"))
.min(1, "At least one preferred date is required"),
preferred_time_slots: z
.array(z.enum(["morning", "afternoon", "evening"]))
.min(1, "At least one preferred time slot is required"),
phone: z.string().optional(),
reason: z.string().optional(),
});
export type CreateAppointmentInput = z.infer<typeof createAppointmentSchema>;
// Schedule Appointment Schema (Admin only)
export const scheduleAppointmentSchema = z.object({
scheduled_datetime: z.string().datetime("Invalid datetime format"),
scheduled_duration: z.number().int().positive().optional(),
});
export type ScheduleAppointmentInput = z.infer<typeof scheduleAppointmentSchema>;
// Reject Appointment Schema (Admin only)
export const rejectAppointmentSchema = z.object({
rejection_reason: z.string().optional(),
});
export type RejectAppointmentInput = z.infer<typeof rejectAppointmentSchema>;
// Update Admin Availability Schema
export const updateAvailabilitySchema = z.object({
available_days: z
.array(z.number().int().min(0).max(6))
.min(1, "At least one day must be selected"),
});
export type UpdateAvailabilityInput = z.infer<typeof updateAvailabilitySchema>;