diff --git a/src/components/reviews/ConsultingHistory.tsx b/src/components/reviews/ConsultingHistory.tsx new file mode 100644 index 0000000..9cc30a6 --- /dev/null +++ b/src/components/reviews/ConsultingHistory.tsx @@ -0,0 +1,243 @@ +import { useEffect, useState } from 'react'; +import { supabase } from '@/integrations/supabase/client'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Video, Calendar, Clock, Star, MessageSquare, CheckCircle } from 'lucide-react'; +import { format } from 'date-fns'; +import { id } from 'date-fns/locale'; +import { ReviewModal } from './ReviewModal'; + +interface ConsultingSlot { + id: string; + date: string; + start_time: string; + end_time: string; + status: string; + topic_category: string | null; + meet_link: string | null; + order_id: string | null; +} + +interface ConsultingHistoryProps { + userId: string; +} + +export function ConsultingHistory({ userId }: ConsultingHistoryProps) { + const [slots, setSlots] = useState([]); + const [reviewedSlotIds, setReviewedSlotIds] = useState>(new Set()); + const [loading, setLoading] = useState(true); + const [reviewModal, setReviewModal] = useState<{ + open: boolean; + slotId: string; + orderId: string | null; + label: string; + }>({ open: false, slotId: '', orderId: null, label: '' }); + + useEffect(() => { + fetchData(); + }, [userId]); + + const fetchData = async () => { + // Fetch consulting slots + const { data: slotsData } = await supabase + .from('consulting_slots') + .select('id, date, start_time, end_time, status, topic_category, meet_link, order_id') + .eq('user_id', userId) + .order('date', { ascending: false }); + + if (slotsData) { + setSlots(slotsData); + + // Check which slots have been reviewed + // We use a combination approach: check for consulting reviews by this user + // For consulting, we'll track by order_id since that's how we link them + const orderIds = slotsData + .filter(s => s.order_id) + .map(s => s.order_id as string); + + if (orderIds.length > 0) { + const { data: reviewsData } = await supabase + .from('reviews') + .select('order_id') + .eq('user_id', userId) + .eq('type', 'consulting') + .in('order_id', orderIds); + + if (reviewsData) { + const reviewedOrderIds = new Set(reviewsData.map(r => r.order_id)); + // Map order_id back to slot_id + const reviewedIds = new Set( + slotsData + .filter(s => s.order_id && reviewedOrderIds.has(s.order_id)) + .map(s => s.id) + ); + setReviewedSlotIds(reviewedIds); + } + } + } + + setLoading(false); + }; + + const getStatusBadge = (status: string) => { + switch (status) { + case 'done': + return Selesai; + case 'confirmed': + return Terkonfirmasi; + case 'pending_payment': + return Menunggu Pembayaran; + case 'cancelled': + return Dibatalkan; + default: + return {status}; + } + }; + + const openReviewModal = (slot: ConsultingSlot) => { + const dateLabel = format(new Date(slot.date), 'd MMMM yyyy', { locale: id }); + const timeLabel = `${slot.start_time.substring(0, 5)} - ${slot.end_time.substring(0, 5)}`; + setReviewModal({ + open: true, + slotId: slot.id, + orderId: slot.order_id, + label: `Sesi konsultasi ${dateLabel}, ${timeLabel}`, + }); + }; + + const handleReviewSuccess = () => { + // Mark this slot as reviewed + setReviewedSlotIds(prev => new Set([...prev, reviewModal.slotId])); + }; + + const doneSlots = slots.filter(s => s.status === 'done'); + const upcomingSlots = slots.filter(s => s.status === 'confirmed'); + + if (loading) { + return ( + + + + + +
+ {[...Array(2)].map((_, i) => ( + + ))} +
+
+
+ ); + } + + if (slots.length === 0) { + return null; + } + + return ( + <> + + + + + + + {/* Upcoming sessions */} + {upcomingSlots.length > 0 && ( +
+

Sesi Mendatang

+ {upcomingSlots.map((slot) => ( +
+
+ +
+

+ {format(new Date(slot.date), 'd MMM yyyy', { locale: id })} +

+

+ + {slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)} + {slot.topic_category && ` • ${slot.topic_category}`} +

+
+
+
+ {getStatusBadge(slot.status)} + {slot.meet_link && ( + + )} +
+
+ ))} +
+ )} + + {/* Completed sessions */} + {doneSlots.length > 0 && ( +
+

Sesi Selesai

+ {doneSlots.map((slot) => { + const hasReviewed = reviewedSlotIds.has(slot.id); + return ( +
+
+ +
+

+ {format(new Date(slot.date), 'd MMM yyyy', { locale: id })} +

+

+ + {slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)} + {slot.topic_category && ` • ${slot.topic_category}`} +

+
+
+
+ {getStatusBadge(slot.status)} + {hasReviewed ? ( + + + Sudah diulas + + ) : ( + + )} +
+
+ ); + })} +
+ )} +
+
+ + setReviewModal({ ...reviewModal, open })} + userId={userId} + productId={null} + orderId={reviewModal.orderId} + type="consulting" + contextLabel={reviewModal.label} + onSuccess={handleReviewSuccess} + /> + + ); +} diff --git a/src/components/reviews/ReviewModal.tsx b/src/components/reviews/ReviewModal.tsx new file mode 100644 index 0000000..7d5787f --- /dev/null +++ b/src/components/reviews/ReviewModal.tsx @@ -0,0 +1,149 @@ +import { useState } from 'react'; +import { supabase } from '@/integrations/supabase/client'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { toast } from '@/hooks/use-toast'; +import { Star, X } from 'lucide-react'; + +interface ReviewModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + userId: string; + productId?: string | null; + orderId?: string | null; + type: 'consulting' | 'bootcamp' | 'webinar' | 'general'; + contextLabel?: string; + onSuccess?: () => void; +} + +export function ReviewModal({ + open, + onOpenChange, + userId, + productId, + orderId, + type, + contextLabel, + onSuccess, +}: ReviewModalProps) { + const [rating, setRating] = useState(0); + const [hoverRating, setHoverRating] = useState(0); + const [title, setTitle] = useState(''); + const [body, setBody] = useState(''); + const [submitting, setSubmitting] = useState(false); + + const handleSubmit = async () => { + if (rating === 0) { + toast({ title: 'Error', description: 'Pilih rating terlebih dahulu', variant: 'destructive' }); + return; + } + if (!title.trim()) { + toast({ title: 'Error', description: 'Judul tidak boleh kosong', variant: 'destructive' }); + return; + } + + setSubmitting(true); + const { error } = await supabase.from('reviews').insert({ + user_id: userId, + product_id: productId || null, + order_id: orderId || null, + type, + rating, + title: title.trim(), + body: body.trim() || null, + is_approved: false, + }); + + if (error) { + console.error('Review submit error:', error); + toast({ title: 'Error', description: 'Gagal mengirim ulasan', variant: 'destructive' }); + } else { + toast({ title: 'Berhasil', description: 'Terima kasih! Ulasan Anda akan ditinjau oleh admin.' }); + // Reset form + setRating(0); + setTitle(''); + setBody(''); + onOpenChange(false); + onSuccess?.(); + } + setSubmitting(false); + }; + + const handleClose = () => { + if (!submitting) { + onOpenChange(false); + } + }; + + return ( + + + + Beri Ulasan + {contextLabel && ( + {contextLabel} + )} + + +
+
+ +
+ {[1, 2, 3, 4, 5].map((i) => ( + + ))} +
+
+ +
+ + setTitle(e.target.value)} + placeholder="Ringkasan pengalaman Anda" + className="border-2" + maxLength={100} + /> +
+ +
+ +