feat: guest reservations and messages
- Remove Reserve button from rooms page - Calendar modal now supports direct reservation for guests - Message modal works for guests (asks for name, contact method, contact) - Add guest_name, guest_contact_method, guest_contact to reservations and messages tables - LEFT JOIN users in reservations GET to include guest reservations - Auto-fill form for logged-in users, show input fields for guests
This commit is contained in:
+162
-44
@@ -33,6 +33,7 @@ interface User {
|
||||
role: 'admin' | 'user';
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
email?: string;
|
||||
comments_disabled: boolean;
|
||||
}
|
||||
|
||||
@@ -68,6 +69,7 @@ export default function RoomsPage() {
|
||||
const [messageText, setMessageText] = useState('');
|
||||
const [reviewText, setReviewText] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [guestForm, setGuestForm] = useState({ name: '', contactMethod: 'email', contact: '' });
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/rooms')
|
||||
@@ -186,10 +188,17 @@ export default function RoomsPage() {
|
||||
};
|
||||
|
||||
const handleReserve = async () => {
|
||||
if (!selectedRoom || !reservationDates.checkIn || !reservationDates.checkOut || !user) {
|
||||
alert('Please log in and select dates');
|
||||
if (!selectedRoom || !reservationDates.checkIn || !reservationDates.checkOut) {
|
||||
alert('Please select dates');
|
||||
return;
|
||||
}
|
||||
|
||||
// For guests, require name and contact
|
||||
if (!user && (!guestForm.name.trim() || !guestForm.contact.trim())) {
|
||||
alert('Please provide your name and contact info');
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const res = await fetch('/api/reservations', {
|
||||
@@ -200,11 +209,18 @@ export default function RoomsPage() {
|
||||
room_id: selectedRoom.id,
|
||||
check_in: reservationDates.checkIn,
|
||||
check_out: reservationDates.checkOut,
|
||||
...(user ? {} : {
|
||||
guest_name: guestForm.name.trim(),
|
||||
guest_contact_method: guestForm.contactMethod,
|
||||
guest_contact: guestForm.contact.trim(),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
alert('Reservation request sent! You will receive a confirmation email shortly.');
|
||||
alert('Reservation request sent! You will receive a confirmation shortly.');
|
||||
closeModal();
|
||||
setReservationDates({ checkIn: '', checkOut: '' });
|
||||
setGuestForm({ name: '', contactMethod: 'email', contact: '' });
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert(err.error || 'Failed to create reservation');
|
||||
@@ -216,10 +232,17 @@ export default function RoomsPage() {
|
||||
};
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
if (!selectedRoom || !messageText.trim() || !user) {
|
||||
alert('Please log in and enter a message');
|
||||
if (!selectedRoom || !messageText.trim()) {
|
||||
alert('Please enter a message');
|
||||
return;
|
||||
}
|
||||
|
||||
// For guests, require name and contact
|
||||
if (!user && (!guestForm.name.trim() || !guestForm.contact.trim())) {
|
||||
alert('Please provide your name and contact info');
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const res = await fetch('/api/messages', {
|
||||
@@ -229,11 +252,18 @@ export default function RoomsPage() {
|
||||
body: JSON.stringify({
|
||||
room_id: selectedRoom.id,
|
||||
message: messageText.trim(),
|
||||
...(user ? {} : {
|
||||
guest_name: guestForm.name.trim(),
|
||||
guest_contact_method: guestForm.contactMethod,
|
||||
guest_contact: guestForm.contact.trim(),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
alert('Message sent to our team!');
|
||||
closeModal();
|
||||
setMessageText('');
|
||||
setGuestForm({ name: '', contactMethod: 'email', contact: '' });
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert(err.error || 'Failed to send message');
|
||||
@@ -484,23 +514,7 @@ export default function RoomsPage() {
|
||||
</div>
|
||||
|
||||
{/* ─── Action Buttons ─── */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '0.5rem', marginTop: '1rem' }}>
|
||||
<button
|
||||
onClick={() => openModal('reserve', room)}
|
||||
style={{
|
||||
padding: '0.6rem 0.5rem',
|
||||
borderRadius: '10px',
|
||||
border: 'none',
|
||||
background: gold,
|
||||
color: navy,
|
||||
fontWeight: 600,
|
||||
fontSize: '12px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
>
|
||||
Reserve
|
||||
</button>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '0.5rem', marginTop: '1rem' }}>
|
||||
<button
|
||||
onClick={() => openModal('message', room)}
|
||||
style={{
|
||||
@@ -531,7 +545,7 @@ export default function RoomsPage() {
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
>
|
||||
Calendar
|
||||
Reserve
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openModal('review', room)}
|
||||
@@ -725,34 +739,138 @@ export default function RoomsPage() {
|
||||
<>
|
||||
<h3 style={{ margin: '0 0 1rem', color: navy, fontSize: '24px' }}>Send a Message</h3>
|
||||
<p style={{ color: '#555', marginBottom: '1rem' }}>Send a message about {selectedRoom.name} to our team.</p>
|
||||
{!user ? (
|
||||
<p style={{ color: '#777' }}>Please <a href="/login" style={{ color: gold }}>log in</a> to send a message.</p>
|
||||
) : (
|
||||
<>
|
||||
<textarea
|
||||
value={messageText}
|
||||
onChange={(e) => setMessageText(e.target.value)}
|
||||
placeholder="Your message..."
|
||||
rows={5}
|
||||
style={{ width: '100%', padding: '0.75rem', borderRadius: '10px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '14px', resize: 'vertical' }}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '1rem', marginTop: '1.5rem' }}>
|
||||
<button onClick={closeModal} style={{ flex: 1, padding: '0.75rem', borderRadius: '10px', border: `2px solid ${navy}`, background: 'transparent', color: navy, fontWeight: 600, cursor: 'pointer' }}>Cancel</button>
|
||||
<button onClick={handleSendMessage} disabled={submitting || !messageText.trim()} style={{ flex: 1, padding: '0.75rem', borderRadius: '10px', border: 'none', background: gold, color: navy, fontWeight: 600, cursor: 'pointer', opacity: submitting ? 0.6 : 1 }}>{submitting ? 'Sending...' : 'Send Message'}</button>
|
||||
{!user && (
|
||||
<div style={{ marginBottom: '1rem', padding: '1rem', background: '#f8f8f8', borderRadius: '10px' }}>
|
||||
<div style={{ marginBottom: '0.75rem' }}>
|
||||
<label style={{ display: 'block', marginBottom: '0.25rem', fontWeight: 600, color: navy, fontSize: '13px' }}>Your Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={guestForm.name}
|
||||
onChange={(e) => setGuestForm({ ...guestForm, name: e.target.value })}
|
||||
placeholder="Enter your name"
|
||||
style={{ width: '100%', padding: '0.5rem', borderRadius: '8px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '14px' }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<div style={{ marginBottom: '0.75rem' }}>
|
||||
<label style={{ display: 'block', marginBottom: '0.25rem', fontWeight: 600, color: navy, fontSize: '13px' }}>Preferred Contact Method</label>
|
||||
<select
|
||||
value={guestForm.contactMethod}
|
||||
onChange={(e) => setGuestForm({ ...guestForm, contactMethod: e.target.value })}
|
||||
style={{ width: '100%', padding: '0.5rem', borderRadius: '8px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '14px' }}
|
||||
>
|
||||
<option value="email">Email</option>
|
||||
<option value="whatsapp">WhatsApp</option>
|
||||
<option value="phone">Phone</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '0.25rem', fontWeight: 600, color: navy, fontSize: '13px' }}>
|
||||
{guestForm.contactMethod === 'email' ? 'Email Address' : guestForm.contactMethod === 'whatsapp' ? 'WhatsApp Number' : 'Phone Number'} *
|
||||
</label>
|
||||
<input
|
||||
type={guestForm.contactMethod === 'email' ? 'email' : 'tel'}
|
||||
value={guestForm.contact}
|
||||
onChange={(e) => setGuestForm({ ...guestForm, contact: e.target.value })}
|
||||
placeholder={guestForm.contactMethod === 'email' ? 'your@email.com' : '+593 99 999 9999'}
|
||||
style={{ width: '100%', padding: '0.5rem', borderRadius: '8px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '14px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{user && (
|
||||
<div style={{ marginBottom: '1rem', padding: '0.75rem', background: '#f8f8f8', borderRadius: '10px', fontSize: '13px', color: '#555' }}>
|
||||
<strong>From:</strong> {user.first_name} {user.last_name} ({user.email})
|
||||
</div>
|
||||
)}
|
||||
<textarea
|
||||
value={messageText}
|
||||
onChange={(e) => setMessageText(e.target.value)}
|
||||
placeholder="Your message..."
|
||||
rows={5}
|
||||
style={{ width: '100%', padding: '0.75rem', borderRadius: '10px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '14px', resize: 'vertical' }}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '1rem', marginTop: '1.5rem' }}>
|
||||
<button onClick={closeModal} style={{ flex: 1, padding: '0.75rem', borderRadius: '10px', border: `2px solid ${navy}`, background: 'transparent', color: navy, fontWeight: 600, cursor: 'pointer' }}>Cancel</button>
|
||||
<button onClick={handleSendMessage} disabled={submitting || !messageText.trim() || (!user && (!guestForm.name.trim() || !guestForm.contact.trim()))} style={{ flex: 1, padding: '0.75rem', borderRadius: '10px', border: 'none', background: gold, color: navy, fontWeight: 600, cursor: 'pointer', opacity: submitting ? 0.6 : 1 }}>{submitting ? 'Sending...' : 'Send Message'}</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Calendar Modal */}
|
||||
{/* Reserve/Calendar Modal */}
|
||||
{activeModal === 'calendar' && (
|
||||
<>
|
||||
<h3 style={{ margin: '0 0 1rem', color: navy, fontSize: '24px' }}>Availability Calendar</h3>
|
||||
<p style={{ color: '#555', marginBottom: '1rem' }}>Check availability for {selectedRoom.name}.</p>
|
||||
<RoomCalendar roomId={selectedRoom.id} />
|
||||
<div style={{ marginTop: '1.5rem' }}>
|
||||
<button onClick={closeModal} style={{ width: '100%', padding: '0.75rem', borderRadius: '10px', border: `2px solid ${navy}`, background: 'transparent', color: navy, fontWeight: 600, cursor: 'pointer' }}>Close</button>
|
||||
<h3 style={{ margin: '0 0 1rem', color: navy, fontSize: '24px' }}>Reserve {selectedRoom.name}</h3>
|
||||
<p style={{ color: '#555', marginBottom: '1rem' }}>Select your dates and we'll contact you to confirm.</p>
|
||||
{!user && (
|
||||
<div style={{ marginBottom: '1rem', padding: '1rem', background: '#f8f8f8', borderRadius: '10px' }}>
|
||||
<div style={{ marginBottom: '0.75rem' }}>
|
||||
<label style={{ display: 'block', marginBottom: '0.25rem', fontWeight: 600, color: navy, fontSize: '13px' }}>Your Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={guestForm.name}
|
||||
onChange={(e) => setGuestForm({ ...guestForm, name: e.target.value })}
|
||||
placeholder="Enter your name"
|
||||
style={{ width: '100%', padding: '0.5rem', borderRadius: '8px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '14px' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: '0.75rem' }}>
|
||||
<label style={{ display: 'block', marginBottom: '0.25rem', fontWeight: 600, color: navy, fontSize: '13px' }}>Preferred Contact Method</label>
|
||||
<select
|
||||
value={guestForm.contactMethod}
|
||||
onChange={(e) => setGuestForm({ ...guestForm, contactMethod: e.target.value })}
|
||||
style={{ width: '100%', padding: '0.5rem', borderRadius: '8px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '14px' }}
|
||||
>
|
||||
<option value="email">Email</option>
|
||||
<option value="whatsapp">WhatsApp</option>
|
||||
<option value="phone">Phone</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '0.25rem', fontWeight: 600, color: navy, fontSize: '13px' }}>
|
||||
{guestForm.contactMethod === 'email' ? 'Email Address' : guestForm.contactMethod === 'whatsapp' ? 'WhatsApp Number' : 'Phone Number'} *
|
||||
</label>
|
||||
<input
|
||||
type={guestForm.contactMethod === 'email' ? 'email' : 'tel'}
|
||||
value={guestForm.contact}
|
||||
onChange={(e) => setGuestForm({ ...guestForm, contact: e.target.value })}
|
||||
placeholder={guestForm.contactMethod === 'email' ? 'your@email.com' : '+593 99 999 9999'}
|
||||
style={{ width: '100%', padding: '0.5rem', borderRadius: '8px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '14px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{user && (
|
||||
<div style={{ marginBottom: '1rem', padding: '0.75rem', background: '#f8f8f8', borderRadius: '10px', fontSize: '13px', color: '#555' }}>
|
||||
<strong>Reserving as:</strong> {user.first_name} {user.last_name} ({user.email})
|
||||
</div>
|
||||
)}
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 600, color: navy }}>Check-in Date *</label>
|
||||
<input
|
||||
type="date"
|
||||
value={reservationDates.checkIn}
|
||||
onChange={(e) => setReservationDates({ ...reservationDates, checkIn: e.target.value })}
|
||||
min={new Date().toISOString().split('T')[0]}
|
||||
style={{ width: '100%', padding: '0.75rem', borderRadius: '10px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '16px' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 600, color: navy }}>Check-out Date *</label>
|
||||
<input
|
||||
type="date"
|
||||
value={reservationDates.checkOut}
|
||||
onChange={(e) => setReservationDates({ ...reservationDates, checkOut: e.target.value })}
|
||||
min={reservationDates.checkIn || new Date().toISOString().split('T')[0]}
|
||||
style={{ width: '100%', padding: '0.75rem', borderRadius: '10px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '16px' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 600, color: navy }}>Availability Calendar</label>
|
||||
<RoomCalendar roomId={selectedRoom.id} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '1rem', marginTop: '1.5rem' }}>
|
||||
<button onClick={closeModal} style={{ flex: 1, padding: '0.75rem', borderRadius: '10px', border: `2px solid ${navy}`, background: 'transparent', color: navy, fontWeight: 600, cursor: 'pointer' }}>Cancel</button>
|
||||
<button onClick={handleReserve} disabled={submitting || !reservationDates.checkIn || !reservationDates.checkOut || (!user && (!guestForm.name.trim() || !guestForm.contact.trim()))} style={{ flex: 1, padding: '0.75rem', borderRadius: '10px', border: 'none', background: gold, color: navy, fontWeight: 600, cursor: 'pointer', opacity: submitting ? 0.6 : 1 }}>{submitting ? 'Processing...' : 'Request Reservation'}</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user