235 lines
10 KiB
TypeScript
235 lines
10 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { X, AlertTriangle, XCircle, Loader2, Info } from 'lucide-react';
|
|
import { cancelBooking, type Booking } from '../services/bookingService';
|
|
import { toast } from 'react-toastify';
|
|
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
|
|
|
|
interface CancelBookingModalProps {
|
|
isOpen: boolean;
|
|
booking: Booking | null;
|
|
onClose: () => void;
|
|
onSuccess: () => void;
|
|
}
|
|
|
|
const CancelBookingModal: React.FC<CancelBookingModalProps> = ({
|
|
isOpen,
|
|
booking,
|
|
onClose,
|
|
onSuccess,
|
|
}) => {
|
|
const { formatCurrency } = useFormatCurrency();
|
|
const [cancelling, setCancelling] = useState(false);
|
|
|
|
if (!isOpen || !booking) return null;
|
|
|
|
// Check if booking is fully paid
|
|
const isFullyPaid = (() => {
|
|
// Check payment_status first
|
|
if (booking.payment_status === 'paid') {
|
|
return true;
|
|
}
|
|
|
|
// Check payment_balance
|
|
if (booking.payment_balance?.is_fully_paid === true) {
|
|
return true;
|
|
}
|
|
|
|
// Check payments array - sum all completed payments
|
|
if (booking.payments && Array.isArray(booking.payments)) {
|
|
const totalPaid = booking.payments
|
|
.filter((p: { payment_status?: string }) => p.payment_status === 'completed')
|
|
.reduce((sum: number, p: { amount?: number | string }) => sum + parseFloat(p.amount?.toString() || '0'), 0);
|
|
|
|
return totalPaid >= booking.total_price - 0.01; // Allow small rounding differences
|
|
}
|
|
|
|
return false;
|
|
})();
|
|
|
|
const cancellationFee = booking.total_price * 0.2;
|
|
const refundAmount = isFullyPaid ? booking.total_price - cancellationFee : 0;
|
|
|
|
const handleCancel = async () => {
|
|
try {
|
|
setCancelling(true);
|
|
const response = await cancelBooking(booking.id);
|
|
|
|
if (response.success || (response as { status?: string }).status === 'success') {
|
|
toast.error(
|
|
`Booking ${booking.booking_number} has been cancelled`
|
|
);
|
|
onSuccess();
|
|
onClose();
|
|
} else {
|
|
throw new Error(response.message || 'Unable to cancel booking');
|
|
}
|
|
} catch (err: unknown) {
|
|
console.error('Error cancelling booking:', err);
|
|
const errorResponse = (err && typeof err === 'object' && 'response' in err && err.response && typeof err.response === 'object' && 'data' in err.response && err.response.data && typeof err.response.data === 'object') ? err.response.data as { detail?: string; message?: string } : null;
|
|
const errorMessage = err instanceof Error ? err.message : undefined;
|
|
const message =
|
|
errorResponse?.detail ||
|
|
errorResponse?.message ||
|
|
errorMessage ||
|
|
'Unable to cancel booking. Please try again.';
|
|
toast.error(message);
|
|
} finally {
|
|
setCancelling(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[10001] flex items-center justify-center p-2 sm:p-4">
|
|
<div
|
|
className="fixed inset-0 bg-black/90 backdrop-blur-sm"
|
|
onClick={onClose}
|
|
/>
|
|
<div className="relative w-full max-w-md max-h-[95vh] bg-gradient-to-br from-[#0a0a0a] via-[#1a1a1a] to-[#0a0a0a] rounded-xl sm:rounded-2xl border border-red-500/30 shadow-2xl shadow-red-500/20 overflow-hidden flex flex-col">
|
|
{/* Header */}
|
|
<div className="px-3 sm:px-4 md:px-6 py-3 sm:py-4 border-b border-red-500/20 bg-gradient-to-r from-[#1a1a1a] to-[#0a0a0a] flex-shrink-0">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className="flex items-center gap-2 sm:gap-3 min-w-0 flex-1">
|
|
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-red-500/20 rounded-full flex items-center justify-center border border-red-500/30 flex-shrink-0">
|
|
<AlertTriangle className="w-4 h-4 sm:w-5 sm:h-5 md:w-6 md:h-6 text-red-400" />
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<h2 className="text-base sm:text-lg md:text-xl font-serif font-bold text-white tracking-tight truncate">
|
|
Cancel Booking
|
|
</h2>
|
|
<p className="text-xs text-gray-400 mt-0.5 truncate">
|
|
Booking #{booking.booking_number}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
disabled={cancelling}
|
|
className="p-1.5 sm:p-2 hover:bg-red-500/10 rounded-lg transition-colors text-gray-400 hover:text-white disabled:opacity-50 flex-shrink-0"
|
|
>
|
|
<X className="w-4 h-4 sm:w-5 sm:h-5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 overflow-y-auto px-3 sm:px-4 md:px-6 py-4 sm:py-6 space-y-4 sm:space-y-6">
|
|
{/* Warning Message */}
|
|
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-3 sm:p-4">
|
|
<div className="flex items-start gap-2 sm:gap-3">
|
|
<AlertTriangle className="w-4 h-4 sm:w-5 sm:h-5 text-red-400 mt-0.5 flex-shrink-0" />
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-xs sm:text-sm font-semibold text-red-300 mb-1">
|
|
Are you sure you want to cancel this booking?
|
|
</p>
|
|
<p className="text-xs text-red-200/80 leading-relaxed">
|
|
This action cannot be undone. The room will be made available for other guests.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Booking Details */}
|
|
<div className="bg-[#0a0a0a] border border-gray-700/50 rounded-lg p-3 sm:p-4 space-y-2 sm:space-y-3">
|
|
<h3 className="text-xs sm:text-sm font-semibold text-gray-300 mb-2 sm:mb-3">Booking Details</h3>
|
|
<div className="space-y-1.5 sm:space-y-2 text-xs sm:text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-400">Room</span>
|
|
<span className="text-white font-medium">
|
|
{booking.room?.room_number || 'N/A'}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-400">Check-in</span>
|
|
<span className="text-white font-medium">
|
|
{new Date(booking.check_in_date).toLocaleDateString('en-US')}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-400">Check-out</span>
|
|
<span className="text-white font-medium">
|
|
{new Date(booking.check_out_date).toLocaleDateString('en-US')}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between pt-2 border-t border-gray-700/50">
|
|
<span className="text-gray-400">Total Amount</span>
|
|
<span className="text-white font-semibold">
|
|
{formatCurrency(booking.total_price)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Cancellation Policy */}
|
|
<div className="bg-amber-500/10 border border-amber-500/30 rounded-lg p-3 sm:p-4">
|
|
<div className="flex items-start gap-2 sm:gap-3">
|
|
<Info className="w-4 h-4 sm:w-5 sm:h-5 text-amber-400 mt-0.5 flex-shrink-0" />
|
|
<div className="flex-1 min-w-0">
|
|
<h4 className="text-xs sm:text-sm font-semibold text-amber-300 mb-1.5 sm:mb-2">
|
|
Cancellation Policy
|
|
</h4>
|
|
<div className="space-y-1.5 sm:space-y-2 text-xs text-amber-200/80">
|
|
<div className="flex justify-between">
|
|
<span>Cancellation Fee (20%)</span>
|
|
<span className="font-semibold text-amber-300">
|
|
-{formatCurrency(cancellationFee)}
|
|
</span>
|
|
</div>
|
|
{isFullyPaid && (
|
|
<>
|
|
<div className="flex justify-between pt-2 border-t border-amber-500/20">
|
|
<span>Refund Amount (80%)</span>
|
|
<span className="font-semibold text-green-400">
|
|
{formatCurrency(refundAmount)}
|
|
</span>
|
|
</div>
|
|
<p className="text-amber-200/70 mt-3 pt-3 border-t border-amber-500/20">
|
|
The refund will be processed to your original payment method within 5-7 business days.
|
|
</p>
|
|
</>
|
|
)}
|
|
{!isFullyPaid && (
|
|
<p className="text-amber-200/70 mt-3 pt-3 border-t border-amber-500/20">
|
|
Since this booking is not fully paid, no refund will be issued. The cancellation fee applies to the total booking amount.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex flex-col gap-2 sm:gap-3 pt-2">
|
|
<button
|
|
onClick={handleCancel}
|
|
disabled={cancelling}
|
|
className="w-full bg-red-500/20 hover:bg-red-500/30 border-2 border-red-500/50 text-red-300 font-semibold py-2.5 sm:py-3 px-4 sm:px-6 rounded-lg transition-all flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed text-sm sm:text-base min-h-[44px]"
|
|
>
|
|
{cancelling ? (
|
|
<>
|
|
<Loader2 className="w-4 h-4 sm:w-5 sm:h-5 animate-spin" />
|
|
<span>Cancelling...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<XCircle className="w-4 h-4 sm:w-5 sm:h-5" />
|
|
<span>Yes, Cancel Booking</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={onClose}
|
|
disabled={cancelling}
|
|
className="w-full bg-transparent border border-gray-600 text-gray-300 font-medium py-2.5 sm:py-3 px-4 sm:px-6 rounded-lg hover:bg-gray-800/50 transition-all disabled:opacity-50 disabled:cursor-not-allowed text-sm sm:text-base min-h-[44px]"
|
|
>
|
|
Keep Booking
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CancelBookingModal;
|
|
|