Compare commits

...

2 Commits

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}`;
const timeSlot = String(slot.time_slot).toLowerCase().trim();
const timeLabel = timeSlotLabels[timeSlot] || slot.time_slot;
return ( // Group slots by date
<div const slotsByDate: Record<string, typeof appointment.selected_slots> = {};
key={idx} appointment.selected_slots.forEach((slot: any) => {
className={`px-4 py-3 rounded-xl border ${isDark ? "bg-green-500/10 border-green-500/30" : "bg-green-50 border-green-200"}`} const date = slot.date || "";
> if (!slotsByDate[date]) {
<p className={`text-sm font-semibold ${isDark ? "text-green-300" : "text-green-700"}`}> slotsByDate[date] = [];
{dayName} }
</p> slotsByDate[date].push(slot);
<p className={`text-xs mt-1 ${isDark ? "text-green-400" : "text-green-600"}`}> });
{timeLabel}
</p> return (
{slot.date && ( <div className="space-y-4">
<p className={`text-xs mt-1 ${isDark ? "text-gray-400" : "text-gray-500"}`}> {Object.entries(slotsByDate).map(([date, slots]) => (
{formatShortDate(slot.date)} <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">
)} <p className={`text-base font-semibold ${isDark ? "text-white" : "text-gray-900"}`}>
</div> {formatShortDate(date)}
); </p>
})} {slots.length > 0 && slots[0]?.day !== undefined && (
</div> <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>
</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}`;
const timeSlot = String(slot.time_slot).toLowerCase().trim();
const timeLabel = timeSlotLabels[timeSlot] || slot.time_slot;
return ( // Group slots by date
<div const slotsByDate: Record<string, typeof appointment.selected_slots> = {};
key={idx} appointment.selected_slots.forEach((slot: any) => {
className={`px-4 py-3 rounded-xl border ${isDark ? "bg-green-500/10 border-green-500/30" : "bg-green-50 border-green-200"}`} const date = slot.date || "";
> if (!slotsByDate[date]) {
<p className={`text-sm font-semibold ${isDark ? "text-green-300" : "text-green-700"}`}> slotsByDate[date] = [];
{dayName} }
</p> slotsByDate[date].push(slot);
<p className={`text-xs mt-1 ${isDark ? "text-green-400" : "text-green-600"}`}> });
{timeLabel}
</p> return (
{slot.date && ( <div className="space-y-4">
<p className={`text-xs mt-1 ${isDark ? "text-gray-400" : "text-gray-500"}`}> {Object.entries(slotsByDate).map(([date, slots]) => (
{formatShortDate(slot.date)} <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">
)} <p className={`text-base font-semibold ${isDark ? "text-white" : "text-gray-900"}`}>
</div> {formatShortDate(date)}
); </p>
})} {slots.length > 0 && slots[0]?.day !== undefined && (
</div> <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>
</div> </div>
)} )}