feat: room status table view with all rooms

This commit is contained in:
2026-07-03 00:34:43 -05:00
parent 4e514c2fbb
commit 9286f3e323
+123 -207
View File
@@ -1814,218 +1814,134 @@ function RoomStatusSection({ onToast }: { onToast: (msg: string, type: 'success'
) : rooms.length === 0 ? (
<Empty icon="🏨" title="No rooms" desc="Add rooms in the Rooms section" />
) : (
<div style={{ display: 'grid', gap: '12px' }}>
{rooms.map(room => {
const statusColors = getStatusColor(room.clean_status);
const priorityStyle = getPriorityStyle(room.priority);
const isEditing = editingRoom === room.id;
return (
<div key={room.id} style={{ background: '#fff', borderRadius: 12, border: `1px solid ${C.border}`, overflow: 'hidden' }}>
{/* Header Row */}
<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 */}
<div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: '10px', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ fontSize: '20px' }}>{getStatusIcon(room.clean_status)}</span>
<span style={{ fontWeight: 600, fontSize: '16px', color: C.navy }}>{room.name}</span>
</div>
{room.is_vip && (
<span style={{ padding: '2px 8px', borderRadius: 12, fontSize: '11px', fontWeight: 700, background: '#fbbf24', color: '#78350f' }}>
VIP
<>
{/* Compact Room Availability Table */}
<div style={{ background: '#fff', borderRadius: 12, border: `1px solid ${C.border}`, marginBottom: '16px', overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ background: C.navy, color: '#fff' }}>
<th style={{ padding: '12px 16px', textAlign: 'left', fontWeight: 600 }}>Room</th>
<th style={{ padding: '12px 16px', textAlign: 'center', fontWeight: 600 }}>Status</th>
<th style={{ padding: '12px 16px', textAlign: 'center', fontWeight: 600 }}>Occupancy</th>
<th style={{ padding: '12px 16px', textAlign: 'center', fontWeight: 600 }}>Priority</th>
<th style={{ padding: '12px 16px', textAlign: 'left', fontWeight: 600 }}>Guest</th>
<th style={{ padding: '12px 16px', textAlign: 'right', fontWeight: 600 }}>Price</th>
</tr>
</thead>
<tbody>
{rooms.map((room, i) => (
<tr key={room.id} style={{ background: i % 2 === 0 ? '#fff' : '#f9fafb', cursor: 'pointer' }} onClick={() => { setEditingRoom(editingRoom === room.id ? null : room.id); if (editingRoom !== room.id) startEdit(room); }}>
<td style={{ padding: '10px 16px', fontWeight: 600, display: 'flex', alignItems: 'center', gap: '8px' }}>
{room.is_vip && <span></span>}
{room.name}
</td>
<td style={{ padding: '10px 16px', textAlign: 'center' }}>
<span style={{ padding: '4px 10px', borderRadius: 12, fontSize: '11px', fontWeight: 600, background: getStatusColor(room.clean_status).bg, color: getStatusColor(room.clean_status).color }}>
{getStatusIcon(room.clean_status)} {room.clean_status}
</span>
)}
<span style={{
padding: '4px 10px',
borderRadius: 12,
fontSize: '12px',
fontWeight: 600,
background: statusColors.bg,
color: statusColors.color,
border: `1px solid ${statusColors.border}`
}}>
{room.clean_status}
</span>
<span style={{
padding: '4px 10px',
borderRadius: 12,
fontSize: '12px',
fontWeight: 600,
background: room.occupancy_status === 'occupied' ? '#d1fae5' : '#e0e7ff',
color: room.occupancy_status === 'occupied' ? '#047857' : '#4338ca'
}}>
{room.occupancy_status === 'occupied' ? '🛏️ Occupied' : '🚪 Available'}
</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' : ''}
</td>
<td style={{ padding: '10px 16px', textAlign: 'center' }}>
<span style={{ padding: '4px 10px', borderRadius: 12, fontSize: '11px', fontWeight: 600, background: room.occupancy_status === 'occupied' ? '#d1fae5' : '#e0e7ff', color: room.occupancy_status === 'occupied' ? '#047857' : '#4338ca' }}>
{room.occupancy_status === 'occupied' ? '🛏️ Occupied' : '🚪 Available'}
</span>
)}
</div>
{/* Price */}
<div style={{ fontWeight: 600, color: C.gold, fontSize: '15px' }}>
${parseFloat(room.price).toFixed(2)}/night
</div>
{/* Edit Button */}
<Btn size="sm" variant="secondary" onClick={() => isEditing ? setEditingRoom(null) : startEdit(room)}>
{isEditing ? 'Cancel' : 'Edit'}
</Btn>
</div>
{/* Details Row */}
<div style={{ padding: '12px 16px', borderTop: `1px solid ${C.border}` }}>
{/* Quick Info Grid */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '8px', marginBottom: '12px' }}>
{/* Current Guest */}
{room.current_guest && (
<div style={{ fontSize: '13px' }}>
<span style={{ color: '#666' }}>Guest: </span>
<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 */}
{room.upcoming_reservations && room.upcoming_reservations.length > 0 && (
<div style={{ marginBottom: '8px' }}>
<div style={{ fontSize: '12px', color: '#888', marginBottom: '4px' }}>Upcoming:</div>
{room.upcoming_reservations.map((res, i) => (
<div key={res.id || i} style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px', marginBottom: '2px' }}>
<span>📅 {formatDate(res.check_in)} {formatDate(res.check_out)}</span>
<span style={{ fontWeight: 500 }}>{res.guest_name || res.first_name || 'Guest'}</span>
{getPaymentBadge(res.payment_status)}
</div>
))}
</td>
<td style={{ padding: '10px 16px', textAlign: 'center' }}>
<span style={{ padding: '4px 8px', borderRadius: 12, fontSize: '11px', fontWeight: 600, background: getPriorityStyle(room.priority).bg, color: getPriorityStyle(room.priority).color }}>
{getPriorityStyle(room.priority).icon} {room.priority}
</span>
</td>
<td style={{ padding: '10px 16px', fontSize: '13px' }}>
{room.current_guest ? (
<span>{room.current_guest} {getPaymentBadge(room.payment_status)}</span>
) : (
<span style={{ color: '#9ca3af' }}></span>
)}
</td>
<td style={{ padding: '10px 16px', textAlign: 'right', fontWeight: 600, color: C.gold }}>
${parseFloat(room.price).toFixed(2)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Detailed Room Cards (click to expand) */}
{editingRoom !== null && (
<div style={{ background: '#fff', borderRadius: 12, border: `1px solid ${C.border}`, overflow: 'hidden' }}>
{rooms.filter(r => r.id === editingRoom).map(room => {
const isEditing = true;
const statusColors = getStatusColor(room.clean_status);
const priorityStyle = getPriorityStyle(room.priority);
return (
<div key={room.id} style={{ padding: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<h3 style={{ margin: 0, fontSize: '18px', fontWeight: 700, color: C.navy }}>
{room.is_vip && '⭐ '}{room.name}
</h3>
<Btn size="sm" variant="secondary" onClick={() => setEditingRoom(null)}>Close</Btn>
</div>
)}
{/* Notes */}
{room.notes && !isEditing && (
<div style={{ fontSize: '13px', color: '#666', fontStyle: 'italic' }}>
📝 {room.notes}
</div>
)}
{/* Edit Form */}
{isEditing && (
<div style={{ marginTop: '12px', display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '12px' }}>
<S
label="Clean Status"
value={editData.clean_status}
onChange={(v: any) => setEditData(prev => ({ ...prev, clean_status: v }))}
options={[
{ value: 'clean', label: '✨ Clean' },
{ 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
label="Notes"
value={editData.notes}
onChange={(v: string) => setEditData(prev => ({ ...prev, notes: v }))}
rows={2}
placeholder="e.g., Need extra towels, AC broken..."
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '12px' }}>
<S
label="Clean Status"
value={editData.clean_status}
onChange={(v: any) => setEditData(prev => ({ ...prev, clean_status: v }))}
options={[
{ value: 'clean', label: '✨ Clean' },
{ 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 style={{ display: 'flex', gap: '8px' }}>
<Btn onClick={updateRoomStatus} size="sm">Save</Btn>
<Btn variant="secondary" onClick={() => setEditingRoom(null)} size="sm">Cancel</Btn>
</div>
</div>
)}
</div>
</div>
);
})}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: '12px', marginTop: '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', marginTop: '12px' }}>
<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 label="Notes" value={editData.notes} onChange={(v: string) => setEditData(prev => ({ ...prev, notes: v }))} rows={2} placeholder="e.g., Need extra towels, AC broken..." style={{ marginTop: '12px' }} />
<div style={{ display: 'flex', gap: '8px', marginTop: '16px' }}>
<Btn onClick={updateRoomStatus} size="sm">Save</Btn>
<Btn variant="secondary" onClick={() => setEditingRoom(null)} size="sm">Cancel</Btn>
</div>
{room.upcoming_reservations && room.upcoming_reservations.length > 0 && (
<div style={{ marginTop: '16px', padding: '12px', background: '#f9fafb', borderRadius: 8 }}>
<div style={{ fontWeight: 600, marginBottom: '8px' }}>📅 Upcoming Reservations</div>
{room.upcoming_reservations.map((res, i) => (
<div key={res.id || i} style={{ fontSize: '13px', marginBottom: '4px' }}>
{formatDate(res.check_in)} {formatDate(res.check_out)}: {res.guest_name || res.first_name || 'Guest'}
</div>
))}
</div>
)}
</div>
);
})}
</div>
)}
</>
)}
</div>
);