Opening Hours
This guide covers configuring and managing your company's business hours, including daily schedules, closed days, and timezone handling.
Overview
What are Opening Hours?
Opening hours define when your business operates:
- Daily Schedule - Open and close times for each day of the week
- Closed Days - Days when the business is closed
- Scheduling Integration - Affects shift scheduling and availability
Access Levels
| Role | View | Edit | Request Changes |
|---|---|---|---|
| System Admin | ✅ | ✅ | N/A |
| Company Manager | ✅ | ❌ | ✅ |
| Staff | ✅ | ❌ | ❌ |
Only System Administrators can directly modify opening hours. Company Managers can request changes which notifies all System Admins for review.
Requesting Changes
For Company Managers
If you need to modify opening hours, you can submit a change request:
- Navigate to Dashboard → Company Settings → Hours tab
- Click the "Request Changes" button
- Enter a detailed description of the changes needed
- Submit the request
What Happens Next
- All System Administrators receive a notification
- A System Admin will review your request
- They may contact you for clarification
- Changes will be applied if approved
Tips for Good Requests
- Be specific: "Change Friday closing time from 17:30 to 19:00"
- Include context: "To accommodate evening appointments"
- Mention dates: "Starting from 1st February"
The more detail you provide, the faster your request can be processed.
Viewing Opening Hours
Navigation
- Navigate to Settings → Opening Hours from the sidebar
- View the weekly schedule with times for each day
- Closed days are clearly marked
Opening Hours API
Endpoint: GET /api/companies/[companyId]/hours
// Source: src/app/api/companies/[companyId]/hours/route.ts:18-60
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ companyId: string }> }
) {
const supabase = await createClient()
const resolvedParams = await params
// Fetch opening hours ordered by day of week
const { data: openingHours, error } = await supabase
.from('company_opening_hours')
.select('*')
.eq('company_id', resolvedParams.companyId)
.order('day_of_week', { ascending: true })
if (error) {
return ApiResponses.error(
new DatabaseError('Failed to fetch opening hours', 500, 'FETCH_ERROR')
)
}
return ApiResponses.success(openingHours || [])
}
Response Format
// Opening hours response
[
{
id: "hours-uuid-1",
company_id: "company-uuid",
day_of_week: 0, // Sunday
open_time: null,
close_time: null,
is_closed: true
},
{
id: "hours-uuid-2",
company_id: "company-uuid",
day_of_week: 1, // Monday
open_time: "09:00",
close_time: "17:30",
is_closed: false
},
// ... continues for all 7 days
]
Opening Hours Structure
Day of Week Mapping
| Value | Day | Notes |
|---|---|---|
| 0 | Sunday | Typically closed |
| 1 | Monday | Start of UK business week |
| 2 | Tuesday | |
| 3 | Wednesday | |
| 4 | Thursday | |
| 5 | Friday | |
| 6 | Saturday | Often reduced hours |
Database Schema
// Source: src/types/database.types.ts - company_opening_hours table
interface CompanyOpeningHours {
id: string
company_id: string
day_of_week: number // 0-6 (Sunday-Saturday)
open_time: string | null // HH:MM format (24-hour)
close_time: string | null // HH:MM format (24-hour)
is_closed: boolean // True if closed on this day
created_at: string
updated_at: string
}
Time Format
All times use 24-hour format (UK standard):
| Format | Example | Description |
|---|---|---|
| HH:MM | 09:00 | 9:00 AM |
| HH:MM | 17:30 | 5:30 PM |
| HH:MM | 00:00 | Midnight |
| HH:MM | 23:59 | End of day |
Updating Opening Hours
Update API
Endpoint: PUT /api/companies/[companyId]/hours
Only System Administrators can update opening hours.
// Source: src/app/api/companies/[companyId]/hours/route.ts:62-169
export async function PUT(
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 opening hours array
const validationResult = parseWithErrors(openingHoursArraySchema, body.hours)
if (!validationResult.success) {
return ApiResponses.validationError(validationResult.errors)
}
// Delete existing hours
const { error: deleteError } = await supabase
.from('company_opening_hours')
.delete()
.eq('company_id', resolvedParams.companyId)
if (deleteError) {
return ApiResponses.error(
new DatabaseError('Failed to update opening hours', 500, 'DELETE_ERROR')
)
}
// Insert new hours
const hoursToInsert = validationResult.data.map(hour => ({
...hour,
company_id: resolvedParams.companyId
}))
const { data: newHours, error: insertError } = await supabase
.from('company_opening_hours')
.insert(hoursToInsert)
.select()
.order('day_of_week')
if (insertError) {
return ApiResponses.error(
new DatabaseError('Failed to save opening hours', 500, 'INSERT_ERROR')
)
}
return ApiResponses.success(newHours)
}
Validation Schema
// Source: src/lib/validation/company.schemas.ts:87-125
export const dayOfWeekSchema = z.number().int().min(0).max(6)
export const timeSchema = z.string().regex(
/^([01]\d|2[0-3]):([0-5]\d)$/,
'Invalid time format. Use HH:MM (24-hour)'
)
export const openingHoursSchema = z.object({
day_of_week: dayOfWeekSchema,
open_time: timeSchema.nullable(),
close_time: timeSchema.nullable(),
is_closed: z.boolean().default(false)
}).refine(
(data) => {
// If not closed, both times required
if (!data.is_closed) {
return data.open_time !== null && data.close_time !== null
}
return true
},
{ message: 'Open and close times required for open days' }
).refine(
(data) => {
// Close time must be after open time
if (data.open_time && data.close_time) {
return data.close_time > data.open_time
}
return true
},
{ message: 'Close time must be after open time' }
)
export const openingHoursArraySchema = z.array(openingHoursSchema).length(7)
Example Update Request
// Update opening hours
const response = await fetch(`/api/companies/${companyId}/hours`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
hours: [
{ day_of_week: 0, is_closed: true, open_time: null, close_time: null },
{ day_of_week: 1, is_closed: false, open_time: "09:00", close_time: "17:30" },
{ day_of_week: 2, is_closed: false, open_time: "09:00", close_time: "17:30" },
{ day_of_week: 3, is_closed: false, open_time: "09:00", close_time: "17:30" },
{ day_of_week: 4, is_closed: false, open_time: "09:00", close_time: "17:30" },
{ day_of_week: 5, is_closed: false, open_time: "09:00", close_time: "17:00" },
{ day_of_week: 6, is_closed: true, open_time: null, close_time: null }
]
})
})
const data = await response.json()
Common Schedule Patterns
Standard UK Business Hours
// Monday-Friday 9:00-17:30, closed weekends
const standardHours = [
{ day_of_week: 0, is_closed: true, open_time: null, close_time: null },
{ day_of_week: 1, is_closed: false, open_time: "09:00", close_time: "17:30" },
{ day_of_week: 2, is_closed: false, open_time: "09:00", close_time: "17:30" },
{ day_of_week: 3, is_closed: false, open_time: "09:00", close_time: "17:30" },
{ day_of_week: 4, is_closed: false, open_time: "09:00", close_time: "17:30" },
{ day_of_week: 5, is_closed: false, open_time: "09:00", close_time: "17:30" },
{ day_of_week: 6, is_closed: true, open_time: null, close_time: null }
]
Extended Hours (GP Practice)
// Early morning and evening appointments
const extendedHours = [
{ day_of_week: 0, is_closed: true, open_time: null, close_time: null },
{ day_of_week: 1, is_closed: false, open_time: "07:30", close_time: "19:00" },
{ day_of_week: 2, is_closed: false, open_time: "07:30", close_time: "19:00" },
{ day_of_week: 3, is_closed: false, open_time: "07:30", close_time: "18:30" },
{ day_of_week: 4, is_closed: false, open_time: "07:30", close_time: "19:00" },
{ day_of_week: 5, is_closed: false, open_time: "07:30", close_time: "18:00" },
{ day_of_week: 6, is_closed: false, open_time: "09:00", close_time: "12:00" }
]
Restaurant Hours
// Split service with lunch and dinner
const restaurantHours = [
{ day_of_week: 0, is_closed: false, open_time: "12:00", close_time: "22:00" },
{ day_of_week: 1, is_closed: true, open_time: null, close_time: null },
{ day_of_week: 2, is_closed: false, open_time: "12:00", close_time: "22:00" },
{ day_of_week: 3, is_closed: false, open_time: "12:00", close_time: "22:00" },
{ day_of_week: 4, is_closed: false, open_time: "12:00", close_time: "22:00" },
{ day_of_week: 5, is_closed: false, open_time: "12:00", close_time: "23:00" },
{ day_of_week: 6, is_closed: false, open_time: "12:00", close_time: "23:00" }
]
Integration with Scheduling
Shift Validation
Opening hours affect shift scheduling:
// Shifts validated against opening hours
const validateShiftAgainstHours = async (shift: Shift, companyId: string) => {
const { data: hours } = await supabase
.from('company_opening_hours')
.select('*')
.eq('company_id', companyId)
.eq('day_of_week', new Date(shift.date).getDay())
.single()
// Check if business is open
if (hours.is_closed) {
return {
valid: false,
error: 'Cannot schedule shifts on closed days'
}
}
// Validate shift times against business hours
if (shift.start_time < hours.open_time) {
return {
valid: false,
error: `Shift cannot start before business opens at ${hours.open_time}`
}
}
if (shift.end_time > hours.close_time) {
return {
valid: false,
error: `Shift cannot end after business closes at ${hours.close_time}`
}
}
return { valid: true }
}
Calendar Display
Opening hours are displayed on the scheduling calendar:
// Visual indication of business hours
const renderBusinessHours = (day: Date, hours: OpeningHours[]) => {
const dayHours = hours.find(h => h.day_of_week === day.getDay())
if (dayHours?.is_closed) {
return <div className="bg-gray-100 text-gray-400">Closed</div>
}
return (
<div className="text-sm text-secondary-text">
{dayHours?.open_time} - {dayHours?.close_time}
</div>
)
}
Timezone Handling
UK Timezone Rules
// Source: src/lib/utils/timezone.ts
export const UK_TIMEZONE = 'Europe/London'
// Opening hours are stored without timezone
// but displayed in UK local time
const formatOpeningHours = (hours: OpeningHours) => {
const openMoment = moment.tz(hours.open_time, 'HH:mm', UK_TIMEZONE)
const closeMoment = moment.tz(hours.close_time, 'HH:mm', UK_TIMEZONE)
return {
open: openMoment.format('HH:mm'),
close: closeMoment.format('HH:mm')
}
}
DST Considerations
| Period | UK Time | Notes |
|---|---|---|
| GMT | Nov - Mar | No adjustment |
| BST | Mar - Oct | +1 hour |
Opening hours are stored as local times and do not require DST adjustment.
Display Components
Weekly Schedule Display
// Opening hours display component
const OpeningHoursDisplay = ({ hours }: { hours: OpeningHours[] }) => {
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
return (
<div className="space-y-2">
{days.map((day, index) => {
const dayHours = hours.find(h => h.day_of_week === index)
return (
<div key={day} className="flex justify-between">
<span className="font-medium">{day}</span>
{dayHours?.is_closed ? (
<span className="text-secondary-text">Closed</span>
) : (
<span>{dayHours?.open_time} - {dayHours?.close_time}</span>
)}
</div>
)
})}
</div>
)
}
Compact Display
// Compact single-line summary
const OpeningHoursSummary = ({ hours }: { hours: OpeningHours[] }) => {
const weekdayHours = hours.find(h => h.day_of_week === 1) // Monday
if (!weekdayHours || weekdayHours.is_closed) {
return <span>Hours vary</span>
}
return (
<span>
Mon-Fri: {weekdayHours.open_time} - {weekdayHours.close_time}
</span>
)
}
Best Practices
For Configuring Hours
- Be Accurate - Ensure times match actual operating hours
- Include Buffer - Allow time for setup/closedown
- Consider Patterns - Use industry-appropriate schedules
- Review Regularly - Update when hours change
For Scheduling Integration
- Respect Hours - Don't schedule outside business hours
- Early Start - Consider staff arriving before opening
- Late Finish - Allow for closing procedures
- Special Days - Account for bank holidays separately
Validation Rules
Time Format
| Rule | Requirement |
|---|---|
| Format | HH:MM (24-hour) |
| Hours | 00-23 |
| Minutes | 00-59 |
| Order | Close > Open |
Business Rules
| Rule | Description |
|---|---|
| Complete Week | All 7 days must be defined |
| Open Day Requirements | Non-closed days need both times |
| Closed Day Requirements | Closed days can have null times |
| Time Order | Close time must be after open time |
Troubleshooting
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Validation fails | Wrong time format | Use HH:MM (e.g., 09:00) |
| Cannot save | Missing days | Ensure all 7 days defined |
| Times rejected | Close before open | Swap open and close times |
| Update fails | Insufficient permissions | Contact System Admin |
Error Messages
| Error | Meaning | Action |
|---|---|---|
| "Invalid time format" | Not HH:MM format | Use 24-hour format |
| "Close time must be after open time" | Logic error | Correct time order |
| "Open and close times required" | Missing times for open day | Add both times |
| "Forbidden" | Not System Admin | Contact administrator |
Related Documentation
- Company Profile - Company information
- Rooms & Facilities - Manage rooms
- Creating Shifts - Shift scheduling
Source Files:
src/app/api/companies/[companyId]/hours/route.ts- Opening hours APIsrc/lib/validation/company.schemas.ts- Validation schemassrc/types/database.types.ts- Database type definitionssrc/lib/utils/timezone.ts- Timezone utilities