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(() => {
let mounted = true;
// First, get the initial session
supabase.auth.getSession().then(({ data: { session } }) => {
if (!mounted) return;
setSession(session);
setUser(session?.user ?? null);
if (session?.user) {
checkAdminRole(session.user.id);
}
// Only set loading to false after initial session is loaded
setLoading(false);
});
// Then listen for auth state changes
const { data: { subscription } } = supabase.auth.onAuthStateChange( const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => { (event, session) => {
if (!mounted) return;
setSession(session); setSession(session);
setUser(session?.user ?? null); setUser(session?.user ?? null);
if (session?.user) { if (session?.user) {
setTimeout(() => { checkAdminRole(session.user.id);
checkAdminRole(session.user.id);
}, 0);
} else { } else {
setIsAdmin(false); setIsAdmin(false);
} }
// Ensure loading is false after auth state changes
setLoading(false);
} }
); );
supabase.auth.getSession().then(({ data: { session } }) => { return () => {
setSession(session); mounted = false;
setUser(session?.user ?? null); subscription.unsubscribe();
if (session?.user) { };
checkAdminRole(session.user.id);
}
setLoading(false);
});
return () => subscription.unsubscribe();
}, []); }, []);
const checkAdminRole = async (userId: string) => { const checkAdminRole = async (userId: string) => {

View File

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