diff --git a/src/app/api/portfolio/[portfolioId]/colloborators/route.ts b/src/app/api/portfolio/[portfolioId]/collaborators/route.ts similarity index 97% rename from src/app/api/portfolio/[portfolioId]/colloborators/route.ts rename to src/app/api/portfolio/[portfolioId]/collaborators/route.ts index 3fc72965..53bdb444 100644 --- a/src/app/api/portfolio/[portfolioId]/colloborators/route.ts +++ b/src/app/api/portfolio/[portfolioId]/collaborators/route.ts @@ -30,7 +30,7 @@ import { resolvePortfolioPrivilege, } from "@/app/lib/resolvePortfolioPrivilege"; -// Get colloborators (users) that have access to the portfolio, plus the +// Get collaborators (users) that have access to the portfolio, plus the // effective privilege of the requesting user (so the UI knows which actions // to expose). export async function GET( @@ -140,7 +140,7 @@ export async function DELETE( { status: 200 }, ); } catch (err) { - console.error("DELETE /colloborators error:", err); + console.error("DELETE /collaborators error:", err); return NextResponse.json( { error: "Failed to remove user" }, { status: 500 }, @@ -184,7 +184,7 @@ export async function PUT( { status: 200 } ); } catch (err) { - console.error("PUT /colloborators error:", err); + console.error("PUT /collaborators error:", err); return NextResponse.json( { error: "Failed to update role" }, { status: 500 } @@ -372,7 +372,7 @@ export async function POST( { status: 200 }, ); } catch (err) { - console.error("POST /colloborators error:", err); + console.error("POST /collaborators error:", err); return NextResponse.json( { error: "Failed to invite user" }, { status: 500 } diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/CapabilitiesCard.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/CapabilitiesCard.tsx index 533c2e3c..b9ac1ef5 100644 --- a/src/app/portfolio/[slug]/(portfolio)/settings/CapabilitiesCard.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/settings/CapabilitiesCard.tsx @@ -11,6 +11,10 @@ import { import { Button } from "@/app/shadcn_components/ui/button"; import { Badge } from "@/app/shadcn_components/ui/badge"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { + COLLABORATORS_QUERY_KEY, + fetchCollaborators, +} from "./collaboratorsClient"; type Capability = "approver" | "contractor"; @@ -30,20 +34,6 @@ async function getCapabilities(portfolioId: string): Promise return res.json(); } -async function getCollaborators( - portfolioId: string, -): Promise<{ userId: string; name: string | null; email: string }[]> { - const res = await fetch(`/api/portfolio/${portfolioId}/colloborators`); - if (!res.ok) throw new Error("Failed to fetch collaborators"); - const json = await res.json(); - const users = Array.isArray(json) ? json : json.users ?? []; - return users.map((u: any) => ({ - userId: String(u.userId), - name: u.name ?? null, - email: u.email ?? "", - })); -} - async function assignCapability( portfolioId: string, userId: string, @@ -81,19 +71,20 @@ export function CapabilitiesCard({ portfolioId }: { portfolioId: string }) { refetchOnWindowFocus: false, }); - const { data: collaborators = [], isLoading: loadingCollabs } = useQuery({ - queryKey: ["portfolioUsers", portfolioId], - queryFn: () => getCollaborators(portfolioId), + const { data: collaboratorsResponse, isLoading: loadingCollabs } = useQuery({ + queryKey: COLLABORATORS_QUERY_KEY(portfolioId), + queryFn: () => fetchCollaborators(portfolioId), enabled: !!portfolioId, refetchOnWindowFocus: false, }); + const collaborators = collaboratorsResponse?.users ?? []; const isLoading = loadingCaps || loadingCollabs; // Build a map: userId -> { capabilities: [] } const capMap: CapabilityMap = {}; for (const c of collaborators) { - capMap[c.userId] = { name: c.name, email: c.email, capabilities: [] }; + capMap[c.userId] = { name: c.name ?? null, email: c.email, capabilities: [] }; } for (const e of entries) { if (capMap[e.userId]) { diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/UsersPermissionsCard.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/UsersPermissionsCard.tsx index 6e6d58d9..a5f61cf3 100644 --- a/src/app/portfolio/[slug]/(portfolio)/settings/UsersPermissionsCard.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/settings/UsersPermissionsCard.tsx @@ -22,6 +22,11 @@ import { canAdminister, type PortfolioPrivilege, } from "@/app/lib/portfolioAdmin"; +import { + COLLABORATORS_QUERY_KEY, + fetchCollaborators, + type CollaboratorsResponse, +} from "./collaboratorsClient"; type PendingInvitation = { invitationId: string; @@ -30,35 +35,6 @@ type PendingInvitation = { createdAt: string; }; -type CollaboratorsResponse = { - users: Collaborator[]; - currentUser?: { privilege: PortfolioPrivilege }; -}; - -async function getPortfolioUsers( - portfolioId: string, -): Promise { - const res = await fetch(`/api/portfolio/${portfolioId}/colloborators`, { - method: "GET", - headers: { "Content-Type": "application/json" }, - }); - if (!res.ok) throw new Error("Failed to fetch users"); - const json = await res.json(); - const rawUsers = Array.isArray(json) ? json : json.users; - const users: Collaborator[] = Array.isArray(rawUsers) - ? rawUsers.map((u: any) => ({ - portfolioUserId: String(u.portfolioUserId), - userId: String(u.userId), - name: u.name ?? null, - email: u.email ?? "", - role: u.role, - })) - : []; - const privilege: PortfolioPrivilege | undefined = - json?.currentUser?.privilege; - return privilege ? { users, currentUser: { privilege } } : { users }; -} - async function getPortfolioInvitations( portfolioId: string, ): Promise { @@ -84,7 +60,7 @@ async function updatePortfolioUserRole( portfolioUserId: string, role: Role, ): Promise { - const res = await fetch(`/api/portfolio/${portfolioId}/colloborators`, { + const res = await fetch(`/api/portfolio/${portfolioId}/collaborators`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ portfolioUserId, role }), @@ -101,7 +77,7 @@ async function invitePortfolioUser( role: Role, name: string, ): Promise { - const res = await fetch(`/api/portfolio/${portfolioId}/colloborators`, { + const res = await fetch(`/api/portfolio/${portfolioId}/collaborators`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, role, name }), @@ -116,7 +92,7 @@ async function removePortfolioUser( portfolioId: string, portfolioUserId: string, ): Promise { - const res = await fetch(`/api/portfolio/${portfolioId}/colloborators`, { + const res = await fetch(`/api/portfolio/${portfolioId}/collaborators`, { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ portfolioUserId }), @@ -149,7 +125,7 @@ export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) { const queryClient = useQueryClient(); - const usersKey = ["portfolioUsers", portfolioId]; + const usersKey = COLLABORATORS_QUERY_KEY(portfolioId); const invitationsKey = ["portfolioInvitations", portfolioId]; const { @@ -159,7 +135,7 @@ export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) { refetch, } = useQuery({ queryKey: usersKey, - queryFn: () => getPortfolioUsers(portfolioId), + queryFn: () => fetchCollaborators(portfolioId), enabled: !!portfolioId, refetchOnWindowFocus: false, }); diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/collaboratorsClient.ts b/src/app/portfolio/[slug]/(portfolio)/settings/collaboratorsClient.ts new file mode 100644 index 00000000..85d5a171 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/settings/collaboratorsClient.ts @@ -0,0 +1,35 @@ +import type { Collaborator } from "./roles"; +import type { PortfolioPrivilege } from "@/app/lib/portfolioAdmin"; + +export type CollaboratorsResponse = { + users: Collaborator[]; + currentUser?: { privilege: PortfolioPrivilege }; +}; + +// Shared fetcher used by every component that queries the portfolio user +// list. Keeping a single function (and a single response shape) means +// useQuery deduping behaves correctly when the user-access page mounts +// both UsersPermissionsCard and CapabilitiesCard against the same key. +export async function fetchCollaborators( + portfolioId: string, +): Promise { + const res = await fetch(`/api/portfolio/${portfolioId}/collaborators`); + if (!res.ok) throw new Error("Failed to fetch collaborators"); + const json = await res.json(); + const rawUsers = Array.isArray(json) ? json : (json?.users ?? []); + const users: Collaborator[] = Array.isArray(rawUsers) + ? rawUsers.map((u: any) => ({ + portfolioUserId: String(u.portfolioUserId), + userId: String(u.userId), + name: u.name ?? null, + email: u.email ?? "", + role: u.role, + })) + : []; + const privilege: PortfolioPrivilege | undefined = + json?.currentUser?.privilege; + return privilege ? { users, currentUser: { privilege } } : { users }; +} + +export const COLLABORATORS_QUERY_KEY = (portfolioId: string) => + ["portfolioUsers", portfolioId] as const;