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:
| Category | Scope | Description |
|---|---|---|
| Company Settings | Per company | Hours, contact, branding |
| Opening Hours | Per company | Business operating times |
| Industry Templates | Platform-wide | Template configurations |
| User Management | Platform-wide | User and company oversight |
Settings by Role
| Role | Access Level |
|---|---|
| System Admin | All platform settings |
| Company Manager | Company-specific settings only |
| Staff | No settings access |
Company Opening Hours
What Are Opening Hours?
Opening hours define when a company operates:
| Field | Description |
|---|---|
| day_of_week | 0-6 (Sunday-Saturday) |
| open_time | Opening time (HH:mm) |
| close_time | Closing time (HH:mm) |
| is_closed | Whether 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:
| Field | Description | Default |
|---|---|---|
| full_time_hours_per_week | Standard full-time hours | 37.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:
| Field | Description |
|---|---|
| feature_id | Feature being subscribed to |
| billing_cycle | monthly, yearly |
| start_date | Subscription start |
| end_date | Subscription end (null = ongoing) |
| is_active | Currently 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:
| Variable | Purpose |
|---|---|
| NODE_ENV | Environment (development/production) |
| NEXT_PUBLIC_SUPABASE_URL | Supabase project URL |
| SUPABASE_SERVICE_ROLE_KEY | Admin access key |
| RESEND_API_KEY | Email 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
| Setting | System Admin | Company 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
| Practice | Description |
|---|---|
| Set all days | Configure all 7 days for consistency |
| Use 24-hour format | Follow UK time conventions |
| Mark closed days | Use is_closed rather than empty times |
| Validate times | Ensure close > open |
Company Settings
| Practice | Description |
|---|---|
| Complete all fields | Fill contact details |
| Use valid formats | UK postal codes, phone numbers |
| Regular review | Update settings when changes occur |
| Document changes | Keep audit trail of modifications |
Troubleshooting
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Hours not saving | Invalid time format | Use HH:mm format |
| Forbidden error | Not System Admin | Login as admin |
| Company not found | Invalid company ID | Verify ID exists |
| Validation failed | Missing required fields | Check all fields |
Error Messages
| Error | Meaning | Action |
|---|---|---|
| "Only System Admins can update" | Insufficient permissions | Use admin account |
| "Invalid request - hours must be an array" | Wrong format | Pass array of hours |
| "Company not found" | Invalid company ID | Check company exists |
| "Unauthorized" | Not logged in | Login 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
}
Related Documentation
- Editing Companies - Company updates
- Industry Templates - Template configuration
- Company Status - Activate/deactivate
- User Management - User administration
Source Files:
src/app/api/companies/[companyId]/hours/route.ts- Opening hours APIsrc/app/api/companies/[companyId]/route.ts- Company settings APIsrc/lib/validation/company.schemas.ts- Validation schemassrc/types/database.types.ts- Database schema