Files
Hotel-Booking/Frontend/src/pages/customer/FullPaymentPage.tsx
Iliyan Angelov 6f85b8cf17 updates
2025-11-21 01:20:51 +02:00

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;