Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions src/app/(protected)/dashboard/org/my-event/[my-eventId]/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ import {
Minus,
Plus,
Megaphone,
Tag
Tag,
Landmark
} from "lucide-react";
import toast from "react-hot-toast";
import { getImageUrl, getErrorMessage } from "../../../../../../lib/utils";
Expand All @@ -46,6 +47,7 @@ import useTempBookingStore from "@/store/tempBookingStore";
function BookForAttendeeModal({ isOpen, onClose, event, eventId, onSuccess, onManualPaymentRequired }) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [paymentMethod, setPaymentMethod] = useState('paystack');
const [formData, setFormData] = useState({
firstname: '',
lastname: '',
Expand Down Expand Up @@ -112,14 +114,14 @@ function BookForAttendeeModal({ isOpen, onClose, event, eventId, onSuccess, onMa
]
};
if (isPaidEvent) {
payload.payment_method = 'paystack';
payload.payment_method = paymentMethod;
}

const response = await api.post('/tickets/organizer/book-for-attendee/', payload);
const result = response.data;

// Paid event: store booking with base subtotal so checkout page adds platform + Paystack fees
if (isPaidEvent && result.booking_id != null && result.payment_url != null) {
if (isPaidEvent && result.booking_id != null) {
const price = selectedCategory?.price ?? 0;
const subtotal = price * formData.quantity;
const items = [{
Expand All @@ -139,6 +141,7 @@ function BookForAttendeeModal({ isOpen, onClose, event, eventId, onSuccess, onMa
payment_url: result.payment_url || null,
payment_reference: result.payment_reference || null,
tickets: result.tickets || [],
payment_method: paymentMethod,
created_at: new Date().toISOString(),
organizer_booking: {
returnUrl: window.location.pathname,
Expand All @@ -150,7 +153,10 @@ function BookForAttendeeModal({ isOpen, onClose, event, eventId, onSuccess, onMa
onSuccess?.();
onClose();
toast.success('Proceeding to checkout...');
router.push(`/checkout/payment/${result.booking_id}`);
const checkoutUrl = paymentMethod === 'manual_bank_transfer'
? `/checkout/payment/${result.booking_id}?method=bank_transfer`
: `/checkout/payment/${result.booking_id}`;
router.push(checkoutUrl);
return;
}

Expand Down Expand Up @@ -287,6 +293,40 @@ function BookForAttendeeModal({ isOpen, onClose, event, eventId, onSuccess, onMa
</div>
</div>

{/* Payment Method Selection */}
{isPaidEvent && (
<div className="bg-white/2 border border-white/5 rounded-xl sm:rounded-2xl p-3 sm:p-4 space-y-3 sm:space-y-4">
<div className="flex items-center gap-2 text-[10px] sm:text-xs font-bold text-gray-500 uppercase tracking-widest">
<CreditCard className="w-3 h-3 sm:w-3.5 sm:h-3.5" />
Payment Method
</div>
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => setPaymentMethod('paystack')}
className={`py-2 px-3 rounded-xl border text-[10px] font-bold uppercase tracking-wider transition-all flex items-center justify-center gap-2 ${
paymentMethod === 'paystack'
? 'bg-rose-600 border-rose-600 text-white'
: 'bg-white/5 border-white/10 text-gray-400 hover:border-white/20'
}`}
>
<CreditCard className="w-3 h-3" /> Paystack
</button>
<button
type="button"
onClick={() => setPaymentMethod('manual_bank_transfer')}
className={`py-2 px-3 rounded-xl border text-[10px] font-bold uppercase tracking-wider transition-all flex items-center justify-center gap-2 ${
paymentMethod === 'manual_bank_transfer'
? 'bg-rose-600 border-rose-600 text-white'
: 'bg-white/5 border-white/10 text-gray-400 hover:border-white/20'
}`}
>
<Landmark className="w-3 h-3" /> Transfer
</button>
</div>
</div>
)}

{/* Ticket Selection Card */}
<div className="bg-white/2 border border-white/5 rounded-xl sm:rounded-2xl p-3 sm:p-4 space-y-3 sm:space-y-4">
<div className="flex items-center gap-2 text-[10px] sm:text-xs font-bold text-gray-500 uppercase tracking-widest">
Expand Down
11 changes: 8 additions & 3 deletions src/app/(protected)/dashboard/user/events/[event_id]/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -377,13 +377,18 @@ const EventDetailsPage = () => {

if (validReferral) clearReferral();

toast.success("Booking created! Redirecting to checkout...", { id: toastId });
router.push(`/checkout/payment/${bookingId}`);
if (checkoutPaymentMethod === "manual_bank_transfer") {
toast.success("Booking created! Please complete the bank transfer.", { id: toastId });
router.push(`/checkout/payment/${bookingId}?method=bank_transfer`);
} else {
toast.success("Booking created! Redirecting to checkout...", { id: toastId });
router.push(`/checkout/payment/${bookingId}`);
}
return;
}

// Fallback: If there's a direct payment URL, redirect to it
if (response.data.payment_url) {
if (response.data.payment_url && checkoutPaymentMethod !== "manual_bank_transfer") {
toast.success("Redirecting to payment...", { id: toastId });
window.location.href = response.data.payment_url;
return;
Expand Down
8 changes: 8 additions & 0 deletions src/app/checkout/payment/[booking_id]/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ export default function CheckoutPaymentPage() {
const [error, setError] = useState(null);
const [activeTab, setActiveTab] = useState("paystack");

// Sync active tab with URL parameters for direct redirection (e.g. from organizer dashboard)
useEffect(() => {
const method = searchParams.get('method');
if (method === 'bank_transfer') {
setActiveTab('manual_bank_transfer');
}
}, [searchParams]);

useEffect(() => {
const fetchBookingData = async () => {
if (!booking_id) {
Expand Down
39 changes: 18 additions & 21 deletions src/app/events/[event_id]/EventDetailsClient.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Loader2, MapPin, Calendar, Clock, Ticket, Info, Share2, Copy, Check, X, Maximize2, Plus, Minus, ShoppingCart } from "lucide-react";
import { Loader2, MapPin, Calendar, Clock, Ticket, Info, Share2, Copy, Check, X, Maximize2, Plus, Minus, ShoppingCart, CreditCard, Landmark, Landmark as Bank } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import toast from "react-hot-toast";
import useAuthStore from "@/store/authStore";
Expand All @@ -35,6 +35,7 @@ const EventDetailsClient = ({ event_id, initialEvent }) => {
const [event, setEvent] = useState(initialEvent || null);
const [loading, setLoading] = useState(!initialEvent);
const [bookingLoading, setBookingLoading] = useState(false);
const [paymentMethod, setPaymentMethod] = useState("paystack"); // 'paystack' or 'manual_bank_transfer'

//Getting booking id for tracking tickets
const [bookingID,setBookingID] = useState(null)
Expand Down Expand Up @@ -183,13 +184,15 @@ const EventDetailsClient = ({ event_id, initialEvent }) => {
}
});

// Paystack calculates fee on total amount INCLUDING platform service fee
const paystackFee = subtotal > 0 ? calculatePaystackFee(subtotal + PLATFORM_FEE) : 0;
const platformFee = subtotal > 0 ? PLATFORM_FEE + paystackFee : 0;
// Base platform service fee
const baseServiceFee = subtotal > 0 ? PLATFORM_FEE : 0;
// Paystack processing fee only applies if Paystack is the selected method
const paystackFee = (subtotal > 0 && paymentMethod === "paystack") ? calculatePaystackFee(subtotal + PLATFORM_FEE) : 0;
const platformFee = baseServiceFee + paystackFee;
const total = subtotal + platformFee;

return { selectedItems, subtotal, platformFee, total, totalQuantity };
}, [ticketSelections, categories]);
return { selectedItems, subtotal, platformFee, total, totalQuantity, paystackFee };
}, [ticketSelections, categories, paymentMethod]);

// Handle quantity change for a category
const handleQuantityChange = (categoryId, delta) => {
Expand Down Expand Up @@ -276,6 +279,7 @@ const EventDetailsClient = ({ event_id, initialEvent }) => {
const payload = {
event_id: eventIdToUse,
items: items,
payment_method: paymentMethod,
// Scoped referral source for event:TO-56363
...(eventIdToUse === "event:TO-56363" && {
referral: refUsername, // Pass referee username as 'referral' to backend
Expand Down Expand Up @@ -314,22 +318,15 @@ const EventDetailsClient = ({ event_id, initialEvent }) => {
};
localStorage.setItem(`booking_${bookingId}`, JSON.stringify(bookingDataForCheckout));

toast.success("Booking created! Redirecting to payment...", { id: toastId });
router.push(`/checkout/payment/${bookingId}`);
return;
}

// Fallback for cases where booking_id isn't directly returned but payment_url is
if (response.data.payment_url) {
toast.success("Redirecting to payment...", { id: toastId });
window.location.href = response.data.payment_url;
if (paymentMethod === "manual_bank_transfer") {
toast.success("Booking created! Please complete the bank transfer.", { id: toastId });
router.push(`/checkout/payment/${bookingId}?method=bank_transfer`);
} else {
toast.success("Booking created! Redirecting to payment...", { id: toastId });
router.push(`/checkout/payment/${bookingId}`);
}
return;
}

toast.success("Ticket booked successfully!", { id: toastId });
if (typeof window !== "undefined") window.dispatchEvent(new CustomEvent("tickets-updated"));
router.push("/dashboard/user/my-tickets");

} catch (error) {
console.error("Booking error:", error);
let errorMessage = error.response?.data?.error || "Failed to book ticket";
Expand Down Expand Up @@ -410,7 +407,7 @@ const EventDetailsClient = ({ event_id, initialEvent }) => {
</span>
</div>
</div>

{/* Referral Badge */}
<AnimatePresence>
{refUsername && (
Expand Down
13 changes: 9 additions & 4 deletions src/components/organizer/BulkBookForAttendeeModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -410,12 +410,17 @@ export default function BulkBookForAttendeeModal({
items,
total_quantity: result.ticket_count ?? ticketCount,
subtotal,
payment_method: "manual_bank_transfer",
total_manual_amount: result.total_amount || (subtotal + 80),
payment_url: result.payment_url || null, // Only for online payments
payment_reference: result.payment_reference || null,
payment_method: paymentMethod, // Use the selected payment method
total_manual_amount: result.total_amount || (subtotal + 80), // Only for manual transfer, assuming 80 is platform fee
tickets: result.tickets || [],
created_at: new Date().toISOString(),
organizer_booking: { returnUrl: window.location.pathname },
organizer_booking: {
returnUrl: window.location.pathname,
attendeeEmail: attendees[0]?.email,
attendeeName: `${attendees[0]?.firstname} ${attendees[0]?.lastname}`,
},
};
localStorage.setItem(
`booking_${result.booking_id}`,
Expand Down Expand Up @@ -463,7 +468,7 @@ export default function BulkBookForAttendeeModal({
return;
}

// Free event
// Handle Free Event (Booking is immediate and doesn't require payment routing)
toast.success(
`Successfully booked ${result.ticket_count} ticket(s) for ${result.unique_attendees ?? result.attendees?.length ?? ticketCount} attendee(s)`,
);
Expand Down