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:
parent
041c36079d
commit
37531f2b2b
@ -30,7 +30,9 @@ import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { LoginDialog } from "@/components/LoginDialog";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useAppointments } from "@/hooks/useAppointments";
|
||||
import { toast } from "sonner";
|
||||
import type { Appointment } from "@/lib/models/appointments";
|
||||
|
||||
interface User {
|
||||
ID: number;
|
||||
@ -77,6 +79,7 @@ export default function BookNowPage() {
|
||||
const { theme } = useAppTheme();
|
||||
const isDark = theme === "dark";
|
||||
const { isAuthenticated, logout } = useAuth();
|
||||
const { create, isCreating } = useAppointments();
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
@ -86,7 +89,6 @@ export default function BookNowPage() {
|
||||
preferredTimes: [] as string[],
|
||||
message: "",
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [booking, setBooking] = useState<Booking | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showLoginDialog, setShowLoginDialog] = useState(false);
|
||||
@ -100,131 +102,123 @@ export default function BookNowPage() {
|
||||
// Handle submit button click
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
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 () => {
|
||||
// Close login dialog
|
||||
setShowLoginDialog(false);
|
||||
// After successful login, proceed with booking submission
|
||||
await submitBooking();
|
||||
};
|
||||
|
||||
const submitBooking = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
if (formData.preferredDays.length === 0) {
|
||||
setError("Please select at least one available day.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.preferredTimes.length === 0) {
|
||||
setError("Please select at least one preferred time.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// For now, we'll use the first selected day and first selected time
|
||||
// This can be adjusted based on your backend requirements
|
||||
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
|
||||
// Convert day names to dates (YYYY-MM-DD format)
|
||||
// Get next occurrence of each selected day
|
||||
const today = new Date();
|
||||
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
const targetDayIndex = days.indexOf(firstDay);
|
||||
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];
|
||||
const preferredDates: string[] = [];
|
||||
|
||||
// Combine date and time into scheduled_at (ISO format)
|
||||
const dateTimeString = `${dateString}T${time24}:00Z`;
|
||||
formData.preferredDays.forEach((dayName) => {
|
||||
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 = {
|
||||
first_name: formData.firstName,
|
||||
last_name: formData.lastName,
|
||||
email: formData.email,
|
||||
phone: formData.phone,
|
||||
scheduled_at: dateTimeString,
|
||||
duration: 60, // Default to 60 minutes
|
||||
preferred_days: formData.preferredDays,
|
||||
preferred_times: formData.preferredTimes,
|
||||
notes: formData.message || "",
|
||||
preferred_dates: preferredDates,
|
||||
preferred_time_slots: preferredTimeSlots,
|
||||
...(formData.phone && { phone: formData.phone }),
|
||||
...(formData.message && { reason: formData.message }),
|
||||
};
|
||||
|
||||
// Simulate API call - Replace with actual API endpoint
|
||||
const response = await fetch("/api/bookings", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// Call the actual API using the hook
|
||||
const appointmentData = await create(payload);
|
||||
|
||||
// Convert API response to Booking format for display
|
||||
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),
|
||||
}).catch(() => {
|
||||
// Fallback to mock data if API is not available
|
||||
return null;
|
||||
});
|
||||
|
||||
let bookingData: Booking;
|
||||
|
||||
if (response && response.ok) {
|
||||
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",
|
||||
};
|
||||
}
|
||||
scheduled_at: appointmentData.scheduled_datetime || "",
|
||||
duration: appointmentData.scheduled_duration || 60,
|
||||
status: appointmentData.status || "pending_review",
|
||||
jitsi_room_id: appointmentData.jitsi_room_id || "",
|
||||
jitsi_room_url: appointmentData.jitsi_meet_url || "",
|
||||
payment_id: "",
|
||||
payment_status: "pending",
|
||||
amount: 0,
|
||||
notes: appointmentData.reason || "",
|
||||
};
|
||||
|
||||
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(() => {
|
||||
router.push("/");
|
||||
}, 2000);
|
||||
router.push("/user/dashboard");
|
||||
}, 3000);
|
||||
} catch (err) {
|
||||
setError("Failed to submit booking. Please try again.");
|
||||
setLoading(false);
|
||||
const errorMessage = err instanceof Error ? err.message : "Failed to submit booking. Please try again.";
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
console.error("Booking error:", err);
|
||||
}
|
||||
};
|
||||
@ -638,10 +632,10 @@ export default function BookNowPage() {
|
||||
<Button
|
||||
type="submit"
|
||||
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"
|
||||
>
|
||||
{loading ? (
|
||||
{isCreating ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Submitting...
|
||||
|
||||
207
hooks/useAppointments.ts
Normal file
207
hooks/useAppointments.ts
Normal 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
364
lib/actions/appointments.ts
Normal 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;
|
||||
}
|
||||
|
||||
@ -23,6 +23,10 @@ export const API_ENDPOINTS = {
|
||||
},
|
||||
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;
|
||||
|
||||
|
||||
78
lib/models/appointments.ts
Normal file
78
lib/models/appointments.ts
Normal 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;
|
||||
}
|
||||
|
||||
43
lib/schema/appointments.ts
Normal file
43
lib/schema/appointments.ts
Normal 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>;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user