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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user