439 lines
17 KiB
TypeScript
439 lines
17 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useParams, useNavigate, Link } from 'react-router-dom';
|
|
import {
|
|
CheckCircle,
|
|
AlertCircle,
|
|
CreditCard,
|
|
ArrowLeft,
|
|
} from 'lucide-react';
|
|
import { toast } from 'react-toastify';
|
|
import { getBookingById, type Booking } from
|
|
'../../services/api/bookingService';
|
|
import {
|
|
getPaymentsByBookingId,
|
|
type Payment,
|
|
} from '../../services/api/paymentService';
|
|
import Loading from '../../components/common/Loading';
|
|
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
|
import { parseDateLocal } from '../../utils/format';
|
|
import StripePaymentWrapper from '../../components/payments/StripePaymentWrapper';
|
|
|
|
const FullPaymentPage: React.FC = () => {
|
|
const { bookingId } = useParams<{ bookingId: string }>();
|
|
const navigate = useNavigate();
|
|
const { formatCurrency } = useFormatCurrency();
|
|
|
|
const [booking, setBooking] = useState<Booking | null>(null);
|
|
const [stripePayment, setStripePayment] = useState<Payment | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [paymentSuccess, setPaymentSuccess] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (bookingId) {
|
|
fetchData(Number(bookingId));
|
|
}
|
|
}, [bookingId]);
|
|
|
|
const fetchData = async (id: number) => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
|
|
const bookingResponse = await getBookingById(id);
|
|
if (!bookingResponse.success || !bookingResponse.data?.booking) {
|
|
throw new Error('Booking not found');
|
|
}
|
|
|
|
const bookingData = bookingResponse.data.booking;
|
|
setBooking(bookingData);
|
|
|
|
|
|
if (bookingData.status === 'confirmed' || bookingData.status === 'checked_in') {
|
|
toast.success('Booking is already confirmed!');
|
|
navigate(`/bookings/${id}`);
|
|
return;
|
|
}
|
|
|
|
|
|
if (bookingData.payment_method !== 'stripe') {
|
|
toast.info('This booking does not use Stripe payment');
|
|
navigate(`/bookings/${id}`);
|
|
return;
|
|
}
|
|
|
|
|
|
const paymentsResponse = await getPaymentsByBookingId(id);
|
|
console.log('Payments response:', paymentsResponse);
|
|
|
|
if (paymentsResponse.success && paymentsResponse.data?.payments) {
|
|
const payments = paymentsResponse.data.payments;
|
|
console.log('Payments found:', payments);
|
|
|
|
|
|
const stripePaymentFound = payments.find(
|
|
(p: Payment) =>
|
|
(p.payment_method === 'stripe' || p.payment_method === 'credit_card') &&
|
|
p.payment_status === 'pending'
|
|
);
|
|
|
|
if (stripePaymentFound) {
|
|
console.log('Found pending Stripe payment:', stripePaymentFound);
|
|
setStripePayment(stripePaymentFound);
|
|
} else {
|
|
|
|
const completedPayment = payments.find(
|
|
(p: Payment) =>
|
|
(p.payment_method === 'stripe' || p.payment_method === 'credit_card') &&
|
|
p.payment_status === 'completed'
|
|
);
|
|
|
|
if (completedPayment) {
|
|
console.log('Found completed Stripe payment:', completedPayment);
|
|
setStripePayment(completedPayment);
|
|
setPaymentSuccess(true);
|
|
|
|
if ((bookingData.status as string) === 'confirmed' || (bookingData.status as string) === 'checked_in') {
|
|
toast.info('Payment already completed. Booking is confirmed.');
|
|
setTimeout(() => {
|
|
navigate(`/bookings/${id}`);
|
|
}, 1500);
|
|
return;
|
|
}
|
|
} else {
|
|
|
|
console.warn('No Stripe payment found in payments array:', payments);
|
|
console.warn('Booking payment method:', bookingData.payment_method);
|
|
|
|
|
|
throw new Error('No Stripe payment record found for this booking. The payment may not have been created properly.');
|
|
}
|
|
}
|
|
} else {
|
|
|
|
console.warn('Payments response not successful or no payments data:', paymentsResponse);
|
|
|
|
if (bookingData.payments && bookingData.payments.length > 0) {
|
|
console.log('Using payments from booking data:', bookingData.payments);
|
|
const stripePaymentFromBooking = bookingData.payments.find(
|
|
(p: any) =>
|
|
(p.payment_method === 'stripe' || p.payment_method === 'credit_card') &&
|
|
p.payment_status === 'pending'
|
|
);
|
|
|
|
if (stripePaymentFromBooking) {
|
|
setStripePayment(stripePaymentFromBooking as unknown as Payment);
|
|
} else {
|
|
throw new Error('No pending Stripe payment found for this booking');
|
|
}
|
|
} else {
|
|
|
|
console.error('No payments found for booking. This might be a timing issue.');
|
|
throw new Error('Payment information not found. Please wait a moment and refresh, or contact support if the issue persists.');
|
|
}
|
|
}
|
|
} catch (err: any) {
|
|
console.error('Error fetching data:', err);
|
|
const message =
|
|
err.response?.data?.message || err.message || 'Unable to load payment information';
|
|
setError(message);
|
|
toast.error(message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const formatPrice = (price: number) => formatCurrency(price);
|
|
|
|
if (loading) {
|
|
return <Loading fullScreen text="Loading..." />;
|
|
}
|
|
|
|
if (error || !booking || !stripePayment) {
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] py-8">
|
|
<div className="max-w-4xl mx-auto px-4">
|
|
<div
|
|
className="bg-gradient-to-br from-red-900/20 to-red-800/10
|
|
border border-red-500/30 rounded-xl p-8 text-center
|
|
backdrop-blur-xl shadow-2xl shadow-red-500/10"
|
|
>
|
|
<AlertCircle className="w-12 h-12 text-red-400 mx-auto mb-4" />
|
|
<p className="text-red-300 font-medium mb-6 tracking-wide">
|
|
{error || 'Payment information not found'}
|
|
</p>
|
|
<Link
|
|
to="/bookings"
|
|
className="inline-flex items-center gap-2
|
|
bg-gradient-to-r from-[#d4af37] to-[#c9a227]
|
|
text-[#0f0f0f] px-6 py-3 rounded-sm
|
|
hover:from-[#f5d76e] hover:to-[#d4af37]
|
|
transition-all duration-300 font-medium
|
|
tracking-wide shadow-lg shadow-[#d4af37]/30"
|
|
>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
Back to booking list
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
|
|
let paymentAmount = parseFloat(stripePayment.amount.toString());
|
|
const isPaymentCompleted = stripePayment.payment_status === 'completed';
|
|
|
|
|
|
console.log('Payment amount from payment record:', paymentAmount);
|
|
console.log('Booking total price:', booking?.total_price);
|
|
|
|
|
|
if (paymentAmount > 999999.99 || (booking && Math.abs(paymentAmount - booking.total_price) > 0.01)) {
|
|
console.warn('Payment amount seems incorrect, using booking total price instead');
|
|
if (booking) {
|
|
paymentAmount = parseFloat(booking.total_price.toString());
|
|
console.log('Using booking total price:', paymentAmount);
|
|
}
|
|
}
|
|
|
|
|
|
if (paymentAmount > 999999.99) {
|
|
const errorMsg = `Payment amount $${paymentAmount.toLocaleString()} exceeds Stripe's maximum. Please contact support.`;
|
|
console.error(errorMsg);
|
|
setError(errorMsg);
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] py-8">
|
|
<div className="max-w-4xl mx-auto px-4">
|
|
{}
|
|
<Link
|
|
to={`/bookings/${bookingId}`}
|
|
className="inline-flex items-center gap-2
|
|
text-[#d4af37]/80 hover:text-[#d4af37]
|
|
mb-6 transition-all duration-300
|
|
group font-light tracking-wide"
|
|
>
|
|
<ArrowLeft className="w-5 h-5 group-hover:-translate-x-1 transition-transform" />
|
|
<span>Back to booking details</span>
|
|
</Link>
|
|
|
|
{}
|
|
{isPaymentCompleted && (
|
|
<div
|
|
className="bg-gradient-to-br from-green-900/20 to-green-800/10
|
|
border-2 border-green-500/30 rounded-xl p-6 mb-6
|
|
backdrop-blur-xl shadow-2xl shadow-green-500/10"
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<div
|
|
className="w-16 h-16 bg-green-500/20 rounded-full
|
|
flex items-center justify-center border border-green-500/30"
|
|
>
|
|
<CheckCircle className="w-10 h-10 text-green-400" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<h1 className="text-2xl font-serif font-bold text-green-300 mb-1 tracking-wide">
|
|
Payment successful!
|
|
</h1>
|
|
<p className="text-green-400/80 font-light tracking-wide">
|
|
Your booking has been confirmed.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{}
|
|
{!isPaymentCompleted && (
|
|
<div
|
|
className="bg-gradient-to-br from-[#d4af37]/10 to-[#c9a227]/5
|
|
border-2 border-[#d4af37]/30 rounded-xl p-6 mb-6
|
|
backdrop-blur-xl shadow-2xl shadow-[#d4af37]/10"
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<div
|
|
className="w-16 h-16 bg-[#d4af37]/20 rounded-full
|
|
flex items-center justify-center border border-[#d4af37]/30"
|
|
>
|
|
<CreditCard className="w-10 h-10 text-[#d4af37]" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<h1 className="text-2xl font-serif font-bold text-[#d4af37] mb-1 tracking-wide">
|
|
Complete Payment
|
|
</h1>
|
|
<p className="text-gray-300 font-light tracking-wide">
|
|
Please complete your payment to confirm your booking
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{}
|
|
<div className="lg:col-span-2 space-y-6">
|
|
{}
|
|
<div
|
|
className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
|
rounded-xl border border-[#d4af37]/20
|
|
backdrop-blur-xl shadow-2xl shadow-[#d4af37]/5 p-6"
|
|
>
|
|
<h2 className="text-xl font-serif font-semibold text-[#d4af37] mb-4 flex items-center gap-2">
|
|
<div className="w-1 h-6 bg-gradient-to-b from-[#d4af37] to-[#c9a227]" />
|
|
Payment Information
|
|
</h2>
|
|
|
|
<div className="space-y-3">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-400 font-light">Total Room Price</span>
|
|
<span className="font-medium text-white">
|
|
{formatPrice(booking.total_price)}
|
|
</span>
|
|
</div>
|
|
|
|
<div
|
|
className="flex justify-between border-t border-[#d4af37]/20 pt-3
|
|
text-[#d4af37]"
|
|
>
|
|
<span className="font-medium">
|
|
Amount to Pay
|
|
</span>
|
|
<span className="text-xl font-bold">
|
|
{formatPrice(paymentAmount)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{isPaymentCompleted && (
|
|
<div className="mt-4 bg-green-500/10 border border-green-500/30 rounded-lg p-3">
|
|
<p className="text-sm text-green-400 font-light">
|
|
✓ Payment completed on:{' '}
|
|
{stripePayment.payment_date
|
|
? new Date(stripePayment.payment_date).toLocaleString('en-US')
|
|
: 'N/A'}
|
|
</p>
|
|
{stripePayment.transaction_id && (
|
|
<p className="text-xs text-green-400/70 mt-1 font-light">
|
|
Transaction ID: {stripePayment.transaction_id}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{}
|
|
{!isPaymentCompleted && booking && stripePayment && (
|
|
<div
|
|
className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
|
rounded-xl border border-[#d4af37]/20
|
|
backdrop-blur-xl shadow-2xl shadow-[#d4af37]/5 p-6"
|
|
>
|
|
<h2 className="text-xl font-serif font-semibold text-[#d4af37] mb-4 flex items-center gap-2">
|
|
<div className="w-1 h-6 bg-gradient-to-b from-[#d4af37] to-[#c9a227]" />
|
|
<CreditCard className="w-5 h-5" />
|
|
Card Payment
|
|
</h2>
|
|
|
|
{paymentSuccess ? (
|
|
<div className="bg-green-500/10 border border-green-500/30 rounded-lg p-6 text-center">
|
|
<CheckCircle className="w-12 h-12 text-green-400 mx-auto mb-3" />
|
|
<h3 className="text-lg font-serif font-bold text-green-300 mb-2 tracking-wide">
|
|
Payment Successful!
|
|
</h3>
|
|
<p className="text-green-400/80 font-light mb-4 tracking-wide">
|
|
Your payment has been confirmed.
|
|
</p>
|
|
<button
|
|
onClick={() => navigate(`/bookings/${booking.id}`)}
|
|
className="bg-gradient-to-r from-[#d4af37] to-[#c9a227]
|
|
text-[#0f0f0f] px-6 py-3 rounded-sm
|
|
hover:from-[#f5d76e] hover:to-[#d4af37]
|
|
transition-all duration-300 font-medium
|
|
tracking-wide shadow-lg shadow-[#d4af37]/30"
|
|
>
|
|
View Booking
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<StripePaymentWrapper
|
|
bookingId={booking.id}
|
|
amount={paymentAmount}
|
|
onSuccess={() => {
|
|
setPaymentSuccess(true);
|
|
toast.success('✅ Payment successful! Your booking has been confirmed.');
|
|
|
|
setTimeout(() => {
|
|
navigate(`/bookings/${booking.id}`);
|
|
}, 2000);
|
|
}}
|
|
onError={(error) => {
|
|
toast.error(error || 'Payment failed');
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{}
|
|
<div className="lg:col-span-1">
|
|
<div
|
|
className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
|
rounded-xl border border-[#d4af37]/20
|
|
backdrop-blur-xl shadow-2xl shadow-[#d4af37]/5 p-6
|
|
sticky top-6"
|
|
>
|
|
<h3 className="text-lg font-serif font-semibold text-[#d4af37] mb-4 flex items-center gap-2">
|
|
<div className="w-1 h-5 bg-gradient-to-b from-[#d4af37] to-[#c9a227]" />
|
|
Booking Summary
|
|
</h3>
|
|
|
|
<div className="space-y-3 text-sm">
|
|
<div>
|
|
<span className="text-gray-400 font-light">Booking Number</span>
|
|
<p className="text-white font-medium">{booking.booking_number}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<span className="text-gray-400 font-light">Room</span>
|
|
<p className="text-white font-medium">
|
|
{booking.room?.room_number || 'N/A'}
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<span className="text-gray-400 font-light">Check-in</span>
|
|
<p className="text-white font-medium">
|
|
{parseDateLocal(booking.check_in_date).toLocaleDateString('en-US')}
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<span className="text-gray-400 font-light">Check-out</span>
|
|
<p className="text-white font-medium">
|
|
{parseDateLocal(booking.check_out_date).toLocaleDateString('en-US')}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="pt-3 border-t border-[#d4af37]/20">
|
|
<span className="text-gray-400 font-light">Total Amount</span>
|
|
<p className="text-xl font-bold text-[#d4af37]">
|
|
{formatPrice(booking.total_price)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default FullPaymentPage;
|
|
|