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:
| Metric | Description |
|---|---|
| Total Users | All users across platform |
| Total Companies | Active companies on platform |
| System Admins | Platform administrator count |
| Company Managers | Company-level manager count |
| Staff | Regular employee count |
Statistics Sources
| Data | Source Table | Query Method |
|---|---|---|
| System Admins | system_admins | Direct count |
| Company Managers | staff | hierarchy_level = 1 |
| Staff | staff | Total minus managers |
| Companies | companies | Direct 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
| Component | Source |
|---|---|
| systemAdmins | Count from system_admins table |
| companyManagers | Staff with hierarchy_level = 1 |
| staff | Total 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
| Role | Access Level |
|---|---|
| System Admin | Full platform statistics |
| Company Manager | Company-specific only |
| Staff | No 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
| Metric | Insight |
|---|---|
| Total Users | Platform growth |
| Companies | Customer acquisition |
| Staff/Company Ratio | Company size trends |
| Manager/Staff Ratio | Management overhead |
Decision Making
| Question | Metric 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
| Issue | Cause | Solution |
|---|---|---|
| Statistics not loading | Network error | Check connection |
| Forbidden error | Not System Admin | Login correctly |
| Stale data | Cache delay | Refresh manually |
| Zero counts | Empty database | Add test data |
Error Messages
| Error | Meaning | Action |
|---|---|---|
| "Unauthorized" | Not logged in | Login first |
| "Forbidden" | Not System Admin | Use admin account |
| "Server error" | Database issue | Contact support |
Related Documentation
- Viewing Users - Full user list
- Company Management - Company operations
- Platform Settings - System configuration
Source Files:
src/app/api/users/stats/route.ts- Statistics API endpointsrc/lib/auth/metadata.ts- Role checking utilitiessrc/types/api.ts- Type definitions