feat: room amenities selection in room editor
This commit is contained in:
+35
-2
@@ -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<string[]>(room.photos || []);
|
||||
const r = { ...room, photos: localPhotos };
|
||||
const [allAmenities, setAllAmenities] = useState<Amenity[]>([]);
|
||||
const [selectedAmenities, setSelectedAmenities] = useState<number[]>(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) {
|
||||
</div>
|
||||
</div>
|
||||
<Tgl label="Active" checked={r.active} onChange={(v: boolean) => setRoom({ ...r, active: v })} />
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<label style={{ fontSize: '13px', fontWeight: 600, color: C.text }}>Amenities</label>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
|
||||
{allAmenities.map(a => (
|
||||
<button key={a.id} onClick={() => toggleAmenity(a.id)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: '4px',
|
||||
padding: '6px 10px', borderRadius: 6, border: `1px solid ${selectedAmenities.includes(a.id) ? C.gold : C.border}`,
|
||||
background: selectedAmenities.includes(a.id) ? C.goldLight : '#fff',
|
||||
cursor: 'pointer', fontSize: '12px', transition: 'all 150ms',
|
||||
color: selectedAmenities.includes(a.id) ? C.gold : C.text
|
||||
}}>
|
||||
<span style={{ width: 16, height: 16 }} dangerouslySetInnerHTML={{ __html: a.icon_svg.replace(/width="24"/g, 'width="16"').replace(/height="24"/g, 'height="16"') }} />
|
||||
{a.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '10px', justifyContent: 'flex-end', marginTop: '24px' }}>
|
||||
<Btn onClick={onCancel} variant="secondary">Cancel</Btn>
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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<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);
|
||||
}
|
||||
|
||||
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 });
|
||||
|
||||
Reference in New Issue
Block a user