Skip to main content

Groups & Teams

Groups & Teams

Component File: src/features/personnel/components/PersonnelCRM.tsx Route / navigation: Path /administration, Zustand administrationTool = groups-teams. See ../administration/00-ADMINISTRATION-HUB.md. Access Level: Any parent_org membership, or fund_user with director + read_write Last Updated: April 21, 2026 Status: ✅ Implemented

Overview

The Groups & Teams component provides comprehensive management of personnel across all nonprofit organizations. It displays team members in a tile-based grid layout, supports user account creation with email invitations, and allows editing and removing team members. This is the central hub for managing staff, employees, and contractors.

Roster scope (staff directory): Rows from organization_users are shown only for staff org roles — parent_org and fund_user. Portal logins with volunteer or donor org scope are excluded from this grid (use Volunteers / donor experiences elsewhere). Card subtitles show position (Director, Bookkeeper, Assistant, Custom Access), per DEVELOPER-PLAYBOOK §4 layer 2 — not a separate “parent org admin” marketing line on the tile.

Implementation Status

Backend Integration

  • ✅ Personnel records stored in personnel table
  • ✅ Organization members stored in organization_usersusers tables
  • ✅ User invitation via invite-user Edge Function
  • ✅ Password reset via reset-user-password Edge Function
  • ✅ Role and access level management
  • ✅ Delete user removes from organization_users

Recent Fixes (April 2026)

  • Create Account org resolution for existing personnel (Apr 15) — selectedMemberDetail.entityId can already be an org UUID for non-account personnel rows. PersonnelCRM now resolves org IDs with UUID-awareness, preventing false Invalid organization failures when creating accounts from member detail.
  • Profile edit email flow aligned with users-table hardening (Apr 15) — staff edits now use update-user-profile service-role sync for auth + public.users updates instead of direct client writes to public.users.email (blocked by privileged-field trigger).
  • Remove action permission parity (Apr 15) — UI now shows account deletion only where backend allows it (parent org admins for user accounts), preventing avoidable 403 failures.
  • Multi-org personnel-only add path now matches UI contract (Apr 15) — when "Create User Account" is off, Groups & Teams creates one personnel row per selected organization (instead of only the first selection).
  • User-limit check counts unique staff users (Apr 15) — frontend limit guard deduplicates by user identity, so multi-org memberships don't inflate plan-limit checks.
  • Verification SQL updated for staff-only roster semantics (Apr 15) — supabase/scripts/verify_groups_teams_members.sql now reports staff_membership_rows (parent_org + fund_user) alongside total memberships to match the UI's intentional scope.

Recent Fixes (March 2026)

  • `organization_users` RLS `42P17` / missing entity dropdown (Mar 27) — SELECT/UPDATE policies added for co-membership visibility used EXISTS (SELECT … FROM organization_users …) on organization_users, which re-triggers RLS on the same table and breaks the whole app (empty org switcher, useOrganizationInit errors). Fixed by migration 20260331130000_fix_organization_users_rls_42p17_recursion.sql (auth_is_staff_in_org, auth_is_fund_director_rw_in_org, JWT-only tree clause). Rule: never subquery organization_users inside a policy on organization_users; see DEVELOPER-PLAYBOOK §4.1 RLS patterns and §20.
  • Staff-only roster + position-first card labels (Mar 26) — PersonnelCRM filters organization_users to parent_org / fund_user for the grid; volunteer / donor portal rows are omitted. Tile subtitle uses position (Director, …) for staff, matching the wireframes below and DEVELOPER-PLAYBOOK §4.
  • Permission tree + Change Access i18n + org-role naming (Mar 25, updated Apr 21) — Add Team Member / Create Account use the same PERMISSION_TREE as Change Access (`permissionConstants.ts`); Role (org access) copy and admin.roleAccess.* keys align with DEVELOPER-PLAYBOOK §4. For custom staff positions, only Dashboard and Minty AI remain implicit; My Workspace, Settings, and the rest of the staff hubs/tools are controlled by the picker. Empty custom-role selections are rejected before invite/save so admins must grant at least one hub or tool. Change Access resolves admin.positions.* / admin.permTree.* / admin.accessLevels.* via the `auth` namespace (not personnel) so localized hub and tool labels load correctly. Props/state use orgRole / ORG_ROLE_OPTIONS where org scope is meant.
  • Invite gate parity with backend policy (Apr 21) — the Groups & Teams UI now shows Add Team Member / Create Account only when the current session matches the same invite policy enforced by invite-user: any parent_org membership, or a fund_user who is a director with read_write. This prevents false-positive UI affordances for assistants/bookkeepers/custom roles and restores the parent-org read-only path the edge function already allowed.
  • Fixed invite-user 403 for parent org admins (Mar 8) - getAuthClaims() in _shared/auth-claims.ts was reading raw_app_meta_data from getUser() (which only has {"provider":"email"}) instead of the JWT payload where the custom access token hook injects is_parent_org_admin, org_memberships, etc. All custom claims were missing, causing canInviteUsers() to return false for every user. Fixed by decoding claims from JWT payload directly. See DEVELOPER-PLAYBOOK §45.
  • Fixed autofill targeting search box behind Add Team Member dialog (Mar 8) - Browser autofill was populating the search bar (behind the dialog overlay) instead of the dialog's email field because the search input lacked autoComplete="off" and name attributes. Chrome/Opera autofill heuristics matched it as a form field candidate since it appeared earlier in DOM order than the dialog inputs.

Recent Fixes (January 2026)

  • Added Global Search Bar (Jan 31) - Added a prominent search bar at the top of the Groups & Teams page for real-time filtering across all organizations. Search by user name, email, role, or fund name. Results update dynamically, showing "X of Y members" when filtering within a fund.
  • Fixed Profile View Reset Password (Jan 1) - The "Send Link" button in the Profile View's Account tab was showing a fake success message without actually sending an email. Now properly calls sendPasswordResetEmail() to send the password reset email.
  • Fixed Create Account not refreshing UI (Jan 1) - After creating an account via the Member Detail popup, the UI now properly refreshes to show the updated member status.
  • Fixed Change Access not refreshing UI (Jan 1) - After changing a user's access level, the UI now properly refreshes to reflect the change.
  • Added Resend Invite button (Jan 1) - For users with pending status who haven't received or clicked their invitation email, admins can now click "Resend Invite" in the Member Detail popup to send a new login link. This uses the password reset flow which is more reliable than re-inviting.
  • Parent org always first, hide empty funds (Jan 1) - In the "All Nonprofits" view, the parent organization (parent org) is now always displayed first in the list. Child funds are only shown if they have team members assigned. This declutters the view by hiding empty fund sections while ensuring the parent org is always visible at the top.

Recent Fixes (December 2025)

1. Scroll fix: Increased dropdown height from 256px to 300px and set fixed width for proper scrolling 2. Parent org mutual exclusivity: Selecting parent org (InFocus Admin) now clears all fund selections; selecting any fund clears parent org selection. Parent org is OR, not AND. 3. Click anywhere to select: Clicking the organization name now selects it, not just the checkbox

  • Fixed parent org view showing only admin users (Dec 31) - When the parent org was selected, Groups & Teams only showed the "InFocus Admin" card with admin staff. Now correctly shows ALL organization cards with their respective personnel, matching the behavior of the "All Nonprofits" view.
  • Fixed Organization Selector Issues (Dec 31) - Three fixes to the multi-org selector in Add Personnel dialog:
  • Fixed Change Access button not working (Dec 30) - The Change Access button in the Member Detail Popup was not opening the dialog because it didn't close the parent dialog first. Now properly closes the member detail popup before opening the Change Access dialog. Also pre-populates the dialog with the user's current access level.
  • Fixed Change Access (Dec 30) - Change Access now only updates access level (read_write/read_only), not user type. User type is determined by organization type (parent_org for parent org, fund_user for child nonprofits). Position is managed separately.
  • Fixed invite-user edge function (Dec 30) - Fixed getUserByEmail is not a function error by replacing with listUsers() and email filter. User invitations now work correctly.
  • Fixed delete user (Dec 30) - Delete now properly removes ALL organization memberships and the users table record, not just one org link. Also closes the member detail popup after deletion.
  • Deployed reset-user-password edge function (Dec 30) - Password reset functionality now works via Supabase Auth resetPasswordForEmail.
  • Bidirectional user type restriction (Dec 30) - User type selection is now enforced based on organization type:
  • Parent org selected → Only parent_org user type available (fund_user disabled)
  • Child nonprofit selected → Only fund_user user type available (parent_org disabled)
  • User type auto-switches when organization is changed to enforce this rule
  • Improved invite validation (Dec 30) - Added explicit validation for firstName and email before calling the invite-user edge function to prevent 400 errors from missing required fields.
  • Enhanced edge function logging (Dec 30) - Added detailed logging to the invite-user edge function to capture specific error details for debugging 500 errors.
  • Disabled user account creation without nonprofit (Dec 29) - "Create User Account" switch is now disabled until a specific nonprofit organization is selected. Users cannot be created tied to "All Nonprofits" (InFocus Admin/Super Admin context).
  • Added Edit Profile button (Dec 21) - Member Detail Popup now includes Edit Profile button for users with write access
  • Reorganized Member Detail Popup (Dec 21) - Separated "Actions" (Edit, Remove) from "Account Management" (admin-only)
  • Fixed permission gating (Dec 21) - Fund users with read_write access can now edit/remove team members
  • Fixed delete user - Now properly refetches organization members after deletion
  • Fixed edit user - Organization members can now be edited (updates users table)
  • Removed table view - Individual fund view now uses tile grid (consistent with "All Nonprofits" view)

UI Features

Main Features

  • Tile Grid Layout:
  • Responsive grid (1-4 columns based on screen size)
  • Avatar with initials
  • Name, role, department
  • Employment type and status badges
  • Email contact info
  • Click to view profile
  • Search & Filter:
  • Global Search Bar: Real-time search across all organizations
  • Search by user name, email, or role
  • Search by fund/organization name
  • Filters results dynamically as you type
  • Shows "X of Y members" when filtering within a fund
  • Clear button to reset search
  • Empty state when no results found
  • Filter by employment type (Full-Time, Part-Time, Contractor, Volunteer)
  • Filter by status (Active, On Leave, Inactive)
  • Sort by name, role, or hire date
  • Add Personnel:
  • Full name, role, department
  • Email and phone
  • Employment type selection
  • Nonprofit organization selector
  • Optional user account creation with org role, position, access level, and custom hub/tool grants
  • custom staff positions must select at least one hub or tool before invite/save
  • Member Detail Popup:
  • Contact information
  • Status display
  • Actions section (Edit Profile, Remove) - available to users with write access
  • Account Management section (Create Account, Resend Invite, Reset Password, Change Access) - any parent_org membership, or fund_user director with read_write
  • Edit Personnel:
  • Update name, role, department
  • Update email and phone
  • Works for both personnel records AND organization members

View Modes

All Nonprofits View (selectedEntity === 'all')

Groups personnel by nonprofit organization in expandable cards with tile grids. Includes a prominent search bar for filtering across all organizations.

Groups & Teams
View all personnel across organizations

┌─────────────────────────────────────────────────────────────────┐
│ 🔍 Search by name, email, role, or fund...                 [X]  │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ Awakenings (2 of 3 members)           [filtered by "john"]      │
├─────────────────────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐                                        │
│ │ [Avatar] │ │ [Avatar] │                                        │
│ │ John Doe │ │ Johnny S │                                        │
│ │ Director │ │ Manager  │                                        │
│ │ Active   │ │ Active   │                                        │
│ └──────────┘ └──────────┘                                        │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ Cornerstone (1 of 5 members)          [filtered by "john"]      │
├─────────────────────────────────────────────────────────────────┤
│ ┌──────────┐                                                     │
│ │ [Avatar] │                                                     │
│ │ Johnson  │                                                     │
│ │ Staff    │                                                     │
│ │ Active   │                                                     │
│ └──────────┘                                                     │
└─────────────────────────────────────────────────────────────────┘

Search Behavior:

  • Typing filters users by name, email, or role across all funds
  • Typing a fund name shows all users in matching funds
  • Results update in real-time
  • Empty cards are hidden when no members match
  • "Clear Search" button (X) appears when search has text

Individual Fund View (selectedEntity !== 'all')

Shows all personnel for the selected nonprofit in a responsive tile grid.

Groups & Teams
Manage staff, employees, and contractors

[Search...] [Sort ▼] [Filter ▼] [+ Add Personnel]

Showing 12 of 12 team members

┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ [Avatar] │ │ [Avatar] │ │ [Avatar] │ │ [Avatar] │
│ John Doe │ │ Jane Doe │ │ Bob Smith│ │ Alice W. │
│ Director │ │ Manager  │ │ Staff    │ │ Coord.   │
│ Full-Time│ │ Full-Time│ │ Part-Time│ │ Contract │
│ Active   │ │ Active   │ │ Active   │ │ Active   │
│ email... │ │ email... │ │ email... │ │ email... │
└──────────┘ └──────────┘ └──────────┘ └──────────┘

Data Requirements

Personnel Record (personnel table)

  • id (uuid) - Unique identifier
  • organization_id (uuid) - Organization
  • first_name (string) - First name
  • last_name (string) - Last name
  • email (string) - Email address
  • phone (string) - Phone number
  • position (string) - Job title/role
  • department (string) - Department
  • employment_type (enum) - 'full-time', 'part-time', 'contractor'
  • hire_date (date) - Hire date
  • status (enum) - 'active', 'on-leave', 'inactive'
  • created_at (timestamp) - Creation date
  • updated_at (timestamp) - Last update

Organization Member (organization_users → users)

  • id (uuid) - Membership ID
  • user_id (uuid) - User ID
  • organization_id (uuid) - Organization
  • role (string) - 'parent_org' or 'fund_user'
  • position (string) - 'director', 'bookkeeper', 'assistant', or 'custom'
  • access_level (string) - 'read_write' or 'read_only'
  • users.first_name (string) - First name
  • users.last_name (string) - Last name
  • users.email (string) - Email
  • users.phone (string) - Phone
  • users.job_title (string) - Job title
  • users.status (string) - Account status

Database Functions (db.ts)

fetchPersonnel(entityId)

Fetches all personnel records for an organization.

fetchOrganizationMembers(entityId)

Fetches all organization members (users with accounts) for an organization.

  • For "all" view: Shows parent org users once (deduplicated)
  • For individual org view: Filters out parent org users

createPersonnel(data)

Creates a new personnel record in the personnel table.

updatePersonnel(id, data)

Updates a personnel record in the personnel table.

deletePersonnel(id)

Deletes a personnel record from the personnel table.

inviteUser(params)

Calls the invite-user Edge Function to: 1. Create a user in Supabase Auth 2. Create a record in the users table 3. Create a record in organization_users 4. Send a one-time login email

sendPasswordResetEmail(email)

Calls the reset-user-password Edge Function to send a password reset email.

Edge Functions Required

invite-user

Creates a new user account and sends invitation email.

Authorization (Updated January 2026):

  • Parent Org Admins: Can invite users to ANY organization
  • Directors: Can invite users to their organization
  • Other positions (read_write): Can invite users to their OWN organization only
  • Read-only users: Cannot invite users
  • Role Escalation Prevention: Non-admins cannot create parent_org accounts

Request:

{
  "email": "user@example.com",
  "firstName": "John",
  "lastName": "Doe",
  "phone": "(555) 123-4567",
  "organizationId": "uuid",
  "role": "fund_user",
  "position": "director",
  "accessLevel": "read_write",
  "jobTitle": "Program Coordinator"
}

When position is custom, include customPermissions (same shape as organization_users.custom_permissions). The hub/tool matrix is defined in `permissionConstants.ts` (PERMISSION_TREE) and must stay in sync with TOOL_HUB_MAP in useTierAccess.ts — see DEVELOPER-PLAYBOOK §4 / §4.2.

{
  "email": "user@example.com",
  "firstName": "John",
  "lastName": "Doe",
  "organizationId": "uuid",
  "role": "fund_user",
  "position": "custom",
  "accessLevel": "read_write",
  "customPermissions": {
    "hubs": { "donors": true, "reports": true },
    "tools": { "donor-management": true, "balance-sheet": true }
  }
}

Custom position — always-available staff surfaces: For position: custom, only Dashboard and Minty AI remain implicit. My Workspace, Settings, and the rest of the configurable staff hubs/tools are granted only when selected in Add Team Member, Create Account, or Change Access dialogs (see CUSTOM_POSITION_ALWAYS_ON_HUBS and PERMISSION_TREE in lib/permissions.ts / permissionConstants.ts).

Response:

{
  "success": true,
  "userId": "uuid"
}

Error Responses:

  • 403 - "You do not have permission to invite users to this organization"
  • 403 - "Only parent org admins can create parent_org accounts"
  • 409 - "A user with this email already exists"
  • 400 - "Missing required fields..."

reset-user-password

Sends a password reset email to the user.

Request:

{
  "email": "user@example.com"
}

Response:

{
  "success": true
}

update-user-profile

Updates auth + app profile fields for existing org members (email, name, phone) through a permission-checked service-role flow.

Notes:

  • Used by Groups & Teams edit flows for member-* rows.
  • Applies authorization checks (canManageUsers + manageable target scope) before updating.
  • Keeps auth.users and public.users in sync for profile edits.

Authentication & Authorization

Required Permissions

  • personnel:read - View personnel
  • personnel:create - Add personnel
  • personnel:update - Edit personnel
  • personnel:delete - Remove personnel

Role-Based Access

  • Parent Org Admin: View and manage all personnel across all funds
  • Director: View and manage their organization's personnel
  • Bookkeeper/Assistant: Limited access based on position
  • Fund User: View and manage their assigned fund's personnel
  • Donor/Volunteer: No access

Business Logic & Validations

Frontend Validations

  • Name required
  • Email required and valid format
  • Role/position required
  • Organization required (for parent orgs adding personnel)

Backend Validations

  • Valid organization access
  • Email uniqueness for user accounts
  • Valid role and access level values

Business Rules

  • Users cannot delete themselves
  • Parent org admins appear once in "All Nonprofits" view
  • Parent org admins are hidden from individual org views
  • Organization members (users) are edited via update-user-profile (service-role sync to auth.users + public.users)
  • Personnel records (non-users) are edited via personnel table
  • Organization member account deletion is parent-org-admin only (UI and edge function both enforce this)
  • Parent org user type restriction: Fund user roles (fund_user) cannot be created for parent organizations. Only parent org user type (parent_org) is allowed for the parent org to prevent unintended access to all organization data.

State Management

Local State

  • searchQuery - Search filter text
  • sortBy - Sort option
  • employmentFilter - Employment type filter
  • statusFilter - Status filter
  • addPersonOpen - Add dialog visibility
  • editPersonOpen - Edit dialog visibility
  • deleteConfirmOpen - Delete confirmation visibility
  • memberDetailOpen - Member detail popup visibility
  • personToEdit - Person being edited
  • personToDelete - Person being deleted
  • selectedMemberDetail - Member for detail popup

Derived State (useMemo)

  • searchFilteredPersonnel - Personnel filtered by search query (name, email, role)
  • searchFilteredEntities - Entities that have matching personnel or match entity name
  • getFilteredPersonnelForEntity(entityId) - Returns filtered personnel for a specific entity

Data Hooks

  • usePersonnel(entityId) - Fetches personnel records
  • useOrganizationMembers(entityId) - Fetches organization members
  • useCreatePersonnel() - Creates personnel
  • useUpdatePersonnel() - Updates personnel
  • useDeletePersonnel() - Deletes personnel

Dependencies

Internal Dependencies

  • useAppStore - Zustand global state (selectedEntity)
  • useAuth - Current user from AuthContext
  • usePermissions - Permission checks
  • UI components (Card, Button, Dialog, Avatar, etc.)

External Libraries

  • lucide-react - Icons
  • sonner - Toast notifications
  • @tanstack/react-query - Data fetching

Error Handling

Error Scenarios

1. Failed to load personnel: Show toast "Failed to load personnel" 2. Failed to add personnel: Show toast "Failed to add personnel. Please try again." 3. Failed to update: Show toast "Failed to update. Please try again." 4. Failed to delete: Show toast "Failed to delete personnel. Please try again." 5. Failed to invite user: Show toast with error message 6. Cannot delete self: Show toast "You cannot remove yourself from the organization."

Loading States

  • Initial load: Loading spinner
  • Add personnel: Button shows "Creating..." with spinner
  • Update role: Button shows loading state
  • Delete: Confirmation dialog

Related Documentation

Additional Notes

Data Sources

Personnel data comes from two sources: 1. Personnel table - Manual entries without user accounts 2. Organization users - Users with platform accounts (auto-populated)

These are combined and deduplicated by email to show a unified view.

Editing Behavior

  • Personnel records (no member- prefix): Updates personnel table
  • Organization members (member- prefix): Calls update-user-profile Edge Function to sync both auth.users and public.users

Delete Behavior

1. Looks up auth user via get_auth_user_by_email RPC 2. Unlinks donor_users, donors, volunteers (preserves historical records) 3. Deletes organization_users, team_members 4. Cleans up conversation_participants, message_email_queue, global_alert_dismissals, chat_conversations 5. Cleans up admin tables (deletes owned rows, nullifies secondary references) 6. Nullifies audit columns without ON DELETE SET NULL 7. Deletes from public.users (cascades to notifications, todos, user_preferences, etc.) 8. Deletes from auth.users (cascades to sessions)

  • Personnel records: Deletes from personnel table
  • Organization members: Calls delete-user Edge Function which performs full user deletion:

Recent Fixes (February 2026)

  • Fixed delete user crash (Feb 11) — delete-user edge function called getUserByEmail() which doesn't exist in Deno runtime. Replaced with get_auth_user_by_email RPC. Also fixed: orphaned conversation_participants/chat_conversations/global_alert_dismissals (no FK to public.users), admin table Step 6 was deleting instead of nullifying secondary references, email not normalized before auth lookup.
  • Fixed reset password crash (Feb 11) — reset-user-password edge function had same getUserByEmail() bug. Replaced with RPC + email normalization.
  • Added delete loading state (Feb 11) — "Remove Member" button now shows loading state during async deletion to prevent double-clicks.

Synced from IFMmvp-Frontend documentation: pages/people/02-GROUPS-AND-TEAMS.md

Ready to Get Started?

See how Alignmint can simplify your nonprofit's operations. Schedule a free demo with our team and we'll walk you through everything.

Questions? Email us at steven@getalignmint.org

Ready to get started?Start Plus Trial