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:
2026-07-01 00:05:39 -05:00
parent 72b507e434
commit ae6ea0ea46
4 changed files with 224 additions and 63 deletions
+19 -6
View File
@@ -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 });
}
const { room_id, message } = 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()) { 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)`);
+22 -10
View File
@@ -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
View File
@@ -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
View File
@@ -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)`);