updates
This commit is contained in:
437
Frontend/src/pages/customer/FullPaymentPage.tsx
Normal file
437
Frontend/src/pages/customer/FullPaymentPage.tsx
Normal file
@@ -0,0 +1,437 @@
|
||||
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 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);
|
||||
|
||||
// Fetch booking details
|
||||
const bookingResponse = await getBookingById(id);
|
||||
if (!bookingResponse.success || !bookingResponse.data?.booking) {
|
||||
throw new Error('Booking not found');
|
||||
}
|
||||
|
||||
const bookingData = bookingResponse.data.booking;
|
||||
setBooking(bookingData);
|
||||
|
||||
// Check if booking is already confirmed - redirect to booking details
|
||||
if (bookingData.status === 'confirmed' || bookingData.status === 'checked_in') {
|
||||
toast.success('Booking is already confirmed!');
|
||||
navigate(`/bookings/${id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if booking uses Stripe payment method
|
||||
if (bookingData.payment_method !== 'stripe') {
|
||||
toast.info('This booking does not use Stripe payment');
|
||||
navigate(`/bookings/${id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch payments
|
||||
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);
|
||||
|
||||
// Find pending Stripe payment (full payment)
|
||||
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 {
|
||||
// Check if payment is already completed
|
||||
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 payment is completed and booking is confirmed, redirect
|
||||
if (bookingData.status === 'confirmed' || bookingData.status === 'checked_in') {
|
||||
toast.info('Payment already completed. Booking is confirmed.');
|
||||
setTimeout(() => {
|
||||
navigate(`/bookings/${id}`);
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// If no Stripe payment found, check if we can use booking data to create payment info
|
||||
console.warn('No Stripe payment found in payments array:', payments);
|
||||
console.warn('Booking payment method:', bookingData.payment_method);
|
||||
|
||||
// If booking uses Stripe but no payment record exists, this is an error
|
||||
throw new Error('No Stripe payment record found for this booking. The payment may not have been created properly.');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If payments endpoint fails or returns no payments, check booking payments array
|
||||
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 Payment);
|
||||
} else {
|
||||
throw new Error('No pending Stripe payment found for this booking');
|
||||
}
|
||||
} else {
|
||||
// If no payments found at all, this might be a timing issue - wait a moment and retry
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
// Get payment amount, but validate it's reasonable
|
||||
let paymentAmount = parseFloat(stripePayment.amount.toString());
|
||||
const isPaymentCompleted = stripePayment.payment_status === 'completed';
|
||||
|
||||
// Log payment amount for debugging
|
||||
console.log('Payment amount from payment record:', paymentAmount);
|
||||
console.log('Booking total price:', booking?.total_price);
|
||||
|
||||
// If payment amount seems incorrect (too large or doesn't match booking), use booking total
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Final validation - ensure amount is reasonable for Stripe
|
||||
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">
|
||||
{/* Back Button */}
|
||||
<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>
|
||||
|
||||
{/* Success Header (if paid) */}
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* Pending Header */}
|
||||
{!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">
|
||||
{/* Payment Info */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Payment Summary */}
|
||||
<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>
|
||||
|
||||
{/* Stripe Payment Panel */}
|
||||
{!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.');
|
||||
// Navigate to booking details after successful payment
|
||||
setTimeout(() => {
|
||||
navigate(`/bookings/${booking.id}`);
|
||||
}, 2000);
|
||||
}}
|
||||
onError={(error) => {
|
||||
toast.error(error || 'Payment failed');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Booking Summary Sidebar */}
|
||||
<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">
|
||||
{new Date(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">
|
||||
{new Date(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;
|
||||
|
||||
Reference in New Issue
Block a user