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 Settings → Holidays. 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