Viewing Users
This guide covers how to view all platform users, understanding user types, and filtering options.
Overview
User Types
Shyfts has three distinct user roles:
| Role | Description | Access Level |
|---|---|---|
| System Admin | Platform administrators | Full platform access |
| Company Manager | Company-level managers | Full company access |
| Staff | Regular employees | Limited company access |
User Data Sources
| Role | Database Table | Identification |
|---|---|---|
| System Admin | system_admins | Direct table membership |
| Company Manager | staff | hierarchy_level = 1 in staff_roles |
| Staff | staff | All other staff members |
Users API
List All Users
Endpoint: GET /api/users
// Source: src/app/api/users/route.ts:9-118
export async function GET() {
try {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user || getAuthRole(user) !== 'SYSTEM_ADMIN') {
return ApiResponses.forbidden('Forbidden')
}
// Get system admins
const { data: systemAdmins, error: systemError } = await supabase
.from('system_admins')
.select('*')
if (systemError) throw systemError
// Get company managers (staff with hierarchy_level = 1)
const { data: companyManagers, error: companyError } = await supabase
.from('staff')
.select(`
*,
staff_roles!inner (
hierarchy_level
),
companies (
id,
name,
industry_templates (
id,
name
)
)
`)
.eq('staff_roles.hierarchy_level', 1)
if (companyError) throw companyError
// Get all staff with company info
const { data: staff, error: staffError } = await supabase
.from('staff')
.select(`
*,
companies (
id,
name,
industry_templates (
id,
name
)
)
`)
if (staffError) throw staffError
// Combine into unified format
const allUsers: UserWithCompany[] = [
...(systemAdmins || []).map(admin => ({
role: 'SYSTEM_ADMIN' as const,
// ... mapped fields
})),
...(companyManagers || []).map(manager => ({
role: 'COMPANY_MANAGER' as const,
// ... mapped fields
})),
...(staff || []).map(member => ({
role: 'STAFF' as const,
// ... mapped fields
}))
]
// Sort by created_at descending
const users = allUsers.sort((a, b) => {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
})
return ApiResponses.success({ users })
} catch (error) {
return handleApiError(error)
}
}
Request Example
// Fetch all platform users
const getAllUsers = async () => {
const response = await fetch('/api/users')
if (!response.ok) {
throw new Error('Failed to fetch users')
}
return response.json()
}
Response Format
// Users list response
{
success: true,
data: {
users: [
{
id: "user-uuid",
email: "admin@cflow.com",
first_name: "John",
last_name: "Smith",
full_name: "John Smith",
role: "SYSTEM_ADMIN",
company_id: null,
created_at: "2025-01-01T00:00:00Z",
updated_at: "2025-01-14T10:30:00Z",
companies: null
},
{
id: "manager-uuid",
email: "manager@practice.com",
first_name: "Jane",
last_name: "Doe",
full_name: "Jane Doe",
role: "COMPANY_MANAGER",
company_id: "company-uuid",
created_at: "2025-01-05T00:00:00Z",
updated_at: "2025-01-14T10:30:00Z",
companies: {
id: "company-uuid",
name: "Example Practice",
industry_templates: {
id: "template-uuid",
name: "gp_practice"
}
}
},
{
id: "staff-uuid",
email: "staff@practice.com",
first_name: "Bob",
last_name: "Johnson",
full_name: "Bob Johnson",
role: "STAFF",
company_id: "company-uuid",
created_at: "2025-01-10T00:00:00Z",
updated_at: "2025-01-14T10:30:00Z",
companies: {
id: "company-uuid",
name: "Example Practice",
industry_templates: {
id: "template-uuid",
name: "gp_practice"
}
}
}
]
}
}
User Data Structure
UserWithCompany Type
// Source: src/types/api.ts
interface UserWithCompany {
id: string
email: string
first_name: string
last_name: string
full_name: string
role: 'SYSTEM_ADMIN' | 'COMPANY_MANAGER' | 'STAFF'
company_id: string | null
created_at: string
updated_at: string
companies: {
id: string
name: string
industry_templates: {
id: string
name: string
} | null
} | null
}
Role Determination
| Role | How Determined |
|---|---|
| SYSTEM_ADMIN | User exists in system_admins table |
| COMPANY_MANAGER | Staff with staff_roles.hierarchy_level = 1 |
| STAFF | All other staff members |
Filtering Users
By Role
// Filter users by role
const filterUsersByRole = (users: UserWithCompany[], role: string) => {
return users.filter(user => user.role === role)
}
// Get only system admins
const systemAdmins = filterUsersByRole(users, 'SYSTEM_ADMIN')
// Get only company managers
const managers = filterUsersByRole(users, 'COMPANY_MANAGER')
// Get only staff
const staff = filterUsersByRole(users, 'STAFF')
By Company
// Filter users by company
const filterUsersByCompany = (users: UserWithCompany[], companyId: string) => {
return users.filter(user => user.company_id === companyId)
}
// Get users for specific company
const companyUsers = filterUsersByCompany(users, 'company-uuid')
By Date Range
// Filter users by creation date
const filterUsersByDate = (
users: UserWithCompany[],
startDate: string,
endDate: string
) => {
const start = new Date(startDate)
const end = new Date(endDate)
return users.filter(user => {
const created = new Date(user.created_at)
return created >= start && created <= end
})
}
Combined Filters
// Apply multiple filters
const applyFilters = (
users: UserWithCompany[],
filters: {
role?: string
companyId?: string
searchTerm?: string
}
) => {
let filtered = [...users]
if (filters.role) {
filtered = filtered.filter(u => u.role === filters.role)
}
if (filters.companyId) {
filtered = filtered.filter(u => u.company_id === filters.companyId)
}
if (filters.searchTerm) {
const term = filters.searchTerm.toLowerCase()
filtered = filtered.filter(u =>
u.full_name.toLowerCase().includes(term) ||
u.email.toLowerCase().includes(term)
)
}
return filtered
}
User List Display
Table Component
// User list table component
const UserListTable = ({ users }: { users: UserWithCompany[] }) => (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-white/10">
<th className="text-left py-3 px-4">Name</th>
<th className="text-left py-3 px-4">Email</th>
<th className="text-left py-3 px-4">Role</th>
<th className="text-left py-3 px-4">Company</th>
<th className="text-left py-3 px-4">Created</th>
</tr>
</thead>
<tbody>
{users.map(user => (
<tr key={user.id} className="border-b border-white/5 hover:bg-white/5">
<td className="py-3 px-4">{user.full_name}</td>
<td className="py-3 px-4 text-secondary-text">{user.email}</td>
<td className="py-3 px-4">
<RoleBadge role={user.role} />
</td>
<td className="py-3 px-4">
{user.companies?.name || '-'}
</td>
<td className="py-3 px-4 text-secondary-text">
{formatDate(user.created_at)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)
Role Badge Component
// Role badge component
const RoleBadge = ({ role }: { role: string }) => {
const styles = {
SYSTEM_ADMIN: 'bg-purple-500/20 text-purple-300',
COMPANY_MANAGER: 'bg-blue-500/20 text-blue-300',
STAFF: 'bg-green-500/20 text-green-300'
}
const labels = {
SYSTEM_ADMIN: 'System Admin',
COMPANY_MANAGER: 'Company Manager',
STAFF: 'Staff'
}
return (
<span className={`px-2 py-1 rounded text-xs font-medium ${styles[role]}`}>
{labels[role]}
</span>
)
}
Search Component
// User search component
const UserSearch = ({ onSearch }: { onSearch: (term: string) => void }) => {
const [searchTerm, setSearchTerm] = useState('')
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const term = e.target.value
setSearchTerm(term)
onSearch(term)
}
return (
<div className="relative">
<input
type="text"
value={searchTerm}
onChange={handleSearch}
placeholder="Search users by name or email..."
className="form-input w-full pl-10"
/>
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-secondary-text" />
</div>
)
}
Sorting Users
Sort Options
| Sort By | Description |
|---|---|
| Name | Alphabetical by full name |
| Alphabetical by email | |
| Role | System Admin → Manager → Staff |
| Company | Alphabetical by company name |
| Created | Most recent first (default) |
Sort Implementation
// Sort users by different criteria
const sortUsers = (
users: UserWithCompany[],
sortBy: string,
direction: 'asc' | 'desc' = 'asc'
) => {
const sorted = [...users].sort((a, b) => {
let comparison = 0
switch (sortBy) {
case 'name':
comparison = a.full_name.localeCompare(b.full_name)
break
case 'email':
comparison = a.email.localeCompare(b.email)
break
case 'role':
const roleOrder = { SYSTEM_ADMIN: 1, COMPANY_MANAGER: 2, STAFF: 3 }
comparison = roleOrder[a.role] - roleOrder[b.role]
break
case 'company':
const aCompany = a.companies?.name || ''
const bCompany = b.companies?.name || ''
comparison = aCompany.localeCompare(bCompany)
break
case 'created':
default:
comparison = new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
}
return direction === 'desc' ? -comparison : comparison
})
return sorted
}
Pagination
Paginated Results
// Paginate user list
const paginateUsers = (
users: UserWithCompany[],
page: number,
pageSize: number = 25
) => {
const startIndex = (page - 1) * pageSize
const endIndex = startIndex + pageSize
return {
users: users.slice(startIndex, endIndex),
totalUsers: users.length,
totalPages: Math.ceil(users.length / pageSize),
currentPage: page,
pageSize
}
}
Pagination Component
// Pagination controls
const Pagination = ({
currentPage,
totalPages,
onPageChange
}: {
currentPage: number
totalPages: number
onPageChange: (page: number) => void
}) => (
<div className="flex items-center justify-center gap-2 mt-4">
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage <= 1}
className="form-button secondary px-3 py-1"
>
Previous
</button>
<span className="text-secondary-text">
Page {currentPage} of {totalPages}
</span>
<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage >= totalPages}
className="form-button secondary px-3 py-1"
>
Next
</button>
</div>
)
User Actions
Available Actions
| Action | Description | Available For |
|---|---|---|
| View Details | See full user profile | All users |
| Edit User | Update user information | Managers, Staff |
| Reset Password | Initiate password reset | All users |
| Deactivate | Suspend user access | Managers, Staff |
| Delete | Remove user | Staff only |
Action Menu
// User action menu
const UserActions = ({ user, onAction }: { user: UserWithCompany, onAction: (action: string) => void }) => (
<div className="relative">
<DropdownMenu>
<DropdownMenuTrigger>
<button className="p-2 hover:bg-white/10 rounded">
<MoreVertical className="w-4 h-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => onAction('view')}>
View Details
</DropdownMenuItem>
{user.role !== 'SYSTEM_ADMIN' && (
<>
<DropdownMenuItem onClick={() => onAction('edit')}>
Edit User
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onAction('reset-password')}>
Reset Password
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => onAction('deactivate')}
className="text-red-400"
>
Deactivate User
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
)
Export Users
Export Formats
| Format | Description | Use Case |
|---|---|---|
| CSV | Comma-separated values | Spreadsheet import |
| JSON | JavaScript Object Notation | API integration |
| Portable Document Format | Reporting |
CSV Export
// Export users to CSV
const exportUsersToCSV = (users: UserWithCompany[]) => {
const headers = ['Name', 'Email', 'Role', 'Company', 'Created']
const rows = users.map(user => [
user.full_name,
user.email,
user.role,
user.companies?.name || '',
formatDate(user.created_at)
])
const csv = [
headers.join(','),
...rows.map(row => row.map(cell => `"${cell}"`).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 = `users-${formatDate(new Date())}.csv`
link.click()
}
Access Control
Who Can View Users
| Role | Can View |
|---|---|
| System Admin | All users on platform |
| Company Manager | Only their company users |
| Staff | Cannot view user list |
API Security
// Source: src/app/api/users/route.ts:13-16
// Only System Admins can access user list
if (!user || getAuthRole(user) !== 'SYSTEM_ADMIN') {
return ApiResponses.forbidden('Forbidden')
}
Troubleshooting
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Empty list | No users in database | Create users first |
| Missing users | Filtering applied | Clear filters |
| Forbidden error | Not System Admin | Login as System Admin |
| Slow loading | Large user count | Use pagination |
Error Messages
| Error | Meaning | Action |
|---|---|---|
| "Forbidden" | Not authorised | Login as System Admin |
| "Failed to fetch" | Network error | Check connection |
| "Server error" | Database issue | Contact support |
Related Documentation
- User Statistics - Platform analytics
- Company Management - Manage companies
- Creating Companies - Add new companies
Source Files:
src/app/api/users/route.ts- Users listing APIsrc/types/api.ts- UserWithCompany type definitionsrc/lib/auth/metadata.ts- Role checking utilities