feat: room status table view with all rooms
This commit is contained in:
+123
-207
@@ -1814,218 +1814,134 @@ function RoomStatusSection({ onToast }: { onToast: (msg: string, type: 'success'
|
|||||||
) : rooms.length === 0 ? (
|
) : rooms.length === 0 ? (
|
||||||
<Empty icon="🏨" title="No rooms" desc="Add rooms in the Rooms section" />
|
<Empty icon="🏨" title="No rooms" desc="Add rooms in the Rooms section" />
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'grid', gap: '12px' }}>
|
<>
|
||||||
{rooms.map(room => {
|
{/* Compact Room Availability Table */}
|
||||||
const statusColors = getStatusColor(room.clean_status);
|
<div style={{ background: '#fff', borderRadius: 12, border: `1px solid ${C.border}`, marginBottom: '16px', overflow: 'hidden' }}>
|
||||||
const priorityStyle = getPriorityStyle(room.priority);
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||||
const isEditing = editingRoom === room.id;
|
<thead>
|
||||||
|
<tr style={{ background: C.navy, color: '#fff' }}>
|
||||||
return (
|
<th style={{ padding: '12px 16px', textAlign: 'left', fontWeight: 600 }}>Room</th>
|
||||||
<div key={room.id} style={{ background: '#fff', borderRadius: 12, border: `1px solid ${C.border}`, overflow: 'hidden' }}>
|
<th style={{ padding: '12px 16px', textAlign: 'center', fontWeight: 600 }}>Status</th>
|
||||||
{/* Header Row */}
|
<th style={{ padding: '12px 16px', textAlign: 'center', fontWeight: 600 }}>Occupancy</th>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', padding: '12px 16px', background: room.is_vip ? '#fef3c7' : room.occupancy_status === 'occupied' ? '#f0fdf4' : '#fafafa', gap: '12px' }}>
|
<th style={{ padding: '12px 16px', textAlign: 'center', fontWeight: 600 }}>Priority</th>
|
||||||
{/* Room Name & Status */}
|
<th style={{ padding: '12px 16px', textAlign: 'left', fontWeight: 600 }}>Guest</th>
|
||||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: '10px', flexWrap: 'wrap' }}>
|
<th style={{ padding: '12px 16px', textAlign: 'right', fontWeight: 600 }}>Price</th>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
</tr>
|
||||||
<span style={{ fontSize: '20px' }}>{getStatusIcon(room.clean_status)}</span>
|
</thead>
|
||||||
<span style={{ fontWeight: 600, fontSize: '16px', color: C.navy }}>{room.name}</span>
|
<tbody>
|
||||||
</div>
|
{rooms.map((room, i) => (
|
||||||
{room.is_vip && (
|
<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); }}>
|
||||||
<span style={{ padding: '2px 8px', borderRadius: 12, fontSize: '11px', fontWeight: 700, background: '#fbbf24', color: '#78350f' }}>
|
<td style={{ padding: '10px 16px', fontWeight: 600, display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
⭐ VIP
|
{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>
|
||||||
)}
|
</td>
|
||||||
<span style={{
|
<td style={{ padding: '10px 16px', textAlign: 'center' }}>
|
||||||
padding: '4px 10px',
|
<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' }}>
|
||||||
borderRadius: 12,
|
{room.occupancy_status === 'occupied' ? '🛏️ Occupied' : '🚪 Available'}
|
||||||
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' : ''}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
</td>
|
||||||
</div>
|
<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 }}>
|
||||||
{/* Price */}
|
{getPriorityStyle(room.priority).icon} {room.priority}
|
||||||
<div style={{ fontWeight: 600, color: C.gold, fontSize: '15px' }}>
|
</span>
|
||||||
${parseFloat(room.price).toFixed(2)}/night
|
</td>
|
||||||
</div>
|
<td style={{ padding: '10px 16px', fontSize: '13px' }}>
|
||||||
|
{room.current_guest ? (
|
||||||
{/* Edit Button */}
|
<span>{room.current_guest} {getPaymentBadge(room.payment_status)}</span>
|
||||||
<Btn size="sm" variant="secondary" onClick={() => isEditing ? setEditingRoom(null) : startEdit(room)}>
|
) : (
|
||||||
{isEditing ? 'Cancel' : 'Edit'}
|
<span style={{ color: '#9ca3af' }}>—</span>
|
||||||
</Btn>
|
)}
|
||||||
</div>
|
</td>
|
||||||
|
<td style={{ padding: '10px 16px', textAlign: 'right', fontWeight: 600, color: C.gold }}>
|
||||||
{/* Details Row */}
|
${parseFloat(room.price).toFixed(2)}
|
||||||
<div style={{ padding: '12px 16px', borderTop: `1px solid ${C.border}` }}>
|
</td>
|
||||||
{/* Quick Info Grid */}
|
</tr>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '8px', marginBottom: '12px' }}>
|
))}
|
||||||
{/* Current Guest */}
|
</tbody>
|
||||||
{room.current_guest && (
|
</table>
|
||||||
<div style={{ fontSize: '13px' }}>
|
</div>
|
||||||
<span style={{ color: '#666' }}>Guest: </span>
|
|
||||||
<span style={{ fontWeight: 600, color: C.navy }}>{room.current_guest}</span>
|
{/* Detailed Room Cards (click to expand) */}
|
||||||
{getPaymentBadge(room.payment_status)}
|
{editingRoom !== null && (
|
||||||
</div>
|
<div style={{ background: '#fff', borderRadius: 12, border: `1px solid ${C.border}`, overflow: 'hidden' }}>
|
||||||
)}
|
{rooms.filter(r => r.id === editingRoom).map(room => {
|
||||||
|
const isEditing = true;
|
||||||
{/* Checkout */}
|
const statusColors = getStatusColor(room.clean_status);
|
||||||
{room.checkout_date && (
|
const priorityStyle = getPriorityStyle(room.priority);
|
||||||
<div style={{ fontSize: '13px' }}>
|
return (
|
||||||
<span style={{ color: '#666' }}>Checkout: </span>
|
<div key={room.id} style={{ padding: '16px' }}>
|
||||||
<span style={{ fontWeight: 500 }}>{formatDate(room.checkout_date)}</span>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||||
{room.checkout_time && <span> at {formatTime(room.checkout_time)}</span>}
|
<h3 style={{ margin: 0, fontSize: '18px', fontWeight: 700, color: C.navy }}>
|
||||||
</div>
|
{room.is_vip && '⭐ '}{room.name}
|
||||||
)}
|
</h3>
|
||||||
|
<Btn size="sm" variant="secondary" onClick={() => setEditingRoom(null)}>Close</Btn>
|
||||||
{/* 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>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '12px' }}>
|
||||||
|
<S
|
||||||
{/* Notes */}
|
label="Clean Status"
|
||||||
{room.notes && !isEditing && (
|
value={editData.clean_status}
|
||||||
<div style={{ fontSize: '13px', color: '#666', fontStyle: 'italic' }}>
|
onChange={(v: any) => setEditData(prev => ({ ...prev, clean_status: v }))}
|
||||||
📝 {room.notes}
|
options={[
|
||||||
</div>
|
{ value: 'clean', label: '✨ Clean' },
|
||||||
)}
|
{ value: 'dirty', label: '🧹 Dirty' },
|
||||||
|
{ value: 'maintenance', label: '🔧 Maintenance' },
|
||||||
{/* Edit Form */}
|
]}
|
||||||
{isEditing && (
|
/>
|
||||||
<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="Priority"
|
||||||
<S
|
value={editData.priority}
|
||||||
label="Clean Status"
|
onChange={(v: any) => setEditData(prev => ({ ...prev, priority: v }))}
|
||||||
value={editData.clean_status}
|
options={[
|
||||||
onChange={(v: any) => setEditData(prev => ({ ...prev, clean_status: v }))}
|
{ value: 'urgent', label: '🔴 Urgent' },
|
||||||
options={[
|
{ value: 'normal', label: '🔵 Normal' },
|
||||||
{ value: 'clean', label: '✨ Clean' },
|
{ value: 'low', label: '⚪ Low' },
|
||||||
{ value: 'dirty', label: '🧹 Dirty' },
|
]}
|
||||||
{ value: 'maintenance', label: '🔧 Maintenance' },
|
/>
|
||||||
]}
|
<S
|
||||||
/>
|
label="Assigned Staff"
|
||||||
<S
|
value={editData.assigned_staff_id?.toString() || ''}
|
||||||
label="Priority"
|
onChange={(v: any) => setEditData(prev => ({ ...prev, assigned_staff_id: v ? parseInt(v) : null }))}
|
||||||
value={editData.priority}
|
options={[
|
||||||
onChange={(v: any) => setEditData(prev => ({ ...prev, priority: v }))}
|
{ value: '', label: '— Unassigned —' },
|
||||||
options={[
|
...staff.map(s => ({ value: s.id.toString(), label: `${s.first_name || ''} ${s.last_name || ''}`.trim() || s.username }))
|
||||||
{ 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: 'flex', gap: '8px' }}>
|
|
||||||
<Btn onClick={updateRoomStatus} size="sm">Save</Btn>
|
|
||||||
<Btn variant="secondary" onClick={() => setEditingRoom(null)} size="sm">Cancel</Btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: '12px', marginTop: '12px' }}>
|
||||||
</div>
|
<I label="Issues Count" type="number" value={editData.issues_count.toString()} onChange={(v: string) => setEditData(prev => ({ ...prev, issues_count: parseInt(v) || 0 }))} placeholder="0" />
|
||||||
</div>
|
<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>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user