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