feat: add housekeeping fields to room status
- Add checkout time/date for current guest - Add last_cleaned timestamp - Add assigned_staff_id with staff dropdown - Add priority (urgent/normal/low) for cleaning order - Add issues_count for tracking problems - Add is_vip flag for VIP guests - Update RoomStatusSection UI to display and edit all new fields - Auto-set last_cleaned when status changes to 'clean'
This commit is contained in:
+193
-33
@@ -1374,6 +1374,7 @@ function RoomsSection({ rooms, setRooms, images, onToast }: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Room Status Section ─── */
|
/* ─── Room Status Section ─── */
|
||||||
|
type StaffMember = { id: number; first_name: string | null; last_name: string | null; username: string };
|
||||||
type RoomStatus = {
|
type RoomStatus = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -1381,6 +1382,14 @@ type RoomStatus = {
|
|||||||
clean_status: 'clean' | 'dirty' | 'maintenance';
|
clean_status: 'clean' | 'dirty' | 'maintenance';
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
active: boolean;
|
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';
|
occupancy_status: 'available' | 'occupied';
|
||||||
current_guest: string | null;
|
current_guest: string | null;
|
||||||
reservation_id: number | null;
|
reservation_id: number | null;
|
||||||
@@ -1399,10 +1408,28 @@ type RoomStatus = {
|
|||||||
|
|
||||||
function RoomStatusSection({ onToast }: { onToast: (msg: string, type: 'success' | 'error') => void }) {
|
function RoomStatusSection({ onToast }: { onToast: (msg: string, type: 'success' | 'error') => void }) {
|
||||||
const [rooms, setRooms] = useState<RoomStatus[]>([]);
|
const [rooms, setRooms] = useState<RoomStatus[]>([]);
|
||||||
|
const [staff, setStaff] = useState<StaffMember[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [editingRoom, setEditingRoom] = useState<number | null>(null);
|
const [editingRoom, setEditingRoom] = useState<number | null>(null);
|
||||||
const [editNotes, setEditNotes] = useState('');
|
const [editData, setEditData] = useState<{
|
||||||
const [editStatus, setEditStatus] = useState<'clean' | 'dirty' | 'maintenance'>('clean');
|
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(); }, []);
|
useEffect(() => { loadRooms(); }, []);
|
||||||
|
|
||||||
@@ -1412,25 +1439,51 @@ function RoomStatusSection({ onToast }: { onToast: (msg: string, type: 'success'
|
|||||||
const res = await fetch('/api/admin/room-status');
|
const res = await fetch('/api/admin/room-status');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setRooms(data.rooms || []);
|
setRooms(data.rooms || []);
|
||||||
|
setStaff(data.staff || []);
|
||||||
} catch { onToast('Error loading room status', 'error'); }
|
} catch { onToast('Error loading room status', 'error'); }
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateRoomStatus = async (roomId: number, cleanStatus: 'clean' | 'dirty' | 'maintenance', notes: string) => {
|
const updateRoomStatus = async () => {
|
||||||
|
if (editingRoom === null) return;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/admin/room-status', {
|
const res = await fetch('/api/admin/room-status', {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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) {
|
if (res.ok) {
|
||||||
setRooms(prev => prev.map(r => r.id === roomId ? { ...r, clean_status: cleanStatus, notes } : r));
|
await loadRooms();
|
||||||
onToast('Room status updated', 'success');
|
onToast('Room status updated', 'success');
|
||||||
}
|
}
|
||||||
} catch { onToast('Error updating status', 'error'); }
|
} catch { onToast('Error updating status', 'error'); }
|
||||||
setEditingRoom(null);
|
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) => {
|
const getStatusIcon = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'clean': return '✨';
|
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) => {
|
const getPaymentBadge = (status: string | null) => {
|
||||||
if (!status) return null;
|
if (!status) return null;
|
||||||
const colors: Record<string, { bg: string; color: string }> = {
|
const colors: Record<string, { bg: string; color: string }> = {
|
||||||
@@ -1465,6 +1527,8 @@ function RoomStatusSection({ onToast }: { onToast: (msg: string, type: 'success'
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (d: string) => new Date(d).toLocaleDateString();
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -1477,18 +1541,24 @@ function RoomStatusSection({ onToast }: { onToast: (msg: string, type: 'success'
|
|||||||
<div style={{ display: 'grid', gap: '12px' }}>
|
<div style={{ display: 'grid', gap: '12px' }}>
|
||||||
{rooms.map(room => {
|
{rooms.map(room => {
|
||||||
const statusColors = getStatusColor(room.clean_status);
|
const statusColors = getStatusColor(room.clean_status);
|
||||||
|
const priorityStyle = getPriorityStyle(room.priority);
|
||||||
const isEditing = editingRoom === room.id;
|
const isEditing = editingRoom === room.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={room.id} style={{ background: '#fff', borderRadius: 12, border: `1px solid ${C.border}`, overflow: 'hidden' }}>
|
<div key={room.id} style={{ background: '#fff', borderRadius: 12, border: `1px solid ${C.border}`, overflow: 'hidden' }}>
|
||||||
{/* Header Row */}
|
{/* Header Row */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', padding: '12px 16px', background: room.occupancy_status === 'occupied' ? '#f0fdf4' : '#fafafa', gap: '12px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', padding: '12px 16px', background: room.is_vip ? '#fef3c7' : room.occupancy_status === 'occupied' ? '#f0fdf4' : '#fafafa', gap: '12px' }}>
|
||||||
{/* Room Name & Status */}
|
{/* Room Name & Status */}
|
||||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: '10px' }}>
|
<div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: '10px', flexWrap: 'wrap' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||||
<span style={{ fontSize: '20px' }}>{getStatusIcon(room.clean_status)}</span>
|
<span style={{ fontSize: '20px' }}>{getStatusIcon(room.clean_status)}</span>
|
||||||
<span style={{ fontWeight: 600, fontSize: '16px', color: C.navy }}>{room.name}</span>
|
<span style={{ fontWeight: 600, fontSize: '16px', color: C.navy }}>{room.name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{room.is_vip && (
|
||||||
|
<span style={{ padding: '2px 8px', borderRadius: 12, fontSize: '11px', fontWeight: 700, background: '#fbbf24', color: '#78350f' }}>
|
||||||
|
⭐ VIP
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span style={{
|
<span style={{
|
||||||
padding: '4px 10px',
|
padding: '4px 10px',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
@@ -1510,6 +1580,21 @@ function RoomStatusSection({ onToast }: { onToast: (msg: string, type: 'success'
|
|||||||
}}>
|
}}>
|
||||||
{room.occupancy_status === 'occupied' ? '🛏️ Occupied' : '🚪 Available'}
|
{room.occupancy_status === 'occupied' ? '🛏️ Occupied' : '🚪 Available'}
|
||||||
</span>
|
</span>
|
||||||
|
<span style={{
|
||||||
|
padding: '4px 10px',
|
||||||
|
borderRadius: 12,
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: 600,
|
||||||
|
background: priorityStyle.bg,
|
||||||
|
color: priorityStyle.color
|
||||||
|
}}>
|
||||||
|
{priorityStyle.icon} {room.priority}
|
||||||
|
</span>
|
||||||
|
{room.issues_count > 0 && (
|
||||||
|
<span style={{ padding: '2px 8px', borderRadius: 12, fontSize: '11px', fontWeight: 600, background: '#fee2e2', color: '#b91c1c' }}>
|
||||||
|
⚠️ {room.issues_count} issue{room.issues_count > 1 ? 's' : ''}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Price */}
|
{/* Price */}
|
||||||
@@ -1518,25 +1603,49 @@ function RoomStatusSection({ onToast }: { onToast: (msg: string, type: 'success'
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Edit Button */}
|
{/* Edit Button */}
|
||||||
<Btn size="sm" variant="secondary" onClick={() => {
|
<Btn size="sm" variant="secondary" onClick={() => isEditing ? setEditingRoom(null) : startEdit(room)}>
|
||||||
setEditingRoom(isEditing ? null : room.id);
|
|
||||||
setEditStatus(room.clean_status);
|
|
||||||
setEditNotes(room.notes || '');
|
|
||||||
}}>
|
|
||||||
{isEditing ? 'Cancel' : 'Edit'}
|
{isEditing ? 'Cancel' : 'Edit'}
|
||||||
</Btn>
|
</Btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Details Row */}
|
{/* Details Row */}
|
||||||
<div style={{ padding: '12px 16px', borderTop: `1px solid ${C.border}` }}>
|
<div style={{ padding: '12px 16px', borderTop: `1px solid ${C.border}` }}>
|
||||||
{/* Current Guest */}
|
{/* Quick Info Grid */}
|
||||||
{room.current_guest && (
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '8px', marginBottom: '12px' }}>
|
||||||
<div style={{ marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
{/* Current Guest */}
|
||||||
<span style={{ fontSize: '13px', color: '#666' }}>Current Guest:</span>
|
{room.current_guest && (
|
||||||
<span style={{ fontWeight: 600, color: C.navy }}>{room.current_guest}</span>
|
<div style={{ fontSize: '13px' }}>
|
||||||
{getPaymentBadge(room.payment_status)}
|
<span style={{ color: '#666' }}>Guest: </span>
|
||||||
</div>
|
<span style={{ fontWeight: 600, color: C.navy }}>{room.current_guest}</span>
|
||||||
)}
|
{getPaymentBadge(room.payment_status)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Checkout */}
|
||||||
|
{room.checkout_date && (
|
||||||
|
<div style={{ fontSize: '13px' }}>
|
||||||
|
<span style={{ color: '#666' }}>Checkout: </span>
|
||||||
|
<span style={{ fontWeight: 500 }}>{formatDate(room.checkout_date)}</span>
|
||||||
|
{room.checkout_time && <span> at {formatTime(room.checkout_time)}</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Last Cleaned */}
|
||||||
|
{room.last_cleaned && (
|
||||||
|
<div style={{ fontSize: '13px' }}>
|
||||||
|
<span style={{ color: '#666' }}>Last cleaned: </span>
|
||||||
|
<span style={{ fontWeight: 500 }}>{formatDateTime(room.last_cleaned)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Assigned Staff */}
|
||||||
|
{room.assigned_staff && (
|
||||||
|
<div style={{ fontSize: '13px' }}>
|
||||||
|
<span style={{ color: '#666' }}>Staff: </span>
|
||||||
|
<span style={{ fontWeight: 500 }}>{room.assigned_staff.first_name || ''} {room.assigned_staff.last_name || ''}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Upcoming Reservations */}
|
{/* Upcoming Reservations */}
|
||||||
{room.upcoming_reservations && room.upcoming_reservations.length > 0 && (
|
{room.upcoming_reservations && room.upcoming_reservations.length > 0 && (
|
||||||
@@ -1562,25 +1671,76 @@ function RoomStatusSection({ onToast }: { onToast: (msg: string, type: 'success'
|
|||||||
{/* Edit Form */}
|
{/* Edit Form */}
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<div style={{ marginTop: '12px', display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
<div style={{ marginTop: '12px', display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||||
<S
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '12px' }}>
|
||||||
label="Clean Status"
|
<S
|
||||||
value={editStatus}
|
label="Clean Status"
|
||||||
onChange={(v: any) => setEditStatus(v)}
|
value={editData.clean_status}
|
||||||
options={[
|
onChange={(v: any) => setEditData(prev => ({ ...prev, clean_status: v }))}
|
||||||
{ value: 'clean', label: '✨ Clean' },
|
options={[
|
||||||
{ value: 'dirty', label: '🧹 Dirty' },
|
{ value: 'clean', label: '✨ Clean' },
|
||||||
{ value: 'maintenance', label: '🔧 Maintenance' },
|
{ value: 'dirty', label: '🧹 Dirty' },
|
||||||
]}
|
{ value: 'maintenance', label: '🔧 Maintenance' },
|
||||||
/>
|
]}
|
||||||
|
/>
|
||||||
|
<S
|
||||||
|
label="Priority"
|
||||||
|
value={editData.priority}
|
||||||
|
onChange={(v: any) => setEditData(prev => ({ ...prev, priority: v }))}
|
||||||
|
options={[
|
||||||
|
{ value: 'urgent', label: '🔴 Urgent' },
|
||||||
|
{ value: 'normal', label: '🔵 Normal' },
|
||||||
|
{ value: 'low', label: '⚪ Low' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<S
|
||||||
|
label="Assigned Staff"
|
||||||
|
value={editData.assigned_staff_id?.toString() || ''}
|
||||||
|
onChange={(v: any) => 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 }))
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: '12px' }}>
|
||||||
|
<I
|
||||||
|
label="Issues Count"
|
||||||
|
type="number"
|
||||||
|
value={editData.issues_count.toString()}
|
||||||
|
onChange={(v: string) => setEditData(prev => ({ ...prev, issues_count: parseInt(v) || 0 }))}
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
<I
|
||||||
|
label="Checkout Date"
|
||||||
|
type="date"
|
||||||
|
value={editData.checkout_date}
|
||||||
|
onChange={(v: string) => setEditData(prev => ({ ...prev, checkout_date: v }))}
|
||||||
|
/>
|
||||||
|
<I
|
||||||
|
label="Checkout Time"
|
||||||
|
type="time"
|
||||||
|
value={editData.checkout_time}
|
||||||
|
onChange={(v: string) => setEditData(prev => ({ ...prev, checkout_time: v }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={editData.is_vip}
|
||||||
|
onChange={(e) => setEditData(prev => ({ ...prev, is_vip: e.target.checked }))}
|
||||||
|
style={{ width: 18, height: 18 }}
|
||||||
|
/>
|
||||||
|
<span style={{ fontWeight: 500 }}>⭐ VIP Guest</span>
|
||||||
|
</label>
|
||||||
<TA
|
<TA
|
||||||
label="Notes"
|
label="Notes"
|
||||||
value={editNotes}
|
value={editData.notes}
|
||||||
onChange={setEditNotes}
|
onChange={(v: string) => setEditData(prev => ({ ...prev, notes: v }))}
|
||||||
rows={2}
|
rows={2}
|
||||||
placeholder="e.g., Need extra towels, AC broken..."
|
placeholder="e.g., Need extra towels, AC broken..."
|
||||||
/>
|
/>
|
||||||
<div style={{ display: 'flex', gap: '8px' }}>
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
<Btn onClick={() => updateRoomStatus(room.id, editStatus, editNotes)} size="sm">Save</Btn>
|
<Btn onClick={updateRoomStatus} size="sm">Save</Btn>
|
||||||
<Btn variant="secondary" onClick={() => setEditingRoom(null)} size="sm">Cancel</Btn>
|
<Btn variant="secondary" onClick={() => setEditingRoom(null)} size="sm">Cancel</Btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export async function GET() {
|
|||||||
try {
|
try {
|
||||||
await requireRole('admin');
|
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(`
|
const { rows } = await db.query(`
|
||||||
SELECT
|
SELECT
|
||||||
r.id,
|
r.id,
|
||||||
@@ -17,6 +17,13 @@ export async function GET() {
|
|||||||
r.clean_status,
|
r.clean_status,
|
||||||
r.notes,
|
r.notes,
|
||||||
r.active,
|
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.id as reservation_id,
|
||||||
rr.guest_name,
|
rr.guest_name,
|
||||||
rr.check_in,
|
rr.check_in,
|
||||||
@@ -26,6 +33,8 @@ export async function GET() {
|
|||||||
u.first_name,
|
u.first_name,
|
||||||
u.last_name,
|
u.last_name,
|
||||||
u.username,
|
u.username,
|
||||||
|
s.first_name as staff_first_name,
|
||||||
|
s.last_name as staff_last_name,
|
||||||
CASE
|
CASE
|
||||||
WHEN rr.id IS NOT NULL AND rr.status = 'confirmed'
|
WHEN rr.id IS NOT NULL AND rr.status = 'confirmed'
|
||||||
AND CURRENT_DATE >= rr.check_in
|
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_in
|
||||||
AND CURRENT_DATE <= rr.check_out
|
AND CURRENT_DATE <= rr.check_out
|
||||||
LEFT JOIN users u ON rr.user_id = u.id
|
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
|
// Get upcoming reservations for each room
|
||||||
@@ -67,10 +83,23 @@ export async function GET() {
|
|||||||
...room,
|
...room,
|
||||||
upcoming_reservations: upcoming,
|
upcoming_reservations: upcoming,
|
||||||
current_guest: room.guest_name || (room.first_name ? `${room.first_name} ${room.last_name || ''}`.trim() : room.username) || null,
|
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) {
|
} catch (error) {
|
||||||
console.error('Get room status error:', error);
|
console.error('Get room status error:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -85,7 +114,19 @@ export async function PATCH(request: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
await requireRole('admin');
|
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) {
|
if (!id) {
|
||||||
return NextResponse.json({ error: 'Room ID is required' }, { status: 400 });
|
return NextResponse.json({ error: 'Room ID is required' }, { status: 400 });
|
||||||
@@ -107,6 +148,53 @@ export async function PATCH(request: NextRequest) {
|
|||||||
paramIndex++;
|
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) {
|
if (updates.length === 0) {
|
||||||
return NextResponse.json({ error: 'No updates provided' }, { status: 400 });
|
return NextResponse.json({ error: 'No updates provided' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -538,6 +538,13 @@ export async function migrateFromLegacyPlaces() {
|
|||||||
// Add room status columns for housekeeping management
|
// 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 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 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
|
// Add payment_status to room_reservations
|
||||||
await db.query(`ALTER TABLE room_reservations ADD COLUMN IF NOT EXISTS payment_status VARCHAR(20) DEFAULT 'pending'`);
|
await db.query(`ALTER TABLE room_reservations ADD COLUMN IF NOT EXISTS payment_status VARCHAR(20) DEFAULT 'pending'`);
|
||||||
|
|||||||
Reference in New Issue
Block a user