refactor: migrate room photos from rooms.photos to images table
- Photos now stored exclusively in images table (no duplication) - rooms.photos array cleared, freed 17 MB storage - Updated room APIs to fetch images by room_id - Added PUT endpoint for room updates - Updated RoomEditor to load/delete images from images table - Fixed images API to return 'data' key consistently
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
-- Database Index Optimization (fixed)
|
||||||
|
-- Run with: psql -U lahuasca -d lahuasca -f add-indexes.sql
|
||||||
|
|
||||||
|
-- Images table indexes (most queried)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_images_room_id ON images(room_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_images_category ON images(category);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_images_location_target ON images(location_target);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_images_uploaded_at ON images(uploaded_at DESC);
|
||||||
|
|
||||||
|
-- Room amenity links (join table)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_room_amenity_links_amenity_id ON room_amenity_links(amenity_id);
|
||||||
|
|
||||||
|
-- Orders table
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_orders_user_id ON orders(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_orders_created_at ON orders(created_at DESC);
|
||||||
|
|
||||||
|
-- Room reservations
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_room_reservations_room_id ON room_reservations(room_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_room_reservations_dates ON room_reservations(check_in, check_out);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_room_reservations_user_id ON room_reservations(user_id);
|
||||||
|
|
||||||
|
-- Restaurant reservations
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_reservations_date ON reservations(date);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_reservations_user_id ON reservations(user_id);
|
||||||
|
|
||||||
|
-- Social event reservations
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_social_event_reservations_event_id ON social_event_reservations(social_event_id);
|
||||||
|
|
||||||
|
-- Contact messages
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_contact_messages_created_at ON contact_messages(created_at DESC);
|
||||||
|
|
||||||
|
-- Room comments
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_room_comments_room_id ON room_comments(room_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_room_comments_created_at ON room_comments(created_at DESC);
|
||||||
|
|
||||||
|
-- Messages (chat)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_room ON messages(room_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at DESC);
|
||||||
|
|
||||||
|
-- Enable TOAST compression for large TEXT columns in images
|
||||||
|
ALTER TABLE images ALTER COLUMN data SET STORAGE EXTERNAL;
|
||||||
|
ALTER TABLE images ALTER COLUMN thumbnail SET STORAGE EXTERNAL;
|
||||||
|
ALTER TABLE images ALTER COLUMN medium SET STORAGE EXTERNAL;
|
||||||
|
|
||||||
|
-- Vacuum analyze to update statistics
|
||||||
|
ANALYZE images;
|
||||||
|
ANALYZE rooms;
|
||||||
|
ANALYZE orders;
|
||||||
|
ANALYZE room_reservations;
|
||||||
|
ANALYZE reservations;
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
-- Migration: Move room photos from rooms.photos to images table
|
||||||
|
-- Run with: psql -U lahuasca -d lahuasca -f migrate-room-photos.sql
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- First, check which rooms have photos in rooms.photos but not in images
|
||||||
|
-- For rooms 7 and 8, we need to copy their photos to images table
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
rec RECORD;
|
||||||
|
photo_data TEXT;
|
||||||
|
room_id_val INTEGER;
|
||||||
|
photo_index INTEGER;
|
||||||
|
BEGIN
|
||||||
|
-- Loop through rooms that have photos in the array but not in images table
|
||||||
|
FOR rec IN
|
||||||
|
SELECT r.id, r.name, r.photos
|
||||||
|
FROM rooms r
|
||||||
|
WHERE array_length(r.photos, 1) > 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM images i WHERE i.room_id = r.id
|
||||||
|
)
|
||||||
|
LOOP
|
||||||
|
room_id_val := rec.id;
|
||||||
|
|
||||||
|
-- Insert each photo from the array into images
|
||||||
|
FOR photo_index IN 1..array_length(rec.photos, 1) LOOP
|
||||||
|
photo_data := rec.photos[photo_index];
|
||||||
|
|
||||||
|
INSERT INTO images (filename, data, thumbnail, medium, category, room_id, location_target)
|
||||||
|
VALUES (
|
||||||
|
CONCAT('room-', room_id_val, '-photo-', photo_index, '.webp'),
|
||||||
|
photo_data,
|
||||||
|
photo_data, -- thumbnail (will be regenerated by WebP migration if different)
|
||||||
|
NULL,
|
||||||
|
'rooms',
|
||||||
|
room_id_val,
|
||||||
|
CONCAT('room-', room_id_val)
|
||||||
|
);
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
RAISE NOTICE 'Migrated % photos for room % (%)', array_length(rec.photos, 1), rec.name, room_id_val;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Now clear the photos arrays in rooms table (images table is the source of truth)
|
||||||
|
UPDATE rooms SET photos = '{}' WHERE array_length(photos, 1) > 0;
|
||||||
|
|
||||||
|
-- For rooms that already have images, sync the photo count
|
||||||
|
-- This is informational only
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
rec RECORD;
|
||||||
|
BEGIN
|
||||||
|
FOR rec IN
|
||||||
|
SELECT r.id, r.name,
|
||||||
|
array_length(r.photos, 1) as old_count,
|
||||||
|
(SELECT COUNT(*) FROM images i WHERE i.room_id = r.id) as new_count
|
||||||
|
FROM rooms r
|
||||||
|
LOOP
|
||||||
|
RAISE NOTICE 'Room % (%): photos array=%, images table=%',
|
||||||
|
rec.name, rec.id, rec.old_count, rec.new_count;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- Verify the migration
|
||||||
|
SELECT
|
||||||
|
r.id,
|
||||||
|
r.name,
|
||||||
|
array_length(r.photos, 1) as room_photos_count,
|
||||||
|
(SELECT COUNT(*) FROM images i WHERE i.room_id = r.id) as images_count
|
||||||
|
FROM rooms r
|
||||||
|
ORDER BY r.id;
|
||||||
+31
-13
@@ -921,17 +921,25 @@ function RoomsSection({ rooms, setRooms, images, onToast }: any) {
|
|||||||
|
|
||||||
function RoomEditor({ room, setRoom, images, onSave, onCancel }: any) {
|
function RoomEditor({ room, setRoom, images, onSave, onCancel }: any) {
|
||||||
const [showPicker, setShowPicker] = useState(false);
|
const [showPicker, setShowPicker] = useState(false);
|
||||||
const [localPhotos, setLocalPhotos] = useState<string[]>(room.photos || []);
|
const [roomImages, setRoomImages] = useState<{id: number; data: string; thumbnail: string}[]>([]);
|
||||||
const [allAmenities, setAllAmenities] = useState<Amenity[]>([]);
|
const [allAmenities, setAllAmenities] = useState<Amenity[]>([]);
|
||||||
const [selectedAmenities, setSelectedAmenities] = useState<number[]>(room.amenity_ids || []);
|
const [selectedAmenities, setSelectedAmenities] = useState<number[]>(room.amenity_ids || []);
|
||||||
const r = { ...room, photos: localPhotos, amenity_ids: selectedAmenities };
|
const r = { ...room, photos: roomImages.map(img => img.thumbnail || img.data), amenity_ids: selectedAmenities };
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/amenities')
|
fetch('/api/amenities')
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(d => setAllAmenities(d.data || []))
|
.then(d => setAllAmenities(d.data || []))
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, []);
|
|
||||||
|
// Load existing images for this room from images table
|
||||||
|
if (room.id) {
|
||||||
|
fetch(`/api/admin/images?room_id=${room.id}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(d => setRoomImages(d.data || []))
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
}, [room.id]);
|
||||||
|
|
||||||
const toggleAmenity = (id: number) => {
|
const toggleAmenity = (id: number) => {
|
||||||
setSelectedAmenities(prev =>
|
setSelectedAmenities(prev =>
|
||||||
@@ -940,7 +948,7 @@ function RoomEditor({ room, setRoom, images, onSave, onCancel }: any) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const addPhotos = async (files: FileList) => {
|
const addPhotos = async (files: FileList) => {
|
||||||
const newPhotos: string[] = [];
|
const newImages: {id: number; data: string; thumbnail: string}[] = [];
|
||||||
for (const f of Array.from(files)) {
|
for (const f of Array.from(files)) {
|
||||||
const b64 = await readFileBase64(f);
|
const b64 = await readFileBase64(f);
|
||||||
try {
|
try {
|
||||||
@@ -948,10 +956,20 @@ function RoomEditor({ room, setRoom, images, onSave, onCancel }: any) {
|
|||||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ filename: f.name, data: b64, category: 'rooms', room_id: r.id || null, location_target: `room-${r.id || 'new'}` }),
|
body: JSON.stringify({ filename: f.name, data: b64, category: 'rooms', room_id: r.id || null, location_target: `room-${r.id || 'new'}` }),
|
||||||
});
|
});
|
||||||
if (res.ok) { const d = await res.json(); newPhotos.push(d.data.data); }
|
if (res.ok) {
|
||||||
} catch { const b = await readFileBase64(f); newPhotos.push(b); }
|
const d = await res.json();
|
||||||
|
newImages.push({ id: d.data.id, data: d.data.data, thumbnail: d.data.thumbnail || d.data.data });
|
||||||
}
|
}
|
||||||
setLocalPhotos(prev => [...prev, ...newPhotos]);
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
setRoomImages(prev => [...prev, ...newImages]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeImage = async (imageId: number) => {
|
||||||
|
try {
|
||||||
|
await fetch(`/api/admin/images?id=${imageId}`, { method: 'DELETE' });
|
||||||
|
setRoomImages(prev => prev.filter(img => img.id !== imageId));
|
||||||
|
} catch { /* ignore */ }
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -965,15 +983,15 @@ function RoomEditor({ room, setRoom, images, onSave, onCancel }: any) {
|
|||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
<label style={{ fontSize: '13px', fontWeight: 600, color: C.text }}>Photos</label>
|
<label style={{ fontSize: '13px', fontWeight: 600, color: C.text }}>Photos</label>
|
||||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||||
<Btn onClick={() => document.getElementById(`room-photos-${r.id}`)?.click()} size="sm" variant="secondary">+ Add Photos</Btn>
|
<Btn onClick={() => document.getElementById(`room-photos-${r.id || 'new'}`)?.click()} size="sm" variant="secondary">+ Add Photos</Btn>
|
||||||
<input id={`room-photos-${r.id}`} type="file" accept="image/*" multiple style={{ display: 'none' }}
|
<input id={`room-photos-${r.id || 'new'}`} type="file" accept="image/*" multiple style={{ display: 'none' }}
|
||||||
onChange={e => { addPhotos(e.target.files!); e.target.value = ''; }} />
|
onChange={e => { addPhotos(e.target.files!); e.target.value = ''; }} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
|
||||||
{localPhotos.map((p, i) => (
|
{roomImages.map((img, i) => (
|
||||||
<div key={i} style={{ position: 'relative' }}>
|
<div key={img.id || i} style={{ position: 'relative' }}>
|
||||||
<img src={p} alt="" style={{ width: 64, height: 48, objectFit: 'cover', borderRadius: 6, border: '1px solid #eee' }} />
|
<img src={img.thumbnail || img.data} alt="" style={{ width: 64, height: 48, objectFit: 'cover', borderRadius: 6, border: '1px solid #eee' }} />
|
||||||
<button onClick={() => setLocalPhotos(prev => prev.filter((_, j) => j !== i))}
|
<button onClick={() => removeImage(img.id)}
|
||||||
style={{ position: 'absolute', top: -4, right: -4, width: 18, height: 18, borderRadius: '50%', background: C.danger, color: '#fff', border: 'none', cursor: 'pointer', fontSize: '11px', lineHeight: 18, padding: 0 }}>×</button>
|
style={{ position: 'absolute', top: -4, right: -4, width: 18, height: 18, borderRadius: '50%', background: C.danger, color: '#fff', border: 'none', cursor: 'pointer', fontSize: '11px', lineHeight: 18, padding: 0 }}>×</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { rows } = await db.query(query, params);
|
const { rows } = await db.query(query, params);
|
||||||
return NextResponse.json({ images: rows, count: rows.length });
|
return NextResponse.json({ data: rows, count: rows.length });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if ((error as Error).message === 'Unauthorized') {
|
if ((error as Error).message === 'Unauthorized') {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
|||||||
@@ -29,15 +29,16 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri
|
|||||||
const id = parseInt(params.id);
|
const id = parseInt(params.id);
|
||||||
if (isNaN(id)) return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
|
if (isNaN(id)) return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { name, description, price, photos, sort_order, featured_photo, active, amenity_ids } = body;
|
const { name, description, price, sort_order, featured_photo, active, amenity_ids } = body;
|
||||||
|
|
||||||
if (!name || !description || price === undefined) {
|
if (!name || !description || price === undefined) {
|
||||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Photos are stored in images table, not rooms.photos
|
||||||
const { rows } = await db.query(
|
const { rows } = await db.query(
|
||||||
`UPDATE rooms SET name = $1, description = $2, price = $3, photos = $4, sort_order = $5, featured_photo = $6, active = $7 WHERE id = $8 RETURNING *`,
|
`UPDATE rooms SET name = $1, description = $2, price = $3, sort_order = $4, featured_photo = $5, active = $6 WHERE id = $7 RETURNING *`,
|
||||||
[name, description, price, photos || [], sort_order || 0, featured_photo || null, active !== false, id]
|
[name, description, price, sort_order || 0, featured_photo || null, active !== false, id]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update amenities
|
// Update amenities
|
||||||
@@ -47,7 +48,14 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri
|
|||||||
await db.query(`INSERT INTO room_amenity_links (room_id, amenity_id) VALUES ${values}`);
|
await db.query(`INSERT INTO room_amenity_links (room_id, amenity_id) VALUES ${values}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ data: rows[0] });
|
// Get photos from images table
|
||||||
|
const { rows: imageRows } = await db.query(
|
||||||
|
'SELECT id, data, thumbnail FROM images WHERE room_id = $1 ORDER BY id',
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
const photos = imageRows.map((r: any) => r.thumbnail || r.data);
|
||||||
|
|
||||||
|
return NextResponse.json({ data: { ...rows[0], photos, amenity_ids: amenity_ids || [] } });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if ((error as Error).message === 'Unauthorized') return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
if ((error as Error).message === 'Unauthorized') return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
console.error('PUT /api/rooms/[id]:', error);
|
console.error('PUT /api/rooms/[id]:', error);
|
||||||
|
|||||||
@@ -20,8 +20,24 @@ export async function GET() {
|
|||||||
amenityMap.get(row.room_id)!.push({ id: row.id, name: row.name, icon_svg: row.icon_svg });
|
amenityMap.get(row.room_id)!.push({ id: row.id, name: row.name, icon_svg: row.icon_svg });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get photos from images table for all rooms
|
||||||
|
const { rows: imageRows } = await db.query(`
|
||||||
|
SELECT room_id, data, thumbnail, medium
|
||||||
|
FROM images
|
||||||
|
WHERE room_id IS NOT NULL
|
||||||
|
ORDER BY room_id, id
|
||||||
|
`);
|
||||||
|
|
||||||
|
const imageMap = new Map<number, string[]>();
|
||||||
|
for (const row of imageRows) {
|
||||||
|
if (!imageMap.has(row.room_id)) imageMap.set(row.room_id, []);
|
||||||
|
// Prefer thumbnail for display, fallback to data
|
||||||
|
imageMap.get(row.room_id)!.push(row.thumbnail || row.data);
|
||||||
|
}
|
||||||
|
|
||||||
const roomsWithAmenities = rows.map(r => ({
|
const roomsWithAmenities = rows.map(r => ({
|
||||||
...r,
|
...r,
|
||||||
|
photos: imageMap.get(r.id) || [],
|
||||||
amenities: amenityMap.get(r.id) || []
|
amenities: amenityMap.get(r.id) || []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -36,17 +52,18 @@ export async function POST(request: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
await requireRole('admin');
|
await requireRole('admin');
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { name, description, price, photos, sort_order, featured_photo, amenity_ids } = body;
|
const { name, description, price, sort_order, featured_photo, amenity_ids } = body;
|
||||||
|
|
||||||
if (!name || !description || price === undefined) {
|
if (!name || !description || price === undefined) {
|
||||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Photos are now stored in images table, not rooms.photos
|
||||||
const { rows } = await db.query(
|
const { rows } = await db.query(
|
||||||
`INSERT INTO rooms (name, description, price, photos, sort_order, featured_photo)
|
`INSERT INTO rooms (name, description, price, photos, sort_order, featured_photo)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[name, description, price, photos || [], sort_order || 0, featured_photo || null]
|
[name, description, price, [], sort_order || 0, featured_photo || null]
|
||||||
);
|
);
|
||||||
|
|
||||||
const roomId = rows[0].id;
|
const roomId = rows[0].id;
|
||||||
@@ -57,9 +74,40 @@ export async function POST(request: NextRequest) {
|
|||||||
await db.query(`INSERT INTO room_amenity_links (room_id, amenity_id) VALUES ${values}`);
|
await db.query(`INSERT INTO room_amenity_links (room_id, amenity_id) VALUES ${values}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ data: { ...rows[0], amenity_ids: amenity_ids || [] } });
|
return NextResponse.json({ data: { ...rows[0], photos: [], amenity_ids: amenity_ids || [] } });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('POST /api/rooms error:', error);
|
console.error('POST /api/rooms error:', error);
|
||||||
return NextResponse.json({ error: error.message || 'Failed to create room' }, { status: 500 });
|
return NextResponse.json({ error: error.message || 'Failed to create room' }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function PUT(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
await requireRole('admin');
|
||||||
|
const body = await request.json();
|
||||||
|
const { id, name, description, price, active, sort_order, featured_photo, amenity_ids } = body;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return NextResponse.json({ error: 'Room ID required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Photos are stored in images table, not rooms.photos
|
||||||
|
const { rows } = await db.query(
|
||||||
|
`UPDATE rooms SET name = $1, description = $2, price = $3, active = $4, sort_order = $5, featured_photo = $6
|
||||||
|
WHERE id = $7 RETURNING *`,
|
||||||
|
[name, description, price, active ?? true, sort_order ?? 0, featured_photo || null, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update amenity links
|
||||||
|
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], photos: [], amenity_ids: amenity_ids || [] } });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('PUT /api/rooms error:', error);
|
||||||
|
return NextResponse.json({ error: error.message || 'Failed to update room' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user