diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 03bb6d7..893eef0 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1374,6 +1374,7 @@ function RoomsSection({ rooms, setRooms, images, onToast }: any) { } /* ─── Room Status Section ─── */ +type StaffMember = { id: number; first_name: string | null; last_name: string | null; username: string }; type RoomStatus = { id: number; name: string; @@ -1381,6 +1382,14 @@ type RoomStatus = { clean_status: 'clean' | 'dirty' | 'maintenance'; notes: string | null; active: boolean; + last_cleaned: string | null; + assigned_staff_id: number | null; + assigned_staff: { id: number; first_name: string | null; last_name: string | null } | null; + priority: 'urgent' | 'normal' | 'low'; + issues_count: number; + is_vip: boolean; + checkout_time: string | null; + checkout_date: string | null; occupancy_status: 'available' | 'occupied'; current_guest: string | null; reservation_id: number | null; @@ -1399,10 +1408,28 @@ type RoomStatus = { function RoomStatusSection({ onToast }: { onToast: (msg: string, type: 'success' | 'error') => void }) { const [rooms, setRooms] = useState([]); + const [staff, setStaff] = useState([]); const [loading, setLoading] = useState(true); const [editingRoom, setEditingRoom] = useState(null); - const [editNotes, setEditNotes] = useState(''); - const [editStatus, setEditStatus] = useState<'clean' | 'dirty' | 'maintenance'>('clean'); + const [editData, setEditData] = useState<{ + clean_status: 'clean' | 'dirty' | 'maintenance'; + notes: string; + priority: 'urgent' | 'normal' | 'low'; + assigned_staff_id: number | null; + issues_count: number; + is_vip: boolean; + checkout_time: string; + checkout_date: string; + }>({ + clean_status: 'clean', + notes: '', + priority: 'normal', + assigned_staff_id: null, + issues_count: 0, + is_vip: false, + checkout_time: '', + checkout_date: '', + }); useEffect(() => { loadRooms(); }, []); @@ -1412,25 +1439,51 @@ function RoomStatusSection({ onToast }: { onToast: (msg: string, type: 'success' const res = await fetch('/api/admin/room-status'); const data = await res.json(); setRooms(data.rooms || []); + setStaff(data.staff || []); } catch { onToast('Error loading room status', 'error'); } setLoading(false); }; - const updateRoomStatus = async (roomId: number, cleanStatus: 'clean' | 'dirty' | 'maintenance', notes: string) => { + const updateRoomStatus = async () => { + if (editingRoom === null) return; try { const res = await fetch('/api/admin/room-status', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ id: roomId, clean_status: cleanStatus, notes }), + body: JSON.stringify({ + id: editingRoom, + clean_status: editData.clean_status, + notes: editData.notes || null, + priority: editData.priority, + assigned_staff_id: editData.assigned_staff_id, + issues_count: editData.issues_count, + is_vip: editData.is_vip, + checkout_time: editData.checkout_time || null, + checkout_date: editData.checkout_date || null, + }), }); if (res.ok) { - setRooms(prev => prev.map(r => r.id === roomId ? { ...r, clean_status: cleanStatus, notes } : r)); + await loadRooms(); onToast('Room status updated', 'success'); } } catch { onToast('Error updating status', 'error'); } setEditingRoom(null); }; + const startEdit = (room: RoomStatus) => { + setEditingRoom(room.id); + setEditData({ + clean_status: room.clean_status, + notes: room.notes || '', + priority: room.priority, + assigned_staff_id: room.assigned_staff_id, + issues_count: room.issues_count, + is_vip: room.is_vip, + checkout_time: room.checkout_time || '', + checkout_date: room.checkout_date || '', + }); + }; + const getStatusIcon = (status: string) => { switch (status) { case 'clean': return '✨'; @@ -1449,6 +1502,15 @@ function RoomStatusSection({ onToast }: { onToast: (msg: string, type: 'success' } }; + const getPriorityStyle = (priority: string) => { + switch (priority) { + case 'urgent': return { bg: '#fee2e2', color: '#b91c1c', icon: '🔴' }; + case 'normal': return { bg: '#dbeafe', color: '#1d4ed8', icon: '🔵' }; + case 'low': return { bg: '#f3f4f6', color: '#6b7280', icon: '⚪' }; + default: return { bg: '#f3f4f6', color: '#6b7280', icon: '⚪' }; + } + }; + const getPaymentBadge = (status: string | null) => { if (!status) return null; const colors: Record = { @@ -1465,6 +1527,8 @@ function RoomStatusSection({ onToast }: { onToast: (msg: string, type: 'success' }; const formatDate = (d: string) => new Date(d).toLocaleDateString(); + const formatDateTime = (d: string) => new Date(d).toLocaleString(); + const formatTime = (t: string) => t ? t.substring(0, 5) : ''; return (
@@ -1477,18 +1541,24 @@ function RoomStatusSection({ onToast }: { onToast: (msg: string, type: 'success'
{rooms.map(room => { const statusColors = getStatusColor(room.clean_status); + const priorityStyle = getPriorityStyle(room.priority); const isEditing = editingRoom === room.id; return (
{/* Header Row */} -
+
{/* Room Name & Status */} -
+
{getStatusIcon(room.clean_status)} {room.name}
+ {room.is_vip && ( + + ⭐ VIP + + )} {room.occupancy_status === 'occupied' ? '🛏️ Occupied' : '🚪 Available'} + + {priorityStyle.icon} {room.priority} + + {room.issues_count > 0 && ( + + ⚠️ {room.issues_count} issue{room.issues_count > 1 ? 's' : ''} + + )}
{/* Price */} @@ -1518,25 +1603,49 @@ function RoomStatusSection({ onToast }: { onToast: (msg: string, type: 'success'
{/* Edit Button */} - { - setEditingRoom(isEditing ? null : room.id); - setEditStatus(room.clean_status); - setEditNotes(room.notes || ''); - }}> + isEditing ? setEditingRoom(null) : startEdit(room)}> {isEditing ? 'Cancel' : 'Edit'}
{/* Details Row */}
- {/* Current Guest */} - {room.current_guest && ( -
- Current Guest: - {room.current_guest} - {getPaymentBadge(room.payment_status)} -
- )} + {/* Quick Info Grid */} +
+ {/* Current Guest */} + {room.current_guest && ( +
+ Guest: + {room.current_guest} + {getPaymentBadge(room.payment_status)} +
+ )} + + {/* Checkout */} + {room.checkout_date && ( +
+ Checkout: + {formatDate(room.checkout_date)} + {room.checkout_time && at {formatTime(room.checkout_time)}} +
+ )} + + {/* Last Cleaned */} + {room.last_cleaned && ( +
+ Last cleaned: + {formatDateTime(room.last_cleaned)} +
+ )} + + {/* Assigned Staff */} + {room.assigned_staff && ( +
+ Staff: + {room.assigned_staff.first_name || ''} {room.assigned_staff.last_name || ''} +
+ )} +
{/* Upcoming Reservations */} {room.upcoming_reservations && room.upcoming_reservations.length > 0 && ( @@ -1562,25 +1671,76 @@ function RoomStatusSection({ onToast }: { onToast: (msg: string, type: 'success' {/* Edit Form */} {isEditing && (
- setEditStatus(v)} - options={[ - { value: 'clean', label: '✨ Clean' }, - { value: 'dirty', label: '🧹 Dirty' }, - { value: 'maintenance', label: '🔧 Maintenance' }, - ]} - /> +
+ setEditData(prev => ({ ...prev, clean_status: v }))} + options={[ + { value: 'clean', label: '✨ Clean' }, + { value: 'dirty', label: '🧹 Dirty' }, + { value: 'maintenance', label: '🔧 Maintenance' }, + ]} + /> + setEditData(prev => ({ ...prev, priority: v }))} + options={[ + { value: 'urgent', label: '🔴 Urgent' }, + { value: 'normal', label: '🔵 Normal' }, + { value: 'low', label: '⚪ Low' }, + ]} + /> + setEditData(prev => ({ ...prev, assigned_staff_id: v ? parseInt(v) : null }))} + options={[ + { value: '', label: '— Unassigned —' }, + ...staff.map(s => ({ value: s.id.toString(), label: `${s.first_name || ''} ${s.last_name || ''}`.trim() || s.username })) + ]} + /> +
+
+ setEditData(prev => ({ ...prev, issues_count: parseInt(v) || 0 }))} + placeholder="0" + /> + setEditData(prev => ({ ...prev, checkout_date: v }))} + /> + setEditData(prev => ({ ...prev, checkout_time: v }))} + /> +
+ setEditData(prev => ({ ...prev, notes: v }))} rows={2} placeholder="e.g., Need extra towels, AC broken..." />
- updateRoomStatus(room.id, editStatus, editNotes)} size="sm">Save + Save setEditingRoom(null)} size="sm">Cancel
diff --git a/src/app/api/admin/room-status/route.ts b/src/app/api/admin/room-status/route.ts index 68c9b98..311adfe 100644 --- a/src/app/api/admin/room-status/route.ts +++ b/src/app/api/admin/room-status/route.ts @@ -8,7 +8,7 @@ export async function GET() { try { await requireRole('admin'); - // Get all rooms with current reservation info + // Get all rooms with current reservation info and staff assignment const { rows } = await db.query(` SELECT r.id, @@ -17,6 +17,13 @@ export async function GET() { r.clean_status, r.notes, r.active, + r.last_cleaned, + r.assigned_staff_id, + r.priority, + r.issues_count, + r.is_vip, + r.checkout_time, + r.checkout_date, rr.id as reservation_id, rr.guest_name, rr.check_in, @@ -26,6 +33,8 @@ export async function GET() { u.first_name, u.last_name, u.username, + s.first_name as staff_first_name, + s.last_name as staff_last_name, CASE WHEN rr.id IS NOT NULL AND rr.status = 'confirmed' AND CURRENT_DATE >= rr.check_in @@ -39,7 +48,14 @@ export async function GET() { AND CURRENT_DATE >= rr.check_in AND CURRENT_DATE <= rr.check_out LEFT JOIN users u ON rr.user_id = u.id - ORDER BY r.name + LEFT JOIN users s ON r.assigned_staff_id = s.id + ORDER BY + CASE r.priority + WHEN 'urgent' THEN 1 + WHEN 'normal' THEN 2 + WHEN 'low' THEN 3 + END, + r.name `); // Get upcoming reservations for each room @@ -67,10 +83,23 @@ export async function GET() { ...room, upcoming_reservations: upcoming, current_guest: room.guest_name || (room.first_name ? `${room.first_name} ${room.last_name || ''}`.trim() : room.username) || null, + assigned_staff: room.assigned_staff_id ? { + id: room.assigned_staff_id, + first_name: room.staff_first_name, + last_name: room.staff_last_name, + } : null, }; })); - return NextResponse.json({ rooms }); + // Get all staff members (users who can be assigned to rooms) + const { rows: staff } = await db.query(` + SELECT id, first_name, last_name, username + FROM users + WHERE role IN ('admin', 'staff') OR role = 'user' + ORDER BY first_name, last_name + `); + + return NextResponse.json({ rooms, staff }); } catch (error) { console.error('Get room status error:', error); return NextResponse.json( @@ -85,7 +114,19 @@ export async function PATCH(request: NextRequest) { try { await requireRole('admin'); - const { id, clean_status, notes } = await request.json(); + const body = await request.json(); + const { + id, + clean_status, + notes, + last_cleaned, + assigned_staff_id, + priority, + issues_count, + is_vip, + checkout_time, + checkout_date, + } = body; if (!id) { return NextResponse.json({ error: 'Room ID is required' }, { status: 400 }); @@ -107,6 +148,53 @@ export async function PATCH(request: NextRequest) { paramIndex++; } + if (last_cleaned !== undefined) { + updates.push(`last_cleaned = $${paramIndex}`); + values.push(last_cleaned); + paramIndex++; + } + + if (assigned_staff_id !== undefined) { + updates.push(`assigned_staff_id = $${paramIndex}`); + values.push(assigned_staff_id || null); + paramIndex++; + } + + if (priority !== undefined) { + updates.push(`priority = $${paramIndex}`); + values.push(priority); + paramIndex++; + } + + if (issues_count !== undefined) { + updates.push(`issues_count = $${paramIndex}`); + values.push(issues_count); + paramIndex++; + } + + if (is_vip !== undefined) { + updates.push(`is_vip = $${paramIndex}`); + values.push(is_vip); + paramIndex++; + } + + if (checkout_time !== undefined) { + updates.push(`checkout_time = $${paramIndex}`); + values.push(checkout_time || null); + paramIndex++; + } + + if (checkout_date !== undefined) { + updates.push(`checkout_date = $${paramIndex}`); + values.push(checkout_date || null); + paramIndex++; + } + + // Auto-set last_cleaned when status changes to 'clean' + if (clean_status === 'clean') { + updates.push(`last_cleaned = NOW()`); + } + if (updates.length === 0) { return NextResponse.json({ error: 'No updates provided' }, { status: 400 }); } diff --git a/src/lib/schema.ts b/src/lib/schema.ts index c09c6e9..93b5e4e 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -538,6 +538,13 @@ export async function migrateFromLegacyPlaces() { // Add room status columns for housekeeping management await db.query(`ALTER TABLE rooms ADD COLUMN IF NOT EXISTS clean_status VARCHAR(20) DEFAULT 'clean'`); await db.query(`ALTER TABLE rooms ADD COLUMN IF NOT EXISTS notes TEXT`); + await db.query(`ALTER TABLE rooms ADD COLUMN IF NOT EXISTS last_cleaned TIMESTAMP`); + await db.query(`ALTER TABLE rooms ADD COLUMN IF NOT EXISTS assigned_staff_id INTEGER REFERENCES users(id)`); + await db.query(`ALTER TABLE rooms ADD COLUMN IF NOT EXISTS priority VARCHAR(10) DEFAULT 'normal'`); + await db.query(`ALTER TABLE rooms ADD COLUMN IF NOT EXISTS issues_count INTEGER DEFAULT 0`); + await db.query(`ALTER TABLE rooms ADD COLUMN IF NOT EXISTS is_vip BOOLEAN DEFAULT FALSE`); + await db.query(`ALTER TABLE rooms ADD COLUMN IF NOT EXISTS checkout_time TIME`); + await db.query(`ALTER TABLE rooms ADD COLUMN IF NOT EXISTS checkout_date DATE`); // Add payment_status to room_reservations await db.query(`ALTER TABLE room_reservations ADD COLUMN IF NOT EXISTS payment_status VARCHAR(20) DEFAULT 'pending'`);