feat: room booking system with 4 action buttons
- Display amenity icons on room cards - Reserve button: date picker, availability check, booking - Message button: contact admins via platform - Calendar button: view room availability - Comment button: submit reviews for admin approval - Admin comment queue for approve/reject/edit - Reservations, availability, and messages tables
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { requireAuth } from '@/lib/auth';
|
||||
|
||||
// GET - list pending comments for admin review
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await requireAuth();
|
||||
if (session.role !== 'admin') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const status = searchParams.get('status') || 'pending';
|
||||
|
||||
const { rows } = await db.query(`
|
||||
SELECT rc.*, r.name as room_name, u.username
|
||||
FROM room_comments rc
|
||||
LEFT JOIN rooms r ON rc.room_id = r.id
|
||||
LEFT JOIN users u ON rc.user_id = u.id
|
||||
WHERE rc.status = $1
|
||||
ORDER BY rc.created_at DESC
|
||||
`, [status]);
|
||||
|
||||
return NextResponse.json({ comments: rows });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: err.message || 'Failed to fetch comments' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH - approve, reject, or edit a comment
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
const session = await requireAuth();
|
||||
if (session.role !== 'admin') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { comment_id, action, text } = await request.json();
|
||||
|
||||
if (!comment_id || !action) {
|
||||
return NextResponse.json({ error: 'comment_id and action required' }, { status: 400 });
|
||||
}
|
||||
|
||||
let result;
|
||||
|
||||
if (action === 'approve') {
|
||||
result = await db.query(`
|
||||
UPDATE room_comments SET status = 'approved' WHERE id = $1 RETURNING *
|
||||
`, [comment_id]);
|
||||
} else if (action === 'reject') {
|
||||
result = await db.query(`
|
||||
UPDATE room_comments SET status = 'rejected' WHERE id = $1 RETURNING *
|
||||
`, [comment_id]);
|
||||
} else if (action === 'edit') {
|
||||
if (!text) {
|
||||
return NextResponse.json({ error: 'text required for edit action' }, { status: 400 });
|
||||
}
|
||||
result = await db.query(`
|
||||
UPDATE room_comments SET text = $1, status = 'approved' WHERE id = $2 RETURNING *
|
||||
`, [text, comment_id]);
|
||||
} else if (action === 'delete') {
|
||||
result = await db.query(`
|
||||
UPDATE room_comments SET deleted_by_admin = TRUE WHERE id = $1 RETURNING *
|
||||
`, [comment_id]);
|
||||
} else {
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, comment: result?.rows?.[0] });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: err.message || 'Failed to update comment' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
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();
|
||||
|
||||
if (!message || !message.trim()) {
|
||||
return NextResponse.json({ error: 'Message is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { rows } = await db.query(`
|
||||
INSERT INTO messages (room_id, user_id, message)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING *
|
||||
`, [room_id || null, user.id, message.trim()]);
|
||||
|
||||
// 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)`);
|
||||
|
||||
// TODO: Send email/SMS notifications to admins
|
||||
|
||||
return NextResponse.json({ success: true, message: 'Message sent successfully' });
|
||||
} catch (error) {
|
||||
console.error('Message error:', error);
|
||||
return NextResponse.json({ error: 'Failed to send message' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const user = await getSession();
|
||||
if (!user || user.role !== 'admin') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const unreadOnly = searchParams.get('unread') === 'true';
|
||||
|
||||
let query = `
|
||||
SELECT m.*, r.name as room_name, u.username, u.first_name, u.last_name, u.email, u.phone
|
||||
FROM messages m
|
||||
LEFT JOIN rooms r ON m.room_id = r.id
|
||||
JOIN users u ON m.user_id = u.id
|
||||
`;
|
||||
|
||||
if (unreadOnly) {
|
||||
query += ' WHERE m.read_by_admins = FALSE';
|
||||
}
|
||||
|
||||
query += ' ORDER BY m.created_at DESC LIMIT 100';
|
||||
|
||||
const { rows } = await db.query(query);
|
||||
return NextResponse.json({ messages: rows });
|
||||
} catch (error) {
|
||||
console.error('Get messages error:', error);
|
||||
return NextResponse.json({ error: 'Failed to get messages' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(request: Request) {
|
||||
try {
|
||||
const user = await getSession();
|
||||
if (!user || user.role !== 'admin') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { message_ids } = await request.json();
|
||||
|
||||
if (!message_ids || !Array.isArray(message_ids)) {
|
||||
return NextResponse.json({ error: 'message_ids array is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
await db.query(`
|
||||
UPDATE messages SET read_by_admins = TRUE WHERE id = ANY($1)
|
||||
`, [message_ids]);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Mark messages read error:', error);
|
||||
return NextResponse.json({ error: 'Failed to mark messages as read' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
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, check_in, check_out } = await request.json();
|
||||
|
||||
if (!room_id || !check_in || !check_out) {
|
||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if room exists
|
||||
const { rows: roomRows } = await db.query('SELECT * FROM rooms WHERE id = $1', [room_id]);
|
||||
if (roomRows.length === 0) {
|
||||
return NextResponse.json({ error: 'Room not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const room = roomRows[0];
|
||||
|
||||
// Check for conflicting reservations
|
||||
const { rows: conflicts } = await db.query(`
|
||||
SELECT * FROM reservations
|
||||
WHERE room_id = $1
|
||||
AND status != 'cancelled'
|
||||
AND (
|
||||
(check_in <= $2 AND check_out > $2) OR
|
||||
(check_in < $3 AND check_out >= $3) OR
|
||||
(check_in >= $2 AND check_out <= $3)
|
||||
)
|
||||
`, [room_id, check_in, check_out]);
|
||||
|
||||
if (conflicts.length > 0) {
|
||||
return NextResponse.json({ error: 'Room is not available for selected dates' }, { status: 409 });
|
||||
}
|
||||
|
||||
// Check for blocked dates
|
||||
const { rows: blocked } = await db.query(`
|
||||
SELECT * FROM room_availability
|
||||
WHERE room_id = $1
|
||||
AND date >= $2
|
||||
AND date < $3
|
||||
AND status != 'available'
|
||||
`, [room_id, check_in, check_out]);
|
||||
|
||||
if (blocked.length > 0) {
|
||||
return NextResponse.json({ error: 'Some dates are not available' }, { status: 409 });
|
||||
}
|
||||
|
||||
// Calculate total price
|
||||
const nights = Math.ceil((new Date(check_out).getTime() - new Date(check_in).getTime()) / (1000 * 60 * 60 * 24));
|
||||
const pricePerNight = parseFloat(room.price) || 0;
|
||||
const total_price = nights * pricePerNight;
|
||||
|
||||
// Create reservation
|
||||
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)
|
||||
RETURNING *
|
||||
`, [room_id, user.id, check_in, check_out, total_price]);
|
||||
|
||||
// Block the dates
|
||||
const insertPromises = [];
|
||||
const checkInDate = new Date(check_in);
|
||||
const checkOutDate = new Date(check_out);
|
||||
for (let d = new Date(checkInDate); d < checkOutDate; d.setDate(d.getDate() + 1)) {
|
||||
insertPromises.push(
|
||||
db.query(`
|
||||
INSERT INTO room_availability (room_id, date, status, reason)
|
||||
VALUES ($1, $2, 'booked', $3)
|
||||
ON CONFLICT (room_id, date) DO UPDATE SET status = 'booked', reason = $3
|
||||
`, [room_id, d.toISOString().split('T')[0], `Reservation #${rows[0].id}`])
|
||||
);
|
||||
}
|
||||
await Promise.all(insertPromises);
|
||||
|
||||
// Get admin emails for notification
|
||||
const { rows: admins } = await db.query(`SELECT email, phone FROM users WHERE role = 'admin' AND email IS NOT NULL`);
|
||||
|
||||
// TODO: Send email notifications to admins
|
||||
// TODO: Send welcome email to user with WiFi password and instructions
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
reservation: rows[0],
|
||||
message: 'Reservation request submitted. You will receive confirmation shortly.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Reservation error:', error);
|
||||
return NextResponse.json({ error: 'Failed to create reservation' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const user = await getSession();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
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
|
||||
FROM reservations r
|
||||
JOIN rooms rm ON r.room_id = rm.id
|
||||
JOIN users u ON r.user_id = u.id
|
||||
`;
|
||||
|
||||
const params: any[] = [];
|
||||
if (status !== 'all') {
|
||||
query += ' WHERE r.status = $1';
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
query += ' ORDER BY r.created_at DESC';
|
||||
|
||||
const { rows } = await db.query(query, params);
|
||||
return NextResponse.json({ reservations: rows });
|
||||
} catch (error) {
|
||||
console.error('Get reservations error:', error);
|
||||
return NextResponse.json({ error: 'Failed to get reservations' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const roomId = parseInt(params.id);
|
||||
const { searchParams } = new URL(request.url);
|
||||
const start = searchParams.get('start');
|
||||
const end = searchParams.get('end');
|
||||
|
||||
if (!start || !end) {
|
||||
return NextResponse.json({ error: 'start and end dates required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get availability status for the date range
|
||||
const { rows: availability } = await db.query(`
|
||||
SELECT date, status, reason
|
||||
FROM room_availability
|
||||
WHERE room_id = $1 AND date >= $2 AND date <= $3
|
||||
`, [roomId, start, end]);
|
||||
|
||||
// Get reservations in the date range
|
||||
const { rows: reservations } = await db.query(`
|
||||
SELECT check_in, check_out, status
|
||||
FROM reservations
|
||||
WHERE room_id = $1
|
||||
AND status != 'cancelled'
|
||||
AND check_in <= $3
|
||||
AND check_out >= $2
|
||||
`, [roomId, start, end]);
|
||||
|
||||
// Build day-by-day status
|
||||
const days: Array<{ date: string; status: 'available' | 'booked' | 'maintenance' }> = [];
|
||||
const startDate = new Date(start);
|
||||
const endDate = new Date(end);
|
||||
const availabilityMap = new Map(availability.map(a => [a.date, a]));
|
||||
|
||||
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
|
||||
const dateStr = d.toISOString().split('T')[0];
|
||||
const avail = availabilityMap.get(dateStr);
|
||||
|
||||
if (avail) {
|
||||
days.push({ date: dateStr, status: avail.status as 'available' | 'booked' | 'maintenance' });
|
||||
} else {
|
||||
// Check if date falls within any reservation
|
||||
const isBooked = reservations.some(r => {
|
||||
const checkIn = new Date(r.check_in);
|
||||
const checkOut = new Date(r.check_out);
|
||||
return d >= checkIn && d < checkOut;
|
||||
});
|
||||
|
||||
days.push({ date: dateStr, status: isBooked ? 'booked' : 'available' });
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ days });
|
||||
} catch (error) {
|
||||
console.error('Availability error:', error);
|
||||
return NextResponse.json({ error: 'Failed to get availability' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const roomId = parseInt(params.id);
|
||||
const { date, status, reason } = await request.json();
|
||||
|
||||
if (!date || !status) {
|
||||
return NextResponse.json({ error: 'date and status required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { rows } = await db.query(`
|
||||
INSERT INTO room_availability (room_id, date, status, reason)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (room_id, date) DO UPDATE SET status = $3, reason = $4
|
||||
RETURNING *
|
||||
`, [roomId, date, status, reason || null]);
|
||||
|
||||
return NextResponse.json({ success: true, availability: rows[0] });
|
||||
} catch (error) {
|
||||
console.error('Set availability error:', error);
|
||||
return NextResponse.json({ error: 'Failed to set availability' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,9 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri
|
||||
const photoUrl = searchParams.get('photo_url') || null;
|
||||
|
||||
const { rows } = await db.query(
|
||||
`SELECT id, room_id, photo_url, user_id, first_name, last_initial, text, created_at, deleted_by_admin
|
||||
`SELECT id, room_id, photo_url, user_id, first_name, last_initial, text, created_at, deleted_by_admin, status
|
||||
FROM room_comments
|
||||
WHERE room_id = $1 AND ($2::varchar IS NULL OR photo_url = $2) AND deleted_by_admin = FALSE
|
||||
WHERE room_id = $1 AND ($2::varchar IS NULL OR photo_url = $2) AND deleted_by_admin = FALSE AND status = 'approved'
|
||||
ORDER BY created_at DESC`,
|
||||
[roomId, photoUrl]
|
||||
);
|
||||
@@ -47,12 +47,12 @@ export async function POST(request: NextRequest, { params }: { params: { id: str
|
||||
const lastInitial = user.last_name ? user.last_name.charAt(0).toUpperCase() : null;
|
||||
|
||||
const { rows: inserted } = await db.query(
|
||||
`INSERT INTO room_comments (room_id, photo_url, user_id, first_name, last_initial, text)
|
||||
VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`,
|
||||
`INSERT INTO room_comments (room_id, photo_url, user_id, first_name, last_initial, text, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'pending') RETURNING *`,
|
||||
[roomId, photo_url || null, session.id, firstName, lastInitial, text.trim()]
|
||||
);
|
||||
|
||||
return NextResponse.json({ data: inserted[0] });
|
||||
return NextResponse.json({ data: inserted[0], message: 'Comment submitted for review' });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err.message || 'Failed to post comment' },
|
||||
|
||||
@@ -6,17 +6,23 @@ export async function GET() {
|
||||
try {
|
||||
const { rows } = await db.query('SELECT * FROM rooms WHERE active = TRUE ORDER BY sort_order, id');
|
||||
|
||||
// Get amenity links for all rooms
|
||||
const { rows: amenityLinks } = await db.query('SELECT room_id, amenity_id FROM room_amenity_links');
|
||||
const amenityMap = new Map<number, number[]>();
|
||||
for (const link of amenityLinks) {
|
||||
if (!amenityMap.has(link.room_id)) amenityMap.set(link.room_id, []);
|
||||
amenityMap.get(link.room_id)!.push(link.amenity_id);
|
||||
// Get amenity details with icons for all rooms
|
||||
const { rows: amenityRows } = await db.query(`
|
||||
SELECT ral.room_id, ra.id, ra.name, ra.icon_svg
|
||||
FROM room_amenity_links ral
|
||||
JOIN room_amenities ra ON ral.amenity_id = ra.id
|
||||
ORDER BY ra.sort_order
|
||||
`);
|
||||
|
||||
const amenityMap = new Map<number, Array<{id: number; name: string; icon_svg: string}>>();
|
||||
for (const row of amenityRows) {
|
||||
if (!amenityMap.has(row.room_id)) amenityMap.set(row.room_id, []);
|
||||
amenityMap.get(row.room_id)!.push({ id: row.id, name: row.name, icon_svg: row.icon_svg });
|
||||
}
|
||||
|
||||
const roomsWithAmenities = rows.map(r => ({
|
||||
...r,
|
||||
amenity_ids: amenityMap.get(r.id) || []
|
||||
amenities: amenityMap.get(r.id) || []
|
||||
}));
|
||||
|
||||
return NextResponse.json({ data: roomsWithAmenities });
|
||||
|
||||
Reference in New Issue
Block a user