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