Skip to main content

Calendar Overview

The Shyfts scheduling calendar is a room-based time grid designed for efficient shift planning and management. This guide covers the calendar interface, navigation, and core features.


Accessing the Calendar

Location: /dashboard/scheduling

Required Role: COMPANY_MANAGER

// Source: src/app/dashboard/scheduling/page.tsx:13-79
export default function SchedulingPage() {
const isMobile = useMediaQuery('(max-width: 768px)')
const [selectedView, setSelectedView] = useState<'calendar' | 'list'>('calendar')

return (
<CalendarProvider>
<SchedulingProvider>
{/* Mobile view toggle */}
{isMobile && selectedView === 'list' ? (
<MobileSchedulingView />
) : (
<RoomTimeGridCalendar />
)}
</SchedulingProvider>
</CalendarProvider>
)
}

Calendar Layout

Room-Based Time Grid

The calendar displays rooms as horizontal rows with time slots across the top:

┌──────────────┬────────┬────────┬────────┬────────┬────────┐
│ │ 08:00 │ 09:00 │ 10:00 │ 11:00 │ 12:00 │
├──────────────┼────────┼────────┼────────┼────────┼────────┤
│ Room 1 │ ██████████████████████ │ │ │
├──────────────┼────────┼────────┼────────┼────────┼────────┤
│ Room 2 │ │ █████████████████████████████████ │
├──────────────┼────────┼────────┼────────┼────────┼────────┤
│ Room 3 │ ██████████████ │ │ ████████████████ │
└──────────────┴────────┴────────┴────────┴────────┴────────┘

Layout Dimensions

// Source: src/components/calendar/RoomTimeGridCalendar.tsx:37-46
const DESKTOP_SLOT_WIDTH = 60 // 60px per hour slot
const MOBILE_SLOT_WIDTH = 48 // 48px per hour slot on mobile
const DESKTOP_ROOM_COLUMN_WIDTH = 192 // Room name column width
const MOBILE_ROOM_COLUMN_WIDTH = 100 // Narrower on mobile
ElementDesktopMobile
Time slot width60px48px
Room column width192px100px
Time rangeCompany hoursCompany hours

Header Navigation

┌─────────────────────────────────────────────────────────────────┐
│ ← Prev │ Mon 13/01/2025 │ Next → │ Today │ Now │ 📅 │
└─────────────────────────────────────────────────────────────────┘
ControlActionKeyboard
PreviousNavigate to previous dayN/A
Date DisplayShows current date (DD/MM/YYYY)N/A
NextNavigate to next dayN/A
TodayJump to current dateN/A
NowScroll to current timeN/A
Date PickerOpen calendar for date selectionN/A

Date Navigation

// Source: src/components/calendar/RoomTimeGridCalendar.tsx:147-161
const handlePrevDay = () => {
setCurrentDate(prev => moment(prev).subtract(1, 'day').format('YYYY-MM-DD'))
}

const handleNextDay = () => {
setCurrentDate(prev => moment(prev).add(1, 'day').format('YYYY-MM-DD'))
}

const handleToday = () => {
setCurrentDate(moment.tz(UK_TIMEZONE).format('YYYY-MM-DD'))
}

Now Button

The Now button scrolls the calendar to the current time indicator:

// Source: src/components/calendar/RoomTimeGridCalendar.tsx:163-175
const handleScrollToNow = () => {
const now = moment.tz(UK_TIMEZONE)
const currentHour = now.hour()
const currentMinute = now.minute()

const scrollPosition = (currentHour - startHour) * slotWidth +
(currentMinute / 60) * slotWidth

if (timeGridRef.current) {
timeGridRef.current.scrollTo({
left: Math.max(0, scrollPosition - 200),
behavior: 'smooth'
})
}
}

Time Grid

Operating Hours

The calendar displays time slots based on your company's operating hours:

// Source: src/components/calendar/RoomTimeGridCalendar.tsx:119-133
// Default hours if not configured
const startHour = companyHours?.start_hour ?? 8 // 08:00
const endHour = companyHours?.end_hour ?? 20 // 20:00

// Generate time slots
const timeSlots = useMemo(() => {
const slots = []
for (let hour = startHour; hour <= endHour; hour++) {
slots.push({
hour,
label: `${hour.toString().padStart(2, '0')}:00`
})
}
return slots
}, [startHour, endHour])

Time Slot Display

FormatExample
Time format24-hour (08:00, 14:00, 20:00)
Slot duration1 hour (60 minutes)
Minimum shift15 minutes

Current Time Indicator

A red vertical line shows the current time:

// Source: src/components/calendar/RoomTimeGridCalendar.tsx:287-305
const CurrentTimeIndicator = () => {
const now = moment.tz(UK_TIMEZONE)
const currentHour = now.hour()
const currentMinute = now.minute()

if (currentHour < startHour || currentHour > endHour) return null

const position = (currentHour - startHour) * slotWidth +
(currentMinute / 60) * slotWidth

return (
<div
className="absolute top-0 bottom-0 w-0.5 bg-red-500 z-20"
style={{ left: `${position}px` }}
/>
)
}

Room Display

Room Rows

Each room in your company displays as a horizontal row:

// Source: src/components/calendar/RoomTimeGridCalendar.tsx:307-345
{rooms.map((room) => (
<div key={room.id} className="flex border-b border-white/10">
{/* Room name column */}
<div
className="sticky left-0 z-10 bg-dark-800 border-r border-white/10"
style={{ width: roomColumnWidth }}
>
<div className="p-3 h-20 flex items-center">
<span className="text-primary-text font-medium truncate">
{room.name}
</span>
</div>
</div>

{/* Time grid for this room */}
<div className="flex-1 relative h-20">
{/* Shift blocks rendered here */}
</div>
</div>
))}

Room Information

PropertyDescription
NameRoom display name
TypeRoom category (e.g., Consultation, Meeting)
CapacityMaximum concurrent shifts
StatusActive/Inactive

View Modes

Desktop View

On screens wider than 768px, the full room grid calendar is displayed with:

  • All rooms visible
  • Full time slot width (60px)
  • Drag-and-drop enabled
  • Bulk selection mode available

Mobile View

On screens narrower than 768px, a view toggle appears:

// Source: src/app/dashboard/scheduling/page.tsx:41-58
{isMobile && (
<div className="flex gap-2 mb-4">
<button
onClick={() => setSelectedView('calendar')}
className={`px-4 py-2 rounded-lg ${
selectedView === 'calendar'
? 'bg-coral-500 text-white'
: 'bg-white/10 text-white/70'
}`}
>
Calendar
</button>
<button
onClick={() => setSelectedView('list')}
className={`px-4 py-2 rounded-lg ${
selectedView === 'list'
? 'bg-coral-500 text-white'
: 'bg-white/10 text-white/70'
}`}
>
List
</button>
</div>
)}
ViewDescription
CalendarCompact room grid with narrower slots
ListMobile-optimised list of shifts by time

Shift Blocks

Visual Representation

Shifts appear as coloured blocks positioned within their time range:

// Source: src/components/calendar/RoomTimeGridCalendar.tsx:389-425
const ShiftBlock = ({ shift, roomId }) => {
const startMinutes = moment(shift.start_time).diff(
moment(shift.start_time).startOf('day'), 'minutes'
)
const endMinutes = moment(shift.end_time).diff(
moment(shift.end_time).startOf('day'), 'minutes'
)

const left = ((startMinutes / 60) - startHour) * slotWidth
const width = ((endMinutes - startMinutes) / 60) * slotWidth

return (
<div
className="absolute top-1 bottom-1 rounded-lg cursor-pointer"
style={{
left: `${left}px`,
width: `${width}px`,
backgroundColor: shift.staff?.role?.color || '#FFB5B0'
}}
>
<div className="p-1 text-xs text-white truncate">
{shift.staff?.first_name} {shift.staff?.last_name}
</div>
</div>
)
}

Shift Block Information

ElementDescription
ColourBased on staff role colour
TextStaff name
PositionAligned to start/end time
HeightFills room row

Shift Block Actions

ActionTrigger
View detailsClick on shift
Edit shiftClick, then edit in modal
Move shiftDrag to new time/room
Select for bulkClick in bulk mode

Interactive Features

Click to Create

Click on any empty time slot to create a new shift:

// Source: src/components/calendar/RoomTimeGridCalendar.tsx:230-250
const handleSlotClick = (roomId: string, hour: number) => {
// Calculate time from click position
const startTime = `${hour.toString().padStart(2, '0')}:00`
const endTime = `${(hour + 1).toString().padStart(2, '0')}:00`

setCreateModalState({
isOpen: true,
defaultRoomId: roomId,
defaultDate: currentDate,
defaultTime: startTime
})
}

Drag and Drop

Shift blocks can be dragged to new positions:

// Source: src/components/calendar/RoomTimeGridCalendar.tsx:77-93
const mouseSensor = useSensor(MouseSensor, {
activationConstraint: {
delay: 200, // 200ms delay before drag starts
tolerance: 5 // 5px movement tolerance
}
})

const touchSensor = useSensor(TouchSensor, {
activationConstraint: {
delay: 200,
tolerance: 5
}
})

const sensors = useSensors(mouseSensor, touchSensor)

The 200ms delay distinguishes between clicks (view/edit) and drags (move).

Bulk Selection

Enable bulk mode to select multiple shifts:

// Source: src/components/calendar/RoomTimeGridCalendar.tsx:95-110
const [bulkMode, setBulkMode] = useState(false)
const [selectedShifts, setSelectedShifts] = useState<Set<string>>(new Set())

const toggleShiftSelection = (shiftId: string) => {
setSelectedShifts(prev => {
const next = new Set(prev)
if (next.has(shiftId)) {
next.delete(shiftId)
} else {
next.add(shiftId)
}
return next
})
}

Loading States

Initial Load

// Source: src/components/calendar/RoomTimeGridCalendar.tsx:200-215
if (loading) {
return (
<div className="card-glass rounded-xl p-6">
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-coral-500" />
<span className="ml-3 text-secondary-text">Loading calendar...</span>
</div>
</div>
)
}

Empty State

// Source: src/components/calendar/RoomTimeGridCalendar.tsx:217-228
if (rooms.length === 0) {
return (
<div className="card-glass rounded-xl p-6">
<div className="text-center py-12">
<Calendar className="h-12 w-12 mx-auto text-secondary-text mb-4" />
<h3 className="text-primary-text font-medium">No rooms configured</h3>
<p className="text-secondary-text mt-2">
Add rooms in Settings to start scheduling
</p>
</div>
</div>
)
}

UK Timezone Compliance

All calendar operations use UK timezone:

// Source: src/components/calendar/RoomTimeGridCalendar.tsx:15-20
import { UK_TIMEZONE } from '@/lib/utils/timezone'
import moment from 'moment-timezone'

// All dates formatted in UK timezone
const currentDate = moment.tz(UK_TIMEZONE).format('YYYY-MM-DD')

// Week starts on Monday (isoWeek)
const weekStart = moment.tz(UK_TIMEZONE).startOf('isoWeek')
StandardImplementation
Date formatDD/MM/YYYY
Time format24-hour (HH:mm)
Week startMonday
TimezoneEurope/London

Holidays on the Calendar

How Holidays Are Displayed

Shyfts shows company holidays (bank holidays, company closures, and custom holidays) as visual markers on the calendar, helping you avoid scheduling on closed days.

Day View

When you navigate to a day that is a holiday:

ElementDescription
Amber badgeAppears in the date header showing the holiday name (e.g., "Christmas Day")
Closed-day messageA building icon (🏛️) with a message replaces the normal time grid
Shift creation warningIf you try to create a shift on a holiday, a warning is displayed
// Source: src/components/calendar/RoomTimeGridCalendar.tsx:740-743
// Holiday banner in day view header
{isHoliday(dateString) && (
<span className="text-amber-400">🏛️ {getHolidayName(dateString)}</span>
)}

Week View

In the weekly calendar, holidays are highlighted across the entire column:

ElementDescription
Holiday nameDisplayed underneath the date in the column header
Amber column tintThe entire day column has an amber-tinted background
Building icon🏛️ icon next to the holiday name
// Source: src/components/calendar/WeeklyRoomGrid.tsx:288-289
// Holiday indicator in week view column headers
{dayIsHoliday && (
<div className="text-xs text-amber-400">🏛️ {getHolidayName(dateStr)}</div>
)}

Shift Creation on Holidays

When creating a shift on a holiday date via the Shift Create Modal:

  • The modal shows the holiday name as a warning
  • You can still create the shift (holidays are warnings, not hard blocks)
  • This is useful for companies that need some staff on bank holidays
// Source: src/components/calendar/ShiftCreateModal.tsx:69-71
// Warning when creating shifts on holidays
const holidayName = isHoliday(selectedDate) ? getHolidayName(selectedDate) : null

Copy-Week and Bulk Scheduling

ToolHoliday Behaviour
Copy WeekResponse includes target_week_holidays listing any holidays in the target week
Bulk SchedulingToast warning when the target week contains holidays

Managing Holidays

To add, import, or remove holidays, go to SettingsHolidays. See Managing Holidays for the full guide.



Source Files:

  • src/app/dashboard/scheduling/page.tsx - Scheduling page component
  • src/components/calendar/RoomTimeGridCalendar.tsx - Main calendar component
  • src/contexts/SchedulingContext.tsx - Scheduling state management
  • src/contexts/CalendarContext.tsx - Calendar provider