Skip to main content

User Statistics

This guide covers how to view platform-wide user statistics, understanding metrics, and using analytics for decision making.


Overview

What Statistics Are Available?

Shyfts provides key platform metrics:

MetricDescription
Total UsersAll users across platform
Total CompaniesActive companies on platform
System AdminsPlatform administrator count
Company ManagersCompany-level manager count
StaffRegular employee count

Statistics Sources

DataSource TableQuery Method
System Adminssystem_adminsDirect count
Company Managersstaffhierarchy_level = 1
StaffstaffTotal minus managers
CompaniescompaniesDirect count

Statistics API

Get Platform Statistics

Endpoint: GET /api/users/stats

// Source: src/app/api/users/stats/route.ts:8-80
export async function GET() {
try {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()

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

// Check if user is system admin
const isSystemAdmin = getAuthRole(user) === 'SYSTEM_ADMIN'

if (!isSystemAdmin) {
// Fallback check in system_admins table
const { data: adminRecord } = await supabase
.from('system_admins')
.select('id')
.eq('id', user.id)
.single()

if (!adminRecord) {
return ApiResponses.forbidden('Forbidden')
}
}

// Get system admin count
const { count: systemAdminCount, error: systemError } = await supabase
.from('system_admins')
.select('*', { count: 'exact', head: true })

if (systemError) throw systemError

// Get total staff count
const { count: totalStaffCount } = await supabase
.from('staff')
.select('*', { count: 'exact', head: true })

// Get manager count (hierarchy_level = 1)
const { count: staffManagerCount } = await supabase
.from('staff')
.select('*, staff_roles!inner(*)', { count: 'exact', head: true })
.eq('staff_roles.hierarchy_level', 1)
.eq('is_active', true)

const companyManagerCount = staffManagerCount || 0
const staffWithoutManagersCount = Math.max(0, (totalStaffCount || 0) - companyManagerCount)

// Get company count
const { count: companyCount, error: companyCountError } = await supabase
.from('companies')
.select('*', { count: 'exact', head: true })

if (companyCountError) throw companyCountError

const stats = {
totalUsers: (systemAdminCount || 0) + companyManagerCount + staffWithoutManagersCount,
totalCompanies: companyCount || 0,
systemAdmins: systemAdminCount || 0,
companyManagers: companyManagerCount,
staff: staffWithoutManagersCount
}

return ApiResponses.success({ stats })

} catch (error) {
return handleApiError(error)
}
}

Request Example

// Fetch platform statistics
const getPlatformStats = async () => {
const response = await fetch('/api/users/stats')

if (!response.ok) {
throw new Error('Failed to fetch statistics')
}

return response.json()
}

Response Format

// Statistics response
{
success: true,
data: {
stats: {
totalUsers: 150,
totalCompanies: 12,
systemAdmins: 2,
companyManagers: 15,
staff: 133
}
}
}

Statistics Breakdown

Total Users

// How total users is calculated
totalUsers = systemAdmins + companyManagers + staff
ComponentSource
systemAdminsCount from system_admins table
companyManagersStaff with hierarchy_level = 1
staffTotal staff minus managers

User Distribution

// Calculate user distribution percentages
const calculateDistribution = (stats: PlatformStats) => {
const total = stats.totalUsers

return {
systemAdminPercent: (stats.systemAdmins / total * 100).toFixed(1),
managerPercent: (stats.companyManagers / total * 100).toFixed(1),
staffPercent: (stats.staff / total * 100).toFixed(1)
}
}

Average Users Per Company

// Calculate average users per company
const avgUsersPerCompany = (stats: PlatformStats) => {
if (stats.totalCompanies === 0) return 0
const companyUsers = stats.companyManagers + stats.staff
return (companyUsers / stats.totalCompanies).toFixed(1)
}

Statistics Display

Stats Card Component

// Statistics card component
const StatsCard = ({
title,
value,
icon: Icon,
description,
trend
}: {
title: string
value: number
icon: React.ComponentType
description?: string
trend?: { value: number, direction: 'up' | 'down' }
}) => (
<div className="card-glass p-6">
<div className="flex items-start justify-between">
<div>
<p className="text-secondary-text text-sm">{title}</p>
<p className="text-3xl font-bold mt-1">{value.toLocaleString()}</p>
{description && (
<p className="text-xs text-secondary-text mt-1">{description}</p>
)}
</div>
<div className="p-3 bg-coral-gradient/20 rounded-lg">
<Icon className="w-6 h-6 text-coral-primary" />
</div>
</div>
{trend && (
<div className={`flex items-center gap-1 mt-3 text-sm ${
trend.direction === 'up' ? 'text-green-400' : 'text-red-400'
}`}>
{trend.direction === 'up' ? '↑' : '↓'}
{trend.value}% from last month
</div>
)}
</div>
)

Dashboard Grid

// Statistics dashboard grid
const StatsDashboard = ({ stats }: { stats: PlatformStats }) => (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<StatsCard
title="Total Users"
value={stats.totalUsers}
icon={Users}
description="All platform users"
/>
<StatsCard
title="Companies"
value={stats.totalCompanies}
icon={Building2}
description="Active companies"
/>
<StatsCard
title="Company Managers"
value={stats.companyManagers}
icon={UserCog}
description="Company-level admins"
/>
<StatsCard
title="Staff Members"
value={stats.staff}
icon={UserCheck}
description="Regular employees"
/>
</div>
)

Distribution Chart

// User distribution visualisation
const UserDistribution = ({ stats }: { stats: PlatformStats }) => {
const distribution = calculateDistribution(stats)

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

<div className="space-y-4">
{/* System Admins */}
<div>
<div className="flex justify-between text-sm mb-1">
<span>System Admins</span>
<span>{stats.systemAdmins} ({distribution.systemAdminPercent}%)</span>
</div>
<div className="h-2 bg-white/10 rounded overflow-hidden">
<div
className="h-full bg-purple-500"
style={{ width: `${distribution.systemAdminPercent}%` }}
/>
</div>
</div>

{/* Company Managers */}
<div>
<div className="flex justify-between text-sm mb-1">
<span>Company Managers</span>
<span>{stats.companyManagers} ({distribution.managerPercent}%)</span>
</div>
<div className="h-2 bg-white/10 rounded overflow-hidden">
<div
className="h-full bg-blue-500"
style={{ width: `${distribution.managerPercent}%` }}
/>
</div>
</div>

{/* Staff */}
<div>
<div className="flex justify-between text-sm mb-1">
<span>Staff Members</span>
<span>{stats.staff} ({distribution.staffPercent}%)</span>
</div>
<div className="h-2 bg-white/10 rounded overflow-hidden">
<div
className="h-full bg-green-500"
style={{ width: `${distribution.staffPercent}%` }}
/>
</div>
</div>
</div>
</div>
)
}

Statistics Types

PlatformStats Interface

// Platform statistics type
interface PlatformStats {
totalUsers: number
totalCompanies: number
systemAdmins: number
companyManagers: number
staff: number
}

Extended Statistics

For more detailed analytics, you can calculate:

// Extended statistics calculations
const extendedStats = (stats: PlatformStats) => ({
...stats,

// Averages
avgStaffPerCompany: stats.totalCompanies > 0
? (stats.staff / stats.totalCompanies).toFixed(1)
: '0',

avgManagersPerCompany: stats.totalCompanies > 0
? (stats.companyManagers / stats.totalCompanies).toFixed(1)
: '0',

// Ratios
staffToManagerRatio: stats.companyManagers > 0
? (stats.staff / stats.companyManagers).toFixed(1)
: '0',

// Percentages
distribution: {
systemAdmin: ((stats.systemAdmins / stats.totalUsers) * 100).toFixed(1),
manager: ((stats.companyManagers / stats.totalUsers) * 100).toFixed(1),
staff: ((stats.staff / stats.totalUsers) * 100).toFixed(1)
}
})

Real-Time Updates

Auto-Refresh

// Auto-refresh statistics
const useStats = (refreshInterval = 60000) => {
const [stats, setStats] = useState<PlatformStats | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)

const fetchStats = useCallback(async () => {
try {
const response = await fetch('/api/users/stats')
const { data } = await response.json()
setStats(data.stats)
setError(null)
} catch (err) {
setError('Failed to fetch statistics')
} finally {
setLoading(false)
}
}, [])

useEffect(() => {
fetchStats()
const interval = setInterval(fetchStats, refreshInterval)
return () => clearInterval(interval)
}, [fetchStats, refreshInterval])

return { stats, loading, error, refresh: fetchStats }
}

Manual Refresh

// Refresh button component
const RefreshStats = ({ onRefresh, loading }: {
onRefresh: () => void
loading: boolean
}) => (
<button
onClick={onRefresh}
disabled={loading}
className="form-button secondary flex items-center gap-2"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
{loading ? 'Refreshing...' : 'Refresh'}
</button>
)

Data Export

Export Statistics

// Export statistics to CSV
const exportStatsToCSV = (stats: PlatformStats) => {
const rows = [
['Metric', 'Value'],
['Total Users', stats.totalUsers],
['Total Companies', stats.totalCompanies],
['System Admins', stats.systemAdmins],
['Company Managers', stats.companyManagers],
['Staff Members', stats.staff]
]

const csv = rows.map(row => row.join(',')).join('\n')
const blob = new Blob([csv], { type: 'text/csv' })
const url = URL.createObjectURL(blob)

const link = document.createElement('a')
link.href = url
link.download = `platform-stats-${new Date().toISOString().split('T')[0]}.csv`
link.click()
}

JSON Export

// Export statistics to JSON
const exportStatsToJSON = (stats: PlatformStats) => {
const exportData = {
generatedAt: new Date().toISOString(),
statistics: stats,
calculations: {
avgUsersPerCompany: (stats.totalUsers / stats.totalCompanies).toFixed(2),
staffToManagerRatio: (stats.staff / stats.companyManagers).toFixed(2)
}
}

const blob = new Blob([JSON.stringify(exportData, null, 2)], {
type: 'application/json'
})
const url = URL.createObjectURL(blob)

const link = document.createElement('a')
link.href = url
link.download = `platform-stats-${new Date().toISOString().split('T')[0]}.json`
link.click()
}

Access Control

Who Can View Statistics

RoleAccess Level
System AdminFull platform statistics
Company ManagerCompany-specific only
StaffNo access

API Security

// Source: src/app/api/users/stats/route.ts:13-31
// Only System Admins can access statistics
if (!user) {
return ApiResponses.unauthorized()
}

const isSystemAdmin = getAuthRole(user) === 'SYSTEM_ADMIN'

if (!isSystemAdmin) {
const { data: adminRecord } = await supabase
.from('system_admins')
.select('id')
.eq('id', user.id)
.single()

if (!adminRecord) {
return ApiResponses.forbidden('Forbidden')
}
}

Use Cases

Platform Monitoring

MetricInsight
Total UsersPlatform growth
CompaniesCustomer acquisition
Staff/Company RatioCompany size trends
Manager/Staff RatioManagement overhead

Decision Making

QuestionMetric to Check
Is platform growing?Compare total users over time
Are companies active?Staff per company ratio
Management balanced?Manager to staff ratio
Pricing tiers?Company size distribution

Troubleshooting

Common Issues

IssueCauseSolution
Statistics not loadingNetwork errorCheck connection
Forbidden errorNot System AdminLogin correctly
Stale dataCache delayRefresh manually
Zero countsEmpty databaseAdd test data

Error Messages

ErrorMeaningAction
"Unauthorized"Not logged inLogin first
"Forbidden"Not System AdminUse admin account
"Server error"Database issueContact support


Source Files:

  • src/app/api/users/stats/route.ts - Statistics API endpoint
  • src/lib/auth/metadata.ts - Role checking utilities
  • src/types/api.ts - Type definitions