Skip to main content

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:

  1. Consultation Rooms - Where staff work or see clients
  2. Meeting Rooms - For internal meetings
  3. Treatment Areas - Specialised facilities
  4. Workstations - Desk or workspace allocation

Industry Terminology

IndustryRoom Terminology
GP PracticeConsultation Room
Dental PracticeTreatment Room
RestaurantSection, Station
OfficeMeeting Room, Workstation

Access Levels

RoleViewCreateEditDeleteRequest Changes
System AdminN/A
Company Manager
Staff
note

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:

  1. Navigate to DashboardCompany SettingsRooms tab
  2. Click the "Request Changes" button
  3. Enter a detailed description of the changes needed
  4. 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

  1. Navigate to SettingsRooms & Facilities from the sidebar
  2. View list of all configured rooms
  3. 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

Permission Required

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

TypeDescriptionColour
ConsultationStandard consultation room#3B82F6 (Blue)
TreatmentMinor procedures room#10B981 (Green)
NursingNurse consultation#8B5CF6 (Purple)
ReceptionFront desk area#F59E0B (Amber)

Dental Practice

TypeDescriptionColour
SurgeryDental surgery room#3B82F6 (Blue)
HygienistHygiene treatment room#10B981 (Green)
X-RayRadiography room#6366F1 (Indigo)
RecoveryPost-procedure area#EC4899 (Pink)

Restaurant

TypeDescriptionColour
Main DiningMain restaurant floor#3B82F6 (Blue)
BarBar service area#8B5CF6 (Purple)
KitchenKitchen stations#EF4444 (Red)
PrivatePrivate dining rooms#F59E0B (Amber)

Office

TypeDescriptionColour
MeetingMeeting/conference rooms#3B82F6 (Blue)
WorkspaceOpen plan areas#10B981 (Green)
QuietFocus/quiet rooms#6366F1 (Indigo)
CollaborationTeam 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

UtilisationStatusColour
≥80%HighGreen
50-79%ModerateAmber
<50%LowRed

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

StatusDescriptionEffect
ActiveAvailable for schedulingShown in calendar, can assign shifts
InactiveNot availableHidden 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
}
caution

Deactivating a room does not affect existing shifts. Historical data is preserved.


Best Practices

For Room Setup

  1. Meaningful Names - Use clear, descriptive names
  2. Consistent Naming - Follow a naming convention
  3. Set Capacity - Define capacity where relevant
  4. Add Descriptions - Include useful details (location, equipment)

For Room Types

  1. Industry Appropriate - Use types matching your industry
  2. Colour Coding - Use distinct colours for visibility
  3. Limit Types - Keep to essential categories
  4. Update as Needed - Add types when requirements change

For Scheduling

  1. Check Availability - Verify room is free before scheduling
  2. Respect Capacity - Don't exceed room limits
  3. Balance Usage - Distribute shifts across rooms
  4. 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

IssueCauseSolution
Cannot create roomInsufficient permissionsContact System Admin
Room not in calendarRoom inactiveReactivate room
Capacity warningToo many staff assignedReview shift assignments
Room type missingType not configuredCreate room type first

Error Messages

ErrorMeaningAction
"Forbidden - System Admin access required"Not authorisedContact System Admin
"Room name is required"Missing nameEnter room name
"Invalid room type"Room type doesn't existSelect valid type
"Failed to create room"Database errorCheck all fields, retry


Source Files:

  • src/app/api/companies/[companyId]/rooms/route.ts - Rooms API
  • src/types/database.types.ts - Database type definitions
  • src/components/calendar/RoomTimeGridCalendar.tsx - Room calendar component
  • src/lib/db/room.db.ts - Database operations