Add consulting slots display with Join Meet button

- Member OrderDetail page: Shows consulting slots with date/time and Join Meet button
- Admin Orders dialog: Shows consulting slots with meet link access
- Meet button only visible when payment_status is 'paid'
- Both pages show slot status (confirmed/pending)

🤖 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-23 16:45:48 +07:00
parent ce531c8d46
commit 9d7d76b04d
3 changed files with 195 additions and 19 deletions

View File

@@ -9,7 +9,7 @@ import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Separator } from "@/components/ui/separator";
import { formatIDR, formatDate } from "@/lib/format";
import { ArrowLeft, Package, CreditCard, Calendar, AlertCircle } from "lucide-react";
import { ArrowLeft, Package, CreditCard, Calendar, AlertCircle, Video } from "lucide-react";
interface OrderItem {
id: string;
@@ -37,11 +37,21 @@ interface Order {
order_items: OrderItem[];
}
interface ConsultingSlot {
id: string;
date: string;
start_time: string;
end_time: string;
status: string;
meet_link?: string;
}
export default function OrderDetail() {
const { id } = useParams<{ id: string }>();
const { user, loading: authLoading } = useAuth();
const navigate = useNavigate();
const [order, setOrder] = useState<Order | null>(null);
const [consultingSlots, setConsultingSlots] = useState<ConsultingSlot[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -60,7 +70,7 @@ export default function OrderDetail() {
const fetchOrder = async () => {
if (!user || !id) return;
setLoading(true);
setError(null);
@@ -92,8 +102,25 @@ export default function OrderDetail() {
setLoading(false);
return;
}
setOrder(data as Order);
// Fetch consulting slots if this is a consulting order
const hasConsultingProduct = data.order_items.some(
(item: OrderItem) => item.products.type === "consulting"
);
if (hasConsultingProduct) {
const { data: slots } = await supabase
.from("consulting_slots")
.select("*")
.eq("order_id", id)
.order("date", { ascending: true });
if (slots) {
setConsultingSlots(slots as ConsultingSlot[]);
}
}
} catch (err) {
console.error("Unexpected error:", err);
setError("Terjadi kesalahan");
@@ -293,6 +320,68 @@ export default function OrderDetail() {
</CardContent>
</Card>
{/* Consulting Slots */}
{consultingSlots.length > 0 && (
<Card className="border-2 border-primary bg-primary/5">
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<Video className="w-5 h-5" />
Jadwal Konsultasi
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{consultingSlots.map((slot) => (
<div key={slot.id} className="border-2 border-border rounded-lg p-4 bg-background">
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<Badge variant={slot.status === "confirmed" ? "default" : "secondary"}>
{slot.status === "confirmed" ? "Terkonfirmasi" : slot.status}
</Badge>
</div>
<p className="font-medium">
{new Date(slot.date).toLocaleDateString("id-ID", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric"
})}
</p>
<p className="text-sm text-muted-foreground">
{slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)} WIB
</p>
</div>
{slot.meet_link && order.payment_status === "paid" && (
<Button asChild className="shadow-sm">
<a
href={slot.meet_link}
target="_blank"
rel="noopener noreferrer"
>
<Video className="w-4 h-4 mr-2" />
Join Meet
</a>
</Button>
)}
{slot.meet_link && order.payment_status !== "paid" && (
<p className="text-sm text-muted-foreground">
Link tersedia setelah pembayaran
</p>
)}
{!slot.meet_link && (
<p className="text-sm text-muted-foreground">
Link akan dikirim via email
</p>
)}
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Access Info */}
{order.payment_status === "paid" && (
<Card className="border-2 border-primary bg-primary/5">