diff --git a/Backend/src/routes/__pycache__/booking_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/booking_routes.cpython-312.pyc index dcb49264..ef2fbfa7 100644 Binary files a/Backend/src/routes/__pycache__/booking_routes.cpython-312.pyc and b/Backend/src/routes/__pycache__/booking_routes.cpython-312.pyc differ diff --git a/Backend/src/schemas/__pycache__/auth.cpython-312.pyc b/Backend/src/schemas/__pycache__/auth.cpython-312.pyc index 56c5532d..a6dd2072 100644 Binary files a/Backend/src/schemas/__pycache__/auth.cpython-312.pyc and b/Backend/src/schemas/__pycache__/auth.cpython-312.pyc differ diff --git a/Backend/src/schemas/auth.py b/Backend/src/schemas/auth.py index a5b7e9da..661095be 100644 --- a/Backend/src/schemas/auth.py +++ b/Backend/src/schemas/auth.py @@ -21,8 +21,10 @@ class RegisterRequest(BaseModel): @validator('phone') def validate_phone(cls, v): - if v and (not v.isdigit()) or (v and len(v) not in [10, 11]): - raise ValueError('Phone must be 10-11 digits') + if v: + cleaned = ''.join(c for c in v if c.isdigit()) + if len(cleaned) < 5: + raise ValueError('Phone number must contain at least 5 digits') return v class LoginRequest(BaseModel): diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index 9086bfd0..b566429b 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -42,10 +42,8 @@ const RoomDetailPage = lazy(() => import('./pages/customer/RoomDetailPage')); const SearchResultsPage = lazy(() => import('./pages/customer/SearchResultsPage')); const FavoritesPage = lazy(() => import('./pages/customer/FavoritesPage')); const MyBookingsPage = lazy(() => import('./pages/customer/MyBookingsPage')); -const BookingPage = lazy(() => import('./pages/customer/BookingPage')); const BookingSuccessPage = lazy(() => import('./pages/customer/BookingSuccessPage') as Promise<{ default: React.ComponentType }>); const BookingDetailPage = lazy(() => import('./pages/customer/BookingDetailPage')); -const DepositPaymentPage = lazy(() => import('./pages/customer/DepositPaymentPage')); const FullPaymentPage = lazy(() => import('./pages/customer/FullPaymentPage')); const PaymentConfirmationPage = lazy(() => import('./pages/customer/PaymentConfirmationPage')); const PaymentResultPage = lazy(() => import('./pages/customer/PaymentResultPage')); @@ -234,14 +232,6 @@ function App() { } /> - - - - } - /> } /> - - - - } - /> void; + onSuccess: () => void; +} + +const CancelBookingModal: React.FC = ({ + 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: any) => p.payment_status === 'completed') + .reduce((sum: number, p: any) => 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 any).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: any) { + console.error('Error cancelling booking:', err); + const message = + err.response?.data?.detail || + err.response?.data?.message || + err.message || + 'Unable to cancel booking. Please try again.'; + toast.error(message); + } finally { + setCancelling(false); + } + }; + + return ( +
+
+
+ {/* Header */} +
+
+
+
+ +
+
+

+ Cancel Booking +

+

+ Booking #{booking.booking_number} +

+
+
+ +
+
+ + {/* Content */} +
+ {/* Warning Message */} +
+
+ +
+

+ Are you sure you want to cancel this booking? +

+

+ This action cannot be undone. The room will be made available for other guests. +

+
+
+
+ + {/* Booking Details */} +
+

Booking Details

+
+
+ Room + + {booking.room?.room_number || 'N/A'} + +
+
+ Check-in + + {new Date(booking.check_in_date).toLocaleDateString('en-US')} + +
+
+ Check-out + + {new Date(booking.check_out_date).toLocaleDateString('en-US')} + +
+
+ Total Amount + + {formatCurrency(booking.total_price)} + +
+
+
+ + {/* Cancellation Policy */} +
+
+ +
+

+ Cancellation Policy +

+
+
+ Cancellation Fee (20%) + + -{formatCurrency(cancellationFee)} + +
+ {isFullyPaid && ( + <> +
+ Refund Amount (80%) + + {formatCurrency(refundAmount)} + +
+

+ The refund will be processed to your original payment method within 5-7 business days. +

+ + )} + {!isFullyPaid && ( +

+ Since this booking is not fully paid, no refund will be issued. The cancellation fee applies to the total booking amount. +

+ )} +
+
+
+
+ + {/* Action Buttons */} +
+ + +
+
+
+
+ ); +}; + +export default CancelBookingModal; + diff --git a/Frontend/src/components/booking/InvoiceInfoModal.tsx b/Frontend/src/components/booking/InvoiceInfoModal.tsx new file mode 100644 index 00000000..4b6fb49a --- /dev/null +++ b/Frontend/src/components/booking/InvoiceInfoModal.tsx @@ -0,0 +1,145 @@ +import React, { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { X, Building2, Save } from 'lucide-react'; + +interface InvoiceInfoModalProps { + isOpen: boolean; + onClose: () => void; + onSave: (invoiceInfo: any) => void; +} + +interface InvoiceFormData { + company_name: string; + company_address: string; + company_tax_id: string; + customer_tax_id: string; +} + +const InvoiceInfoModal: React.FC = ({ + isOpen, + onClose, + onSave, +}) => { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: { + company_name: '', + company_address: '', + company_tax_id: '', + customer_tax_id: '', + }, + }); + + const onSubmit = (data: InvoiceFormData) => { + onSave(data); + }; + + if (!isOpen) return null; + + return ( +
+
+
+ {/* Header */} +
+
+
+ +

+ Invoice Information +

+
+ +
+
+ + {/* Content */} +
+
+
+ + +
+ +
+ +