Skip to main content

Platform Settings

This guide covers platform configuration options, company opening hours management, and system-wide settings available to System Admins.


Overview

What Settings Are Available?

Shyfts provides several configuration areas:

CategoryScopeDescription
Company SettingsPer companyHours, contact, branding
Opening HoursPer companyBusiness operating times
Industry TemplatesPlatform-wideTemplate configurations
User ManagementPlatform-wideUser and company oversight

Settings by Role

RoleAccess Level
System AdminAll platform settings
Company ManagerCompany-specific settings only
StaffNo settings access

Company Opening Hours

What Are Opening Hours?

Opening hours define when a company operates:

FieldDescription
day_of_week0-6 (Sunday-Saturday)
open_timeOpening time (HH:mm)
close_timeClosing time (HH:mm)
is_closedWhether closed that day

Database Schema

// Source: src/types/database.types.ts:393-440
company_opening_hours: {
Row: {
id: string
company_id: string
day_of_week: number
open_time: string | null
close_time: string | null
is_closed: boolean
created_at: string
updated_at: string
}
}

Opening Hours API

Get Company Hours

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

// Source: src/app/api/companies/[companyId]/hours/route.ts:15-70
export async function GET(_request: NextRequest, { params }: RouteParams) {
try {
const supabase = await createClient()
const { data: { user }, error: authError } = await supabase.auth.getUser()

if (!user) {
return ApiResponses.unauthorized()
}

const userRole = getAuthRole(user)
if (userRole !== 'SYSTEM_ADMIN') {
const accessValidation = await validateCompanyAccess(supabase, params.companyId, user)
if (!accessValidation.isValid) {
return ApiResponses.error(
new Error(accessValidation.error || 'Forbidden'),
accessValidation.statusCode || 403
)
}
}

// Query company opening hours from the database
const { data, error } = await supabase
.from('company_opening_hours')
.select('*')
.eq('company_id', params.companyId)
.order('day_of_week', { ascending: true })

if (error) {
return handleApiError(error)
}

return ApiResponses.success(data || [], { timing })
} catch (error) {
return handleApiError(error)
}
}

Update Company Hours

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

// Source: src/app/api/companies/[companyId]/hours/route.ts:72-168
export async function PUT(request: NextRequest, { params }: RouteParams) {
try {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()

if (!user) {
return ApiResponses.unauthorized()
}

// Check if user is system admin
const userRole = getAuthRole(user)
if (userRole !== 'SYSTEM_ADMIN') {
return ApiResponses.forbidden('Only System Admins can update opening hours')
}

const body = await request.json()
const { hours } = body

if (!Array.isArray(hours)) {
return ApiResponses.badRequest('Invalid request - hours must be an array')
}

// Delete existing hours first
const { error: deleteError } = await supabase
.from('company_opening_hours')
.delete()
.eq('company_id', params.companyId)

if (deleteError) {
return handleApiError(deleteError)
}

// Insert new hours if provided
if (hours.length > 0) {
const insertData = hours.map(hour => ({
...hour,
company_id: params.companyId,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
}))

const { data, error: insertError } = await supabase
.from('company_opening_hours')
.insert(insertData)
.select()

if (insertError) {
return handleApiError(insertError)
}

return ApiResponses.success(data || [], { timing })
}

return ApiResponses.success([], { timing })
} catch (error) {
return handleApiError(error)
}
}

Request Format

// Update opening hours request
const updateOpeningHours = async (companyId: string, hours: OpeningHour[]) => {
const response = await fetch(`/api/companies/${companyId}/hours`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hours })
})

return response.json()
}

// Example hours data
const weeklyHours = [
{ day_of_week: 0, is_closed: true }, // Sunday - closed
{ day_of_week: 1, open_time: '08:00', close_time: '18:00', is_closed: false },
{ day_of_week: 2, open_time: '08:00', close_time: '18:00', is_closed: false },
{ day_of_week: 3, open_time: '08:00', close_time: '18:00', is_closed: false },
{ day_of_week: 4, open_time: '08:00', close_time: '18:00', is_closed: false },
{ day_of_week: 5, open_time: '08:00', close_time: '17:00', is_closed: false },
{ day_of_week: 6, is_closed: true } // Saturday - closed
]

Response Format

// Successful response
{
success: true,
data: [
{
id: "hour-uuid-1",
company_id: "company-uuid",
day_of_week: 1,
open_time: "08:00",
close_time: "18:00",
is_closed: false,
created_at: "2025-01-14T10:00:00Z",
updated_at: "2025-01-14T10:00:00Z"
},
// ... other days
],
timing: "45ms"
}

Opening Hours Display

Day Names Mapping

// Map day_of_week to UK day names
const dayNames: Record<number, string> = {
0: 'Sunday',
1: 'Monday',
2: 'Tuesday',
3: 'Wednesday',
4: 'Thursday',
5: 'Friday',
6: 'Saturday'
}

Opening Hours Component

// Opening hours display component
const OpeningHoursDisplay = ({ hours }: { hours: OpeningHour[] }) => {
const sortedHours = [...hours].sort((a, b) => a.day_of_week - b.day_of_week)

return (
<div className="card-glass p-6">
<h3 className="text-lg font-semibold mb-4">Opening Hours</h3>

<div className="space-y-2">
{sortedHours.map((hour) => (
<div key={hour.id} className="flex justify-between">
<span className="font-medium">{dayNames[hour.day_of_week]}</span>
<span className={hour.is_closed ? 'text-red-400' : 'text-green-400'}>
{hour.is_closed
? 'Closed'
: `${hour.open_time} - ${hour.close_time}`
}
</span>
</div>
))}
</div>
</div>
)
}

Opening Hours Editor

// Opening hours editor component
const OpeningHoursEditor = ({
companyId,
initialHours,
onSave
}: {
companyId: string
initialHours: OpeningHour[]
onSave: (hours: OpeningHour[]) => void
}) => {
const [hours, setHours] = useState(initialHours)
const [saving, setSaving] = useState(false)

const handleDayChange = (dayIndex: number, field: string, value: any) => {
setHours(prev => prev.map((hour, idx) =>
idx === dayIndex ? { ...hour, [field]: value } : hour
))
}

const handleSave = async () => {
setSaving(true)
try {
await updateOpeningHours(companyId, hours)
onSave(hours)
} finally {
setSaving(false)
}
}

return (
<div className="card-glass p-6">
<h3 className="text-lg font-semibold mb-4">Edit Opening Hours</h3>

<div className="space-y-4">
{hours.map((hour, idx) => (
<div key={idx} className="flex items-center gap-4">
<span className="w-24 font-medium">{dayNames[hour.day_of_week]}</span>

<label className="flex items-center gap-2">
<input
type="checkbox"
checked={hour.is_closed}
onChange={(e) => handleDayChange(idx, 'is_closed', e.target.checked)}
className="form-checkbox"
/>
<span>Closed</span>
</label>

{!hour.is_closed && (
<>
<input
type="time"
value={hour.open_time || ''}
onChange={(e) => handleDayChange(idx, 'open_time', e.target.value)}
className="form-input w-32"
/>
<span>to</span>
<input
type="time"
value={hour.close_time || ''}
onChange={(e) => handleDayChange(idx, 'close_time', e.target.value)}
className="form-input w-32"
/>
</>
)}
</div>
))}
</div>

<button
onClick={handleSave}
disabled={saving}
className="form-button primary mt-4"
>
{saving ? 'Saving...' : 'Save Hours'}
</button>
</div>
)
}

Full-Time Hours Configuration

Company Full-Time Hours

Each company can configure full-time hours per week:

FieldDescriptionDefault
full_time_hours_per_weekStandard full-time hours37.5

Database Field

// Source: src/types/database.types.ts (companies table)
companies: {
Row: {
// ...
full_time_hours_per_week: number | null // Default: 37.5
// ...
}
}

Update Full-Time Hours

// Update company full-time hours setting
const updateFullTimeHours = async (companyId: string, hours: number) => {
const response = await fetch(`/api/companies/${companyId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ full_time_hours_per_week: hours })
})

return response.json()
}

// Example: Set to UK standard 37.5 hours
await updateFullTimeHours('company-uuid', 37.5)

Usage in Calculations

// Calculate if staff member is full-time
const isFullTime = (
contractedHours: number,
companyFullTimeHours: number
): boolean => {
return contractedHours >= companyFullTimeHours
}

// Calculate FTE (Full-Time Equivalent)
const calculateFTE = (
contractedHours: number,
companyFullTimeHours: number
): number => {
return contractedHours / companyFullTimeHours
}

Company Subscriptions

What Are Subscriptions?

Subscriptions control company feature access:

FieldDescription
feature_idFeature being subscribed to
billing_cyclemonthly, yearly
start_dateSubscription start
end_dateSubscription end (null = ongoing)
is_activeCurrently active

Database Schema

// Source: src/types/database.types.ts:441-475
company_subscriptions: {
Row: {
id: string
company_id: string
feature_id: string
billing_cycle: string
start_date: string
end_date: string | null
is_active: boolean | null
last_billing_date: string | null
next_billing_date: string | null
created_at: string | null
updated_at: string | null
}
}

Subscription Status Check

// Check if company has active subscription
const hasActiveSubscription = async (
companyId: string,
featureId: string
): Promise<boolean> => {
const { data, error } = await supabase
.from('company_subscriptions')
.select('is_active')
.eq('company_id', companyId)
.eq('feature_id', featureId)
.eq('is_active', true)
.single()

return !error && data?.is_active === true
}

Platform Configuration

Environment Variables

Key platform configuration via environment variables:

VariablePurpose
NODE_ENVEnvironment (development/production)
NEXT_PUBLIC_SUPABASE_URLSupabase project URL
SUPABASE_SERVICE_ROLE_KEYAdmin access key
RESEND_API_KEYEmail service key

Environment Checks

// Source: src/app/api/seed-templates/route.ts:291-296
// Disable certain features in production
if (process.env.NODE_ENV === 'production') {
return NextResponse.json(
{ error: 'Feature disabled in production' },
{ status: 403 }
)
}

Company Settings UI

Settings Dashboard Component

// Company settings dashboard
const CompanySettingsDashboard = ({ company }: { company: Company }) => {
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Basic Information */}
<div className="card-glass p-6">
<h3 className="text-lg font-semibold mb-4">Company Information</h3>
<dl className="space-y-3">
<div>
<dt className="text-secondary-text text-sm">Name</dt>
<dd className="font-medium">{company.name}</dd>
</div>
<div>
<dt className="text-secondary-text text-sm">Industry</dt>
<dd className="font-medium">{company.industryTemplate?.display_name}</dd>
</div>
<div>
<dt className="text-secondary-text text-sm">Subdomain</dt>
<dd className="font-medium">{company.subdomain}</dd>
</div>
</dl>
</div>

{/* Contact Information */}
<div className="card-glass p-6">
<h3 className="text-lg font-semibold mb-4">Contact Details</h3>
<dl className="space-y-3">
<div>
<dt className="text-secondary-text text-sm">Email</dt>
<dd className="font-medium">{company.email || 'Not set'}</dd>
</div>
<div>
<dt className="text-secondary-text text-sm">Phone</dt>
<dd className="font-medium">{company.phone || 'Not set'}</dd>
</div>
<div>
<dt className="text-secondary-text text-sm">Address</dt>
<dd className="font-medium">
{company.address ? (
<>
{company.address}<br />
{company.city}, {company.postalCode}
</>
) : (
'Not set'
)}
</dd>
</div>
</dl>
</div>

{/* Operational Settings */}
<div className="card-glass p-6">
<h3 className="text-lg font-semibold mb-4">Operational Settings</h3>
<dl className="space-y-3">
<div>
<dt className="text-secondary-text text-sm">Full-Time Hours</dt>
<dd className="font-medium">{company.fullTimeHoursPerWeek || 37.5} hours/week</dd>
</div>
<div>
<dt className="text-secondary-text text-sm">Status</dt>
<dd>
<span className={`px-2 py-1 rounded text-sm ${
company.isActive
? 'bg-green-500/20 text-green-300'
: 'bg-red-500/20 text-red-300'
}`}>
{company.isActive ? 'Active' : 'Inactive'}
</span>
</dd>
</div>
</dl>
</div>

{/* Opening Hours */}
<OpeningHoursDisplay hours={company.openingHours || []} />
</div>
)
}

Settings Validation

Opening Hours Validation

// Validate opening hours data
const validateOpeningHours = (hours: OpeningHour[]): ValidationResult => {
const errors: string[] = []

hours.forEach((hour) => {
if (!hour.is_closed) {
if (!hour.open_time) {
errors.push(`${dayNames[hour.day_of_week]}: Missing open time`)
}
if (!hour.close_time) {
errors.push(`${dayNames[hour.day_of_week]}: Missing close time`)
}
if (hour.open_time && hour.close_time && hour.open_time >= hour.close_time) {
errors.push(`${dayNames[hour.day_of_week]}: Close time must be after open time`)
}
}
})

return {
isValid: errors.length === 0,
errors
}
}

Full-Time Hours Validation

// Source: src/lib/validation/company.schemas.ts
// Full-time hours must be positive and reasonable
z.number()
.min(1, 'Full-time hours must be at least 1')
.max(168, 'Full-time hours cannot exceed 168 (hours in a week)')
.optional()

Access Control

Settings Permissions

SettingSystem AdminCompany Manager
View company settings✅ All companies✅ Own company
Edit company settings✅ Yes❌ No
View opening hours✅ All companies✅ Own company
Edit opening hours✅ Yes❌ No
View subscriptions✅ Yes❌ No
Manage subscriptions✅ Yes❌ No

Permission Check Example

// Check if user can modify settings
const canModifySettings = (userRole: string): boolean => {
return userRole === 'SYSTEM_ADMIN'
}

// Usage in component
{canModifySettings(userRole) && (
<button onClick={handleEdit} className="form-button primary">
Edit Settings
</button>
)}

Best Practices

Opening Hours

PracticeDescription
Set all daysConfigure all 7 days for consistency
Use 24-hour formatFollow UK time conventions
Mark closed daysUse is_closed rather than empty times
Validate timesEnsure close > open

Company Settings

PracticeDescription
Complete all fieldsFill contact details
Use valid formatsUK postal codes, phone numbers
Regular reviewUpdate settings when changes occur
Document changesKeep audit trail of modifications

Troubleshooting

Common Issues

IssueCauseSolution
Hours not savingInvalid time formatUse HH:mm format
Forbidden errorNot System AdminLogin as admin
Company not foundInvalid company IDVerify ID exists
Validation failedMissing required fieldsCheck all fields

Error Messages

ErrorMeaningAction
"Only System Admins can update"Insufficient permissionsUse admin account
"Invalid request - hours must be an array"Wrong formatPass array of hours
"Company not found"Invalid company IDCheck company exists
"Unauthorized"Not logged inLogin first

Settings Audit

Activity Logging

Settings changes are logged for audit:

// Activity log entry for settings change
{
action: 'company_settings_updated',
company_id: 'company-uuid',
created_by: 'admin-uuid',
details: {
fields_updated: ['full_time_hours_per_week'],
old_values: { full_time_hours_per_week: 37.5 },
new_values: { full_time_hours_per_week: 40 }
},
created_at: '2025-01-14T10:30:00Z',
ip_address: '192.168.1.1'
}

Viewing Audit History

// Get settings change history
const getSettingsHistory = async (companyId: string) => {
const { data } = await supabase
.from('company_manager_activity_logs')
.select('*')
.eq('company_id', companyId)
.in('action', ['company_settings_updated', 'opening_hours_updated'])
.order('created_at', { ascending: false })

return data
}


Source Files:

  • src/app/api/companies/[companyId]/hours/route.ts - Opening hours API
  • src/app/api/companies/[companyId]/route.ts - Company settings API
  • src/lib/validation/company.schemas.ts - Validation schemas
  • src/types/database.types.ts - Database schema