fix: consulting slots display and auth reload redirect

- Group consulting slots by date in admin order detail modal
- Show time range from first slot start to last slot end
- Display session count badge for multi-slot orders
- Fix page reload redirecting to main page by ensuring loading state
  is properly synchronized with Supabase session initialization
- Add mounted flag to prevent state updates after unmount

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
dwindown
2025-12-27 23:47:53 +07:00
parent 777d989d34
commit 79e1bd82fc
2 changed files with 131 additions and 93 deletions

View File

@@ -21,31 +21,46 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const [isAdmin, setIsAdmin] = useState(false); const [isAdmin, setIsAdmin] = useState(false);
useEffect(() => { useEffect(() => {
const { data: { subscription } } = supabase.auth.onAuthStateChange( let mounted = true;
(event, session) => {
setSession(session);
setUser(session?.user ?? null);
if (session?.user) {
setTimeout(() => {
checkAdminRole(session.user.id);
}, 0);
} else {
setIsAdmin(false);
}
}
);
// First, get the initial session
supabase.auth.getSession().then(({ data: { session } }) => { supabase.auth.getSession().then(({ data: { session } }) => {
if (!mounted) return;
setSession(session); setSession(session);
setUser(session?.user ?? null); setUser(session?.user ?? null);
if (session?.user) { if (session?.user) {
checkAdminRole(session.user.id); checkAdminRole(session.user.id);
} }
// Only set loading to false after initial session is loaded
setLoading(false); setLoading(false);
}); });
return () => subscription.unsubscribe(); // Then listen for auth state changes
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
if (!mounted) return;
setSession(session);
setUser(session?.user ?? null);
if (session?.user) {
checkAdminRole(session.user.id);
} else {
setIsAdmin(false);
}
// Ensure loading is false after auth state changes
setLoading(false);
}
);
return () => {
mounted = false;
subscription.unsubscribe();
};
}, []); }, []);
const checkAdminRole = async (userId: string) => { const checkAdminRole = async (userId: string) => {

View File

@@ -508,29 +508,50 @@ export default function AdminOrders() {
</div> </div>
)} )}
{/* Consulting Slots */} {/* Consulting Slots - Grouped by Date */}
{consultingSlots.length > 0 && ( {consultingSlots.length > 0 && (() => {
// Group slots by date
const slotsByDate = consultingSlots.reduce((acc, slot) => {
if (!acc[slot.date]) {
acc[slot.date] = [];
}
acc[slot.date].push(slot);
return acc;
}, {} as Record<string, typeof consultingSlots>);
return (
<div className="border-t border-border pt-4"> <div className="border-t border-border pt-4">
<p className="font-medium mb-3 flex items-center gap-2"> <p className="font-medium mb-3 flex items-center gap-2">
<Video className="w-4 h-4" /> <Video className="w-4 h-4" />
Jadwal Konsultasi Jadwal Konsultasi
</p> </p>
<div className="space-y-2"> <div className="space-y-3">
{consultingSlots.map((slot) => ( {Object.entries(slotsByDate).map(([date, slots]) => {
<div key={slot.id} className="border-2 border-border rounded-lg p-3 bg-background"> const firstSlot = slots[0];
<div className="flex items-start justify-between gap-3 mb-2"> const lastSlot = slots[slots.length - 1];
const allSlotsHaveMeetLink = slots.every(s => s.meet_link);
const meetLink = firstSlot.meet_link;
return (
<div key={date} className="border-2 border-border rounded-lg p-3 bg-background">
<div className="flex items-start justify-between gap-3">
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-2 mb-1 flex-wrap"> <div className="flex items-center gap-2 mb-1 flex-wrap">
<Badge variant={slot.status === "confirmed" ? "default" : "secondary"} className="text-xs"> <Badge variant={firstSlot.status === "confirmed" ? "default" : "secondary"} className="text-xs">
{slot.status === "confirmed" ? "Terkonfirmasi" : slot.status} {firstSlot.status === "confirmed" ? "Terkonfirmasi" : firstSlot.status}
</Badge> </Badge>
{slot.topic_category && ( {firstSlot.topic_category && (
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{slot.topic_category} {firstSlot.topic_category}
</Badge>
)}
{slots.length > 1 && (
<Badge variant="outline" className="text-xs">
{slots.length} sesi
</Badge> </Badge>
)} )}
{/* Meet Link Status */} {/* Meet Link Status */}
{slot.meet_link ? ( {allSlotsHaveMeetLink ? (
<Badge variant="outline" className="text-xs gap-1 border-green-500 text-green-700"> <Badge variant="outline" className="text-xs gap-1 border-green-500 text-green-700">
<CheckCircle className="w-3 h-3" /> <CheckCircle className="w-3 h-3" />
Meet Link Ready Meet Link Ready
@@ -543,7 +564,7 @@ export default function AdminOrders() {
)} )}
</div> </div>
<p className="text-sm font-medium"> <p className="text-sm font-medium">
{new Date(slot.date).toLocaleDateString("id-ID", { {new Date(date).toLocaleDateString("id-ID", {
weekday: "short", weekday: "short",
day: "numeric", day: "numeric",
month: "short", month: "short",
@@ -551,19 +572,19 @@ export default function AdminOrders() {
})} })}
</p> </p>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)} WIB {firstSlot.start_time.substring(0, 5)} - {lastSlot.end_time.substring(0, 5)} WIB
</p> </p>
{slot.notes && ( {firstSlot.notes && (
<p className="text-xs text-muted-foreground mt-1 italic"> <p className="text-xs text-muted-foreground mt-1 italic">
Catatan: {slot.notes} Catatan: {firstSlot.notes}
</p> </p>
)} )}
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
{slot.meet_link && ( {meetLink && (
<Button asChild variant="outline" size="sm" className="gap-1"> <Button asChild variant="outline" size="sm" className="gap-1">
<a <a
href={slot.meet_link} href={meetLink}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
@@ -576,19 +597,21 @@ export default function AdminOrders() {
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => openMeetLinkDialog(slot.id, slot.meet_link)} onClick={() => openMeetLinkDialog(firstSlot.id, meetLink)}
className="gap-1" className="gap-1"
> >
<LinkIcon className="w-3 h-3" /> <LinkIcon className="w-3 h-3" />
{slot.meet_link ? "Update" : "Buat"} Link {meetLink ? "Update" : "Buat"} Link
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
))} );
})}
</div> </div>
</div> </div>
)} );
})()}
<div className="flex flex-col sm:flex-row gap-2 pt-4"> <div className="flex flex-col sm:flex-row gap-2 pt-4">
{canRefundOrder(selectedOrder.payment_status, selectedOrder.refunded_at) && ( {canRefundOrder(selectedOrder.payment_status, selectedOrder.refunded_at) && (