diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 893eef0..5c268a5 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -34,9 +34,11 @@ const NAV = [ { id: 'private-events', label: 'Private Events', icon: '๐Ÿ”’' }, { id: 'rooms', label: 'Rooms', icon: '๐Ÿจ' }, { id: 'room-status', label: 'Room Status', icon: '๐Ÿงน' }, + { id: 'room-assignments', label: 'Assignments', icon: '๐Ÿ”‘' }, { id: 'room-reservations', label: 'Room Bookings', icon: '๐Ÿ›๏ธ' }, { id: 'restaurant-reservations', label: 'Restaurant', icon: '๐Ÿฝ๏ธ' }, { id: 'orders', label: 'Food Orders', icon: '๐Ÿ“ฆ' }, + { id: 'kitchen', label: 'Kitchen', icon: '๐Ÿ‘จโ€๐Ÿณ' }, { id: 'amenities', label: 'Amenities', icon: 'โœจ' }, { id: 'trips', label: 'Trips', icon: 'โœˆ๏ธ' }, { id: 'messages', label: 'Messages', icon: '๐Ÿ’ฌ' }, @@ -1287,6 +1289,7 @@ function OrdersSection({ onToast }: { onToast: (msg: string, type: 'success' | '
{formatPrice(o.total)} โ€ข {formatDisplayDateTime(o.created_at)}
+ {o.username &&
๐Ÿ‘ค {o.first_name || o.username}
} void }) { + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(true); + const [autoRefresh, setAutoRefresh] = useState(true); + + const loadOrders = async () => { + try { + const res = await fetch('/api/orders?status=all'); + const data = await res.json(); + const active = (data.orders || []).filter((o: Order) => ['pending', 'preparing', 'ready'].includes(o.status)); + setOrders(active); + } catch { onToast('Error loading orders', 'error'); } + setLoading(false); + }; + + useEffect(() => { + loadOrders(); + if (autoRefresh) { + const interval = setInterval(loadOrders, 30000); + return () => clearInterval(interval); + } + }, [autoRefresh]); + + const updateStatus = async (id: number, status: string) => { + try { + const res = await fetch('/api/orders', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ order_id: id, status }), + }); + if (res.ok) { + if (status === 'delivered' || status === 'cancelled') { + setOrders(o => o.filter(x => x.id !== id)); + } else { + setOrders(o => o.map(x => x.id === id ? { ...x, status } : x)); + } + onToast(`Order ${status}`, 'success'); + } + } catch { onToast('Error updating order', 'error'); } + }; + + const formatPrice = (p: string) => `$${parseFloat(p).toFixed(2)}`; + const statusColor = (s: string) => s === 'pending' ? '#f59e0b' : s === 'preparing' ? '#3b82f6' : '#10b981'; + + const pending = orders.filter(o => o.status === 'pending'); + const preparing = orders.filter(o => o.status === 'preparing'); + const ready = orders.filter(o => o.status === 'ready'); + + const OrderCard = ({ o }: { o: Order }) => ( +
+
+
+
+ {o.order_type === 'room_delivery' ? `Room ${o.room_name || o.room_id}` : o.order_type === 'dine_in' ? 'Dine-in' : 'Pickup'} +
+
+ {formatPrice(o.total)} - {formatDisplayDateTime(o.created_at)} +
+ {o.username &&
User: {o.first_name || o.username}
} +
+ {o.status.toUpperCase()} +
+
+ {o.items?.map((item, i) => ( +
+ {item.quantity}x {item.name} + {formatPrice(item.total_price)} +
+ ))} +
+ {o.notes &&
Note: "{o.notes}"
} +
+ {o.status === 'pending' && ( + <> + updateStatus(o.id, 'preparing')}>Start Preparing + updateStatus(o.id, 'cancelled')}>Cancel + + )} + {o.status === 'preparing' && updateStatus(o.id, 'ready')}>Mark Ready} + {o.status === 'ready' && updateStatus(o.id, 'delivered')}>Delivered} +
+
+ ); + + return ( +
+
+

Kitchen Dashboard

+ +
+ + {loading ? : orders.length === 0 ? ( + + ) : ( +
+ {pending.length > 0 && ( +
+

Pending ({pending.length})

+
+ {pending.map(o => )} +
+
+ )} + {preparing.length > 0 && ( +
+

Preparing ({preparing.length})

+
+ {preparing.map(o => )} +
+
+ )} + {ready.length > 0 && ( +
+

Ready for Delivery ({ready.length})

+
+ {ready.map(o => )} +
+
+ )} +
+ )} +
+ ); +} + +/* Room Assignments Section */ +type RoomAssignment = { id: number; room_id: number; room_name: string; user_id: number; username: string; first_name: string | null; last_name: string | null; email: string; check_in: string; check_out: string; notes: string | null }; + +function RoomAssignmentsSection({ onToast }: { onToast: (msg: string, type: 'success' | 'error') => void }) { + const [assignments, setAssignments] = useState([]); + const [rooms, setRooms] = useState([]); + const [users, setUsers] = useState<{ id: number; username: string; first_name: string | null; last_name: string | null }[]>([]); + const [loading, setLoading] = useState(true); + const [showForm, setShowForm] = useState(false); + const [form, setForm] = useState({ room_id: '', user_id: '', check_in: '', check_out: '', notes: '' }); + + useEffect(() => { + Promise.all([ + fetch('/api/room-assignments').then(r => r.json()), + fetch('/api/rooms').then(r => r.json()), + fetch('/api/admin/users').then(r => r.json()), + ]).then(([assignData, roomData, userData]) => { + setAssignments(assignData.assignments || []); + setRooms(roomData.rooms || []); + setUsers(userData.users || []); + }).catch(() => onToast('Error loading data', 'error')) + .finally(() => setLoading(false)); + }, []); + + const today = new Date().toISOString().split('T')[0]; + const active = assignments.filter(a => a.check_in <= today && a.check_out >= today); + const upcoming = assignments.filter(a => a.check_in > today); + + const saveAssignment = async () => { + if (!form.room_id || !form.user_id || !form.check_in || !form.check_out) { + onToast('All fields required', 'error'); + return; + } + try { + const res = await fetch('/api/room-assignments', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(form), + }); + if (res.ok) { + const data = await res.json(); + setAssignments(a => [...a, data.assignment]); + setForm({ room_id: '', user_id: '', check_in: '', check_out: '', notes: '' }); + setShowForm(false); + onToast('Assignment created', 'success'); + } else { + const err = await res.json(); + onToast(err.error || 'Failed to create assignment', 'error'); + } + } catch { onToast('Error saving assignment', 'error'); } + }; + + const deleteAssignment = async (id: number) => { + if (!confirm('Delete this assignment?')) return; + try { + await fetch(`/api/room-assignments?id=${id}`, { method: 'DELETE' }); + setAssignments(a => a.filter(x => x.id !== id)); + onToast('Assignment deleted', 'success'); + } catch { onToast('Error deleting assignment', 'error'); } + }; + + return ( +
+ setShowForm(true)}>+ Assign Room} /> + + {showForm && ( +
+

New Assignment

+
+ setForm(f => ({ ...f, room_id: v }))} options={rooms.map(r => ({ value: r.id.toString(), label: r.name }))} /> + setForm(f => ({ ...f, user_id: v }))} options={users.map(u => ({ value: u.id.toString(), label: `${u.username} (${u.first_name || ''} ${u.last_name || ''})`.trim() }))} /> + setForm(f => ({ ...f, check_in: v }))} /> + setForm(f => ({ ...f, check_out: v }))} /> +
+ setForm(f => ({ ...f, notes: v }))} rows={2} /> +
+ Save Assignment + { setShowForm(false); setForm({ room_id: '', user_id: '', check_in: '', check_out: '', notes: '' }); }}>Cancel +
+
+ )} + + {loading ? : ( +
+
+

Currently Active ({active.length})

+ {active.length === 0 ? ( +
No active assignments
+ ) : ( +
+ {active.map(a => ( +
+
+
{a.room_name}
+
User: {a.first_name || a.username} {a.last_name || ''} ({a.email})
+
Dates: {a.check_in} to {a.check_out}
+ {a.notes &&
Note: {a.notes}
} +
+ deleteAssignment(a.id)}>Remove +
+ ))} +
+ )} +
+ + {upcoming.length > 0 && ( +
+

Upcoming ({upcoming.length})

+
+ {upcoming.map(a => ( +
+
+
{a.room_name}
+
User: {a.first_name || a.username} {a.last_name || ''}
+
Dates: {a.check_in} to {a.check_out}
+
+ deleteAssignment(a.id)}>Remove +
+ ))} +
+
+ )} +
+ )} +
+ ); +} + function RoomsSection({ rooms, setRooms, images, onToast }: any) { const [editing, setEditing] = useState(null); const [confirmDel, setConfirmDel] = useState(null); @@ -3388,6 +3652,8 @@ export default function AdminPage() { case 'room-reservations': return ; case 'restaurant-reservations': return ; case 'orders': return ; + case 'kitchen': return ; + case 'room-assignments': return ; case 'amenities': return ; case 'menu': return ; case 'places': return ; diff --git a/src/app/api/orders/route.ts b/src/app/api/orders/route.ts index a7bc8ed..eb8c026 100644 --- a/src/app/api/orders/route.ts +++ b/src/app/api/orders/route.ts @@ -122,6 +122,7 @@ export async function GET(request: Request) { const { searchParams } = new URL(request.url); const status = searchParams.get('status') || 'all'; const limit = parseInt(searchParams.get('limit') || '50'); + const kitchen = searchParams.get('kitchen') === 'true'; let query = ` SELECT o.*, r.name as room_name, u.username, u.first_name, u.last_name diff --git a/src/app/api/room-assignments/route.ts b/src/app/api/room-assignments/route.ts new file mode 100644 index 0000000..483c34e --- /dev/null +++ b/src/app/api/room-assignments/route.ts @@ -0,0 +1,111 @@ +import { NextResponse } from 'next/server'; +import { db } from '@/lib/db'; +import { getSession } from '@/lib/auth'; + +// GET - list all room assignments (admin) or current user's assignment +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 onlyActive = searchParams.get('active') === 'true'; + + if (user.role !== 'admin') { + // Regular user - return their current active assignment + const today = new Date().toISOString().split('T')[0]; + const { rows } = await db.query(` + SELECT ra.*, r.name as room_name, r.price + FROM room_assignments ra + JOIN rooms r ON ra.room_id = r.id + WHERE ra.user_id = $1 AND ra.check_in <= $2 AND ra.check_out >= $2 + `, [user.id, today]); + return NextResponse.json({ assignments: rows }); + } + + // Admin - return all assignments + let query = ` + SELECT ra.*, r.name as room_name, u.username, u.first_name, u.last_name, u.email + FROM room_assignments ra + JOIN rooms r ON ra.room_id = r.id + JOIN users u ON ra.user_id = u.id + `; + const params: any[] = []; + + if (onlyActive) { + const today = new Date().toISOString().split('T')[0]; + query += ' WHERE ra.check_in <= $1 AND ra.check_out >= $1'; + params.push(today); + } + + query += ' ORDER BY ra.check_in DESC'; + + const { rows } = await db.query(query, params); + return NextResponse.json({ assignments: rows }); + } catch (error) { + console.error('Get room assignments error:', error); + return NextResponse.json({ error: 'Failed to get room assignments' }, { status: 500 }); + } +} + +// POST - create new room assignment (admin only) +export async function POST(request: Request) { + try { + const user = await getSession(); + if (!user || user.role !== 'admin') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { room_id, user_id, check_in, check_out, notes } = await request.json(); + + if (!room_id || !user_id || !check_in || !check_out) { + return NextResponse.json({ error: 'Room, user, check-in, and check-out are required' }, { status: 400 }); + } + + // Check for conflicting assignments + const { rows: conflicts } = await db.query(` + SELECT id FROM room_assignments + WHERE room_id = $1 AND check_in < $3 AND check_out > $2 + `, [room_id, check_in, check_out]); + + if (conflicts.length > 0) { + return NextResponse.json({ error: 'Room has conflicting assignment for these dates' }, { status: 400 }); + } + + const { rows } = await db.query(` + INSERT INTO room_assignments (room_id, user_id, check_in, check_out, notes, created_by) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING * + `, [room_id, user_id, check_in, check_out, notes || null, user.id]); + + return NextResponse.json({ success: true, assignment: rows[0] }); + } catch (error) { + console.error('Create room assignment error:', error); + return NextResponse.json({ error: 'Failed to create room assignment' }, { status: 500 }); + } +} + +// DELETE - remove room assignment (admin only) +export async function DELETE(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 id = searchParams.get('id'); + + if (!id) { + return NextResponse.json({ error: 'Assignment ID required' }, { status: 400 }); + } + + await db.query('DELETE FROM room_assignments WHERE id = $1', [id]); + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Delete room assignment error:', error); + return NextResponse.json({ error: 'Failed to delete room assignment' }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/coffee/page.tsx b/src/app/coffee/page.tsx index f6c13c3..a8fb403 100644 --- a/src/app/coffee/page.tsx +++ b/src/app/coffee/page.tsx @@ -35,6 +35,7 @@ export default function CoffeePage() { const [items, setItems] = useState([]); const [rooms, setRooms] = useState([]); const [user, setUser] = useState(null); + const [assignedRoom, setAssignedRoom] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); @@ -62,10 +63,25 @@ export default function CoffeePage() { return res.json(); }), ]) - .then(([menuData, sessionData, roomsData]) => { + .then(async ([menuData, sessionData, roomsData]) => { setItems(menuData.data || []); setUser(sessionData.user || null); setRooms(roomsData.rooms || []); + + // Fetch user's active room assignment if logged in + if (sessionData.user) { + try { + const assignRes = await fetch('/api/room-assignments'); + if (assignRes.ok) { + const assignData = await assignRes.json(); + if (assignData.assignments?.length > 0) { + const assignment = assignData.assignments[0]; + setAssignedRoom({ id: assignment.room_id, name: assignment.room_name }); + setSelectedRoom(assignment.room_id); + } + } + } catch { /* ignore */ } + } }) .catch((err) => setError(err.message)) .finally(() => setLoading(false)); @@ -359,23 +375,38 @@ export default function CoffeePage() { {/* Room Selection for Delivery */} {orderType === 'room_delivery' && user && (
- - + background: '#fff9e6', + color: navy, + fontWeight: 500, + }}> + ๐Ÿจ {assignedRoom.name} +
+ ) : ( + + )} )} diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 93b5e4e..be0436e 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -498,6 +498,24 @@ export async function initSchema() { await db.query(`CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status)`); await db.query(`CREATE INDEX IF NOT EXISTS idx_order_items_order ON order_items(order_id)`); + // Room assignments - admins assign registered users to rooms + await db.query(` + CREATE TABLE IF NOT EXISTS room_assignments ( + id SERIAL PRIMARY KEY, + room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + check_in DATE NOT NULL, + check_out DATE NOT NULL, + notes TEXT, + created_by INTEGER REFERENCES users(id), + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(room_id, check_in, check_out) + ); + `); + await db.query(`CREATE INDEX IF NOT EXISTS idx_room_assignments_room ON room_assignments(room_id)`); + await db.query(`CREATE INDEX IF NOT EXISTS idx_room_assignments_user ON room_assignments(user_id)`); + await db.query(`CREATE INDEX IF NOT EXISTS idx_room_assignments_dates ON room_assignments(check_in, check_out)`); + // Audit log for admin actions await db.query(` CREATE TABLE IF NOT EXISTS audit_log (