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