From 91fce014fdc3f5f985245136b6fd79ee2c6a90f3 Mon Sep 17 00:00:00 2001 From: muken Date: Mon, 29 Jun 2026 20:49:51 -0500 Subject: [PATCH] feat: room amenities selection in room editor --- src/app/admin/page.tsx | 37 +++++++++++++++++++++++++++++++-- src/app/api/rooms/[id]/route.ts | 20 ++++++++++++++++-- src/app/api/rooms/route.ts | 28 ++++++++++++++++++++++--- 3 files changed, 78 insertions(+), 7 deletions(-) diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 96e6b35..5dc96f7 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -4,7 +4,7 @@ import { useState, useEffect, useRef, useCallback } from 'react'; type Image = { id: number; filename: string; data: string; category: string; room_id: number | null; location_target: string; uploaded_at: string }; type Slide = { id: number; title: string; subtitle: string; image_url: string | null; image_id: number | null; active: boolean; sort_order: number }; -type Room = { id: number; name: string; description: string; price: string; photos: string[]; featured_photo: string | null; active: boolean; sort_order: number }; +type Room = { id: number; name: string; description: string; price: string; photos: string[]; featured_photo: string | null; active: boolean; sort_order: number; amenity_ids?: number[] }; type CalendarEvent = { id: number; date: string; title: string; type: string; color: string; description: string | null; active: boolean }; type SocialEvent = { id: number; name: string; description: string; price: string | null; date: string | null; photos: string[]; featured_photo: string | null; active: boolean; sort_order: number }; type MenuItem = { id: number; category: string; name: string; description: string; price: string; is_daily_special: boolean; active: boolean; sort_order: number }; @@ -848,7 +848,22 @@ function RoomsSection({ rooms, setRooms, images, onToast }: any) { function RoomEditor({ room, setRoom, images, onSave, onCancel }: any) { const [showPicker, setShowPicker] = useState(false); const [localPhotos, setLocalPhotos] = useState(room.photos || []); - const r = { ...room, photos: localPhotos }; + const [allAmenities, setAllAmenities] = useState([]); + const [selectedAmenities, setSelectedAmenities] = useState(room.amenity_ids || []); + const r = { ...room, photos: localPhotos, amenity_ids: selectedAmenities }; + + useEffect(() => { + fetch('/api/amenities') + .then(res => res.json()) + .then(d => setAllAmenities(d.data || [])) + .catch(() => {}); + }, []); + + const toggleAmenity = (id: number) => { + setSelectedAmenities(prev => + prev.includes(id) ? prev.filter(a => a !== id) : [...prev, id] + ); + }; const addPhotos = async (files: FileList) => { const newPhotos: string[] = []; @@ -891,6 +906,24 @@ function RoomEditor({ room, setRoom, images, onSave, onCancel }: any) { setRoom({ ...r, active: v })} /> +
+ +
+ {allAmenities.map(a => ( + + ))} +
+
Cancel diff --git a/src/app/api/rooms/[id]/route.ts b/src/app/api/rooms/[id]/route.ts index c1f1691..f620bc2 100644 --- a/src/app/api/rooms/[id]/route.ts +++ b/src/app/api/rooms/[id]/route.ts @@ -8,7 +8,15 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri if (isNaN(id)) return NextResponse.json({ error: 'Invalid id' }, { status: 400 }); const { rows } = await db.query('SELECT * FROM rooms WHERE id = $1', [id]); if (!rows.length) return NextResponse.json({ error: 'Room not found' }, { status: 404 }); - return NextResponse.json({ data: rows[0] }); + + // Get amenities for this room + const { rows: amenityRows } = await db.query( + 'SELECT amenity_id FROM room_amenity_links WHERE room_id = $1', + [id] + ); + const amenity_ids = amenityRows.map(r => r.amenity_id); + + return NextResponse.json({ data: { ...rows[0], amenity_ids } }); } catch (error: any) { console.error('GET /api/rooms/[id]:', error); return NextResponse.json({ error: error.message }, { status: 500 }); @@ -21,7 +29,7 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri const id = parseInt(params.id); if (isNaN(id)) return NextResponse.json({ error: 'Invalid id' }, { status: 400 }); const body = await request.json(); - const { name, description, price, photos, sort_order, featured_photo, active } = body; + const { name, description, price, photos, sort_order, featured_photo, active, amenity_ids } = body; if (!name || !description || price === undefined) { return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); @@ -31,6 +39,14 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri `UPDATE rooms SET name = $1, description = $2, price = $3, photos = $4, sort_order = $5, featured_photo = $6, active = $7 WHERE id = $8 RETURNING *`, [name, description, price, photos || [], sort_order || 0, featured_photo || null, active !== false, id] ); + + // Update amenities + await db.query('DELETE FROM room_amenity_links WHERE room_id = $1', [id]); + if (amenity_ids && amenity_ids.length > 0) { + const values = amenity_ids.map((aid: number) => `(${id}, ${aid})`).join(','); + await db.query(`INSERT INTO room_amenity_links (room_id, amenity_id) VALUES ${values}`); + } + return NextResponse.json({ data: rows[0] }); } catch (error: any) { if ((error as Error).message === 'Unauthorized') return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); diff --git a/src/app/api/rooms/route.ts b/src/app/api/rooms/route.ts index c8528c0..3cbd6e4 100644 --- a/src/app/api/rooms/route.ts +++ b/src/app/api/rooms/route.ts @@ -5,7 +5,21 @@ import { db } from '@/lib/db'; export async function GET() { try { const { rows } = await db.query('SELECT * FROM rooms WHERE active = TRUE ORDER BY sort_order, id'); - return NextResponse.json({ data: rows }); + + // 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(); + 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); + } + + const roomsWithAmenities = rows.map(r => ({ + ...r, + amenity_ids: amenityMap.get(r.id) || [] + })); + + return NextResponse.json({ data: roomsWithAmenities }); } catch (error: any) { console.error('GET /api/rooms error:', error); return NextResponse.json({ error: error.message || 'Failed to fetch rooms' }, { status: 500 }); @@ -16,7 +30,7 @@ export async function POST(request: NextRequest) { try { await requireRole('admin'); const body = await request.json(); - const { name, description, price, photos, sort_order, featured_photo } = body; + const { name, description, price, photos, sort_order, featured_photo, amenity_ids } = body; if (!name || !description || price === undefined) { return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); @@ -28,8 +42,16 @@ export async function POST(request: NextRequest) { RETURNING *`, [name, description, price, photos || [], sort_order || 0, featured_photo || null] ); + + const roomId = rows[0].id; + + // Insert amenity links + if (amenity_ids && amenity_ids.length > 0) { + const values = amenity_ids.map((aid: number) => `(${roomId}, ${aid})`).join(','); + await db.query(`INSERT INTO room_amenity_links (room_id, amenity_id) VALUES ${values}`); + } - return NextResponse.json({ data: rows[0] }); + return NextResponse.json({ data: { ...rows[0], amenity_ids: amenity_ids || [] } }); } catch (error: any) { console.error('POST /api/rooms error:', error); return NextResponse.json({ error: error.message || 'Failed to create room' }, { status: 500 });