Refactor quick access section to only show scheduled events

Changes:
- Add consulting slots fetching to get confirmed upcoming sessions
- Update getQuickAction logic:
  * Consulting: Only show if has confirmed upcoming slot with meet_link
  * Webinar: Only show if event_start + duration hasn't ended
  * Bootcamp: Removed from quick access (self-paced, not scheduled)
- Filter out items without valid quick actions
- Remove unused Calendar and BookOpen imports

Quick access now truly means "it is scheduled, here's the shortcut to join"

🤖 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-26 01:40:52 +07:00
parent 91bec42c4b
commit 734aa967ac

View File

@@ -8,7 +8,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { formatIDR } from "@/lib/format";
import { Video, Calendar, BookOpen, ArrowRight, Package, Receipt, ShoppingBag } from "lucide-react";
import { Video, ArrowRight, Package, Receipt, ShoppingBag } from "lucide-react";
import { WhatsAppBanner } from "@/components/WhatsAppBanner";
import { ConsultingHistory } from "@/components/reviews/ConsultingHistory";
import { UnpaidOrderAlert } from "@/components/UnpaidOrderAlert";
@@ -22,6 +22,8 @@ interface UserAccess {
type: string;
meeting_link: string | null;
recording_url: string | null;
event_start: string | null;
duration_minutes: number | null;
};
}
@@ -37,12 +39,23 @@ interface UnpaidConsultingOrder {
qr_expires_at: string;
}
interface ConsultingSlot {
id: string;
date: string;
start_time: string;
end_time: string;
status: string;
product_id: string | null;
meet_link: string | null;
}
export default function MemberDashboard() {
const { user, loading: authLoading } = useAuth();
const navigate = useNavigate();
const [access, setAccess] = useState<UserAccess[]>([]);
const [recentOrders, setRecentOrders] = useState<Order[]>([]);
const [unpaidConsultingOrders, setUnpaidConsultingOrders] = useState<UnpaidConsultingOrder[]>([]);
const [consultingSlots, setConsultingSlots] = useState<ConsultingSlot[]>([]);
const [loading, setLoading] = useState(true);
const [hasWhatsApp, setHasWhatsApp] = useState(true);
@@ -109,10 +122,10 @@ export default function MemberDashboard() {
}, []);
const fetchData = async () => {
const [accessRes, ordersRes, paidOrdersRes, profileRes] = await Promise.all([
const [accessRes, ordersRes, paidOrdersRes, profileRes, slotsRes] = await Promise.all([
supabase
.from("user_access")
.select(`id, product:products (id, title, slug, type, meeting_link, recording_url)`)
.select(`id, product:products (id, title, slug, type, meeting_link, recording_url, event_start, duration_minutes)`)
.eq("user_id", user!.id),
supabase.from("orders").select("*").eq("user_id", user!.id).order("created_at", { ascending: false }).limit(3),
// Also get products from paid orders (via order_items)
@@ -121,13 +134,20 @@ export default function MemberDashboard() {
.select(
`
order_items (
product:products (id, title, slug, type, meeting_link, recording_url)
product:products (id, title, slug, type, meeting_link, recording_url, event_start, duration_minutes)
)
`,
)
.eq("user_id", user!.id)
.eq("payment_status", "paid"),
supabase.from("profiles").select("whatsapp_number").eq("id", user!.id).single(),
// Fetch confirmed consulting slots for quick access
supabase
.from("consulting_slots")
.select("id, date, start_time, end_time, status, product_id, meet_link")
.eq("user_id", user!.id)
.eq("status", "confirmed")
.order("date", { ascending: false }),
]);
// Combine access from user_access and paid orders
@@ -149,20 +169,45 @@ export default function MemberDashboard() {
setAccess([...directAccess, ...paidProductAccess]);
if (ordersRes.data) setRecentOrders(ordersRes.data);
if (profileRes.data) setHasWhatsApp(!!profileRes.data.whatsapp_number);
if (slotsRes.data) setConsultingSlots(slotsRes.data as unknown as ConsultingSlot[]);
setLoading(false);
};
const getQuickAction = (item: UserAccess) => {
const now = new Date();
switch (item.product.type) {
case "consulting":
return { label: "Jadwalkan", icon: Calendar, href: item.product.meeting_link };
case "webinar":
if (item.product.recording_url) {
return { label: "Tonton", icon: Video, route: `/webinar/${item.product.slug}` };
case "consulting": {
// Only show if user has a confirmed upcoming consulting slot for this product
const upcomingSlot = consultingSlots.find(
(slot) =>
slot.product_id === item.product.id &&
slot.status === "confirmed" &&
new Date(slot.date) >= new Date(now.setHours(0, 0, 0, 0))
);
if (upcomingSlot && upcomingSlot.meet_link) {
return { label: "Gabung Konsultasi", icon: Video, href: upcomingSlot.meet_link };
}
return { label: "Gabung", icon: Video, href: item.product.meeting_link };
return null;
}
case "webinar": {
// Only show if webinar is joinable (hasn't ended yet)
if (!item.product.event_start) return null;
const eventStart = new Date(item.product.event_start);
const durationMs = (item.product.duration_minutes || 60) * 60 * 1000;
const eventEnd = new Date(eventStart.getTime() + durationMs);
// Only show if webinar hasn't ended
if (now <= eventEnd) {
return { label: "Gabung Webinar", icon: Video, href: item.product.meeting_link };
}
return null;
}
case "bootcamp":
return { label: "Lanjutkan", icon: BookOpen, route: `/bootcamp/${item.product.slug}` };
// Don't show bootcamp in quick access - it's self-paced, not scheduled
return null;
default:
return null;
}
@@ -250,33 +295,28 @@ export default function MemberDashboard() {
</Button>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{access.slice(0, 3).map((item) => {
const action = getQuickAction(item);
return (
{access
.map((item) => ({ item, action: getQuickAction(item) }))
.filter(({ action }) => action !== null)
.slice(0, 3)
.map(({ item, action }) => (
<Card key={item.id} className="border-2 border-border">
<CardHeader className="pb-2">
<CardTitle className="text-lg">{item.product.title}</CardTitle>
<CardDescription className="capitalize">{item.product.type}</CardDescription>
</CardHeader>
<CardContent>
{action &&
(action.route ? (
<Button onClick={() => navigate(action.route!)} className="w-full shadow-sm">
{action && action.href && (
<Button asChild variant="outline" className="w-full border-2">
<a href={action.href} target="_blank" rel="noopener noreferrer">
<action.icon className="w-4 h-4 mr-2" />
{action.label}
</Button>
) : action.href ? (
<Button asChild variant="outline" className="w-full border-2">
<a href={action.href} target="_blank" rel="noopener noreferrer">
<action.icon className="w-4 h-4 mr-2" />
{action.label}
</a>
</Button>
) : null)}
</a>
</Button>
)}
</CardContent>
</Card>
);
})}
))}
</div>
</div>
)}