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

Merged
Hammond merged 1 commits from feat/booking-panel into master 2025-12-03 18:27:46 +00:00
3 changed files with 190 additions and 97 deletions

View File

@ -20,7 +20,7 @@ import {
MapPin, MapPin,
} from "lucide-react"; } from "lucide-react";
import { useAppTheme } from "@/components/ThemeProvider"; 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 { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@ -58,8 +58,23 @@ export default function AppointmentDetailPage() {
setLoading(true); setLoading(true);
try { try {
const data = await getAppointmentDetail(appointmentId); // Fetch both detail and list to get selected_slots from list endpoint
setAppointment(data); 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) { } catch (error) {
toast.error("Failed to load appointment details"); toast.error("Failed to load appointment details");
router.push("/admin/booking"); router.push("/admin/booking");
@ -367,7 +382,7 @@ export default function AppointmentDetailPage() {
</div> </div>
)} )}
{/* Selected Slots (replacing Matching Slots) */} {/* Selected Slots */}
{appointment.selected_slots && Array.isArray(appointment.selected_slots) && appointment.selected_slots.length > 0 && ( {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={`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"}`}> <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> </h2>
</div> </div>
<div className="p-6"> <div className="p-6">
<div className="flex flex-wrap gap-3"> {(() => {
{appointment.selected_slots.map((slot: any, idx: number) => { const dayNames: Record<number, string> = {
const dayNames: Record<number, string> = { 0: "Monday",
0: "Monday", 1: "Tuesday",
1: "Tuesday", 2: "Wednesday",
2: "Wednesday", 3: "Thursday",
3: "Thursday", 4: "Friday",
4: "Friday", 5: "Saturday",
5: "Saturday", 6: "Sunday",
6: "Sunday", };
}; const timeSlotLabels: Record<string, string> = {
const timeSlotLabels: Record<string, string> = { morning: "Morning",
morning: "Morning", afternoon: "Lunchtime",
afternoon: "Lunchtime", evening: "Evening",
evening: "Evening", };
};
const dayName = dayNames[slot.day] || `Day ${slot.day}`; // Group slots by date
const timeSlot = String(slot.time_slot).toLowerCase().trim(); const slotsByDate: Record<string, typeof appointment.selected_slots> = {};
const timeLabel = timeSlotLabels[timeSlot] || slot.time_slot; appointment.selected_slots.forEach((slot: any) => {
const date = slot.date || "";
return ( if (!slotsByDate[date]) {
<div slotsByDate[date] = [];
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"}`} slotsByDate[date].push(slot);
> });
<p className={`text-sm font-semibold ${isDark ? "text-green-300" : "text-green-700"}`}>
{dayName} return (
</p> <div className="space-y-4">
<p className={`text-xs mt-1 ${isDark ? "text-green-400" : "text-green-600"}`}> {Object.entries(slotsByDate).map(([date, slots]) => (
{timeLabel} <div key={date} className={`p-4 rounded-xl border ${isDark ? "bg-gray-700/50 border-gray-600" : "bg-gray-50 border-gray-200"}`}>
</p> <div className="mb-3">
{slot.date && ( <p className={`text-base font-semibold ${isDark ? "text-white" : "text-gray-900"}`}>
<p className={`text-xs mt-1 ${isDark ? "text-gray-400" : "text-gray-500"}`}> {formatShortDate(date)}
{formatShortDate(slot.date)} </p>
</p> {slots.length > 0 && slots[0]?.day !== undefined && (
)} <p className={`text-sm mt-1 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
</div> {dayNames[slots[0].day] || `Day ${slots[0].day}`}
); </p>
})} )}
</div> </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>
</div> </div>
)} )}

View File

@ -680,27 +680,52 @@ export default function Booking() {
</td> </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"}`}> <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) const dates = Array.isArray(appointment.preferred_dates)
? appointment.preferred_dates ? appointment.preferred_dates
: appointment.preferred_dates : appointment.preferred_dates
? [appointment.preferred_dates] ? [appointment.preferred_dates]
: []; : [];
// Handle preferred_time_slots
const timeSlots = Array.isArray(appointment.preferred_time_slots) const timeSlots = Array.isArray(appointment.preferred_time_slots)
? appointment.preferred_time_slots ? appointment.preferred_time_slots
: 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) { if (dates.length === 0 && timeSlots.length === 0) {
return <span>-</span>; return <span>-</span>;
} }

View File

@ -18,7 +18,7 @@ import {
Copy, Copy,
} from "lucide-react"; } from "lucide-react";
import { useAppTheme } from "@/components/ThemeProvider"; 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 { Button } from "@/components/ui/button";
import { Navbar } from "@/components/Navbar"; import { Navbar } from "@/components/Navbar";
import { toast } from "sonner"; import { toast } from "sonner";
@ -40,8 +40,23 @@ export default function UserAppointmentDetailPage() {
setLoading(true); setLoading(true);
try { try {
const data = await getAppointmentDetail(appointmentId); // Fetch both detail and list to get selected_slots from list endpoint
setAppointment(data); 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) { } catch (error) {
toast.error("Failed to load appointment details"); toast.error("Failed to load appointment details");
router.push("/user/dashboard"); router.push("/user/dashboard");
@ -306,7 +321,7 @@ export default function UserAppointmentDetailPage() {
</div> </div>
)} )}
{/* Selected Slots (replacing Matching Slots) */} {/* Selected Slots */}
{appointment.selected_slots && Array.isArray(appointment.selected_slots) && appointment.selected_slots.length > 0 && ( {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={`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"}`}> <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> </h2>
</div> </div>
<div className="p-6"> <div className="p-6">
<div className="flex flex-wrap gap-3"> {(() => {
{appointment.selected_slots.map((slot: any, idx: number) => { const dayNames: Record<number, string> = {
const dayNames: Record<number, string> = { 0: "Monday",
0: "Monday", 1: "Tuesday",
1: "Tuesday", 2: "Wednesday",
2: "Wednesday", 3: "Thursday",
3: "Thursday", 4: "Friday",
4: "Friday", 5: "Saturday",
5: "Saturday", 6: "Sunday",
6: "Sunday", };
}; const timeSlotLabels: Record<string, string> = {
const timeSlotLabels: Record<string, string> = { morning: "Morning",
morning: "Morning", afternoon: "Lunchtime",
afternoon: "Lunchtime", evening: "Evening",
evening: "Evening", };
};
const dayName = dayNames[slot.day] || `Day ${slot.day}`; // Group slots by date
const timeSlot = String(slot.time_slot).toLowerCase().trim(); const slotsByDate: Record<string, typeof appointment.selected_slots> = {};
const timeLabel = timeSlotLabels[timeSlot] || slot.time_slot; appointment.selected_slots.forEach((slot: any) => {
const date = slot.date || "";
return ( if (!slotsByDate[date]) {
<div slotsByDate[date] = [];
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"}`} slotsByDate[date].push(slot);
> });
<p className={`text-sm font-semibold ${isDark ? "text-green-300" : "text-green-700"}`}>
{dayName} return (
</p> <div className="space-y-4">
<p className={`text-xs mt-1 ${isDark ? "text-green-400" : "text-green-600"}`}> {Object.entries(slotsByDate).map(([date, slots]) => (
{timeLabel} <div key={date} className={`p-4 rounded-xl border ${isDark ? "bg-gray-700/50 border-gray-600" : "bg-gray-50 border-gray-200"}`}>
</p> <div className="mb-3">
{slot.date && ( <p className={`text-base font-semibold ${isDark ? "text-white" : "text-gray-900"}`}>
<p className={`text-xs mt-1 ${isDark ? "text-gray-400" : "text-gray-500"}`}> {formatShortDate(date)}
{formatShortDate(slot.date)} </p>
</p> {slots.length > 0 && slots[0]?.day !== undefined && (
)} <p className={`text-sm mt-1 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
</div> {dayNames[slots[0].day] || `Day ${slots[0].day}`}
); </p>
})} )}
</div> </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>
</div> </div>
)} )}