From 4bb35964120879a70e2aefd9a6158a7f6c1b4578 Mon Sep 17 00:00:00 2001 From: muken Date: Sat, 27 Jun 2026 18:06:31 -0500 Subject: [PATCH] Add rooms, menu, places, reviews APIs and site config bulk endpoint --- src/app/api/admin/reviews/[id]/route.ts | 38 +++++++++++++++ src/app/api/admin/reviews/route.ts | 14 ++++++ src/app/api/admin/users/[id]/route.ts | 63 +++++++++++++++++++++++++ src/app/api/admin/users/route.ts | 11 +++++ src/app/api/menu/[id]/route.ts | 39 +++++++++++++++ src/app/api/menu/route.ts | 39 +++++++++++++++ src/app/api/places/[id]/route.ts | 39 +++++++++++++++ src/app/api/places/route.ts | 37 +++++++++++++++ src/app/api/reviews/route.ts | 37 +++++++++++++++ src/app/api/rooms/[id]/route.ts | 39 +++++++++++++++ src/app/api/rooms/route.ts | 37 +++++++++++++++ src/app/api/site-assets/route.ts | 7 ++- src/lib/schema.ts | 56 +++++++++++++++++++++- 13 files changed, 454 insertions(+), 2 deletions(-) create mode 100644 src/app/api/admin/reviews/[id]/route.ts create mode 100644 src/app/api/admin/reviews/route.ts create mode 100644 src/app/api/admin/users/[id]/route.ts create mode 100644 src/app/api/menu/[id]/route.ts create mode 100644 src/app/api/menu/route.ts create mode 100644 src/app/api/places/[id]/route.ts create mode 100644 src/app/api/places/route.ts create mode 100644 src/app/api/reviews/route.ts create mode 100644 src/app/api/rooms/[id]/route.ts create mode 100644 src/app/api/rooms/route.ts diff --git a/src/app/api/admin/reviews/[id]/route.ts b/src/app/api/admin/reviews/[id]/route.ts new file mode 100644 index 0000000..6ce2b40 --- /dev/null +++ b/src/app/api/admin/reviews/[id]/route.ts @@ -0,0 +1,38 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireRole } from '@/lib/auth'; +import { db } from '@/lib/db'; + +export async function PUT(request: NextRequest, { params }: { params: { id: string } }) { + try { + await requireRole('admin'); + const id = parseInt(params.id, 10); + const body = await request.json(); + const { approved } = body; + + const { rows } = await db.query( + 'UPDATE reviews SET approved = $1 WHERE id = $2 RETURNING *', + [approved, id] + ); + + if (rows.length === 0) { + return NextResponse.json({ error: 'Review not found' }, { status: 404 }); + } + + return NextResponse.json({ data: rows[0] }); + } catch (error: any) { + console.error('PUT /api/admin/reviews/:id error:', error); + return NextResponse.json({ error: error.message || 'Failed to update review' }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { + try { + await requireRole('admin'); + const id = parseInt(params.id, 10); + await db.query('DELETE FROM reviews WHERE id = $1', [id]); + return NextResponse.json({ ok: true }); + } catch (error: any) { + console.error('DELETE /api/admin/reviews/:id error:', error); + return NextResponse.json({ error: error.message || 'Failed to delete review' }, { status: 500 }); + } +} diff --git a/src/app/api/admin/reviews/route.ts b/src/app/api/admin/reviews/route.ts new file mode 100644 index 0000000..899a897 --- /dev/null +++ b/src/app/api/admin/reviews/route.ts @@ -0,0 +1,14 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireRole } from '@/lib/auth'; +import { db } from '@/lib/db'; + +export async function GET() { + try { + await requireRole('admin'); + const { rows } = await db.query('SELECT * FROM reviews ORDER BY created_at DESC'); + return NextResponse.json({ data: rows }); + } catch (error: any) { + console.error('GET /api/admin/reviews error:', error); + return NextResponse.json({ error: error.message || 'Failed to fetch reviews' }, { status: 500 }); + } +} diff --git a/src/app/api/admin/users/[id]/route.ts b/src/app/api/admin/users/[id]/route.ts new file mode 100644 index 0000000..e539691 --- /dev/null +++ b/src/app/api/admin/users/[id]/route.ts @@ -0,0 +1,63 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireRole } from '@/lib/auth'; +import { db } from '@/lib/db'; +import bcrypt from 'bcryptjs'; + +export async function PUT(request: NextRequest, { params }: { params: { id: string } }) { + try { + await requireRole('admin'); + const id = parseInt(params.id, 10); + const body = await request.json(); + const { username, password, role } = body; + + if (role && role !== 'admin' && role !== 'user') { + return NextResponse.json({ error: 'Role must be admin or user' }, { status: 400 }); + } + + const updates: string[] = []; + const values: any[] = []; + let idx = 1; + + if (username) { + updates.push(`username = $${idx++}`); + values.push(username); + } + if (role) { + updates.push(`role = $${idx++}`); + values.push(role); + } + if (password) { + updates.push(`password_hash = $${idx++}`); + values.push(bcrypt.hashSync(password, 10)); + } + + if (updates.length === 0) { + return NextResponse.json({ error: 'No fields to update' }, { status: 400 }); + } + + values.push(id); + const { rows } = await db.query( + `UPDATE users SET ${updates.join(', ')} WHERE id = $${idx} RETURNING id, username, role`, + values + ); + + if (rows.length === 0) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + return NextResponse.json({ data: rows[0] }); + } catch (err: any) { + return NextResponse.json({ error: err.message || 'Failed to update user' }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { + try { + await requireRole('admin'); + const id = parseInt(params.id, 10); + await db.query('DELETE FROM users WHERE id = $1', [id]); + return NextResponse.json({ ok: true }); + } catch (err: any) { + return NextResponse.json({ error: err.message || 'Failed to delete user' }, { status: 500 }); + } +} diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts index 0a9868a..d9c3227 100644 --- a/src/app/api/admin/users/route.ts +++ b/src/app/api/admin/users/route.ts @@ -1,5 +1,16 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireRole, createUser } from '@/lib/auth'; +import { db } from '@/lib/db'; + +export async function GET() { + try { + await requireRole('admin'); + const { rows } = await db.query('SELECT id, username, role, created_at FROM users ORDER BY created_at DESC'); + return NextResponse.json({ data: rows }); + } catch (err: any) { + return NextResponse.json({ error: err.message || 'Failed to fetch users' }, { status: err.message === 'Unauthorized' ? 401 : 500 }); + } +} export async function POST(request: NextRequest) { try { diff --git a/src/app/api/menu/[id]/route.ts b/src/app/api/menu/[id]/route.ts new file mode 100644 index 0000000..7dea86d --- /dev/null +++ b/src/app/api/menu/[id]/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireRole } from '@/lib/auth'; +import { db } from '@/lib/db'; + +export async function PUT(request: NextRequest, { params }: { params: { id: string } }) { + try { + await requireRole('admin'); + const id = parseInt(params.id, 10); + const body = await request.json(); + const { category, name, description, price, is_daily_special, active, sort_order } = body; + + const { rows } = await db.query( + `UPDATE menu_items SET category = $1, name = $2, description = $3, price = $4, is_daily_special = $5, active = $6, sort_order = $7 + WHERE id = $8 RETURNING *`, + [category, name, description || '', price, is_daily_special, active, sort_order || 0, id] + ); + + if (rows.length === 0) { + return NextResponse.json({ error: 'Menu item not found' }, { status: 404 }); + } + + return NextResponse.json({ data: rows[0] }); + } catch (error: any) { + console.error('PUT /api/menu/:id error:', error); + return NextResponse.json({ error: error.message || 'Failed to update menu item' }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { + try { + await requireRole('admin'); + const id = parseInt(params.id, 10); + await db.query('DELETE FROM menu_items WHERE id = $1', [id]); + return NextResponse.json({ ok: true }); + } catch (error: any) { + console.error('DELETE /api/menu/:id error:', error); + return NextResponse.json({ error: error.message || 'Failed to delete menu item' }, { status: 500 }); + } +} diff --git a/src/app/api/menu/route.ts b/src/app/api/menu/route.ts new file mode 100644 index 0000000..a47da51 --- /dev/null +++ b/src/app/api/menu/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireRole } from '@/lib/auth'; +import { db } from '@/lib/db'; + +export async function GET() { + try { + const { rows } = await db.query( + 'SELECT * FROM menu_items WHERE active = TRUE ORDER BY category, sort_order, id' + ); + return NextResponse.json({ data: rows }); + } catch (error: any) { + console.error('GET /api/menu error:', error); + return NextResponse.json({ error: error.message || 'Failed to fetch menu' }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + await requireRole('admin'); + const body = await request.json(); + const { category, name, description, price, is_daily_special, sort_order } = body; + + if (!category || !name || price === undefined) { + return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); + } + + const { rows } = await db.query( + `INSERT INTO menu_items (category, name, description, price, is_daily_special, sort_order) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING *`, + [category, name, description || '', price, is_daily_special || false, sort_order || 0] + ); + + return NextResponse.json({ data: rows[0] }); + } catch (error: any) { + console.error('POST /api/menu error:', error); + return NextResponse.json({ error: error.message || 'Failed to create menu item' }, { status: 500 }); + } +} diff --git a/src/app/api/places/[id]/route.ts b/src/app/api/places/[id]/route.ts new file mode 100644 index 0000000..919999a --- /dev/null +++ b/src/app/api/places/[id]/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireRole } from '@/lib/auth'; +import { db } from '@/lib/db'; + +export async function PUT(request: NextRequest, { params }: { params: { id: string } }) { + try { + await requireRole('admin'); + const id = parseInt(params.id, 10); + const body = await request.json(); + const { name, description, photo, distance_km, visit_time, suggestion, active, sort_order } = body; + + const { rows } = await db.query( + `UPDATE places SET name = $1, description = $2, photo = $3, distance_km = $4, visit_time = $5, suggestion = $6, active = $7, sort_order = $8 + WHERE id = $9 RETURNING *`, + [name, description, photo || null, distance_km || null, visit_time || null, suggestion || null, active, sort_order || 0, id] + ); + + if (rows.length === 0) { + return NextResponse.json({ error: 'Place not found' }, { status: 404 }); + } + + return NextResponse.json({ data: rows[0] }); + } catch (error: any) { + console.error('PUT /api/places/:id error:', error); + return NextResponse.json({ error: error.message || 'Failed to update place' }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { + try { + await requireRole('admin'); + const id = parseInt(params.id, 10); + await db.query('DELETE FROM places WHERE id = $1', [id]); + return NextResponse.json({ ok: true }); + } catch (error: any) { + console.error('DELETE /api/places/:id error:', error); + return NextResponse.json({ error: error.message || 'Failed to delete place' }, { status: 500 }); + } +} diff --git a/src/app/api/places/route.ts b/src/app/api/places/route.ts new file mode 100644 index 0000000..4b4ff24 --- /dev/null +++ b/src/app/api/places/route.ts @@ -0,0 +1,37 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireRole } from '@/lib/auth'; +import { db } from '@/lib/db'; + +export async function GET() { + try { + const { rows } = await db.query('SELECT * FROM places WHERE active = TRUE ORDER BY sort_order, id'); + return NextResponse.json({ data: rows }); + } catch (error: any) { + console.error('GET /api/places error:', error); + return NextResponse.json({ error: error.message || 'Failed to fetch places' }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + await requireRole('admin'); + const body = await request.json(); + const { name, description, photo, distance_km, visit_time, suggestion, sort_order } = body; + + if (!name || !description) { + return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); + } + + const { rows } = await db.query( + `INSERT INTO places (name, description, photo, distance_km, visit_time, suggestion, sort_order) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING *`, + [name, description, photo || null, distance_km || null, visit_time || null, suggestion || null, sort_order || 0] + ); + + return NextResponse.json({ data: rows[0] }); + } catch (error: any) { + console.error('POST /api/places error:', error); + return NextResponse.json({ error: error.message || 'Failed to create place' }, { status: 500 }); + } +} diff --git a/src/app/api/reviews/route.ts b/src/app/api/reviews/route.ts new file mode 100644 index 0000000..1b788ad --- /dev/null +++ b/src/app/api/reviews/route.ts @@ -0,0 +1,37 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/lib/db'; + +export async function GET() { + try { + const { rows } = await db.query( + 'SELECT * FROM reviews WHERE approved = TRUE ORDER BY created_at DESC' + ); + return NextResponse.json({ data: rows }); + } catch (error: any) { + console.error('GET /api/reviews error:', error); + return NextResponse.json({ error: error.message || 'Failed to fetch reviews' }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { author_name, rating, text } = body; + + if (!author_name || !rating || !text) { + return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); + } + + const { rows } = await db.query( + `INSERT INTO reviews (author_name, rating, text, approved) + VALUES ($1, $2, $3, FALSE) + RETURNING *`, + [author_name, rating, text] + ); + + return NextResponse.json({ data: rows[0] }); + } catch (error: any) { + console.error('POST /api/reviews error:', error); + return NextResponse.json({ error: error.message || 'Failed to submit review' }, { status: 500 }); + } +} diff --git a/src/app/api/rooms/[id]/route.ts b/src/app/api/rooms/[id]/route.ts new file mode 100644 index 0000000..e407adf --- /dev/null +++ b/src/app/api/rooms/[id]/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireRole } from '@/lib/auth'; +import { db } from '@/lib/db'; + +export async function PUT(request: NextRequest, { params }: { params: { id: string } }) { + try { + await requireRole('admin'); + const id = parseInt(params.id, 10); + const body = await request.json(); + const { name, description, price, photos, active, sort_order } = body; + + const { rows } = await db.query( + `UPDATE rooms SET name = $1, description = $2, price = $3, photos = $4, active = $5, sort_order = $6, updated_at = NOW() + WHERE id = $7 RETURNING *`, + [name, description, price, photos || [], active, sort_order || 0, id] + ); + + if (rows.length === 0) { + return NextResponse.json({ error: 'Room not found' }, { status: 404 }); + } + + return NextResponse.json({ data: rows[0] }); + } catch (error: any) { + console.error('PUT /api/rooms/:id error:', error); + return NextResponse.json({ error: error.message || 'Failed to update room' }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { + try { + await requireRole('admin'); + const id = parseInt(params.id, 10); + await db.query('DELETE FROM rooms WHERE id = $1', [id]); + return NextResponse.json({ ok: true }); + } catch (error: any) { + console.error('DELETE /api/rooms/:id error:', error); + return NextResponse.json({ error: error.message || 'Failed to delete room' }, { status: 500 }); + } +} diff --git a/src/app/api/rooms/route.ts b/src/app/api/rooms/route.ts new file mode 100644 index 0000000..9d32684 --- /dev/null +++ b/src/app/api/rooms/route.ts @@ -0,0 +1,37 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireRole } from '@/lib/auth'; +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 }); + } catch (error: any) { + console.error('GET /api/rooms error:', error); + return NextResponse.json({ error: error.message || 'Failed to fetch rooms' }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + await requireRole('admin'); + const body = await request.json(); + const { name, description, price, photos, sort_order } = body; + + if (!name || !description || price === undefined) { + return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); + } + + const { rows } = await db.query( + `INSERT INTO rooms (name, description, price, photos, sort_order) + VALUES ($1, $2, $3, $4, $5) + RETURNING *`, + [name, description, price, photos || [], sort_order || 0] + ); + + return NextResponse.json({ data: rows[0] }); + } catch (error: any) { + console.error('POST /api/rooms error:', error); + return NextResponse.json({ error: error.message || 'Failed to create room' }, { status: 500 }); + } +} diff --git a/src/app/api/site-assets/route.ts b/src/app/api/site-assets/route.ts index 220961a..e4b0900 100644 --- a/src/app/api/site-assets/route.ts +++ b/src/app/api/site-assets/route.ts @@ -5,7 +5,12 @@ import { db } from '@/lib/db'; export async function GET(request: NextRequest) { const key = request.nextUrl.searchParams.get('key'); if (!key) { - return NextResponse.json({ error: 'Missing key' }, { status: 400 }); + const { rows } = await db.query( + `SELECT key, value FROM config WHERE key IN ('logo','facebook_url','instagram_url','whatsapp_url','address','phone','email','wifi_password')` + ); + const config: Record = {}; + rows.forEach((r: any) => config[r.key] = r.value); + return NextResponse.json({ data: config }); } const { rows } = await db.query('SELECT value FROM config WHERE key = $1', [key]); diff --git a/src/lib/schema.ts b/src/lib/schema.ts index ca1aa21..cb708a7 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -67,6 +67,60 @@ export async function initSchema() { value TEXT NOT NULL ); `); + + await db.query(` + CREATE TABLE IF NOT EXISTS rooms ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + price NUMERIC(10,2) NOT NULL, + photos TEXT[] DEFAULT '{}', + active BOOLEAN DEFAULT TRUE, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + ); + `); + + await db.query(` + CREATE TABLE IF NOT EXISTS menu_items ( + id SERIAL PRIMARY KEY, + category VARCHAR(100) NOT NULL, + name VARCHAR(255) NOT NULL, + description TEXT, + price NUMERIC(10,2) NOT NULL, + is_daily_special BOOLEAN DEFAULT FALSE, + active BOOLEAN DEFAULT TRUE, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW() + ); + `); + + await db.query(` + CREATE TABLE IF NOT EXISTS places ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + photo VARCHAR(500), + distance_km NUMERIC(8,2), + visit_time VARCHAR(100), + suggestion TEXT, + active BOOLEAN DEFAULT TRUE, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW() + ); + `); + + await db.query(` + CREATE TABLE IF NOT EXISTS reviews ( + id SERIAL PRIMARY KEY, + author_name VARCHAR(255) NOT NULL, + rating INTEGER NOT NULL CHECK (rating BETWEEN 1 AND 5), + text TEXT NOT NULL, + approved BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ); + `); } export async function seed() { @@ -145,7 +199,7 @@ export async function seed() { } export async function resetDatabase() { - await db.query('DROP TABLE IF EXISTS config, benefits, offers, coupons, reservations, images, users CASCADE'); + await db.query('DROP TABLE IF EXISTS reviews, places, menu_items, rooms, config, benefits, offers, coupons, reservations, images, users CASCADE'); await initSchema(); await seed(); }