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:
@@ -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) => {
|
||||||
|
|||||||
@@ -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) && (
|
||||||
|
|||||||
Reference in New Issue
Block a user