Merge pull request 'Refactor appointment components to replace Jitsi meeting URLs with moderator and participant join URLs. Update logic for joining meetings and copying room IDs based on user roles, enhancing clarity in the UI. Modify appointment model to support new joi…' (#43) from feat/booking-panel into master

Reviewed-on: http://35.207.46.142/ATTUNE-HEART-THERAPY/website/pulls/43
This commit is contained in:
Hammond 2025-12-03 18:41:03 +00:00
commit 36014ca954
5 changed files with 60 additions and 50 deletions

View File

@ -494,7 +494,7 @@ export default function AppointmentDetailPage() {
)} )}
{/* Meeting Information */} {/* Meeting Information */}
{appointment.jitsi_meet_url && ( {appointment.moderator_join_url && (
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gradient-to-br from-blue-900/20 to-purple-900/20 border-blue-800/30" : "bg-gradient-to-br from-blue-50 to-purple-50 border-blue-200"}`}> <div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gradient-to-br from-blue-900/20 to-purple-900/20 border-blue-800/30" : "bg-gradient-to-br from-blue-50 to-purple-50 border-blue-200"}`}>
<div className={`px-6 py-4 border-b ${isDark ? "border-blue-800/30" : "border-blue-200"}`}> <div className={`px-6 py-4 border-b ${isDark ? "border-blue-800/30" : "border-blue-200"}`}>
<h2 className={`text-lg font-semibold flex items-center gap-2 ${isDark ? "text-white" : "text-gray-900"}`}> <h2 className={`text-lg font-semibold flex items-center gap-2 ${isDark ? "text-white" : "text-gray-900"}`}>
@ -513,10 +513,10 @@ export default function AppointmentDetailPage() {
{appointment.jitsi_room_id} {appointment.jitsi_room_id}
</p> </p>
<button <button
onClick={() => appointment.can_join_meeting && copyToClipboard(appointment.jitsi_room_id!, "Room ID")} onClick={() => appointment.can_join_as_moderator && copyToClipboard(appointment.jitsi_room_id!, "Room ID")}
disabled={!appointment.can_join_meeting} disabled={!appointment.can_join_as_moderator}
className={`p-2 rounded-lg transition-colors ${appointment.can_join_meeting ? (isDark ? "hover:bg-gray-700" : "hover:bg-gray-100") : (isDark ? "opacity-50 cursor-not-allowed" : "opacity-50 cursor-not-allowed")}`} className={`p-2 rounded-lg transition-colors ${appointment.can_join_as_moderator ? (isDark ? "hover:bg-gray-700" : "hover:bg-gray-100") : (isDark ? "opacity-50 cursor-not-allowed" : "opacity-50 cursor-not-allowed")}`}
title={appointment.can_join_meeting ? "Copy room ID" : "Meeting not available"} title={appointment.can_join_as_moderator ? "Copy room ID" : "Meeting not available"}
> >
<Copy className={`w-4 h-4 ${isDark ? "text-gray-400" : "text-gray-500"}`} /> <Copy className={`w-4 h-4 ${isDark ? "text-gray-400" : "text-gray-500"}`} />
</button> </button>
@ -525,21 +525,21 @@ export default function AppointmentDetailPage() {
)} )}
<div> <div>
<p className={`text-xs font-medium mb-2 uppercase tracking-wider ${isDark ? "text-gray-400" : "text-gray-500"}`}> <p className={`text-xs font-medium mb-2 uppercase tracking-wider ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Meeting Link Moderator Meeting Link
</p> </p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{appointment.can_join_meeting ? ( {appointment.can_join_as_moderator ? (
<> <>
<a <a
href={appointment.jitsi_meet_url} href={appointment.moderator_join_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className={`flex-1 text-sm px-3 py-2 rounded-lg truncate ${isDark ? "bg-gray-800 text-blue-400 hover:bg-gray-700" : "bg-white text-blue-600 hover:bg-gray-50 border border-gray-200"}`} className={`flex-1 text-sm px-3 py-2 rounded-lg truncate ${isDark ? "bg-gray-800 text-blue-400 hover:bg-gray-700" : "bg-white text-blue-600 hover:bg-gray-50 border border-gray-200"}`}
> >
{appointment.jitsi_meet_url} {appointment.moderator_join_url}
</a> </a>
<a <a
href={appointment.jitsi_meet_url} href={appointment.moderator_join_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className={`px-4 py-2 rounded-lg font-medium transition-colors ${isDark ? "bg-blue-600 hover:bg-blue-700 text-white" : "bg-blue-600 hover:bg-blue-700 text-white"}`} className={`px-4 py-2 rounded-lg font-medium transition-colors ${isDark ? "bg-blue-600 hover:bg-blue-700 text-white" : "bg-blue-600 hover:bg-blue-700 text-white"}`}
@ -550,7 +550,7 @@ export default function AppointmentDetailPage() {
) : ( ) : (
<> <>
<div className={`flex-1 text-sm px-3 py-2 rounded-lg truncate ${isDark ? "bg-gray-800/50 text-gray-500 border border-gray-700" : "bg-gray-100 text-gray-400 border border-gray-300"}`}> <div className={`flex-1 text-sm px-3 py-2 rounded-lg truncate ${isDark ? "bg-gray-800/50 text-gray-500 border border-gray-700" : "bg-gray-100 text-gray-400 border border-gray-300"}`}>
{appointment.jitsi_meet_url} {appointment.moderator_join_url}
</div> </div>
<button <button
disabled disabled
@ -562,11 +562,11 @@ export default function AppointmentDetailPage() {
)} )}
</div> </div>
</div> </div>
{appointment.can_join_meeting !== undefined && ( {appointment.can_join_as_moderator !== undefined && (
<div className={`flex items-center gap-2 px-4 py-3 rounded-lg ${appointment.can_join_meeting ? (isDark ? "bg-green-500/20 border border-green-500/30" : "bg-green-50 border border-green-200") : (isDark ? "bg-gray-800 border border-gray-700" : "bg-gray-50 border border-gray-200")}`}> <div className={`flex items-center gap-2 px-4 py-3 rounded-lg ${appointment.can_join_as_moderator ? (isDark ? "bg-green-500/20 border border-green-500/30" : "bg-green-50 border border-green-200") : (isDark ? "bg-gray-800 border border-gray-700" : "bg-gray-50 border border-gray-200")}`}>
<div className={`h-2 w-2 rounded-full ${appointment.can_join_meeting ? (isDark ? "bg-green-400" : "bg-green-600") : (isDark ? "bg-gray-500" : "bg-gray-400")}`} /> <div className={`h-2 w-2 rounded-full ${appointment.can_join_as_moderator ? (isDark ? "bg-green-400" : "bg-green-600") : (isDark ? "bg-gray-500" : "bg-gray-400")}`} />
<p className={`text-sm font-medium ${appointment.can_join_meeting ? (isDark ? "text-green-300" : "text-green-700") : (isDark ? "text-gray-400" : "text-gray-500")}`}> <p className={`text-sm font-medium ${appointment.can_join_as_moderator ? (isDark ? "text-green-300" : "text-green-700") : (isDark ? "text-gray-400" : "text-gray-500")}`}>
{appointment.can_join_meeting ? "Meeting is active - You can join now" : "Meeting is not available yet"} {appointment.can_join_as_moderator ? "Meeting is active - You can join as moderator" : "Meeting is not available yet"}
</p> </p>
</div> </div>
)} )}
@ -636,18 +636,18 @@ export default function AppointmentDetailPage() {
)} )}
{/* Join Meeting Button (if scheduled) */} {/* Join Meeting Button (if scheduled) */}
{appointment.status === "scheduled" && appointment.jitsi_meet_url && ( {appointment.status === "scheduled" && appointment.moderator_join_url && (
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gradient-to-br from-blue-900/20 to-purple-900/20 border-blue-800/30" : "bg-gradient-to-br from-blue-50 to-purple-50 border-blue-200"}`}> <div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gradient-to-br from-blue-900/20 to-purple-900/20 border-blue-800/30" : "bg-gradient-to-br from-blue-50 to-purple-50 border-blue-200"}`}>
<div className="p-6"> <div className="p-6">
{appointment.can_join_meeting ? ( {appointment.can_join_as_moderator ? (
<a <a
href={appointment.jitsi_meet_url} href={appointment.moderator_join_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className={`flex items-center justify-center gap-2 w-full bg-blue-600 hover:bg-blue-700 text-white h-12 rounded-lg text-base font-medium transition-colors`} className={`flex items-center justify-center gap-2 w-full bg-blue-600 hover:bg-blue-700 text-white h-12 rounded-lg text-base font-medium transition-colors`}
> >
<Video className="w-5 h-5" /> <Video className="w-5 h-5" />
Join Meeting Join Meeting as Moderator
</a> </a>
) : ( ) : (
<button <button

View File

@ -802,13 +802,13 @@ export default function Booking() {
</button> </button>
</> </>
)} )}
{appointment.jitsi_meet_url && ( {appointment.moderator_join_url && (
<a <a
href={appointment.jitsi_meet_url} href={appointment.moderator_join_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className={`p-1.5 sm:p-2 rounded-lg transition-colors ${isDark ? "text-gray-300 hover:text-white hover:bg-gray-700" : "text-gray-600 hover:text-gray-900 hover:bg-gray-100"}`} className={`p-1.5 sm:p-2 rounded-lg transition-colors ${isDark ? "text-gray-300 hover:text-white hover:bg-gray-700" : "text-gray-600 hover:text-gray-900 hover:bg-gray-100"}`}
title="Join Meeting" title="Join Meeting as Moderator"
> >
<Video className="w-4 h-4" /> <Video className="w-4 h-4" />
</a> </a>

View File

@ -433,7 +433,7 @@ export default function UserAppointmentDetailPage() {
)} )}
{/* Meeting Information */} {/* Meeting Information */}
{appointment.jitsi_meet_url && ( {appointment.participant_join_url && (
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gradient-to-br from-blue-900/20 to-purple-900/20 border-blue-800/30" : "bg-gradient-to-br from-blue-50 to-purple-50 border-blue-200"}`}> <div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gradient-to-br from-blue-900/20 to-purple-900/20 border-blue-800/30" : "bg-gradient-to-br from-blue-50 to-purple-50 border-blue-200"}`}>
<div className={`px-6 py-4 border-b ${isDark ? "border-blue-800/30" : "border-blue-200"}`}> <div className={`px-6 py-4 border-b ${isDark ? "border-blue-800/30" : "border-blue-200"}`}>
<h2 className={`text-lg font-semibold flex items-center gap-2 ${isDark ? "text-white" : "text-gray-900"}`}> <h2 className={`text-lg font-semibold flex items-center gap-2 ${isDark ? "text-white" : "text-gray-900"}`}>
@ -452,10 +452,10 @@ export default function UserAppointmentDetailPage() {
{appointment.jitsi_room_id} {appointment.jitsi_room_id}
</p> </p>
<button <button
onClick={() => appointment.can_join_meeting && copyToClipboard(appointment.jitsi_room_id!, "Room ID")} onClick={() => appointment.can_join_as_participant && copyToClipboard(appointment.jitsi_room_id!, "Room ID")}
disabled={!appointment.can_join_meeting} disabled={!appointment.can_join_as_participant}
className={`p-2 rounded-lg transition-colors ${appointment.can_join_meeting ? (isDark ? "hover:bg-gray-700" : "hover:bg-gray-100") : (isDark ? "opacity-50 cursor-not-allowed" : "opacity-50 cursor-not-allowed")}`} className={`p-2 rounded-lg transition-colors ${appointment.can_join_as_participant ? (isDark ? "hover:bg-gray-700" : "hover:bg-gray-100") : (isDark ? "opacity-50 cursor-not-allowed" : "opacity-50 cursor-not-allowed")}`}
title={appointment.can_join_meeting ? "Copy room ID" : "Meeting not available"} title={appointment.can_join_as_participant ? "Copy room ID" : "Meeting not available"}
> >
<Copy className={`w-4 h-4 ${isDark ? "text-gray-400" : "text-gray-500"}`} /> <Copy className={`w-4 h-4 ${isDark ? "text-gray-400" : "text-gray-500"}`} />
</button> </button>
@ -467,18 +467,18 @@ export default function UserAppointmentDetailPage() {
Meeting Link Meeting Link
</p> </p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{appointment.can_join_meeting ? ( {appointment.can_join_as_participant ? (
<> <>
<a <a
href={appointment.jitsi_meet_url} href={appointment.participant_join_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className={`flex-1 text-sm px-3 py-2 rounded-lg truncate ${isDark ? "bg-gray-800 text-blue-400 hover:bg-gray-700" : "bg-white text-blue-600 hover:bg-gray-50 border border-gray-200"}`} className={`flex-1 text-sm px-3 py-2 rounded-lg truncate ${isDark ? "bg-gray-800 text-blue-400 hover:bg-gray-700" : "bg-white text-blue-600 hover:bg-gray-50 border border-gray-200"}`}
> >
{appointment.jitsi_meet_url} {appointment.participant_join_url}
</a> </a>
<a <a
href={appointment.jitsi_meet_url} href={appointment.participant_join_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className={`px-4 py-2 rounded-lg font-medium transition-colors ${isDark ? "bg-blue-600 hover:bg-blue-700 text-white" : "bg-blue-600 hover:bg-blue-700 text-white"}`} className={`px-4 py-2 rounded-lg font-medium transition-colors ${isDark ? "bg-blue-600 hover:bg-blue-700 text-white" : "bg-blue-600 hover:bg-blue-700 text-white"}`}
@ -489,7 +489,7 @@ export default function UserAppointmentDetailPage() {
) : ( ) : (
<> <>
<div className={`flex-1 text-sm px-3 py-2 rounded-lg truncate ${isDark ? "bg-gray-800/50 text-gray-500 border border-gray-700" : "bg-gray-100 text-gray-400 border border-gray-300"}`}> <div className={`flex-1 text-sm px-3 py-2 rounded-lg truncate ${isDark ? "bg-gray-800/50 text-gray-500 border border-gray-700" : "bg-gray-100 text-gray-400 border border-gray-300"}`}>
{appointment.jitsi_meet_url} {appointment.participant_join_url}
</div> </div>
<button <button
disabled disabled
@ -501,11 +501,11 @@ export default function UserAppointmentDetailPage() {
)} )}
</div> </div>
</div> </div>
{appointment.can_join_meeting !== undefined && ( {appointment.can_join_as_participant !== undefined && (
<div className={`flex items-center gap-2 px-4 py-3 rounded-lg ${appointment.can_join_meeting ? (isDark ? "bg-green-500/20 border border-green-500/30" : "bg-green-50 border border-green-200") : (isDark ? "bg-gray-800 border border-gray-700" : "bg-gray-50 border border-gray-200")}`}> <div className={`flex items-center gap-2 px-4 py-3 rounded-lg ${appointment.can_join_as_participant ? (isDark ? "bg-green-500/20 border border-green-500/30" : "bg-green-50 border border-green-200") : (isDark ? "bg-gray-800 border border-gray-700" : "bg-gray-50 border border-gray-200")}`}>
<div className={`h-2 w-2 rounded-full ${appointment.can_join_meeting ? (isDark ? "bg-green-400" : "bg-green-600") : (isDark ? "bg-gray-500" : "bg-gray-400")}`} /> <div className={`h-2 w-2 rounded-full ${appointment.can_join_as_participant ? (isDark ? "bg-green-400" : "bg-green-600") : (isDark ? "bg-gray-500" : "bg-gray-400")}`} />
<p className={`text-sm font-medium ${appointment.can_join_meeting ? (isDark ? "text-green-300" : "text-green-700") : (isDark ? "text-gray-400" : "text-gray-500")}`}> <p className={`text-sm font-medium ${appointment.can_join_as_participant ? (isDark ? "text-green-300" : "text-green-700") : (isDark ? "text-gray-400" : "text-gray-500")}`}>
{appointment.can_join_meeting ? "Meeting is active - You can join now" : "Meeting is not available yet"} {appointment.can_join_as_participant ? "Meeting is active - You can join now" : "Meeting is not available yet"}
</p> </p>
</div> </div>
)} )}
@ -552,12 +552,12 @@ export default function UserAppointmentDetailPage() {
</div> </div>
{/* Join Meeting Button */} {/* Join Meeting Button */}
{appointment.status === "scheduled" && appointment.jitsi_meet_url && ( {appointment.status === "scheduled" && appointment.participant_join_url && (
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gradient-to-br from-blue-900/20 to-purple-900/20 border-blue-800/30" : "bg-gradient-to-br from-blue-50 to-purple-50 border-blue-200"}`}> <div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gradient-to-br from-blue-900/20 to-purple-900/20 border-blue-800/30" : "bg-gradient-to-br from-blue-50 to-purple-50 border-blue-200"}`}>
<div className="p-6"> <div className="p-6">
{appointment.can_join_meeting ? ( {appointment.can_join_as_participant ? (
<a <a
href={appointment.jitsi_meet_url} href={appointment.participant_join_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className={`flex items-center justify-center gap-2 w-full bg-blue-600 hover:bg-blue-700 text-white h-12 rounded-lg text-base font-medium transition-colors`} className={`flex items-center justify-center gap-2 w-full bg-blue-600 hover:bg-blue-700 text-white h-12 rounded-lg text-base font-medium transition-colors`}

View File

@ -458,13 +458,13 @@ export default function UserDashboard() {
</td> </td>
<td className="px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <td className="px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex items-center justify-end gap-1 sm:gap-2"> <div className="flex items-center justify-end gap-1 sm:gap-2">
{appointment.jitsi_meet_url && ( {appointment.participant_join_url && (
<a <a
href={appointment.jitsi_meet_url} href={appointment.participant_join_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className={`p-1.5 sm:p-2 rounded-lg transition-colors ${ className={`p-1.5 sm:p-2 rounded-lg transition-colors ${
appointment.can_join_meeting appointment.can_join_as_participant
? isDark ? isDark
? "bg-blue-600 hover:bg-blue-700 text-white" ? "bg-blue-600 hover:bg-blue-700 text-white"
: "bg-blue-600 hover:bg-blue-700 text-white" : "bg-blue-600 hover:bg-blue-700 text-white"
@ -472,17 +472,17 @@ export default function UserDashboard() {
? "text-gray-400 hover:text-gray-300 hover:bg-gray-700" ? "text-gray-400 hover:text-gray-300 hover:bg-gray-700"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100" : "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
}`} }`}
title={appointment.can_join_meeting ? "Join Meeting" : "Meeting Not Available"} title={appointment.can_join_as_participant ? "Join Meeting" : "Meeting Not Available"}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (!appointment.can_join_meeting) { if (!appointment.can_join_as_participant) {
e.preventDefault(); e.preventDefault();
} }
}} }}
> >
<Video className="w-4 h-4" /> <Video className="w-4 h-4" />
</a> </a>
)} )}
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -18,8 +18,17 @@ export interface Appointment {
rejection_reason?: string; rejection_reason?: string;
jitsi_meet_url?: string; jitsi_meet_url?: string;
jitsi_room_id?: string; jitsi_room_id?: string;
jitsi_meeting_created?: boolean;
meeting_started_at?: string;
meeting_ended_at?: string;
meeting_duration_actual?: number;
meeting_info?: any;
has_jitsi_meeting?: boolean | string; has_jitsi_meeting?: boolean | string;
can_join_meeting?: boolean | string; can_join_meeting?: boolean | string;
can_join_as_moderator?: boolean | string;
can_join_as_participant?: boolean | string;
moderator_join_url?: string;
participant_join_url?: string;
meeting_status?: string; meeting_status?: string;
matching_availability?: MatchingAvailability | Array<{ matching_availability?: MatchingAvailability | Array<{
date: string; date: string;
@ -39,6 +48,7 @@ export interface Appointment {
export interface SelectedSlot { export interface SelectedSlot {
day: number; // 0-6 (Monday-Sunday) day: number; // 0-6 (Monday-Sunday)
date?: string; // YYYY-MM-DD format
time_slot: "morning" | "afternoon" | "evening"; time_slot: "morning" | "afternoon" | "evening";
} }