← back to Norma
components/OrgProvider.tsx
144 lines
'use client';
import '@/lib/orgFetch'; // Monkey-patch fetch with X-Org-Id header
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
/* ─── Types ──────────────────────────────────────────────────────────────── */
export interface OrgStaff {
name: string;
role: string;
}
export interface OrgLane {
label: string;
color: string;
description: string;
}
export interface OrgProfile {
id: string;
org_name: string;
short_name: string;
ein: string;
website: string;
mission: string;
focus_areas: string[];
org_email: string;
org_phone: string;
brand_color: string;
donate_url: string;
logo_url: string | null;
address_street: string;
address_city: string;
address_state: string;
address_zip: string;
twitter_handle: string;
linkedin_url: string;
instagram_handle: string;
facebook_url: string;
staff: OrgStaff[];
lanes: Record<string, OrgLane>;
voice_modes: string[];
search_keywords: string[];
org_size: string;
annual_budget: string;
year_founded: number | null;
}
interface OrgContextValue {
org: OrgProfile | null;
orgs: OrgProfile[];
loading: boolean;
switchOrg: (id: string) => void;
refreshOrg: () => Promise<void>;
}
const LS_KEY = 'norma-active-org';
/* ─── Context ────────────────────────────────────────────────────────────── */
const OrgContext = createContext<OrgContextValue>({
org: null,
orgs: [],
loading: true,
switchOrg: () => {},
refreshOrg: async () => {},
});
export function useOrg() {
return useContext(OrgContext);
}
/* ─── Provider ───────────────────────────────────────────────────────────── */
export function OrgProvider({ children }: { children: ReactNode }) {
const [orgs, setOrgs] = useState<OrgProfile[]>([]);
const [activeId, setActiveId] = useState<string>(() => {
if (typeof window === 'undefined') return '';
return localStorage.getItem(LS_KEY) || '';
});
const [loading, setLoading] = useState(true);
const fetchOrgs = useCallback(async () => {
try {
const res = await fetch('/api/accounts');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
const list: OrgProfile[] = data.orgs ?? [];
setOrgs(list);
// Auto-select org for staff/pulse roles (org is locked to their account)
if (typeof window !== 'undefined') {
const userRole = localStorage.getItem('norma-user-role');
const userOrgId = localStorage.getItem('norma-user-orgid');
if ((userRole === 'staff' || userRole === 'pulse') && userOrgId) {
// Always force the correct org for non-admin roles, even if activeId is stale
setActiveId(userOrgId);
localStorage.setItem(LS_KEY, userOrgId);
}
}
// For admin: do NOT auto-select — user must pick an org from the dropdown
} catch (err) {
console.error('[OrgProvider] Failed to fetch orgs:', err);
} finally {
setLoading(false);
}
}, [activeId]);
useEffect(() => {
fetchOrgs();
}, [fetchOrgs]);
const switchOrg = useCallback((id: string) => {
// Staff and pulse roles are locked to their session org — ignore switchOrg calls
if (typeof window !== 'undefined') {
const userRole = localStorage.getItem('norma-user-role');
if (userRole === 'staff' || userRole === 'pulse') {
const userOrgId = localStorage.getItem('norma-user-orgid');
if (userOrgId && id !== userOrgId) return; // Block switching to a different org
}
}
setActiveId(id);
localStorage.setItem(LS_KEY, id);
}, []);
// Only return an org if one was explicitly selected (via dropdown or localStorage)
const org = activeId ? (orgs.find((o) => o.id === activeId) ?? null) : null;
return (
<OrgContext.Provider
value={{
org,
orgs,
loading,
switchOrg,
refreshOrg: fetchOrgs,
}}
>
{children}
</OrgContext.Provider>
);
}