diff --git a/app/(admin)/admin/booking/page.tsx b/app/(admin)/admin/booking/page.tsx index 1b03984..7a85db0 100644 --- a/app/(admin)/admin/booking/page.tsx +++ b/app/(admin)/admin/booking/page.tsx @@ -11,9 +11,12 @@ import { X, Loader2, User, + Settings, + Check, } from "lucide-react"; import { useAppTheme } from "@/components/ThemeProvider"; import { listAppointments, scheduleAppointment, rejectAppointment } from "@/lib/actions/appointments"; +import { useAppointments } from "@/hooks/useAppointments"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { @@ -45,6 +48,79 @@ export default function Booking() { const [isRejecting, setIsRejecting] = useState(false); const { theme } = useAppTheme(); const isDark = theme === "dark"; + + // Availability management + const { adminAvailability, isLoadingAdminAvailability, updateAdminAvailability, isUpdatingAvailability } = useAppointments(); + const [selectedDays, setSelectedDays] = useState([]); + const [availabilityDialogOpen, setAvailabilityDialogOpen] = useState(false); + const [startTime, setStartTime] = useState("09:00"); + const [endTime, setEndTime] = useState("17:00"); + + const daysOfWeek = [ + { value: 0, label: "Monday" }, + { value: 1, label: "Tuesday" }, + { value: 2, label: "Wednesday" }, + { value: 3, label: "Thursday" }, + { value: 4, label: "Friday" }, + { value: 5, label: "Saturday" }, + { value: 6, label: "Sunday" }, + ]; + + // Initialize selected days when availability is loaded + useEffect(() => { + if (adminAvailability?.available_days) { + setSelectedDays(adminAvailability.available_days); + } + }, [adminAvailability]); + + // Generate time slots for time picker + const generateTimeSlots = () => { + const slots = []; + for (let hour = 0; hour < 24; hour++) { + for (let minute = 0; minute < 60; minute += 30) { + const timeString = `${hour.toString().padStart(2, "0")}:${minute.toString().padStart(2, "0")}`; + slots.push(timeString); + } + } + return slots; + }; + + const timeSlotsForPicker = generateTimeSlots(); + + const handleDayToggle = (day: number) => { + setSelectedDays((prev) => + prev.includes(day) ? prev.filter((d) => d !== day) : [...prev, day].sort() + ); + }; + + const handleSaveAvailability = async () => { + if (selectedDays.length === 0) { + toast.error("Please select at least one available day"); + return; + } + + if (startTime >= endTime) { + toast.error("End time must be after start time"); + return; + } + + try { + await updateAdminAvailability({ available_days: selectedDays }); + toast.success("Availability updated successfully!"); + setAvailabilityDialogOpen(false); + } catch (error) { + console.error("Failed to update availability:", error); + const errorMessage = error instanceof Error ? error.message : "Failed to update availability"; + toast.error(errorMessage); + } + }; + + const handleOpenAvailabilityDialog = () => { + if (adminAvailability?.available_days) { + setSelectedDays(adminAvailability.available_days); + } + setAvailabilityDialogOpen(true); + }; useEffect(() => { const fetchBookings = async () => { @@ -235,7 +311,16 @@ export default function Booking() { Manage and view all appointment bookings

+ + {/* Search Bar */}
@@ -581,6 +666,188 @@ export default function Booking() { + {/* Availability Management Dialog */} + + + + + Manage Weekly Availability + + + +
+ {isLoadingAdminAvailability ? ( +
+ +
+ ) : ( + <> + {/* Current Availability Display */} + {adminAvailability?.available_days_display && adminAvailability.available_days_display.length > 0 && ( +
+

+ Current availability: {adminAvailability.available_days_display.join(", ")} +

+
+ )} + + {/* Days Selection */} +
+ +

+ Select the days of the week when you accept appointment requests +

+
+ {daysOfWeek.map((day) => ( + + ))} +
+
+ + {/* Time Selection */} +
+
+ +

+ Set the time range when appointments can be scheduled +

+
+ +
+ {/* Start Time */} +
+ + +
+ + {/* End Time */} +
+ + +
+
+ + {/* Time Range Display */} +
+

+ Available hours:{" "} + {new Date(`2000-01-01T${startTime}`).toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + hour12: true, + })}{" "} + -{" "} + {new Date(`2000-01-01T${endTime}`).toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + hour12: true, + })} +

+
+
+ + )} +
+ + + + + +
+
+
); } diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index d7d9bfb..ebd6e09 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -8,7 +8,7 @@ import { InputOTPGroup, InputOTPSlot, } from "@/components/ui/input-otp"; -import { Heart, Eye, EyeOff, X, Loader2, CheckCircle2 } from "lucide-react"; +import { Heart, Eye, EyeOff, X, Loader2, CheckCircle2, Mail } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; @@ -34,6 +34,7 @@ function LoginContent() { const [showPassword2, setShowPassword2] = useState(false); const [rememberMe, setRememberMe] = useState(false); const [registeredEmail, setRegisteredEmail] = useState(""); + const [showResendOtp, setShowResendOtp] = useState(false); // Login form data const [loginData, setLoginData] = useState({ @@ -164,6 +165,15 @@ function LoginContent() { } catch (error) { const errorMessage = error instanceof Error ? error.message : "Login failed. Please try again."; toast.error(errorMessage); + + // Check if error is about email verification + if (errorMessage.toLowerCase().includes("verify your email") || + errorMessage.toLowerCase().includes("email address before logging")) { + setShowResendOtp(true); + } else { + setShowResendOtp(false); + } + setErrors({}); } }; @@ -479,6 +489,56 @@ function LoginContent() { )} + {/* Resend OTP - Show when email verification error occurs */} + {showResendOtp && ( +
+
+ +
+

+ Email verification required +

+

+ Please verify your email address before logging in. We can resend the verification code to {loginData.email}. +

+ +
+
+
+ )} + {/* Remember Me & Forgot Password */}
); diff --git a/components/LoginDialog.tsx b/components/LoginDialog.tsx index 85ef759..a4ee3a9 100644 --- a/components/LoginDialog.tsx +++ b/components/LoginDialog.tsx @@ -11,12 +11,13 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { Eye, EyeOff, Loader2, X } from "lucide-react"; +import { Eye, EyeOff, Loader2, X, Mail } from "lucide-react"; import { useAuth } from "@/hooks/useAuth"; import { loginSchema, type LoginInput } from "@/lib/schema/auth"; import { toast } from "sonner"; import { useRouter } from "next/navigation"; import { ForgotPasswordDialog } from "./ForgotPasswordDialog"; +import { VerifyOtpDialog } from "./VerifyOtpDialog"; interface LoginDialogProps { open: boolean; @@ -38,6 +39,8 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess, prefillEmail, }); const [showPassword, setShowPassword] = useState(false); const [forgotPasswordDialogOpen, setForgotPasswordDialogOpen] = useState(false); + const [showResendOtp, setShowResendOtp] = useState(false); + const [verifyOtpDialogOpen, setVerifyOtpDialogOpen] = useState(false); // Pre-fill email if provided useEffect(() => { @@ -63,6 +66,7 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess, prefillEmail, if (result.tokens && result.user) { toast.success("Login successful!"); setShowPassword(false); + setShowResendOtp(false); onOpenChange(false); // Reset form setLoginData({ email: "", password: "" }); @@ -73,13 +77,48 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess, prefillEmail, } catch (err) { const errorMessage = err instanceof Error ? err.message : "Login failed. Please try again."; toast.error(errorMessage); + + // Check if error is about email verification + if (errorMessage.toLowerCase().includes("verify your email") || + errorMessage.toLowerCase().includes("email address before logging")) { + setShowResendOtp(true); + } else { + setShowResendOtp(false); + } } }; + // Handle resend OTP - just open the verification dialog (it will auto-send OTP) + const handleResendOtp = () => { + if (!loginData.email) { + toast.error("Email address is required to resend OTP."); + return; + } + + // Close login dialog and open OTP verification dialog + // The VerifyOtpDialog will automatically send the OTP when it opens + setShowResendOtp(false); + onOpenChange(false); + setTimeout(() => { + setVerifyOtpDialogOpen(true); + }, 100); + }; + + // Handle OTP verification success + const handleOtpVerificationSuccess = () => { + // After successful verification, user can try logging in again + setVerifyOtpDialogOpen(false); + // Optionally reopen login dialog + setTimeout(() => { + onOpenChange(true); + }, 100); + }; + // Reset form when dialog closes const handleDialogChange = (isOpen: boolean) => { if (!isOpen) { setLoginData({ email: "", password: "" }); + setShowResendOtp(false); } onOpenChange(isOpen); }; @@ -181,6 +220,31 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess, prefillEmail, )} + {/* Resend OTP - Show when email verification error occurs */} + {showResendOtp && ( +
+
+ +
+

+ Email verification required +

+

+ Please verify your email address before logging in. We can resend the verification code to {loginData.email}. +

+ +
+
+
+ )} + {/* Forgot Password */}
+
+ + {/* Scrollable Content */} +
+
+ {/* Loading indicator while sending */} + {isSendingOtp && !otpSent && ( +
+
+ +

+ Sending verification code... +

+
+
+ )} + + {/* Success message after sending */} + {otpSent && ( +
+
+ +
+

+ Check your email +

+

+ We've sent a 6-digit verification code to {email || otpData.email || "your email address"}. +

+
+
+
+ )} + + {/* Show info message if OTP hasn't been sent yet but dialog is open */} + {!isSendingOtp && !otpSent && ( +
+
+ +
+

+ Enter verification code +

+

+ Enter the 6-digit verification code sent to {email || otpData.email || "your email address"}. +

+
+
+
+ )} + + {/* Email Field (if not provided or editable) */} + {!initialEmail && ( +
+ + { + setEmail(e.target.value); + handleOtpChange("email", e.target.value); + }} + className={`h-11 sm:h-12 text-sm sm:text-base ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`} + required + /> +
+ )} + + {/* OTP Field */} +
+ +
+ handleOtpChange("otp", value)} + > + + + + + + + + + +
+
+ + {/* Resend OTP */} +
+ +
+ + {/* Submit Button */} + +
+
+ + + ); +} + diff --git a/lib/actions/appointments.ts b/lib/actions/appointments.ts index 0b4d195..b720369 100644 --- a/lib/actions/appointments.ts +++ b/lib/actions/appointments.ts @@ -287,7 +287,7 @@ export async function getAdminAvailability(): Promise { throw new Error("Authentication required."); } - const response = await fetch(`${API_ENDPOINTS.meetings.base}admin/availability/`, { + const response = await fetch(API_ENDPOINTS.meetings.adminAvailability, { method: "GET", headers: { "Content-Type": "application/json", @@ -295,14 +295,30 @@ export async function getAdminAvailability(): Promise { }, }); - const data: AdminAvailability = await response.json(); + const data: any = await response.json(); if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); throw new Error(errorMessage); } - return data; + // Handle both string and array formats for available_days + let availableDays: number[] = []; + if (typeof data.available_days === 'string') { + try { + availableDays = JSON.parse(data.available_days); + } catch { + // If parsing fails, try splitting by comma + availableDays = data.available_days.split(',').map((d: string) => parseInt(d.trim())).filter((d: number) => !isNaN(d)); + } + } else if (Array.isArray(data.available_days)) { + availableDays = data.available_days; + } + + return { + available_days: availableDays, + available_days_display: data.available_days_display || [], + } as AdminAvailability; } // Update admin availability @@ -315,7 +331,7 @@ export async function updateAdminAvailability( throw new Error("Authentication required."); } - const response = await fetch(`${API_ENDPOINTS.meetings.base}admin/availability/`, { + const response = await fetch(API_ENDPOINTS.meetings.adminAvailability, { method: "PUT", headers: { "Content-Type": "application/json", diff --git a/lib/api_urls.ts b/lib/api_urls.ts index 564b998..0dab8aa 100644 --- a/lib/api_urls.ts +++ b/lib/api_urls.ts @@ -20,6 +20,7 @@ export const API_ENDPOINTS = { createAppointment: `${API_BASE_URL}/meetings/appointments/create/`, listAppointments: `${API_BASE_URL}/meetings/appointments/`, userAppointments: `${API_BASE_URL}/meetings/user/appointments/`, + adminAvailability: `${API_BASE_URL}/meetings/admin/availability/`, }, } as const;