Skip to main content

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:

RoleDescriptionAccess Level
System AdminPlatform administratorsFull platform access
Company ManagerCompany-level managersFull company access
StaffRegular employeesLimited company access

User Data Sources

RoleDatabase TableIdentification
System Adminsystem_adminsDirect table membership
Company Managerstaffhierarchy_level = 1 in staff_roles
StaffstaffAll 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

RoleHow Determined
SYSTEM_ADMINUser exists in system_admins table
COMPANY_MANAGERStaff with staff_roles.hierarchy_level = 1
STAFFAll 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 ByDescription
NameAlphabetical by full name
EmailAlphabetical by email
RoleSystem Admin → Manager → Staff
CompanyAlphabetical by company name
CreatedMost 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

ActionDescriptionAvailable For
View DetailsSee full user profileAll users
Edit UserUpdate user informationManagers, Staff
Reset PasswordInitiate password resetAll users
DeactivateSuspend user accessManagers, Staff
DeleteRemove userStaff 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

FormatDescriptionUse Case
CSVComma-separated valuesSpreadsheet import
JSONJavaScript Object NotationAPI integration
PDFPortable Document FormatReporting

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

RoleCan View
System AdminAll users on platform
Company ManagerOnly their company users
StaffCannot 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

IssueCauseSolution
Empty listNo users in databaseCreate users first
Missing usersFiltering appliedClear filters
Forbidden errorNot System AdminLogin as System Admin
Slow loadingLarge user countUse pagination

Error Messages

ErrorMeaningAction
"Forbidden"Not authorisedLogin as System Admin
"Failed to fetch"Network errorCheck connection
"Server error"Database issueContact support


Source Files:

  • src/app/api/users/route.ts - Users listing API
  • src/types/api.ts - UserWithCompany type definition
  • src/lib/auth/metadata.ts - Role checking utilities