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

@@ -10,7 +10,7 @@ import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Skeleton } from "@/components/ui/skeleton";
import { formatIDR, formatDateTime } from "@/lib/format";
import { Eye, CheckCircle, XCircle } from "lucide-react";
import { Eye, CheckCircle, XCircle, Video, ExternalLink } from "lucide-react";
import { toast } from "@/hooks/use-toast";
interface Order {
@@ -27,11 +27,20 @@ interface Order {
interface OrderItem {
id: string;
product: { title: string };
product: { title: string; type?: string };
unit_price: number;
quantity: number;
}
interface ConsultingSlot {
id: string;
date: string;
start_time: string;
end_time: string;
status: string;
meet_link?: string;
}
export default function AdminOrders() {
const { user, isAdmin, loading: authLoading } = useAuth();
const navigate = useNavigate();
@@ -39,6 +48,7 @@ export default function AdminOrders() {
const [loading, setLoading] = useState(true);
const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);
const [orderItems, setOrderItems] = useState<OrderItem[]>([]);
const [consultingSlots, setConsultingSlots] = useState<ConsultingSlot[]>([]);
const [dialogOpen, setDialogOpen] = useState(false);
useEffect(() => {
@@ -60,8 +70,22 @@ export default function AdminOrders() {
const viewOrderDetails = async (order: Order) => {
setSelectedOrder(order);
const { data } = await supabase.from("order_items").select("*, product:products(title)").eq("order_id", order.id);
setOrderItems((data as unknown as OrderItem[]) || []);
const { data: itemsData } = await supabase.from("order_items").select("*, product:products(title, type)").eq("order_id", order.id);
setOrderItems((itemsData as unknown as OrderItem[]) || []);
// Check if any item is a consulting product and fetch slots
const hasConsulting = (itemsData as unknown as OrderItem[])?.some(item => item.product?.type === "consulting");
if (hasConsulting) {
const { data: slotsData } = await supabase
.from("consulting_slots")
.select("*")
.eq("order_id", order.id)
.order("date", { ascending: true });
setConsultingSlots((slotsData as ConsultingSlot[]) || []);
} else {
setConsultingSlots([]);
}
setDialogOpen(true);
};
@@ -199,6 +223,56 @@ export default function AdminOrders() {
<span className="font-bold">{formatIDR(selectedOrder.total_amount)}</span>
</div>
</div>
{/* Consulting Slots */}
{consultingSlots.length > 0 && (
<div className="border-t border-border pt-4">
<p className="font-medium mb-3 flex items-center gap-2">
<Video className="w-4 h-4" />
Jadwal Konsultasi
</p>
<div className="space-y-2">
{consultingSlots.map((slot) => (
<div key={slot.id} 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 items-center gap-2 mb-1">
<Badge variant={slot.status === "confirmed" ? "default" : "secondary"} className="text-xs">
{slot.status === "confirmed" ? "Terkonfirmasi" : slot.status}
</Badge>
</div>
<p className="text-sm font-medium">
{new Date(slot.date).toLocaleDateString("id-ID", {
weekday: "short",
day: "numeric",
month: "short",
year: "numeric"
})}
</p>
<p className="text-xs text-muted-foreground">
{slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)} WIB
</p>
</div>
{slot.meet_link && (
<Button asChild variant="outline" size="sm" className="gap-1">
<a
href={slot.meet_link}
target="_blank"
rel="noopener noreferrer"
>
<Video className="w-3 h-3" />
Meet
<ExternalLink className="w-3 h-3" />
</a>
</Button>
)}
</div>
</div>
))}
</div>
</div>
)}
<div className="flex gap-2 pt-4">
{selectedOrder.payment_status !== "paid" && (
<Button onClick={() => updateOrderStatus(selectedOrder.id, "paid")} className="flex-1">