diff --git a/app/(admin)/admin/booking/[id]/page.tsx b/app/(admin)/admin/booking/[id]/page.tsx index c4c6d10..fe60ede 100644 --- a/app/(admin)/admin/booking/[id]/page.tsx +++ b/app/(admin)/admin/booking/[id]/page.tsx @@ -367,76 +367,13 @@ export default function AppointmentDetailPage() { )} - {/* Preferred Dates & Times */} - {((appointment.preferred_dates && appointment.preferred_dates.length > 0) || (appointment.preferred_time_slots && appointment.preferred_time_slots.length > 0)) && ( -
-
-

- Preferred Availability -

-
-
- {appointment.preferred_dates && ( -
-

- Preferred Dates -

-
- {Array.isArray(appointment.preferred_dates) ? ( - (appointment.preferred_dates as string[]).map((date, idx) => ( - - {formatShortDate(date)} - - )) - ) : ( - - {appointment.preferred_dates_display || appointment.preferred_dates || 'N/A'} - - )} -
-
- )} - {appointment.preferred_time_slots && ( -
-

- Preferred Time Slots -

-
- {Array.isArray(appointment.preferred_time_slots) ? ( - (appointment.preferred_time_slots as string[]).map((slot, idx) => ( - - {slot} - - )) - ) : ( - - {appointment.preferred_time_slots_display || appointment.preferred_time_slots || 'N/A'} - - )} -
-
- )} -
-
- )} - {/* Matching Availability */} {appointment.matching_availability && Array.isArray(appointment.matching_availability) && appointment.matching_availability.length > 0 && (

- Matching Availability + Preferred Availability {appointment.are_preferences_available !== undefined && ( {appointment.are_preferences_available ? "Available" : "Partially Available"} @@ -558,22 +495,22 @@ export default function AppointmentDetailPage() {
{appointment.can_join_meeting ? ( <> - - {appointment.jitsi_meet_url} - - - - + + {appointment.jitsi_meet_url} + + + + ) : ( <> diff --git a/app/(admin)/admin/booking/page.tsx b/app/(admin)/admin/booking/page.tsx index 5000de8..b0b8cef 100644 --- a/app/(admin)/admin/booking/page.tsx +++ b/app/(admin)/admin/booking/page.tsx @@ -536,29 +536,29 @@ export default function Booking() { else if (adminAvailability.available_days && adminAvailability.available_days.length > 0) { return adminAvailability.available_days.map((dayNum, index) => { const dayName = daysOfWeek.find(d => d.value === dayNum)?.label || adminAvailability.available_days_display?.[index]; - const timeSlots = dayTimeSlots[dayNum] || []; - const slotLabels = timeSlots.map(slot => { - const option = timeSlotOptions.find(opt => opt.value === slot); - return option ? option.label : slot; - }); - return ( -
- - {dayName} - {slotLabels.length > 0 && ( - - ({slotLabels.join(", ")}) - - )} -
- ); + const timeSlots = dayTimeSlots[dayNum] || []; + const slotLabels = timeSlots.map(slot => { + const option = timeSlotOptions.find(opt => opt.value === slot); + return option ? option.label : slot; + }); + return ( +
+ + {dayName} + {slotLabels.length > 0 && ( + + ({slotLabels.join(", ")}) + + )} +
+ ); }); } return null; @@ -684,10 +684,10 @@ export default function Booking() { {Array.isArray(appointment.preferred_dates) ? ( <> {(appointment.preferred_dates as string[]).slice(0, 2).map((date, idx) => ( - {formatDate(date)} - ))} - {appointment.preferred_dates.length > 2 && ( - +{appointment.preferred_dates.length - 2} more + {formatDate(date)} + ))} + {appointment.preferred_dates.length > 2 && ( + +{appointment.preferred_dates.length - 2} more )} ) : ( diff --git a/app/(admin)/admin/dashboard/page.tsx b/app/(admin)/admin/dashboard/page.tsx index d120681..f32965d 100644 --- a/app/(admin)/admin/dashboard/page.tsx +++ b/app/(admin)/admin/dashboard/page.tsx @@ -291,9 +291,9 @@ export default function Dashboard() {

{card.value}

-

- vs last month -

+

+ vs last month +

); diff --git a/app/(pages)/book-now/page.tsx b/app/(pages)/book-now/page.tsx index 46d4073..eb4c50c 100644 --- a/app/(pages)/book-now/page.tsx +++ b/app/(pages)/book-now/page.tsx @@ -150,7 +150,7 @@ export default function BookNowPage() { dayName: dayName, availableSlots: slotsSet, }); - } + } } }); @@ -310,7 +310,7 @@ export default function BookNowPage() { if (!firstName || firstName.length === 0) { setError("First name is required."); return; - } + } if (!lastName || lastName.length === 0) { setError("Last name is required."); return; @@ -567,7 +567,7 @@ export default function BookNowPage() {

{booking.user.phone && ( -
+

Phone

{booking.user.phone} diff --git a/app/(pages)/deliverables/page.tsx b/app/(pages)/deliverables/page.tsx index 15c4cc6..3d34433 100644 --- a/app/(pages)/deliverables/page.tsx +++ b/app/(pages)/deliverables/page.tsx @@ -51,7 +51,47 @@ Your Attune Heart Therapy platform includes a comprehensive system for managing --- -## 🔗 Quick Access Links +## 🎥 Telehealth Sessions Guide + +This section provides step-by-step guidance on how to access and manage telehealth therapy sessions through the Admin Dashboard of the Attune Heart Therapy platform. + +### Accessing the Admin Dashboard + +![Admin Dashboard Access](/ss2.png) + +1. Navigate to: \`https://attunehearttherapy.com/login\` +2. Enter your admin email address: \`admin@attunehearttherapy.com\` +3. Enter your password +4. Click **"Sign In"** +5. You will be automatically redirected to the **Admin Dashboard** +6. Click on **"Bookings"** in the navigation menu to view all appointments + +### Viewing Appointment Details + +1. From the Bookings page, click on any appointment +2. View complete client information, preferred dates, and time slots +3. Check appointment status and review all details + +### Scheduling an Appointment + +1. From the appointment details page, click **"Schedule Appointment"** +2. Select the date and time for the session +3. Choose the duration (15, 30, 45, 60, or 120 minutes) +4. Click **"Schedule"** to confirm +5. The system will automatically create a Jitsi video meeting room, generate a unique meeting URL, send a confirmation email to the client, and update the appointment status to "Scheduled" + +![Scheduling Appointment](/ss1.png) + +### Joining a Video Meeting + +1. Meetings become available **10 minutes before** the scheduled start time +2. Navigate to any scheduled appointment's details page +3. In the sidebar, find the **"Join Meeting"** button +4. Click **"Join Meeting"** when it becomes active (10 minutes before scheduled time) +5. The meeting will open in a new browser tab +6. Both you and the client use the same link to join the session + +### 🔗 Quick Access Links [Visit Attune Heart Therapy](https://attunehearttherapy.com/) - Official website @@ -290,17 +330,32 @@ For technical assistance, questions, or issues: ); }, + img: ({ node, ...props }: any) => ( + {props.alt + ), }; return (

-
+
+ {readmeContent} diff --git a/app/(user)/user/appointments/[id]/page.tsx b/app/(user)/user/appointments/[id]/page.tsx index 9334a01..c15f020 100644 --- a/app/(user)/user/appointments/[id]/page.tsx +++ b/app/(user)/user/appointments/[id]/page.tsx @@ -306,85 +306,13 @@ export default function UserAppointmentDetailPage() {
)} - {/* Preferred Dates & Times */} - {((appointment.preferred_dates && (Array.isArray(appointment.preferred_dates) ? appointment.preferred_dates.length > 0 : appointment.preferred_dates)) || - (appointment.preferred_time_slots && (Array.isArray(appointment.preferred_time_slots) ? appointment.preferred_time_slots.length > 0 : appointment.preferred_time_slots))) && ( -
-
-

- Preferred Availability -

-
-
- {appointment.preferred_dates && ( -
-

- Preferred Dates -

-
- {Array.isArray(appointment.preferred_dates) ? ( - (appointment.preferred_dates as string[]).map((date, idx) => ( - - {formatShortDate(date)} - - )) - ) : ( - - {appointment.preferred_dates_display || appointment.preferred_dates || 'N/A'} - - )} -
-
- )} - {appointment.preferred_time_slots && ( -
-

- Preferred Time Slots -

-
- {Array.isArray(appointment.preferred_time_slots) ? ( - (appointment.preferred_time_slots as string[]).map((slot, idx) => { - const timeSlotLabels: Record = { - morning: "Morning", - afternoon: "Lunchtime", - evening: "Evening", - }; - const normalizedSlot = String(slot).toLowerCase().trim(); - return ( - - {timeSlotLabels[normalizedSlot] || slot} - - ); - }) - ) : ( - - {appointment.preferred_time_slots_display || appointment.preferred_time_slots || 'N/A'} - - )} -
-
- )} -
-
- )} - {/* Matching Availability */} {appointment.matching_availability && Array.isArray(appointment.matching_availability) && appointment.matching_availability.length > 0 && (

- Matching Availability + Preferred Availability {appointment.are_preferences_available !== undefined && ( {appointment.are_preferences_available ? "Available" : "Partially Available"} diff --git a/app/(user)/user/dashboard/page.tsx b/app/(user)/user/dashboard/page.tsx index d5217b9..a72ce44 100644 --- a/app/(user)/user/dashboard/page.tsx +++ b/app/(user)/user/dashboard/page.tsx @@ -225,7 +225,7 @@ export default function UserDashboard() {

+ > {displayStats.scheduled > 0 ? `+${displayStats.scheduled}` : "0"}
diff --git a/components/ContactSection.tsx b/components/ContactSection.tsx index 50b2e41..77381d3 100644 --- a/components/ContactSection.tsx +++ b/components/ContactSection.tsx @@ -2,13 +2,14 @@ import { motion, useInView } from "framer-motion"; import { useRef, useState } from "react"; -import { Send } from "lucide-react"; +import { Send, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Card, CardContent } from "@/components/ui/card"; import { toast } from "sonner"; import { useAppTheme } from "@/components/ThemeProvider"; +import { submitContactForm } from "@/lib/actions/auth"; export function ContactSection() { const ref = useRef(null); @@ -21,13 +22,31 @@ export function ContactSection() { phone: "", message: "", }); + const [isSubmitting, setIsSubmitting] = useState(false); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - toast("Message Received", { - description: "Thank you for reaching out. We'll get back to you soon!", - }); - setFormData({ name: "", email: "", phone: "", message: "" }); + setIsSubmitting(true); + + try { + await submitContactForm({ + name: formData.name, + email: formData.email, + phone: formData.phone, + message: formData.message, + }); + + toast.success("Message Sent Successfully", { + description: "Thank you for reaching out. We'll get back to you soon!", + }); + setFormData({ name: "", email: "", phone: "", message: "" }); + } catch (error) { + toast.error("Failed to Send Message", { + description: error instanceof Error ? error.message : "Please try again later.", + }); + } finally { + setIsSubmitting(false); + } }; return ( @@ -204,11 +223,21 @@ export function ContactSection() {
diff --git a/lib/actions/appointments.ts b/lib/actions/appointments.ts index 4fde1ae..bb225bb 100644 --- a/lib/actions/appointments.ts +++ b/lib/actions/appointments.ts @@ -131,7 +131,7 @@ export async function createAppointment(input: CreateAppointmentInput): Promise< if (!input.selected_slots || input.selected_slots.length === 0) { throw new Error("At least one time slot must be selected"); - } + } const validSlots = validateAndCleanSlots(input.selected_slots); if (validSlots.length === 0) { @@ -211,7 +211,7 @@ export async function getWeeklyAvailability(): Promise { if (!response.ok) { throw new Error(extractErrorMessage(data as unknown as ApiError)); } - + return data; } @@ -357,7 +357,7 @@ export async function scheduleAppointment(id: string, input: ScheduleAppointment } return data.appointment || data; -} + } export async function rejectAppointment(id: string, input: RejectAppointmentInput): Promise { const tokens = getStoredTokens(); @@ -388,7 +388,7 @@ export async function getPublicAvailability(): Promise const weekArray = Array.isArray(weeklyAvailability) ? weeklyAvailability : (weeklyAvailability as any).week || []; - + if (!weekArray || weekArray.length === 0) { return null; } @@ -402,7 +402,7 @@ export async function getPublicAvailability(): Promise availabilitySchedule[day.day.toString()] = day.available_slots; availableDays.push(day.day); availableDaysDisplay.push(day.day_name); - } + } }); return { @@ -533,7 +533,7 @@ export async function updateAdminAvailability(input: UpdateAvailabilityInput): P Authorization: `Bearer ${tokens.access}`, }, body: JSON.stringify({ availability_schedule: sortedSchedule }), - }); + }); } const responseText = await response.text(); @@ -548,7 +548,7 @@ export async function updateAdminAvailability(input: UpdateAvailabilityInput): P let data: any; if (contentType.includes("application/json")) { - try { + try { data = JSON.parse(responseText); } catch { throw new Error(`Server error (${response.status}): Invalid JSON format`); diff --git a/lib/actions/auth.ts b/lib/actions/auth.ts index ddd67c3..ce85bb7 100644 --- a/lib/actions/auth.ts +++ b/lib/actions/auth.ts @@ -439,3 +439,51 @@ export async function updateProfile(input: UpdateProfileInput): Promise { throw new Error("Invalid profile response format"); } +export interface ContactFormInput { + name: string; + email: string; + phone: string; + message: string; +} + +export interface ContactFormResponse { + message?: string; + success?: boolean; +} + +/** + * Submit contact form + */ +export async function submitContactForm( + data: ContactFormInput +): Promise { + try { + const response = await fetch(API_ENDPOINTS.auth.contact, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: data.name, + email: data.email, + phone: data.phone, + message: data.message, + }), + }); + + const responseData = await response.json(); + + if (!response.ok) { + const error: ApiError = responseData; + throw new Error(extractErrorMessage(error)); + } + + return responseData; + } catch (error) { + if (error instanceof Error) { + throw error; + } + throw new Error("Failed to submit contact form"); + } +} + diff --git a/lib/api_urls.ts b/lib/api_urls.ts index e7322b1..ea17857 100644 --- a/lib/api_urls.ts +++ b/lib/api_urls.ts @@ -15,6 +15,7 @@ export const API_ENDPOINTS = { allUsers: `${API_BASE_URL}/auth/all-users/`, getProfile: `${API_BASE_URL}/auth/profile/`, updateProfile: `${API_BASE_URL}/auth/profile/update/`, + contact: `${API_BASE_URL}/auth/contact/`, }, meetings: { base: `${API_BASE_URL}/meetings/`, diff --git a/public/ss1.png b/public/ss1.png new file mode 100644 index 0000000..d855d8c Binary files /dev/null and b/public/ss1.png differ diff --git a/public/ss2.png b/public/ss2.png new file mode 100644 index 0000000..b251fec Binary files /dev/null and b/public/ss2.png differ diff --git a/public/ss3.png b/public/ss3.png new file mode 100644 index 0000000..1af05d2 Binary files /dev/null and b/public/ss3.png differ