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:
@@ -5,21 +5,34 @@ import { getSession } from '@/lib/auth';
|
|||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const user = await getSession();
|
const user = await getSession();
|
||||||
if (!user) {
|
const body = await request.json();
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
||||||
}
|
// Support both logged-in users and guests
|
||||||
|
const { room_id, message, guest_name, guest_contact_method, guest_contact } = body;
|
||||||
const { room_id, message } = await request.json();
|
|
||||||
|
|
||||||
if (!message || !message.trim()) {
|
if (!message || !message.trim()) {
|
||||||
return NextResponse.json({ error: 'Message is required' }, { status: 400 });
|
return NextResponse.json({ error: 'Message is required' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { rows } = await db.query(`
|
// For guests, require name and contact info
|
||||||
|
if (!user && (!guest_name || !guest_contact)) {
|
||||||
|
return NextResponse.json({ error: 'Name and contact info are required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalMessage = message.trim();
|
||||||
|
let finalUserId = user?.id || null;
|
||||||
|
|
||||||
|
// For guests, prepend their contact info to the message
|
||||||
|
if (!user && guest_name && guest_contact) {
|
||||||
|
const methodLabel = guest_contact_method ? ` (${guest_contact_method})` : '';
|
||||||
|
finalMessage = `[Guest: ${guest_name}${methodLabel} - ${guest_contact}]\n\n${message.trim()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await db.query(`
|
||||||
INSERT INTO messages (room_id, user_id, message)
|
INSERT INTO messages (room_id, user_id, message)
|
||||||
VALUES ($1, $2, $3)
|
VALUES ($1, $2, $3)
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`, [room_id || null, user.id, message.trim()]);
|
`, [room_id || null, finalUserId, finalMessage]);
|
||||||
|
|
||||||
// Get admin emails for notification
|
// Get admin emails for notification
|
||||||
const { rows: admins } = await db.query(`SELECT email, phone FROM users WHERE role = 'admin' AND (email IS NOT NULL OR phone IS NOT NULL)`);
|
const { rows: admins } = await db.query(`SELECT email, phone FROM users WHERE role = 'admin' AND (email IS NOT NULL OR phone IS NOT NULL)`);
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import { getSession } from '@/lib/auth';
|
|||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const user = await getSession();
|
const user = await getSession();
|
||||||
if (!user) {
|
const body = await request.json();
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
const { room_id, check_in, check_out, guest_name, guest_contact_method, guest_contact } = body;
|
||||||
}
|
|
||||||
|
|
||||||
const { room_id, check_in, check_out } = await request.json();
|
// Require either logged-in user or guest info
|
||||||
|
if (!user && (!guest_name || !guest_contact)) {
|
||||||
|
return NextResponse.json({ error: 'Please provide your name and contact information' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
if (!room_id || !check_in || !check_out) {
|
if (!room_id || !check_in || !check_out) {
|
||||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||||
@@ -57,12 +59,21 @@ export async function POST(request: Request) {
|
|||||||
const pricePerNight = parseFloat(room.price) || 0;
|
const pricePerNight = parseFloat(room.price) || 0;
|
||||||
const total_price = nights * pricePerNight;
|
const total_price = nights * pricePerNight;
|
||||||
|
|
||||||
// Create reservation
|
// Create reservation - for guests, user_id is NULL
|
||||||
const { rows } = await db.query(`
|
const { rows } = await db.query(`
|
||||||
INSERT INTO reservations (room_id, user_id, check_in, check_out, status, total_price)
|
INSERT INTO reservations (room_id, user_id, check_in, check_out, status, total_price, guest_name, guest_contact_method, guest_contact)
|
||||||
VALUES ($1, $2, $3, $4, 'pending', $5)
|
VALUES ($1, $2, $3, $4, 'pending', $5, $6, $7, $8)
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`, [room_id, user.id, check_in, check_out, total_price]);
|
`, [
|
||||||
|
room_id,
|
||||||
|
user?.id || null,
|
||||||
|
check_in,
|
||||||
|
check_out,
|
||||||
|
total_price,
|
||||||
|
guest_name || null,
|
||||||
|
guest_contact_method || null,
|
||||||
|
guest_contact || null,
|
||||||
|
]);
|
||||||
|
|
||||||
// Block the dates
|
// Block the dates
|
||||||
const insertPromises = [];
|
const insertPromises = [];
|
||||||
@@ -107,10 +118,11 @@ export async function GET(request: Request) {
|
|||||||
const status = searchParams.get('status') || 'all';
|
const status = searchParams.get('status') || 'all';
|
||||||
|
|
||||||
let query = `
|
let query = `
|
||||||
SELECT r.*, rm.name as room_name, u.username, u.first_name, u.last_name, u.email, u.phone
|
SELECT r.*, rm.name as room_name,
|
||||||
|
u.username, u.first_name, u.last_name, u.email, u.phone
|
||||||
FROM reservations r
|
FROM reservations r
|
||||||
JOIN rooms rm ON r.room_id = rm.id
|
JOIN rooms rm ON r.room_id = rm.id
|
||||||
JOIN users u ON r.user_id = u.id
|
LEFT JOIN users u ON r.user_id = u.id
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const params: any[] = [];
|
const params: any[] = [];
|
||||||
|
|||||||
+162
-44
@@ -33,6 +33,7 @@ interface User {
|
|||||||
role: 'admin' | 'user';
|
role: 'admin' | 'user';
|
||||||
first_name: string | null;
|
first_name: string | null;
|
||||||
last_name: string | null;
|
last_name: string | null;
|
||||||
|
email?: string;
|
||||||
comments_disabled: boolean;
|
comments_disabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +69,7 @@ export default function RoomsPage() {
|
|||||||
const [messageText, setMessageText] = useState('');
|
const [messageText, setMessageText] = useState('');
|
||||||
const [reviewText, setReviewText] = useState('');
|
const [reviewText, setReviewText] = useState('');
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [guestForm, setGuestForm] = useState({ name: '', contactMethod: 'email', contact: '' });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/rooms')
|
fetch('/api/rooms')
|
||||||
@@ -186,10 +188,17 @@ export default function RoomsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleReserve = async () => {
|
const handleReserve = async () => {
|
||||||
if (!selectedRoom || !reservationDates.checkIn || !reservationDates.checkOut || !user) {
|
if (!selectedRoom || !reservationDates.checkIn || !reservationDates.checkOut) {
|
||||||
alert('Please log in and select dates');
|
alert('Please select dates');
|
||||||
return;
|
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);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/reservations', {
|
const res = await fetch('/api/reservations', {
|
||||||
@@ -200,11 +209,18 @@ export default function RoomsPage() {
|
|||||||
room_id: selectedRoom.id,
|
room_id: selectedRoom.id,
|
||||||
check_in: reservationDates.checkIn,
|
check_in: reservationDates.checkIn,
|
||||||
check_out: reservationDates.checkOut,
|
check_out: reservationDates.checkOut,
|
||||||
|
...(user ? {} : {
|
||||||
|
guest_name: guestForm.name.trim(),
|
||||||
|
guest_contact_method: guestForm.contactMethod,
|
||||||
|
guest_contact: guestForm.contact.trim(),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
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();
|
closeModal();
|
||||||
|
setReservationDates({ checkIn: '', checkOut: '' });
|
||||||
|
setGuestForm({ name: '', contactMethod: 'email', contact: '' });
|
||||||
} else {
|
} else {
|
||||||
const err = await res.json();
|
const err = await res.json();
|
||||||
alert(err.error || 'Failed to create reservation');
|
alert(err.error || 'Failed to create reservation');
|
||||||
@@ -216,10 +232,17 @@ export default function RoomsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSendMessage = async () => {
|
const handleSendMessage = async () => {
|
||||||
if (!selectedRoom || !messageText.trim() || !user) {
|
if (!selectedRoom || !messageText.trim()) {
|
||||||
alert('Please log in and enter a message');
|
alert('Please enter a message');
|
||||||
return;
|
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);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/messages', {
|
const res = await fetch('/api/messages', {
|
||||||
@@ -229,11 +252,18 @@ export default function RoomsPage() {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
room_id: selectedRoom.id,
|
room_id: selectedRoom.id,
|
||||||
message: messageText.trim(),
|
message: messageText.trim(),
|
||||||
|
...(user ? {} : {
|
||||||
|
guest_name: guestForm.name.trim(),
|
||||||
|
guest_contact_method: guestForm.contactMethod,
|
||||||
|
guest_contact: guestForm.contact.trim(),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
alert('Message sent to our team!');
|
alert('Message sent to our team!');
|
||||||
closeModal();
|
closeModal();
|
||||||
|
setMessageText('');
|
||||||
|
setGuestForm({ name: '', contactMethod: 'email', contact: '' });
|
||||||
} else {
|
} else {
|
||||||
const err = await res.json();
|
const err = await res.json();
|
||||||
alert(err.error || 'Failed to send message');
|
alert(err.error || 'Failed to send message');
|
||||||
@@ -484,23 +514,7 @@ export default function RoomsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ─── Action Buttons ─── */}
|
{/* ─── Action Buttons ─── */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '0.5rem', marginTop: '1rem' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 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>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => openModal('message', room)}
|
onClick={() => openModal('message', room)}
|
||||||
style={{
|
style={{
|
||||||
@@ -531,7 +545,7 @@ export default function RoomsPage() {
|
|||||||
transition: 'all 0.2s',
|
transition: 'all 0.2s',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Calendar
|
Reserve
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => openModal('review', room)}
|
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>
|
<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>
|
<p style={{ color: '#555', marginBottom: '1rem' }}>Send a message about {selectedRoom.name} to our team.</p>
|
||||||
{!user ? (
|
{!user && (
|
||||||
<p style={{ color: '#777' }}>Please <a href="/login" style={{ color: gold }}>log in</a> to send a message.</p>
|
<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>
|
||||||
<textarea
|
<input
|
||||||
value={messageText}
|
type="text"
|
||||||
onChange={(e) => setMessageText(e.target.value)}
|
value={guestForm.name}
|
||||||
placeholder="Your message..."
|
onChange={(e) => setGuestForm({ ...guestForm, name: e.target.value })}
|
||||||
rows={5}
|
placeholder="Enter your name"
|
||||||
style={{ width: '100%', padding: '0.75rem', borderRadius: '10px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '14px', resize: 'vertical' }}
|
style={{ width: '100%', padding: '0.5rem', borderRadius: '8px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '14px' }}
|
||||||
/>
|
/>
|
||||||
<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>
|
|
||||||
</div>
|
</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' && (
|
{activeModal === 'calendar' && (
|
||||||
<>
|
<>
|
||||||
<h3 style={{ margin: '0 0 1rem', color: navy, fontSize: '24px' }}>Availability Calendar</h3>
|
<h3 style={{ margin: '0 0 1rem', color: navy, fontSize: '24px' }}>Reserve {selectedRoom.name}</h3>
|
||||||
<p style={{ color: '#555', marginBottom: '1rem' }}>Check availability for {selectedRoom.name}.</p>
|
<p style={{ color: '#555', marginBottom: '1rem' }}>Select your dates and we'll contact you to confirm.</p>
|
||||||
<RoomCalendar roomId={selectedRoom.id} />
|
{!user && (
|
||||||
<div style={{ marginTop: '1.5rem' }}>
|
<div style={{ marginBottom: '1rem', padding: '1rem', background: '#f8f8f8', borderRadius: '10px' }}>
|
||||||
<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>
|
<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>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+20
-2
@@ -319,11 +319,14 @@ export async function initSchema() {
|
|||||||
CREATE TABLE IF NOT EXISTS reservations (
|
CREATE TABLE IF NOT EXISTS reservations (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
room_id INTEGER NOT NULL REFERENCES rooms(id),
|
room_id INTEGER NOT NULL REFERENCES rooms(id),
|
||||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
user_id INTEGER REFERENCES users(id),
|
||||||
check_in DATE NOT NULL,
|
check_in DATE NOT NULL,
|
||||||
check_out DATE NOT NULL,
|
check_out DATE NOT NULL,
|
||||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||||
total_price DECIMAL(10,2),
|
total_price DECIMAL(10,2),
|
||||||
|
guest_name VARCHAR(100),
|
||||||
|
guest_contact_method VARCHAR(20),
|
||||||
|
guest_contact VARCHAR(255),
|
||||||
created_at TIMESTAMP DEFAULT NOW(),
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
updated_at TIMESTAMP DEFAULT NOW()
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
);
|
);
|
||||||
@@ -345,8 +348,11 @@ export async function initSchema() {
|
|||||||
CREATE TABLE IF NOT EXISTS messages (
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
room_id INTEGER REFERENCES rooms(id),
|
room_id INTEGER REFERENCES rooms(id),
|
||||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
user_id INTEGER REFERENCES users(id),
|
||||||
message TEXT NOT NULL,
|
message TEXT NOT NULL,
|
||||||
|
guest_name VARCHAR(100),
|
||||||
|
guest_contact_method VARCHAR(20),
|
||||||
|
guest_contact VARCHAR(255),
|
||||||
read_by_admins BOOLEAN DEFAULT FALSE,
|
read_by_admins BOOLEAN DEFAULT FALSE,
|
||||||
created_at TIMESTAMP DEFAULT NOW()
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
);
|
);
|
||||||
@@ -354,6 +360,18 @@ export async function initSchema() {
|
|||||||
|
|
||||||
await db.query(`ALTER TABLE room_comments ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'approved'`);
|
await db.query(`ALTER TABLE room_comments ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'approved'`);
|
||||||
|
|
||||||
|
// Add guest columns to reservations
|
||||||
|
await db.query(`ALTER TABLE reservations ADD COLUMN IF NOT EXISTS guest_name VARCHAR(100)`);
|
||||||
|
await db.query(`ALTER TABLE reservations ADD COLUMN IF NOT EXISTS guest_contact_method VARCHAR(20)`);
|
||||||
|
await db.query(`ALTER TABLE reservations ADD COLUMN IF NOT EXISTS guest_contact VARCHAR(255)`);
|
||||||
|
await db.query(`ALTER TABLE reservations ALTER COLUMN user_id DROP NOT NULL`);
|
||||||
|
|
||||||
|
// Add guest columns to messages
|
||||||
|
await db.query(`ALTER TABLE messages ADD COLUMN IF NOT EXISTS guest_name VARCHAR(100)`);
|
||||||
|
await db.query(`ALTER TABLE messages ADD COLUMN IF NOT EXISTS guest_contact_method VARCHAR(20)`);
|
||||||
|
await db.query(`ALTER TABLE messages ADD COLUMN IF NOT EXISTS guest_contact VARCHAR(255)`);
|
||||||
|
await db.query(`ALTER TABLE messages ALTER COLUMN user_id DROP NOT NULL`);
|
||||||
|
|
||||||
await db.query(`CREATE INDEX IF NOT EXISTS idx_reservations_room_dates ON reservations(room_id, check_in, check_out)`);
|
await db.query(`CREATE INDEX IF NOT EXISTS idx_reservations_room_dates ON reservations(room_id, check_in, check_out)`);
|
||||||
await db.query(`CREATE INDEX IF NOT EXISTS idx_room_availability_room_date ON room_availability(room_id, date)`);
|
await db.query(`CREATE INDEX IF NOT EXISTS idx_room_availability_room_date ON room_availability(room_id, date)`);
|
||||||
await db.query(`CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at DESC)`);
|
await db.query(`CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at DESC)`);
|
||||||
|
|||||||
Reference in New Issue
Block a user