Compare commits
No commits in common. "d47b542413729e3d86296b4f5cb36e571dc808e4" and "021c5c2d70abe106d8c720a21504064f97245e15" have entirely different histories.
d47b542413
...
021c5c2d70
@ -18,10 +18,9 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
Copy,
|
Copy,
|
||||||
MapPin,
|
MapPin,
|
||||||
Pencil,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAppTheme } from "@/components/ThemeProvider";
|
import { useAppTheme } from "@/components/ThemeProvider";
|
||||||
import { getAppointmentDetail, scheduleAppointment, rejectAppointment, listAppointments, startMeeting, endMeeting, rescheduleAppointment } from "@/lib/actions/appointments";
|
import { getAppointmentDetail, scheduleAppointment, rejectAppointment, listAppointments, startMeeting, endMeeting } from "@/lib/actions/appointments";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -44,17 +43,12 @@ export default function AppointmentDetailPage() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [scheduleDialogOpen, setScheduleDialogOpen] = useState(false);
|
const [scheduleDialogOpen, setScheduleDialogOpen] = useState(false);
|
||||||
const [rejectDialogOpen, setRejectDialogOpen] = useState(false);
|
const [rejectDialogOpen, setRejectDialogOpen] = useState(false);
|
||||||
const [rescheduleDialogOpen, setRescheduleDialogOpen] = useState(false);
|
|
||||||
const [scheduledDate, setScheduledDate] = useState<Date | undefined>(undefined);
|
const [scheduledDate, setScheduledDate] = useState<Date | undefined>(undefined);
|
||||||
const [scheduledTime, setScheduledTime] = useState<string>("09:00");
|
const [scheduledTime, setScheduledTime] = useState<string>("09:00");
|
||||||
const [scheduledDuration, setScheduledDuration] = useState<number>(60);
|
const [scheduledDuration, setScheduledDuration] = useState<number>(60);
|
||||||
const [rescheduleDate, setRescheduleDate] = useState<Date | undefined>(undefined);
|
|
||||||
const [rescheduleTime, setRescheduleTime] = useState<string>("09:00");
|
|
||||||
const [rescheduleDuration, setRescheduleDuration] = useState<number>(60);
|
|
||||||
const [rejectionReason, setRejectionReason] = useState<string>("");
|
const [rejectionReason, setRejectionReason] = useState<string>("");
|
||||||
const [isScheduling, setIsScheduling] = useState(false);
|
const [isScheduling, setIsScheduling] = useState(false);
|
||||||
const [isRejecting, setIsRejecting] = useState(false);
|
const [isRejecting, setIsRejecting] = useState(false);
|
||||||
const [isRescheduling, setIsRescheduling] = useState(false);
|
|
||||||
const [isStartingMeeting, setIsStartingMeeting] = useState(false);
|
const [isStartingMeeting, setIsStartingMeeting] = useState(false);
|
||||||
const [isEndingMeeting, setIsEndingMeeting] = useState(false);
|
const [isEndingMeeting, setIsEndingMeeting] = useState(false);
|
||||||
const { theme } = useAppTheme();
|
const { theme } = useAppTheme();
|
||||||
@ -751,14 +745,10 @@ export default function AppointmentDetailPage() {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
disabled
|
disabled
|
||||||
className={`flex items-center justify-center gap-2 w-full cursor-not-allowed h-12 rounded-lg text-sm sm:text-base font-medium transition-colors ${isDark ? "bg-gray-700 text-gray-500" : "bg-gray-300 text-gray-500"}`}
|
className={`flex items-center justify-center gap-2 w-full cursor-not-allowed h-12 rounded-lg text-base font-medium transition-colors ${isDark ? "bg-gray-700 text-gray-500" : "bg-gray-300 text-gray-500"}`}
|
||||||
>
|
>
|
||||||
<Video className="w-4 h-4 sm:w-5 sm:h-5 flex-shrink-0" />
|
<Video className="w-5 h-5" />
|
||||||
<span className="text-center px-1">
|
Meeting would be available to join starting at {meetingTime}
|
||||||
<span className="hidden sm:inline">Meeting would be available to join starting at </span>
|
|
||||||
<span className="sm:hidden">Available at </span>
|
|
||||||
<span className="font-semibold">{meetingTime}</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,19 +15,10 @@ import {
|
|||||||
MessageSquare,
|
MessageSquare,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Copy,
|
Copy,
|
||||||
X,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAppTheme } from "@/components/ThemeProvider";
|
import { useAppTheme } from "@/components/ThemeProvider";
|
||||||
import { getAppointmentDetail, listAppointments, cancelAppointment } from "@/lib/actions/appointments";
|
import { getAppointmentDetail, listAppointments } from "@/lib/actions/appointments";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Navbar } from "@/components/Navbar";
|
import { Navbar } from "@/components/Navbar";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { Appointment } from "@/lib/models/appointments";
|
import type { Appointment } from "@/lib/models/appointments";
|
||||||
@ -39,8 +30,6 @@ export default function UserAppointmentDetailPage() {
|
|||||||
|
|
||||||
const [appointment, setAppointment] = useState<Appointment | null>(null);
|
const [appointment, setAppointment] = useState<Appointment | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [cancelDialogOpen, setCancelDialogOpen] = useState(false);
|
|
||||||
const [isCancelling, setIsCancelling] = useState(false);
|
|
||||||
const { theme } = useAppTheme();
|
const { theme } = useAppTheme();
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
|
|
||||||
@ -149,26 +138,6 @@ export default function UserAppointmentDetailPage() {
|
|||||||
toast.success(`${label} copied to clipboard`);
|
toast.success(`${label} copied to clipboard`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancelAppointment = async () => {
|
|
||||||
if (!appointment) return;
|
|
||||||
|
|
||||||
setIsCancelling(true);
|
|
||||||
try {
|
|
||||||
await cancelAppointment(appointment.id);
|
|
||||||
toast.success("Appointment cancelled successfully");
|
|
||||||
setCancelDialogOpen(false);
|
|
||||||
// Refetch appointment to get updated status
|
|
||||||
const updatedAppointment = await getAppointmentDetail(appointmentId);
|
|
||||||
setAppointment(updatedAppointment);
|
|
||||||
router.push("/user/dashboard");
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : "Failed to cancel appointment";
|
|
||||||
toast.error(errorMessage);
|
|
||||||
} finally {
|
|
||||||
setIsCancelling(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className={`min-h-screen ${isDark ? "bg-gray-900" : "bg-gray-50"}`}>
|
<div className={`min-h-screen ${isDark ? "bg-gray-900" : "bg-gray-50"}`}>
|
||||||
@ -641,66 +610,9 @@ export default function UserAppointmentDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Cancel Appointment Button */}
|
|
||||||
{appointment.status === "scheduled" && (
|
|
||||||
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
|
|
||||||
<div className="p-6">
|
|
||||||
<Button
|
|
||||||
onClick={() => setCancelDialogOpen(true)}
|
|
||||||
variant="outline"
|
|
||||||
className={`w-full h-12 text-base font-medium border-red-600 text-red-600 hover:bg-red-50 ${isDark ? "hover:bg-red-900/20 border-red-500" : ""}`}
|
|
||||||
>
|
|
||||||
<X className="w-5 h-5 mr-2" />
|
|
||||||
Cancel Appointment
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Cancel Appointment Confirmation Dialog */}
|
|
||||||
<Dialog open={cancelDialogOpen} onOpenChange={setCancelDialogOpen}>
|
|
||||||
<DialogContent className={isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className={isDark ? "text-white" : "text-gray-900"}>
|
|
||||||
Cancel Appointment
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription className={isDark ? "text-gray-400" : "text-gray-500"}>
|
|
||||||
Are you sure you want to cancel this appointment? This action cannot be undone.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => setCancelDialogOpen(false)}
|
|
||||||
disabled={isCancelling}
|
|
||||||
className={isDark ? "border-gray-600 text-gray-300 hover:bg-gray-700" : ""}
|
|
||||||
>
|
|
||||||
No, Keep Appointment
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleCancelAppointment}
|
|
||||||
disabled={isCancelling}
|
|
||||||
className="bg-red-600 hover:bg-red-700 text-white"
|
|
||||||
>
|
|
||||||
{isCancelling ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
||||||
Cancelling...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<X className="w-4 h-4 mr-2" />
|
|
||||||
Yes, Cancel Appointment
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -801,76 +801,3 @@ export async function endMeeting(id: string): Promise<Appointment> {
|
|||||||
// So we need to refetch the appointment to get the updated state
|
// So we need to refetch the appointment to get the updated state
|
||||||
return await getAppointmentDetail(id);
|
return await getAppointmentDetail(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RescheduleAppointmentInput {
|
|
||||||
new_scheduled_datetime: string; // ISO datetime string
|
|
||||||
new_scheduled_duration: number; // in minutes
|
|
||||||
timezone: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function rescheduleAppointment(id: string, input: RescheduleAppointmentInput): Promise<Appointment> {
|
|
||||||
const tokens = getStoredTokens();
|
|
||||||
if (!tokens.access) {
|
|
||||||
throw new Error("Authentication required.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; // Get user's timezone
|
|
||||||
|
|
||||||
const payload: any = {
|
|
||||||
new_scheduled_datetime: input.new_scheduled_datetime,
|
|
||||||
new_scheduled_duration: input.new_scheduled_duration,
|
|
||||||
timezone: input.timezone !== undefined ? input.timezone : userTimezone,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(`${API_ENDPOINTS.meetings.listAppointments}${id}/reschedule/`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${tokens.access}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await parseResponse(response);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(extractErrorMessage(data as unknown as ApiError));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refetch the appointment to get the updated state
|
|
||||||
return await getAppointmentDetail(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CancelAppointmentInput {
|
|
||||||
action?: string;
|
|
||||||
metadata?: string;
|
|
||||||
recording_url?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function cancelAppointment(id: string, input?: CancelAppointmentInput): Promise<any> {
|
|
||||||
const tokens = getStoredTokens();
|
|
||||||
if (!tokens.access) {
|
|
||||||
throw new Error("Authentication required.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload: any = {};
|
|
||||||
if (input?.action) payload.action = input.action;
|
|
||||||
if (input?.metadata) payload.metadata = input.metadata;
|
|
||||||
if (input?.recording_url) payload.recording_url = input.recording_url;
|
|
||||||
|
|
||||||
const response = await fetch(`${API_ENDPOINTS.meetings.base}meetings/${id}/cancel/`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${tokens.access}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await parseResponse(response);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(extractErrorMessage(data as unknown as ApiError));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refetch the appointment to get the updated state
|
|
||||||
return await getAppointmentDetail(id);
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user