"use client"; // Team-management data layer. Serves EITHER the local mock (when the Shell isn't // configured — the polished demo keeps working) OR the live be-crm data door // (crm.team.*), behind one interface so the component is mode-agnostic. // // Live contract (be-crm, multi-role): // query crm.team.member.search { perPage } -> { items: MemberDTO[], meta } // query crm.team.role.list { includeEmpty } -> { items: RoleDTO[], meta } // query crm.team.invitation.list { status } -> { items: InvitationDTO[], meta } // cmd crm.team.member.setRoles { id, roleIds } // cmd crm.team.member.update { id, jobTitle?, roleIds? } // cmd crm.team.member.remove { id } // cmd crm.team.invitation.create/resend/revoke // cmd crm.team.role.addPermission/removePermission { id, permissionId } import { useCallback, useMemo, useState } from "react"; import { useAppShell, useAuth, useQuery } from "@abe-kap/appshell-sdk/react"; import { isShellConfigured } from "./appshell"; import { members as seedMembers, roles as seedRoles, pendingInvites as seedInvites, type Member as SeedMember, type Role as SeedRole, type Invite as SeedInvite, } from "@/components/dashboard/team-data"; /* ---- UI-facing shapes (multi-role; superset of the mock) ---------------- */ export type UiStatus = "active" | "away" | "offline"; export interface UiRole { id: string; slug: string; name: string; color: string; description: string; system: boolean; isOwner: boolean; perms: string[]; attire: string[]; overlap: string; memberCount?: number; } export interface UiMember { id: string; principalId?: string; name: string; initials: string; email: string; title: string; roleIds: string[]; gradient: string; status: UiStatus; lastActive: string; deals: number; joined: string; isOwner: boolean; isYou: boolean; } export interface UiInvite { id: string; email: string; roleIds: string[]; invitedBy: string; sentAt: string; } export interface TeamData { live: boolean; loading: boolean; error: string | null; members: UiMember[]; roles: UiRole[]; invites: UiInvite[]; setMemberRoles: (id: string, roleIds: string[]) => Promise; updateMember: (id: string, patch: { title?: string; roleIds?: string[] }) => Promise; removeMember: (id: string) => Promise; invite: (email: string, roleIds: string[]) => Promise; resendInvite: (id: string) => Promise; revokeInvite: (id: string) => Promise; setPermission: (roleId: string, permId: string, granted: boolean) => Promise; refetch: () => void; } /* ---- helpers ------------------------------------------------------------ */ const GRADIENTS = [ "linear-gradient(135deg,#fda913,#fd6d13)", "linear-gradient(135deg,#6366f1,#8b5cf6)", "linear-gradient(135deg,#06b6d4,#3b82f6)", "linear-gradient(135deg,#10b981,#059669)", "linear-gradient(135deg,#f43f5e,#ec4899)", "linear-gradient(135deg,#f59e0b,#ef4444)", "linear-gradient(135deg,#8b5cf6,#d946ef)", "linear-gradient(135deg,#14b8a6,#0ea5e9)", ]; const gradientFor = (id: string) => GRADIENTS[[...id].reduce((a, c) => a + c.charCodeAt(0), 0) % GRADIENTS.length]; const initialsOf = (name: string) => name.split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0]).join("").toUpperCase() || "?"; /* ---- be-crm DTO types (subset used here) -------------------------------- */ interface RoleRef { id: string; slug: string; name: string; color: string | null; isSystem: boolean; isOwnerRole: boolean } interface MemberDTO { id: string; principalId: string; jobTitle: string | null; joinedAt: string; status: "active" | "deactivated"; roles: RoleRef[]; openDeals: number } interface RoleDTO { id: string; slug: string; name: string; description: string | null; color: string | null; isSystem: boolean; isOwnerRole: boolean; permissions: string[]; memberCount: number } interface InvitationDTO { id: string; email: string; roles: RoleRef[]; invitedBy: string; createdAt: string } const relTime = (iso: string): string => { const then = Date.parse(iso); if (Number.isNaN(then)) return iso; return new Date(then).toLocaleDateString(undefined, { day: "numeric", month: "short", year: "numeric" }); }; /* ---- mock → UI ---------------------------------------------------------- */ const mockRole = (r: SeedRole): UiRole => ({ id: r.id, slug: r.id, name: r.name, color: r.color, description: r.description, system: r.system, isOwner: r.id === "owner", perms: r.perms, attire: r.attire, overlap: r.overlap, }); const mockMember = (m: SeedMember): UiMember => ({ id: m.id, name: m.name, initials: m.initials, email: m.email, title: m.title, roleIds: [m.roleId], gradient: m.gradient, status: m.status, lastActive: m.lastActive, deals: m.deals, joined: m.joined, isOwner: m.roleId === "owner", isYou: m.roleId === "owner", }); const mockInvite = (i: SeedInvite): UiInvite => ({ id: i.id, email: i.email, roleIds: [i.roleId], invitedBy: i.invitedBy, sentAt: i.sentAt, }); /* ======================================================================== */ /* Mock implementation (no Shell configured) */ /* ======================================================================== */ function useMockTeam(): TeamData { const [members, setMembers] = useState(() => seedMembers.map(mockMember)); const [roles, setRoles] = useState(() => seedRoles.map(mockRole)); const [invites, setInvites] = useState(() => seedInvites.map(mockInvite)); const setMemberRoles = useCallback(async (id: string, roleIds: string[]) => { setMembers((l) => l.map((m) => (m.id === id ? { ...m, roleIds } : m))); }, []); const updateMember = useCallback(async (id: string, patch: { title?: string; roleIds?: string[] }) => { setMembers((l) => l.map((m) => (m.id === id ? { ...m, ...(patch.title !== undefined ? { title: patch.title } : {}), ...(patch.roleIds ? { roleIds: patch.roleIds } : {}) } : m))); }, []); const removeMember = useCallback(async (id: string) => { setMembers((l) => l.filter((m) => m.id !== id)); }, []); const invite = useCallback(async (email: string, roleIds: string[]) => { setInvites((l) => [{ id: `inv_${Date.now()}`, email, roleIds, invitedBy: "James Carter", sentAt: "Just now" }, ...l]); }, []); const resendInvite = useCallback(async () => {}, []); const revokeInvite = useCallback(async (id: string) => { setInvites((l) => l.filter((i) => i.id !== id)); }, []); const setPermission = useCallback(async (roleId: string, permId: string, granted: boolean) => { setRoles((l) => l.map((r) => (r.id !== roleId ? r : { ...r, perms: granted ? [...new Set([...r.perms, permId])] : r.perms.filter((p) => p !== permId), }))); }, []); return { live: false, loading: false, error: null, members, roles, invites, setMemberRoles, updateMember, removeMember, invite, resendInvite, revokeInvite, setPermission, refetch: () => {} }; } /* ======================================================================== */ /* Live implementation (be-crm data door) */ /* ======================================================================== */ function useLiveTeam(): TeamData { const { sdk } = useAppShell(); const { user } = useAuth(); const meId = user?.id; const membersQ = useQuery<{ items: MemberDTO[] }>("crm.team.member.search", { perPage: 100 }); const rolesQ = useQuery<{ items: RoleDTO[] }>("crm.team.role.list", { includeEmpty: true }); const invitesQ = useQuery<{ items: InvitationDTO[] }>("crm.team.invitation.list", { status: "pending" }); const refetch = useCallback(() => { membersQ.refetch(); rolesQ.refetch(); invitesQ.refetch(); }, [membersQ, rolesQ, invitesQ]); const cmd = useCallback(async (action: string, variables: Record) => { await sdk.command(action, variables); refetch(); }, [sdk, refetch]); const roles: UiRole[] = useMemo(() => (rolesQ.data?.items ?? []).map((r) => ({ id: r.id, slug: r.slug, name: r.name, color: r.color ?? "var(--text-2)", description: r.description ?? "", system: r.isSystem, isOwner: r.isOwnerRole, perms: r.permissions, attire: [], overlap: "", memberCount: r.memberCount, })), [rolesQ.data]); const members: UiMember[] = useMemo(() => (membersQ.data?.items ?? []).map((m) => { const isYou = !!meId && m.principalId === meId; const name = isYou && user?.displayName ? user.displayName : (m.jobTitle?.trim() || `Member ${m.principalId.replace(/^pp_/, "").slice(0, 6)}`); return { id: m.id, principalId: m.principalId, name, initials: initialsOf(name), email: isYou && user?.email ? user.email : m.principalId, title: m.jobTitle ?? "", roleIds: m.roles.map((r) => r.id), gradient: gradientFor(m.id), status: (m.status === "active" ? "active" : "offline") as UiStatus, lastActive: m.status === "active" ? "Active" : "—", deals: m.openDeals, joined: relTime(m.joinedAt), isOwner: m.roles.some((r) => r.isOwnerRole), isYou, }; }), [membersQ.data, meId, user]); const invites: UiInvite[] = useMemo(() => (invitesQ.data?.items ?? []).map((i) => ({ id: i.id, email: i.email, roleIds: i.roles.map((r) => r.id), invitedBy: i.invitedBy, sentAt: relTime(i.createdAt), })), [invitesQ.data]); return { live: true, loading: membersQ.loading || rolesQ.loading || invitesQ.loading, error: (membersQ.error ?? rolesQ.error ?? invitesQ.error)?.message ?? null, members, roles, invites, setMemberRoles: (id, roleIds) => cmd("crm.team.member.setRoles", { id, roleIds }), updateMember: (id, patch) => cmd("crm.team.member.update", { id, ...(patch.title !== undefined ? { jobTitle: patch.title } : {}), ...(patch.roleIds ? { roleIds: patch.roleIds } : {}), }), removeMember: (id) => cmd("crm.team.member.remove", { id }), invite: (email, roleIds) => cmd("crm.team.invitation.create", { email, roleIds }), resendInvite: (id) => cmd("crm.team.invitation.resend", { id }), revokeInvite: (id) => cmd("crm.team.invitation.revoke", { id }), setPermission: (roleId, permId, granted) => cmd(granted ? "crm.team.role.addPermission" : "crm.team.role.removePermission", { id: roleId, permissionId: permId }), refetch, }; } /* ---- public hook: pick the implementation at module-config time --------- */ const SHELL = isShellConfigured(); export function useTeamData(): TeamData { // `SHELL` is constant for the life of the bundle (NEXT_PUBLIC_* is build-time), // so the same hook path runs every render — Rules-of-Hooks safe. return SHELL ? useLiveTeam() : useMockTeam(); }