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
| Element | Desktop | Mobile |
|---|---|---|
| Time slot width | 60px | 48px |
| Room column width | 192px | 100px |
| Time range | Company hours | Company hours |
Navigation Controls
Header Navigation
┌─────────────────────────────────────────────────────────────────┐
│ ← Prev │ Mon 13/01/2025 │ Next → │ Today │ Now │ 📅 │
└─────────────────────────────────────────────────────────────────┘
| Control | Action | Keyboard |
|---|---|---|
| Previous | Navigate to previous day | N/A |
| Date Display | Shows current date (DD/MM/YYYY) | N/A |
| Next | Navigate to next day | N/A |
| Today | Jump to current date | N/A |
| Now | Scroll to current time | N/A |
| Date Picker | Open calendar for date selection | N/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
| Format | Example |
|---|---|
| Time format | 24-hour (08:00, 14:00, 20:00) |
| Slot duration | 1 hour (60 minutes) |
| Minimum shift | 15 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
| Property | Description |
|---|---|
| Name | Room display name |
| Type | Room category (e.g., Consultation, Meeting) |
| Capacity | Maximum concurrent shifts |
| Status | Active/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>
)}
| View | Description |
|---|---|
| Calendar | Compact room grid with narrower slots |
| List | Mobile-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
| Element | Description |
|---|---|
| Colour | Based on staff role colour |
| Text | Staff name |
| Position | Aligned to start/end time |
| Height | Fills room row |
Shift Block Actions
| Action | Trigger |
|---|---|
| View details | Click on shift |
| Edit shift | Click, then edit in modal |
| Move shift | Drag to new time/room |
| Select for bulk | Click 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')
| Standard | Implementation |
|---|---|
| Date format | DD/MM/YYYY |
| Time format | 24-hour (HH:mm) |
| Week start | Monday |
| Timezone | Europe/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:
| Element | Description |
|---|---|
| Amber badge | Appears in the date header showing the holiday name (e.g., "Christmas Day") |
| Closed-day message | A building icon (🏛️) with a message replaces the normal time grid |
| Shift creation warning | If 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:
| Element | Description |
|---|---|
| Holiday name | Displayed underneath the date in the column header |
| Amber column tint | The 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
| Tool | Holiday Behaviour |
|---|---|
| Copy Week | Response includes target_week_holidays listing any holidays in the target week |
| Bulk Scheduling | Toast 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.
Related Documentation
- Creating Shifts - Adding new shifts
- Editing Shifts - Modifying existing shifts
- Conflict Resolution - Handling scheduling conflicts
- Bulk Operations - Managing multiple shifts
- Holidays - Managing bank holidays and company closures
Source Files:
src/app/dashboard/scheduling/page.tsx- Scheduling page componentsrc/components/calendar/RoomTimeGridCalendar.tsx- Main calendar componentsrc/contexts/SchedulingContext.tsx- Scheduling state managementsrc/contexts/CalendarContext.tsx- Calendar provider