From ae6ea0ea46445935d5597c084d3909f22e05b67d Mon Sep 17 00:00:00 2001 From: muken Date: Wed, 1 Jul 2026 00:05:39 -0500 Subject: [PATCH] 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 --- src/app/api/messages/route.ts | 27 +++- src/app/api/reservations/route.ts | 32 +++-- src/app/rooms/page.tsx | 206 +++++++++++++++++++++++------- src/lib/schema.ts | 22 +++- 4 files changed, 224 insertions(+), 63 deletions(-) diff --git a/src/app/api/messages/route.ts b/src/app/api/messages/route.ts index e9eb7cf..68fc079 100644 --- a/src/app/api/messages/route.ts +++ b/src/app/api/messages/route.ts @@ -5,21 +5,34 @@ import { getSession } from '@/lib/auth'; export async function POST(request: Request) { try { const user = await getSession(); - if (!user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - const { room_id, message } = await request.json(); + const body = await request.json(); + + // Support both logged-in users and guests + const { room_id, message, guest_name, guest_contact_method, guest_contact } = body; if (!message || !message.trim()) { 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) VALUES ($1, $2, $3) RETURNING * - `, [room_id || null, user.id, message.trim()]); + `, [room_id || null, finalUserId, finalMessage]); // 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)`); diff --git a/src/app/api/reservations/route.ts b/src/app/api/reservations/route.ts index 6767fe3..3e5295a 100644 --- a/src/app/api/reservations/route.ts +++ b/src/app/api/reservations/route.ts @@ -5,11 +5,13 @@ import { getSession } from '@/lib/auth'; export async function POST(request: Request) { try { const user = await getSession(); - if (!user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } + const body = await request.json(); + 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) { 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 total_price = nights * pricePerNight; - // Create reservation + // Create reservation - for guests, user_id is NULL const { rows } = await db.query(` - INSERT INTO reservations (room_id, user_id, check_in, check_out, status, total_price) - VALUES ($1, $2, $3, $4, 'pending', $5) + 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, $6, $7, $8) 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 const insertPromises = []; @@ -107,10 +118,11 @@ export async function GET(request: Request) { const status = searchParams.get('status') || 'all'; 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 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[] = []; diff --git a/src/app/rooms/page.tsx b/src/app/rooms/page.tsx index a678c47..70c8ed7 100644 --- a/src/app/rooms/page.tsx +++ b/src/app/rooms/page.tsx @@ -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() { {/* ─── Action Buttons ─── */} -
- +