Merge pull request 'Refactor appointment detail and booking components to enhance the display of selected time slots. Implement logic to fetch and merge selected slots from the appointment list, improving data handling and user experience. Update UI to group slots by date…' (#42) from feat/booking-panel into master
Reviewed-on: http://35.207.46.142/ATTUNE-HEART-THERAPY/website/pulls/42
This commit is contained in:
commit
13bc135e09
@ -20,7 +20,7 @@ import {
|
||||
MapPin,
|
||||
} from "lucide-react";
|
||||
import { useAppTheme } from "@/components/ThemeProvider";
|
||||
import { getAppointmentDetail, scheduleAppointment, rejectAppointment } from "@/lib/actions/appointments";
|
||||
import { getAppointmentDetail, scheduleAppointment, rejectAppointment, listAppointments } from "@/lib/actions/appointments";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@ -58,8 +58,23 @@ export default function AppointmentDetailPage() {
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getAppointmentDetail(appointmentId);
|
||||
setAppointment(data);
|
||||
// Fetch both detail and list to get selected_slots from list endpoint
|
||||
const [detailData, listData] = await Promise.all([
|
||||
getAppointmentDetail(appointmentId),
|
||||
listAppointments().catch(() => []) // Fallback to empty array if list fails
|
||||
]);
|
||||
|
||||
// Find matching appointment in list to get selected_slots
|
||||
const listAppointment = Array.isArray(listData)
|
||||
? listData.find((apt: Appointment) => apt.id === appointmentId)
|
||||
: null;
|
||||
|
||||
// Merge selected_slots from list into detail data
|
||||
if (listAppointment && listAppointment.selected_slots && Array.isArray(listAppointment.selected_slots) && listAppointment.selected_slots.length > 0) {
|
||||
detailData.selected_slots = listAppointment.selected_slots;
|
||||
}
|
||||
|
||||
setAppointment(detailData);
|
||||
} catch (error) {
|
||||
toast.error("Failed to load appointment details");
|
||||
router.push("/admin/booking");
|
||||
@ -367,7 +382,7 @@ export default function AppointmentDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Selected Slots (replacing Matching Slots) */}
|
||||
{/* Selected Slots */}
|
||||
{appointment.selected_slots && Array.isArray(appointment.selected_slots) && appointment.selected_slots.length > 0 && (
|
||||
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
|
||||
<div className={`px-6 py-4 border-b ${isDark ? "border-gray-700 bg-gray-800/50" : "border-gray-200 bg-gray-50/50"}`}>
|
||||
@ -382,46 +397,65 @@ export default function AppointmentDetailPage() {
|
||||
</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{appointment.selected_slots.map((slot: any, idx: number) => {
|
||||
const dayNames: Record<number, string> = {
|
||||
0: "Monday",
|
||||
1: "Tuesday",
|
||||
2: "Wednesday",
|
||||
3: "Thursday",
|
||||
4: "Friday",
|
||||
5: "Saturday",
|
||||
6: "Sunday",
|
||||
};
|
||||
const timeSlotLabels: Record<string, string> = {
|
||||
morning: "Morning",
|
||||
afternoon: "Lunchtime",
|
||||
evening: "Evening",
|
||||
};
|
||||
const dayName = dayNames[slot.day] || `Day ${slot.day}`;
|
||||
const timeSlot = String(slot.time_slot).toLowerCase().trim();
|
||||
const timeLabel = timeSlotLabels[timeSlot] || slot.time_slot;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`px-4 py-3 rounded-xl border ${isDark ? "bg-green-500/10 border-green-500/30" : "bg-green-50 border-green-200"}`}
|
||||
>
|
||||
<p className={`text-sm font-semibold ${isDark ? "text-green-300" : "text-green-700"}`}>
|
||||
{dayName}
|
||||
</p>
|
||||
<p className={`text-xs mt-1 ${isDark ? "text-green-400" : "text-green-600"}`}>
|
||||
{timeLabel}
|
||||
</p>
|
||||
{slot.date && (
|
||||
<p className={`text-xs mt-1 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
||||
{formatShortDate(slot.date)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{(() => {
|
||||
const dayNames: Record<number, string> = {
|
||||
0: "Monday",
|
||||
1: "Tuesday",
|
||||
2: "Wednesday",
|
||||
3: "Thursday",
|
||||
4: "Friday",
|
||||
5: "Saturday",
|
||||
6: "Sunday",
|
||||
};
|
||||
const timeSlotLabels: Record<string, string> = {
|
||||
morning: "Morning",
|
||||
afternoon: "Lunchtime",
|
||||
evening: "Evening",
|
||||
};
|
||||
|
||||
// Group slots by date
|
||||
const slotsByDate: Record<string, typeof appointment.selected_slots> = {};
|
||||
appointment.selected_slots.forEach((slot: any) => {
|
||||
const date = slot.date || "";
|
||||
if (!slotsByDate[date]) {
|
||||
slotsByDate[date] = [];
|
||||
}
|
||||
slotsByDate[date].push(slot);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{Object.entries(slotsByDate).map(([date, slots]) => (
|
||||
<div key={date} className={`p-4 rounded-xl border ${isDark ? "bg-gray-700/50 border-gray-600" : "bg-gray-50 border-gray-200"}`}>
|
||||
<div className="mb-3">
|
||||
<p className={`text-base font-semibold ${isDark ? "text-white" : "text-gray-900"}`}>
|
||||
{formatShortDate(date)}
|
||||
</p>
|
||||
{slots.length > 0 && slots[0]?.day !== undefined && (
|
||||
<p className={`text-sm mt-1 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
||||
{dayNames[slots[0].day] || `Day ${slots[0].day}`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{slots.map((slot: any, idx: number) => {
|
||||
const timeSlot = String(slot.time_slot).toLowerCase().trim();
|
||||
const timeLabel = timeSlotLabels[timeSlot] || slot.time_slot;
|
||||
return (
|
||||
<span
|
||||
key={idx}
|
||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium ${isDark ? "bg-green-500/20 text-green-300 border border-green-500/30" : "bg-green-50 text-green-700 border border-green-200"}`}
|
||||
>
|
||||
{timeLabel}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -680,27 +680,52 @@ export default function Booking() {
|
||||
</td>
|
||||
<td className={`px-3 sm:px-4 md:px-6 py-4 text-xs sm:text-sm hidden lg:table-cell ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
||||
{(() => {
|
||||
// Handle preferred_dates
|
||||
const dayNames: Record<number, string> = {
|
||||
0: "Monday",
|
||||
1: "Tuesday",
|
||||
2: "Wednesday",
|
||||
3: "Thursday",
|
||||
4: "Friday",
|
||||
5: "Saturday",
|
||||
6: "Sunday",
|
||||
};
|
||||
const timeSlotLabels: Record<string, string> = {
|
||||
morning: "Morning",
|
||||
afternoon: "Lunchtime",
|
||||
evening: "Evening",
|
||||
};
|
||||
|
||||
// Show selected_slots if available
|
||||
if (appointment.selected_slots && Array.isArray(appointment.selected_slots) && appointment.selected_slots.length > 0) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
{appointment.selected_slots.slice(0, 2).map((slot, idx) => (
|
||||
<span key={idx} className="text-xs sm:text-sm">
|
||||
{dayNames[slot.day] || `Day ${slot.day}`} - {timeSlotLabels[slot.time_slot] || slot.time_slot}
|
||||
</span>
|
||||
))}
|
||||
{appointment.selected_slots.length > 2 && (
|
||||
<span className="text-xs opacity-75">
|
||||
+{appointment.selected_slots.length - 2} more
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback to preferred_dates and preferred_time_slots if selected_slots not available
|
||||
const dates = Array.isArray(appointment.preferred_dates)
|
||||
? appointment.preferred_dates
|
||||
: appointment.preferred_dates
|
||||
? [appointment.preferred_dates]
|
||||
: [];
|
||||
|
||||
// Handle preferred_time_slots
|
||||
const timeSlots = Array.isArray(appointment.preferred_time_slots)
|
||||
? appointment.preferred_time_slots
|
||||
: appointment.preferred_time_slots
|
||||
? [appointment.preferred_time_slots]
|
||||
: [];
|
||||
|
||||
// Time slot labels
|
||||
const timeSlotLabels: Record<string, string> = {
|
||||
morning: "Morning",
|
||||
afternoon: "Lunchtime",
|
||||
evening: "Evening",
|
||||
};
|
||||
|
||||
if (dates.length === 0 && timeSlots.length === 0) {
|
||||
return <span>-</span>;
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ import {
|
||||
Copy,
|
||||
} from "lucide-react";
|
||||
import { useAppTheme } from "@/components/ThemeProvider";
|
||||
import { getAppointmentDetail } from "@/lib/actions/appointments";
|
||||
import { getAppointmentDetail, listAppointments } from "@/lib/actions/appointments";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Navbar } from "@/components/Navbar";
|
||||
import { toast } from "sonner";
|
||||
@ -40,8 +40,23 @@ export default function UserAppointmentDetailPage() {
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getAppointmentDetail(appointmentId);
|
||||
setAppointment(data);
|
||||
// Fetch both detail and list to get selected_slots from list endpoint
|
||||
const [detailData, listData] = await Promise.all([
|
||||
getAppointmentDetail(appointmentId),
|
||||
listAppointments().catch(() => []) // Fallback to empty array if list fails
|
||||
]);
|
||||
|
||||
// Find matching appointment in list to get selected_slots
|
||||
const listAppointment = Array.isArray(listData)
|
||||
? listData.find((apt: Appointment) => apt.id === appointmentId)
|
||||
: null;
|
||||
|
||||
// Merge selected_slots from list into detail data
|
||||
if (listAppointment && listAppointment.selected_slots && Array.isArray(listAppointment.selected_slots) && listAppointment.selected_slots.length > 0) {
|
||||
detailData.selected_slots = listAppointment.selected_slots;
|
||||
}
|
||||
|
||||
setAppointment(detailData);
|
||||
} catch (error) {
|
||||
toast.error("Failed to load appointment details");
|
||||
router.push("/user/dashboard");
|
||||
@ -306,7 +321,7 @@ export default function UserAppointmentDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Selected Slots (replacing Matching Slots) */}
|
||||
{/* Selected Slots */}
|
||||
{appointment.selected_slots && Array.isArray(appointment.selected_slots) && appointment.selected_slots.length > 0 && (
|
||||
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
|
||||
<div className={`px-6 py-4 border-b ${isDark ? "border-gray-700 bg-gray-800/50" : "border-gray-200 bg-gray-50/50"}`}>
|
||||
@ -321,46 +336,65 @@ export default function UserAppointmentDetailPage() {
|
||||
</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{appointment.selected_slots.map((slot: any, idx: number) => {
|
||||
const dayNames: Record<number, string> = {
|
||||
0: "Monday",
|
||||
1: "Tuesday",
|
||||
2: "Wednesday",
|
||||
3: "Thursday",
|
||||
4: "Friday",
|
||||
5: "Saturday",
|
||||
6: "Sunday",
|
||||
};
|
||||
const timeSlotLabels: Record<string, string> = {
|
||||
morning: "Morning",
|
||||
afternoon: "Lunchtime",
|
||||
evening: "Evening",
|
||||
};
|
||||
const dayName = dayNames[slot.day] || `Day ${slot.day}`;
|
||||
const timeSlot = String(slot.time_slot).toLowerCase().trim();
|
||||
const timeLabel = timeSlotLabels[timeSlot] || slot.time_slot;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`px-4 py-3 rounded-xl border ${isDark ? "bg-green-500/10 border-green-500/30" : "bg-green-50 border-green-200"}`}
|
||||
>
|
||||
<p className={`text-sm font-semibold ${isDark ? "text-green-300" : "text-green-700"}`}>
|
||||
{dayName}
|
||||
</p>
|
||||
<p className={`text-xs mt-1 ${isDark ? "text-green-400" : "text-green-600"}`}>
|
||||
{timeLabel}
|
||||
</p>
|
||||
{slot.date && (
|
||||
<p className={`text-xs mt-1 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
||||
{formatShortDate(slot.date)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{(() => {
|
||||
const dayNames: Record<number, string> = {
|
||||
0: "Monday",
|
||||
1: "Tuesday",
|
||||
2: "Wednesday",
|
||||
3: "Thursday",
|
||||
4: "Friday",
|
||||
5: "Saturday",
|
||||
6: "Sunday",
|
||||
};
|
||||
const timeSlotLabels: Record<string, string> = {
|
||||
morning: "Morning",
|
||||
afternoon: "Lunchtime",
|
||||
evening: "Evening",
|
||||
};
|
||||
|
||||
// Group slots by date
|
||||
const slotsByDate: Record<string, typeof appointment.selected_slots> = {};
|
||||
appointment.selected_slots.forEach((slot: any) => {
|
||||
const date = slot.date || "";
|
||||
if (!slotsByDate[date]) {
|
||||
slotsByDate[date] = [];
|
||||
}
|
||||
slotsByDate[date].push(slot);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{Object.entries(slotsByDate).map(([date, slots]) => (
|
||||
<div key={date} className={`p-4 rounded-xl border ${isDark ? "bg-gray-700/50 border-gray-600" : "bg-gray-50 border-gray-200"}`}>
|
||||
<div className="mb-3">
|
||||
<p className={`text-base font-semibold ${isDark ? "text-white" : "text-gray-900"}`}>
|
||||
{formatShortDate(date)}
|
||||
</p>
|
||||
{slots.length > 0 && slots[0]?.day !== undefined && (
|
||||
<p className={`text-sm mt-1 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
||||
{dayNames[slots[0].day] || `Day ${slots[0].day}`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{slots.map((slot: any, idx: number) => {
|
||||
const timeSlot = String(slot.time_slot).toLowerCase().trim();
|
||||
const timeLabel = timeSlotLabels[timeSlot] || slot.time_slot;
|
||||
return (
|
||||
<span
|
||||
key={idx}
|
||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium ${isDark ? "bg-green-500/20 text-green-300 border border-green-500/30" : "bg-green-50 text-green-700 border border-green-200"}`}
|
||||
>
|
||||
{timeLabel}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user