Rooms & Facilities
This guide covers managing rooms, facilities, and resources in Shyfts, including room types, capacity settings, and scheduling integration.
Overview
What are Rooms in Shyfts?
Rooms represent physical spaces or resources used for scheduling:
- Consultation Rooms - Where staff work or see clients
- Meeting Rooms - For internal meetings
- Treatment Areas - Specialised facilities
- Workstations - Desk or workspace allocation
Industry Terminology
| Industry | Room Terminology |
|---|---|
| GP Practice | Consultation Room |
| Dental Practice | Treatment Room |
| Restaurant | Section, Station |
| Office | Meeting Room, Workstation |
Access Levels
| Role | View | Create | Edit | Delete | Request Changes |
|---|---|---|---|---|---|
| System Admin | ✅ | ✅ | ✅ | ✅ | N/A |
| Company Manager | ✅ | ❌ | ❌ | ❌ | ✅ |
| Staff | ✅ | ❌ | ❌ | ❌ | ❌ |
Only System Administrators can directly create, edit, or delete rooms. Company Managers can request changes which notifies all System Admins for review.
Requesting Changes
For Company Managers
If you need to add, modify, or remove rooms, you can submit a change request:
- Navigate to Dashboard → Company Settings → Rooms tab
- Click the "Request Changes" button
- Enter a detailed description of the changes needed
- Submit the request
Example Requests
Adding a room:
"Please add a new consultation room:
- Name: Consultation Room 4
- Type: Consultation
- Capacity: 2
- Location: First floor, next to Room 3"
Modifying a room:
"Please update Treatment Room capacity from 2 to 3.
We've added an extra chair for assistant."
Deactivating a room:
"Please deactivate 'Storage Room' - no longer used for scheduling."
Viewing Rooms
Navigation
- Navigate to Settings → Rooms & Facilities from the sidebar
- View list of all configured rooms
- See room types, capacity, and status
Rooms List API
Endpoint: GET /api/companies/[companyId]/rooms
// Source: src/app/api/companies/[companyId]/rooms/route.ts:25-95
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ companyId: string }> }
) {
const supabase = await createClient()
const resolvedParams = await params
const { searchParams } = new URL(request.url)
// Pagination parameters
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '50')
const offset = (page - 1) * limit
// Fetch rooms with room types
const { data: rooms, error, count } = await supabase
.from('rooms')
.select(`
*,
room_type:room_types (
id,
name,
description,
color
)
`, { count: 'exact' })
.eq('company_id', resolvedParams.companyId)
.order('name', { ascending: true })
.range(offset, offset + limit - 1)
if (error) {
return ApiResponses.error(
new DatabaseError('Failed to fetch rooms', 500, 'FETCH_ERROR')
)
}
return ApiResponses.success({
rooms: rooms || [],
pagination: {
page,
limit,
total: count || 0,
totalPages: Math.ceil((count || 0) / limit)
}
})
}
Response Format
// Rooms list response
{
rooms: [
{
id: "room-uuid-1",
company_id: "company-uuid",
name: "Consultation Room 1",
room_type_id: "room-type-uuid",
room_type: {
id: "room-type-uuid",
name: "Consultation",
description: "Standard consultation room",
color: "#3B82F6"
},
capacity: 2,
description: "Ground floor, wheelchair accessible",
is_active: true,
created_at: "2025-01-01T00:00:00Z",
updated_at: "2025-01-10T12:00:00Z"
},
// ... more rooms
],
pagination: {
page: 1,
limit: 50,
total: 12,
totalPages: 1
}
}
Room Database Structure
Rooms Table
// Source: src/types/database.types.ts - rooms table
interface Room {
id: string
company_id: string
name: string // Display name
room_type_id: string | null // Link to room_types
capacity: number | null // Maximum occupancy
description: string | null // Additional notes
is_active: boolean // Active status
created_at: string
updated_at: string
}
Room Types Table
// Source: src/types/database.types.ts - room_types table
interface RoomType {
id: string
company_id: string
name: string // Type name
description: string | null // Type description
color: string | null // Display colour (hex)
is_active: boolean
created_at: string
updated_at: string
}
Creating Rooms
Create Room API
Endpoint: POST /api/companies/[companyId]/rooms
Only System Administrators can create rooms.
// Source: src/app/api/companies/[companyId]/rooms/route.ts:97-175
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ companyId: string }> }
) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
// System Admin only
if (getAuthRole(user) !== 'SYSTEM_ADMIN') {
return ApiResponses.forbidden('Forbidden - System Admin access required')
}
const resolvedParams = await params
const body = await request.json()
// Validate request
const validationResult = parseWithErrors(createRoomSchema, body)
if (!validationResult.success) {
return ApiResponses.validationError(validationResult.errors)
}
// Create room
const { data: room, error } = await supabase
.from('rooms')
.insert({
company_id: resolvedParams.companyId,
name: validationResult.data.name,
room_type_id: validationResult.data.roomTypeId || null,
capacity: validationResult.data.capacity || null,
description: validationResult.data.description || null,
is_active: true
})
.select(`
*,
room_type:room_types (
id,
name,
description,
color
)
`)
.single()
if (error) {
return ApiResponses.error(
new DatabaseError('Failed to create room', 500, 'CREATE_ERROR')
)
}
return ApiResponses.success(room, { status: 201 })
}
Create Room Schema
// Source: src/app/api/companies/[companyId]/rooms/route.ts:15-23
const createRoomSchema = z.object({
name: z.string().min(1, 'Room name is required').max(100),
roomTypeId: z.string().uuid().nullable().optional(),
capacity: z.number().int().positive().max(1000).nullable().optional(),
description: z.string().max(500).nullable().optional()
})
Example Create Request
// Create a new room
const response = await fetch(`/api/companies/${companyId}/rooms`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: "Consultation Room 3",
roomTypeId: "room-type-uuid",
capacity: 3,
description: "First floor, near reception"
})
})
const data = await response.json()
Room Types
Default Room Types by Industry
GP Practice
| Type | Description | Colour |
|---|---|---|
| Consultation | Standard consultation room | #3B82F6 (Blue) |
| Treatment | Minor procedures room | #10B981 (Green) |
| Nursing | Nurse consultation | #8B5CF6 (Purple) |
| Reception | Front desk area | #F59E0B (Amber) |
Dental Practice
| Type | Description | Colour |
|---|---|---|
| Surgery | Dental surgery room | #3B82F6 (Blue) |
| Hygienist | Hygiene treatment room | #10B981 (Green) |
| X-Ray | Radiography room | #6366F1 (Indigo) |
| Recovery | Post-procedure area | #EC4899 (Pink) |
Restaurant
| Type | Description | Colour |
|---|---|---|
| Main Dining | Main restaurant floor | #3B82F6 (Blue) |
| Bar | Bar service area | #8B5CF6 (Purple) |
| Kitchen | Kitchen stations | #EF4444 (Red) |
| Private | Private dining rooms | #F59E0B (Amber) |
Office
| Type | Description | Colour |
|---|---|---|
| Meeting | Meeting/conference rooms | #3B82F6 (Blue) |
| Workspace | Open plan areas | #10B981 (Green) |
| Quiet | Focus/quiet rooms | #6366F1 (Indigo) |
| Collaboration | Team collaboration spaces | #F59E0B (Amber) |
Managing Room Types
// Fetch room types for company
const fetchRoomTypes = async (companyId: string) => {
const { data, error } = await supabase
.from('room_types')
.select('*')
.eq('company_id', companyId)
.eq('is_active', true)
.order('name')
return data
}
Room Utilisation
Utilisation Statistics
// Source: src/app/api/companies/[companyId]/rooms/route.ts:177-230
interface RoomUtilizationStats {
roomId: string
roomName: string
totalShifts: number
totalHours: number
utilizationRate: number // Percentage
peakHours: string[] // e.g., ["09:00", "14:00"]
}
// Calculate room utilisation
const calculateUtilization = async (roomId: string, dateRange: DateRange) => {
const { data: shifts } = await supabase
.from('shifts')
.select('*')
.eq('room_id', roomId)
.gte('start_time', dateRange.start)
.lte('end_time', dateRange.end)
const totalShiftHours = shifts.reduce((sum, shift) => {
const duration = moment(shift.end_time).diff(moment(shift.start_time), 'hours', true)
return sum + duration
}, 0)
const businessHours = calculateBusinessHours(dateRange)
const utilizationRate = (totalShiftHours / businessHours) * 100
return {
totalShifts: shifts.length,
totalHours: totalShiftHours,
utilizationRate: Math.round(utilizationRate * 10) / 10
}
}
Utilisation Display
| Utilisation | Status | Colour |
|---|---|---|
| ≥80% | High | Green |
| 50-79% | Moderate | Amber |
| <50% | Low | Red |
Scheduling Integration
Room-Based Calendar
Rooms are the primary dimension for scheduling:
// Room-based calendar view
const RoomCalendar = ({ rooms, shifts, date }) => {
return (
<div className="grid grid-cols-[auto_1fr]">
{/* Time column */}
<TimeSlots start="08:00" end="20:00" interval={15} />
{/* Room columns */}
<div className="grid" style={{ gridTemplateColumns: `repeat(${rooms.length}, 1fr)` }}>
{rooms.map(room => (
<RoomColumn
key={room.id}
room={room}
shifts={shifts.filter(s => s.room_id === room.id)}
date={date}
/>
))}
</div>
</div>
)
}
Shift Assignment
// Assign shift to room
const assignShiftToRoom = async (shiftId: string, roomId: string) => {
const { data, error } = await supabase
.from('shifts')
.update({ room_id: roomId })
.eq('id', shiftId)
.select()
.single()
return data
}
Capacity Management
Capacity Validation
// Check room capacity for shifts
const validateRoomCapacity = async (roomId: string, staffCount: number) => {
const { data: room } = await supabase
.from('rooms')
.select('capacity')
.eq('id', roomId)
.single()
if (room.capacity && staffCount > room.capacity) {
return {
valid: false,
error: `Room capacity is ${room.capacity}, cannot assign ${staffCount} staff`
}
}
return { valid: true }
}
Capacity Display
// Room capacity indicator
const CapacityIndicator = ({ room, assignedCount }) => {
const percentage = room.capacity
? (assignedCount / room.capacity) * 100
: 0
return (
<div className="flex items-center gap-2">
<span className={percentage >= 100 ? 'text-red-500' : 'text-green-500'}>
{assignedCount}/{room.capacity || '∞'}
</span>
{percentage >= 100 && <Badge variant="warning">Full</Badge>}
</div>
)
}
Room Status
Active vs Inactive Rooms
| Status | Description | Effect |
|---|---|---|
| Active | Available for scheduling | Shown in calendar, can assign shifts |
| Inactive | Not available | Hidden from calendar, preserves history |
Deactivating a Room
// Soft delete - deactivate room
const deactivateRoom = async (roomId: string) => {
const { error } = await supabase
.from('rooms')
.update({ is_active: false })
.eq('id', roomId)
if (error) throw error
}
Deactivating a room does not affect existing shifts. Historical data is preserved.
Best Practices
For Room Setup
- Meaningful Names - Use clear, descriptive names
- Consistent Naming - Follow a naming convention
- Set Capacity - Define capacity where relevant
- Add Descriptions - Include useful details (location, equipment)
For Room Types
- Industry Appropriate - Use types matching your industry
- Colour Coding - Use distinct colours for visibility
- Limit Types - Keep to essential categories
- Update as Needed - Add types when requirements change
For Scheduling
- Check Availability - Verify room is free before scheduling
- Respect Capacity - Don't exceed room limits
- Balance Usage - Distribute shifts across rooms
- Track Utilisation - Review usage patterns regularly
Common Room Configurations
GP Practice Example
// Standard GP practice room setup
const gpRooms = [
{ name: "Reception", type: "Reception", capacity: 4 },
{ name: "Consultation Room 1", type: "Consultation", capacity: 2 },
{ name: "Consultation Room 2", type: "Consultation", capacity: 2 },
{ name: "Consultation Room 3", type: "Consultation", capacity: 2 },
{ name: "Treatment Room", type: "Treatment", capacity: 3 },
{ name: "Nurse Room", type: "Nursing", capacity: 2 },
{ name: "Phlebotomy", type: "Treatment", capacity: 2 }
]
Restaurant Example
// Restaurant section setup
const restaurantRooms = [
{ name: "Main Floor - Section A", type: "Main Dining", capacity: 6 },
{ name: "Main Floor - Section B", type: "Main Dining", capacity: 6 },
{ name: "Bar Area", type: "Bar", capacity: 3 },
{ name: "Private Dining Room", type: "Private", capacity: 2 },
{ name: "Kitchen - Hot Pass", type: "Kitchen", capacity: 4 },
{ name: "Kitchen - Cold Section", type: "Kitchen", capacity: 2 }
]
Troubleshooting
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Cannot create room | Insufficient permissions | Contact System Admin |
| Room not in calendar | Room inactive | Reactivate room |
| Capacity warning | Too many staff assigned | Review shift assignments |
| Room type missing | Type not configured | Create room type first |
Error Messages
| Error | Meaning | Action |
|---|---|---|
| "Forbidden - System Admin access required" | Not authorised | Contact System Admin |
| "Room name is required" | Missing name | Enter room name |
| "Invalid room type" | Room type doesn't exist | Select valid type |
| "Failed to create room" | Database error | Check all fields, retry |
Related Documentation
- Company Profile - Company settings
- Opening Hours - Business hours
- Creating Shifts - Shift scheduling
- Calendar Views - Room-based calendar
Source Files:
src/app/api/companies/[companyId]/rooms/route.ts- Rooms APIsrc/types/database.types.ts- Database type definitionssrc/components/calendar/RoomTimeGridCalendar.tsx- Room calendar componentsrc/lib/db/room.db.ts- Database operations