{"version":3,"sources":["../../src/hooks/useSchedulingAdminGate.ts"],"names":[],"mappings":"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","file":"useSchedulingAdminGate.mjs","sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport {\n  resolveSchedulingBasePath,\n  schedulingGet,\n  schedulingPost,\n  SchedulingClientError,\n} from '../lib/schedulingClient';\nimport { useAuth, type UseAuthResult } from './useAuth';\nimport type { AuthUser } from '../types/auth';\n\n/**\n * Discriminated status for a scheduling-admin gate.\n *\n * - `loading`: auth probe in flight, or /admin/me has not yet resolved.\n * - `signed_out`: visitor is unauthenticated.\n * - `not_admin_no_one_claimed`: signed in, but `scheduling_admin_user` is\n *   empty — the consumer should offer a \"Claim ownership\" CTA.\n * - `not_admin`: signed in, an admin already exists. Consumer should show\n *   \"ask the owner\" copy.\n * - `admin`: signed-in user IS in `scheduling_admin_user` — consumer mounts\n *   the full admin UI.\n */\nexport type SchedulingAdminGateStatus =\n  | 'loading'\n  | 'signed_out'\n  | 'not_admin_no_one_claimed'\n  | 'not_admin'\n  | 'admin';\n\nexport interface UseSchedulingAdminGateOptions {\n  /** Override the API base path. Default: auto-detect via resolveSchedulingBasePath(). */\n  apiBase?: string;\n  /**\n   * Reuse an externally-mounted useAuth result. When omitted, the gate calls\n   * useAuth() internally. Pass an explicit value when the consumer's parent\n   * island already mounts useAuth and wants to share state (avoids a second\n   * /session probe + the two hooks getting out of sync).\n   */\n  auth?: UseAuthResult;\n  /**\n   * If true, skip the initial /admin/me fetch on mount. The consumer drives\n   * via .refresh().\n   */\n  skipInitialLoad?: boolean;\n}\n\nexport interface UseSchedulingAdminGateResult {\n  basePath: string;\n  status: SchedulingAdminGateStatus;\n  /** Convenience: true when status === 'admin'. */\n  isAdmin: boolean;\n  /** Signed-in user when known. Null otherwise. */\n  user: AuthUser | null;\n  /**\n   * Total rows in `scheduling_admin_user`. Null until /admin/me has resolved.\n   * Useful when the consumer wants to differentiate \"first-run claim\" from\n   * \"claim a second seat\" — the latter doesn't exist in MVP, but the field\n   * is exposed for future-compat.\n   */\n  totalAdmins: number | null;\n  /** Last /admin/me or /admin/claim error. Cleared on the next successful op. */\n  error: string | null;\n  /** True while a claim() call is in flight. */\n  claiming: boolean;\n  /** Re-fetch /admin/me. Useful after a fresh sign-in. */\n  refresh: () => Promise<void>;\n  /**\n   * POST /admin/claim — server promotes the signed-in user to scheduling\n   * admin when no row exists yet. Refreshes status on success. On failure,\n   * the error is surfaced via `error` and `status` remains the same.\n   */\n  claim: () => Promise<void>;\n}\n\ninterface AdminMeResponse {\n  isAdmin?: boolean;\n  totalAdmins?: number;\n  email?: string | null;\n}\n\ninterface ClaimResponse {\n  success?: boolean;\n  message?: string;\n}\n\nexport function useSchedulingAdminGate(\n  opts: UseSchedulingAdminGateOptions = {},\n): UseSchedulingAdminGateResult {\n  const basePath = useMemo(\n    () => opts.apiBase ?? resolveSchedulingBasePath(),\n    [opts.apiBase],\n  );\n\n  // Hooks can't be called conditionally, so the internal hook always runs.\n  // Suppress its initial probe when an external auth is supplied so the\n  // /session call doesn't fire twice.\n  const internalAuth = useAuth(opts.auth ? { skipInitialSession: true } : undefined);\n  const auth = opts.auth ?? internalAuth;\n\n  const [isAdmin, setIsAdmin] = useState<boolean | null>(null);\n  const [totalAdmins, setTotalAdmins] = useState<number | null>(null);\n  const [meLoading, setMeLoading] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const [claiming, setClaiming] = useState(false);\n\n  // Drop probes that resolve after auth.user changed underneath us so a\n  // fast sign-in/sign-out doesn't write stale data over the new state.\n  const requestSeq = useRef(0);\n\n  const refresh = useCallback(async () => {\n    if (!auth.user) {\n      // Anonymous — clear any prior admin state. No fetch.\n      setIsAdmin(null);\n      setTotalAdmins(null);\n      setError(null);\n      setMeLoading(false);\n      return;\n    }\n    const mySeq = ++requestSeq.current;\n    setMeLoading(true);\n    setError(null);\n    try {\n      const data = await schedulingGet<AdminMeResponse>(basePath, '/admin/me');\n      if (mySeq !== requestSeq.current) return;\n      setIsAdmin(!!data?.isAdmin);\n      setTotalAdmins(typeof data?.totalAdmins === 'number' ? data.totalAdmins : 0);\n    } catch (err) {\n      if (mySeq !== requestSeq.current) return;\n      const message =\n        err instanceof SchedulingClientError ? err.message\n        : err instanceof Error ? err.message\n        : 'Failed to load admin status.';\n      setError(message);\n      setIsAdmin(false);\n      setTotalAdmins(null);\n    } finally {\n      if (mySeq === requestSeq.current) setMeLoading(false);\n    }\n  }, [auth.user, basePath]);\n\n  // Re-probe whenever the signed-in user identity changes (sign-in, sign-out,\n  // account switch). The auth.user reference is what useAuth returns; depend\n  // on its id when present so a new user with the same ref doesn't spuriously\n  // skip a refresh.\n  const userId = auth.user?.id ?? null;\n  useEffect(() => {\n    if (opts.skipInitialLoad) return;\n    if (!auth.ready) return;\n    void refresh();\n  }, [auth.ready, userId, opts.skipInitialLoad, refresh]);\n\n  const claim = useCallback(async () => {\n    if (!auth.user) {\n      setError('Sign in before claiming admin.');\n      return;\n    }\n    setClaiming(true);\n    setError(null);\n\n    // Capture any claim-side error message so we can re-apply it AFTER the\n    // post-claim refresh — refresh() clears `error` as a normal probe, which\n    // would otherwise wipe a still-relevant claim failure.\n    let claimError: string | null = null;\n    try {\n      const data = await schedulingPost<ClaimResponse>(\n        basePath,\n        '/admin/claim',\n        {},\n      );\n      if (!data?.success) {\n        claimError = data?.message ?? 'Claim failed.';\n      }\n    } catch (err) {\n      claimError =\n        err instanceof SchedulingClientError ? err.message\n        : err instanceof Error ? err.message\n        : 'Claim failed.';\n    }\n\n    // Refresh either way — a concurrent claim by someone else may have\n    // succeeded, in which case our claim 403'd but the gate still updates.\n    await refresh();\n    if (claimError) setError(claimError);\n    setClaiming(false);\n  }, [auth.user, basePath, refresh]);\n\n  // Derived status. Order matters: handle the loading / signed-out cases\n  // before deriving admin/no-claim branches.\n  let status: SchedulingAdminGateStatus;\n  if (!auth.ready) {\n    status = 'loading';\n  } else if (!auth.user) {\n    status = 'signed_out';\n  } else if (meLoading || isAdmin === null) {\n    status = 'loading';\n  } else if (isAdmin) {\n    status = 'admin';\n  } else if (totalAdmins === 0) {\n    status = 'not_admin_no_one_claimed';\n  } else {\n    status = 'not_admin';\n  }\n\n  return {\n    basePath,\n    status,\n    isAdmin: status === 'admin',\n    user: auth.user,\n    totalAdmins,\n    error,\n    claiming,\n    refresh,\n    claim,\n  };\n}\n"]}