"use client"; import { useState, useEffect, useCallback } from "react"; import { useTranslations } from "next-intl"; import { Card } from "@/components/ui/card"; interface Assignment { userId: string; displayName: string; email: string; roles: string[]; assignedAt: string; assignedBy: string; orphan: boolean; } interface OrgMember { userId: string; email: string; displayName: string; roles: string[]; } interface Props { tenantName: string; /** * When false, the panel renders read-only — assignments are visible * but the add-user form and remove ✕ buttons are hidden. Pass * `canEdit` from the parent server component (= canMutate(user)). */ canEdit: boolean; } /** * AssignedUsersPanel — manages the tenant_user_assignments rows for * one tenant. Owner sees: * - List of currently-assigned users with name, email, role, and * an "X" button to revoke. * - Dropdown of org members not yet assigned + "Assign" button. * * `user`-role members see the panel read-only (canEdit=false): they * see who else has access to the tenant they're working with, but * can't change anything. */ export function AssignedUsersPanel({ tenantName, canEdit }: Props) { const t = useTranslations("assignments"); const [assignments, setAssignments] = useState(null); const [members, setMembers] = useState(null); const [error, setError] = useState(""); const [busy, setBusy] = useState(false); const [pickedUserId, setPickedUserId] = useState(""); const refresh = useCallback(async () => { setError(""); try { const [aRes, mRes] = await Promise.all([ fetch(`/api/tenants/${tenantName}/assignments`), canEdit ? fetch(`/api/team`) : Promise.resolve(null), ]); if (!aRes.ok) throw new Error("Failed to load assignments"); const aData = await aRes.json(); setAssignments(aData.assignments ?? []); if (mRes && mRes.ok) { const mData = await mRes.json(); setMembers(mData.members ?? []); } } catch (err: any) { setError(err.message); } }, [tenantName, canEdit]); useEffect(() => { refresh(); }, [refresh]); async function handleAssign() { if (!pickedUserId || busy) return; setBusy(true); setError(""); try { const res = await fetch(`/api/tenants/${tenantName}/assignments`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userId: pickedUserId }), }); if (!res.ok) { const data = await res.json(); throw new Error(data.error || "Assign failed"); } setPickedUserId(""); await refresh(); } catch (err: any) { setError(err.message); } finally { setBusy(false); } } async function handleRevoke(userId: string) { if (busy) return; setBusy(true); setError(""); try { const res = await fetch( `/api/tenants/${tenantName}/assignments/${encodeURIComponent(userId)}`, { method: "DELETE" } ); if (!res.ok) { const data = await res.json(); throw new Error(data.error || "Revoke failed"); } await refresh(); } catch (err: any) { setError(err.message); } finally { setBusy(false); } } if (assignments === null) { return (
{t("loading")}
); } // Compute candidates for the assign dropdown: members of the org // who hold the `user` role (not owners — they have implicit access) // and aren't already assigned. const assignedIds = new Set(assignments.map((a) => a.userId)); const candidates = (members ?? []).filter( (m) => !assignedIds.has(m.userId) && m.roles.includes("user") && !m.roles.includes("owner") ); return ( {error && (
{error}
)} {assignments.length === 0 ? (

{t("noneAssigned")}

) : (
    {assignments.map((a) => (
  • {a.orphan ? ( {a.displayName} ) : ( a.displayName )}
    {a.email && (
    {a.email}
    )}
    {canEdit && ( )}
  • ))}
)} {canEdit && (
{candidates.length === 0 ? (

{t("noCandidates")}

) : (
)}
)}
); }