Skip to main content

Fund Management

Fund Management

Component File: src/features/admin/components/FundManagement.tsx Route / navigation: Path /administration, Zustand administrationTool = fund-management. See 00-ADMINISTRATION-HUB.md. Access Level: Director or Bookkeeper with read_write (canManageOrganizations for mutations). See "Permission Guards" below.

Overview

The Fund Management component allows parent org administrators to manage all organizations and funds within their org tree. This includes adding new funds, editing existing ones, activating/deactivating organizations, and controlling their visibility in the entity selector. This is a critical administrative function for parent orgs managing multiple entities.

SECURITY: The component filters fetchAllOrganizations() results to only orgs in the user's organizationIds org tree. Without this, the RLS policy "Donors can view orgs they donated to" can leak cross-tenant orgs into the list (e.g., a staff-donor seeing IFM's ReImagine Ministry in their own org's Fund Management).

UI Features

Main Features

  • Summary Statistics Cards (3-col grid):
  • Total Organizations
  • Active (text-primary)
  • Inactive (text-destructive)
  • Active/Inactive PillTabs
  • Search by organization name (real-time filtering, alphabetical sort)
  • Organizations Table:
  • Status (Switch toggle + Active/Inactive badge)
  • Name (with type-specific icon)
  • Type (color-coded badge)
  • Date Added
  • Last Activity
  • Actions (Edit — gated on canManageOrganizations)
  • Add Organization Dialog (radio: Fund / Accounting Fund / Parent Org)
  • Edit Organization Dialog
  • DesktopOnlyWarning (§37)
  • SkeletonTable loading state
  • EmptyState when no orgs match filter

Organization Types

| Type | DB Value | Icon | Badge Style | Description |
|------|----------|------|-------------|-------------|
| Fund | `fund` | Building2 | outline, border-primary/50 text-primary | Fiscally sponsored org/ministry (donors, donations, staff) |
| Accounting Fund | `accounting_fund` | Wallet | outline, border-muted-foreground/50 text-muted-foreground | Lightweight accounting designation (JE lines) |
| Parent Org | `parent_org` | Shield | secondary, bg-primary/10 text-primary border-primary/30 | Master admin / tenant root (max 1 per tenant) |

Icon consistency: All icon rendering MUST use the shared EntityIcon component (src/components/shared/EntityIcon.tsx). Never duplicate inline icon logic — Header.tsx, FundManagement.tsx, ReimbursementsManager.tsx, and all other entity selectors delegate to EntityIcon.

Add/Edit Dialog

  • Name Field: Text input
  • Type Selector: Three radio buttons (Fund / Accounting Fund / Parent Org)
  • Description: Textarea
  • Parent Organization: Select (shown only for non-parent_org types)
  • Validation: Name required, slug uniqueness, tier enforcement for child org creation
  • Actions: Cancel or Save

Data Model

OrganizationWithStatus (local component state)

interface OrganizationWithStatus extends Entity {
  orgId: string;         // Supabase UUID
  isActive: boolean;     // status === 'active'
  isVisible: boolean;    // visible column
  dateAdded: string;     // YYYY-MM-DD (from created_at)
  lastActivity?: string; // YYYY-MM-DD (from updated_at)
  description?: string;
  parent_organization_id?: string;
}

OrgFormType

type OrgFormType = 'parent_org' | 'fund' | 'accounting_fund';

Database Functions (via src/lib/db/organizations.ts)

| Function | Purpose |
|----------|--------|
| `fetchAllOrganizations()` | Fetch all orgs (including hidden) — filtered client-side by org tree |
| `fetchOrganizations()` | Fetch visible/active orgs — used by `refreshEntityDropdown()` |
| `createOrganization()` | Create new organization |
| `updateOrganization()` | Update name/type/description |
| `toggleOrganizationVisibility()` | Toggle visible + status columns |

Authentication & Authorization

Permission Guards

  • View list: Any user who can access Administration hub
  • Add button: canManageOrganizations (from usePermissions())
  • Edit button: canManageOrganizations
  • Toggle visibility: canManageOrganizations (parent_org type is also non-toggleable)
  • Create child orgs: canCreateChildOrgs (from useTierAccess() — pro/enterprise/demo/internal only)

canManageOrganizations is true for Director OR Bookkeeper positions with read_write access (and not donor/volunteer roles). It is intentionally MORE permissive than canWriteAdmin (which still gates user management, integrations, payment settings, etc.). Bookkeepers need to maintain the chart of funds during accounting work — gating fund CRUD behind director-only blocked Christy Nelson at IFM (Apr 2026 feedback). DB RLS already scopes organizations to the user's parent-org tree; this is purely a UX gate.

Role-Based Access

  • Parent Org Director / Bookkeeper (`read_write`): Full add/edit/toggle access (canManageOrganizations true)
  • Parent Org Assistant / Custom or `read_only`: View only — mutations blocked
  • Fund User Director (`read_write`): DB RLS allows updating non-parent_org orgs in the user's tree; the UI also gates on canManageOrganizations
  • Donor/Volunteer: No access (portal roles)

Security: Cross-Tenant Isolation

fetchAllOrganizations() returns everything RLS allows. The "Donors can view orgs they donated to" policy can expose orgs from other tenants for staff-donor users. The component MUST filter by `user.organizationIds`:

const userOrgIds = new Set(user?.organizationIds || []);
const filtered = userOrgIds.size > 0
  ? data.filter(org =>
      userOrgIds.has(org.id) ||
      (org.parent_organization_id != null && userOrgIds.has(org.parent_organization_id))
    )
  : data;

This matches the useOrganizationInit pattern used by the Header entity dropdown.

Special Rules

  • Parent org cannot be deactivated (Switch disabled, toast error)
  • Only ONE parent_org allowed per tenant (radio disabled + explanation text)
  • Deactivating hides from entity selector and deletes associated donor pages
  • Deactivation is blocked when the ministry still has non-zero cash/equity balances (DB-enforced guard)
  • refreshEntityDropdown() syncs Header dropdown after every mutation

Business Logic & Validations

Frontend Validations

  • Name required (cannot be empty)
  • Slug must be unique (generated from name: lowercase, alphanumeric, hyphens, max 50 chars)
  • Cannot deactivate parent_org organization
  • Type must be 'parent_org', 'fund', or 'accounting_fund' (DB CHECK constraint chk_org_type_valid)
  • Parent Org radio disabled if one already exists (with explanation text)
  • Non-parent_org types require parent_organization_id selection
  • Tier enforcement: only pro/enterprise can create child orgs (canCreateChildOrgs)

Business Rules

  • New organizations start as active and visible
  • Slug generated from name (sanitized)
  • Deactivating sets visible=false + status='inactive' + deletes donor pages
  • Donor pages are deleted only after a successful inactive transition
  • Inactive transition fails when cash/equity balances are non-zero; user must zero balances first
  • Parent organization always visible (non-toggleable)
  • Search is case-insensitive, results sorted alphabetically
  • Entity dropdown synced after every mutation via refreshEntityDropdown()

State Management

Local State

  • organizations - Array of OrganizationWithStatus
  • organizationsLoading - Loading state
  • searchQuery - Search filter text
  • activeTab - 'active' | 'inactive'
  • dialogType - 'add' | 'edit' | null
  • selectedOrg - Organization being edited
  • formData - Form input values (name, type, description, parent_organization_id)

Global State (Zustand)

  • entities - Entity dropdown list (synced via refreshEntityDropdown())
  • organizations - Organization data store (synced via setOrganizationsStore())

Dependencies

Internal Dependencies

  • useAuth - User org tree (organizationIds) for cross-tenant filtering
  • useAppStore (Zustand) - Entity dropdown sync
  • usePermissions - canManageOrganizations for mutation guards
  • useTierAccess - canCreateChildOrgs, tier for paywall enforcement
  • fetchAllOrganizations, fetchOrganizations, createOrganization, updateOrganization, toggleOrganizationVisibility from src/lib/db
  • UI components: Card, Button, Badge, Table, Dialog, Switch, Select, PillTabs, DesktopOnlyWarning, SkeletonTable, EmptyState, PageHeader, Breadcrumb

External Libraries

  • lucide-react - Icons (Shield, Wallet, Building2, Plus, Search, Edit2, CheckCircle2, XCircle)
  • sonner - Toast notifications
  • react-i18next - i18n ('auth' namespace, admin.fundManagement.* keys)

Error Handling

Error Scenarios

1. Network Error: Show toast "Unable to load nonprofits" 2. Duplicate Name: Show toast "Nonprofit already exists" 3. Empty Name: Show toast "Please enter a name" 4. Add Failed: Show toast "Failed to add nonprofit" 5. Update Failed: Show toast "Failed to update nonprofit" 6. Toggle Failed: Show toast "Failed to update status" 7. Cannot Deactivate Parent: Show toast "Cannot deactivate parent organization"

Loading States

  • Initial load: Skeleton table
  • Search: Instant filtering (no loading)
  • Add/Edit: Button loading state
  • Toggle status: Switch loading state

Implementation Status

Last Updated: March 13, 2026

| Feature | Status | Notes |
|---------|--------|-------|
| Fetch Organizations | ✅ Complete | `fetchAllOrganizations()` + org-tree filter |
| Cross-Tenant Isolation | ✅ Complete | Filters by `user.organizationIds` |
| Add Organization | ✅ Complete | `createOrganization()` + tier enforcement |
| Edit Organization | ✅ Complete | `updateOrganization()` + `canManageOrganizations` guard |
| Toggle Visibility | ✅ Complete | `toggleOrganizationVisibility()` |
| Entity Dropdown Sync | ✅ Complete | `refreshEntityDropdown()` after mutations |
| Color-Coded Type Badges | ✅ Complete | `getTypeBadgeProps()` per org type |
| Icon Consistency | ✅ Complete | Matches Header.tsx entity selector |
| Search/Filter | ✅ Complete | Client-side filtering |
| Active/Inactive Tabs | ✅ Complete | PillTabs, filters by `visible` field |
| Summary Statistics | ✅ Complete | useCountUp animations |
| i18n | ✅ Complete | All strings via `t()` |
| DesktopOnlyWarning | ✅ Complete | §37 |
| EmptyState | ✅ Complete | §43 |
| SkeletonTable | ✅ Complete | Loading state |

Database Tables Used

organizations Table

| Column | Type | Description |
|--------|------|-------------|
| `id` | uuid | Primary key |
| `name` | text | Organization name |
| `slug` | text | URL-friendly identifier |
| `type` | text | `'parent_org'`, `'fund'`, `'accounting_fund'` (CHECK constraint) |
| `status` | text | `'active'` or `'inactive'` |
| `visible` | boolean | Whether visible in entity selector |
| `parent_organization_id` | uuid | FK to parent org (NULL for parent_org type) |
| `description` | text | Optional description |
| `created_at` | timestamp | Created timestamp |
| `updated_at` | timestamp | Updated timestamp |

Recent Fixes (March 13, 2026)

  • P0 Security: Added org-tree filtering to prevent cross-tenant org leakage via donor RLS policies
  • P1: Added refreshEntityDropdown() to sync Header dropdown after add/edit/toggle
  • P1: Added color-coded type badges (getTypeBadgeProps()) — parent_org=secondary, fund=outline/primary, accounting_fund=outline/muted
  • P1: Corrected icon mapping: fund=Building2, accounting_fund=Wallet, parent_org=Shield. All icon rendering now uses shared EntityIcon component (DRY).
  • P1: DB migration: IFM's General Fund, Restricted Cash, InFocus Admin → accounting_fund type. Steven's Food Pantry → accounting_fund.
  • P2: Added canWriteAdmin guard on edit button (was unguarded). *(Apr 2026: replaced with canManageOrganizations to allow bookkeepers.)*
  • P2: Updated entityMapping.ts OrgType to include accounting_fund
  • P2: fetchAllOrganizations() now uses normalizeOrgType() (was raw cast)

Related Documentation

Additional Notes

ID Generation

IDs are generated from nonprofit names:

  • Convert to lowercase
  • Remove special characters
  • Replace spaces with hyphens
  • Truncate to 50 characters
  • Example: "New Hope Community Center" → "new-hope-community-center"

Active vs Inactive

Active nonprofits:

  • Visible in entity selector
  • Can receive donations
  • Can create transactions
  • Users can log in
  • Appears in reports

Inactive nonprofits:

  • Hidden from entity selector
  • Cannot receive new donations
  • Historical data preserved
  • Users cannot log in
  • Excluded from reports (unless specifically included)

Parent Org Organization

  • Only ONE parent_org allowed per tenant
  • Cannot be deactivated or hidden (Switch disabled)
  • Always visible in entity selector
  • Has full access to all data across all child orgs
  • Creating additional parent_org is blocked (radio disabled with explanation)

Visibility Control

Deactivating an organization: 1. Sets visible=false and status='inactive' 2. DB trigger validates the org has zero cash/equity balances before allowing inactive status 3. Deletes all donor_pages for this org (after status update succeeds) 4. Removes from entity dropdown (via refreshEntityDropdown()) 5. Preserves all historical data 6. Can be reactivated later

Integration Points

  • Header Entity Selector: Synced via refreshEntityDropdown() after mutations
  • User Management: Users assigned to organizations via organization_users
  • Reports: Filter by organization
  • Donations: Tagged with organization_id
  • Journal Entries: Lines attributed to organization_id or fund_id

Synced from IFMmvp-Frontend documentation: pages/administration/01-NONPROFIT-MANAGEMENT.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