Skip to main content

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:

  1. Daily Schedule - Open and close times for each day of the week
  2. Closed Days - Days when the business is closed
  3. Scheduling Integration - Affects shift scheduling and availability

Access Levels​

RoleViewEditRequest Changes
System Admin✅✅N/A
Company Manager✅❌✅
Staff✅❌❌
note

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:

  1. Navigate to Dashboard → Company Settings → Hours tab
  2. Click the "Request Changes" button
  3. Enter a detailed description of the changes needed
  4. Submit the request

What Happens Next​

  1. All System Administrators receive a notification
  2. A System Admin will review your request
  3. They may contact you for clarification
  4. 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"
tip

The more detail you provide, the faster your request can be processed.


Viewing Opening Hours​

  1. Navigate to Settings → Opening Hours from the sidebar
  2. View the weekly schedule with times for each day
  3. 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​

ValueDayNotes
0SundayTypically closed
1MondayStart of UK business week
2Tuesday
3Wednesday
4Thursday
5Friday
6SaturdayOften 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):

FormatExampleDescription
HH:MM09:009:00 AM
HH:MM17:305:30 PM
HH:MM00:00Midnight
HH:MM23:59End of day

Updating Opening Hours​

Update API​

Endpoint: PUT /api/companies/[companyId]/hours

Permission Required

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​

PeriodUK TimeNotes
GMTNov - MarNo adjustment
BSTMar - 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​

  1. Be Accurate - Ensure times match actual operating hours
  2. Include Buffer - Allow time for setup/closedown
  3. Consider Patterns - Use industry-appropriate schedules
  4. Review Regularly - Update when hours change

For Scheduling Integration​

  1. Respect Hours - Don't schedule outside business hours
  2. Early Start - Consider staff arriving before opening
  3. Late Finish - Allow for closing procedures
  4. Special Days - Account for bank holidays separately

Validation Rules​

Time Format​

RuleRequirement
FormatHH:MM (24-hour)
Hours00-23
Minutes00-59
OrderClose > Open

Business Rules​

RuleDescription
Complete WeekAll 7 days must be defined
Open Day RequirementsNon-closed days need both times
Closed Day RequirementsClosed days can have null times
Time OrderClose time must be after open time

Troubleshooting​

Common Issues​

IssueCauseSolution
Validation failsWrong time formatUse HH:MM (e.g., 09:00)
Cannot saveMissing daysEnsure all 7 days defined
Times rejectedClose before openSwap open and close times
Update failsInsufficient permissionsContact System Admin

Error Messages​

ErrorMeaningAction
"Invalid time format"Not HH:MM formatUse 24-hour format
"Close time must be after open time"Logic errorCorrect time order
"Open and close times required"Missing times for open dayAdd both times
"Forbidden"Not System AdminContact administrator


Source Files:

  • src/app/api/companies/[companyId]/hours/route.ts - Opening hours API
  • src/lib/validation/company.schemas.ts - Validation schemas
  • src/types/database.types.ts - Database type definitions
  • src/lib/utils/timezone.ts - Timezone utilities