diff --git a/src/app/api/portfolio/[portfolioId]/colloborators/route.ts b/src/app/api/portfolio/[portfolioId]/colloborators/route.ts new file mode 100644 index 0000000..3e7d6a7 --- /dev/null +++ b/src/app/api/portfolio/[portfolioId]/colloborators/route.ts @@ -0,0 +1,100 @@ +import { db } from "@/app/db/db"; +import { NextRequest, NextResponse } from "next/server"; +import { portfolio, portfolioUsers} from "@/app/db/schema/portfolio"; +import { user } from "@/app/db/schema/users"; +import { + recommendation, + recommendationMaterials, + planRecommendations, + plan, + scenario, +} from "@/app/db/schema/recommendations"; +import { + propertyTargets, + propertyDetailsEpc, + property, +} from "@/app/db/schema/property"; +import { eq, inArray, Name } from "drizzle-orm"; +import { z } from "zod"; +import { ROLE_OPTIONS } from "@/app/portfolio/[slug]/(portfolio)/settings/roles"; + + +// Get colloborators (users) that have access to the portfolio +export async function GET( + _req: NextRequest, + props: { params: Promise<{ portfolioId: string }> } +) { + const { portfolioId } = await props.params; + + try { + const rows = await db + .select({ + portfolioUserId: portfolioUsers.id, + userId: portfolioUsers.userId, + role: portfolioUsers.role, + name: user.firstName, + email: user.email, + }) + .from(portfolioUsers) + .leftJoin(user, eq(user.id, portfolioUsers.userId)) + .where(eq(portfolioUsers.portfolioId, BigInt(portfolioId))); + + // Explicitly normalize BigInts to strings + const collaborators = rows.map((r) => ({ + portfolioUserId: r.portfolioUserId ? r.portfolioUserId.toString(): null, + userId: r.userId ? r.userId.toString() : null, + role: r.role, + name: r.name ?? null, + email: r.email ?? "", + })); + + return NextResponse.json({ users: collaborators }, { status: 200 }); + } catch (err) { + console.error("GET /users error:", err); + return NextResponse.json( + { error: "Failed to fetch users" }, + { status: 500 } + ); + } +} + + +// PUT: update a collaborator’s role +export async function PUT( + req: NextRequest, + props: { params: Promise<{ portfolioId: string }> } +) { + const { portfolioId } = await props.params; + + // Validate request body + const bodySchema = z.object({ + portfolioUserId: z.string(), + role: z.enum(ROLE_OPTIONS), // adjust to your Role union + }); + + let body: z.infer; + try { + body = bodySchema.parse(await req.json()); + } catch (err) { + return NextResponse.json({ error: "Invalid body" }, { status: 400 }); + } + + try { + // Update role for this portfolioUserId + await db + .update(portfolioUsers) + .set({ role: body.role }) + .where(eq(portfolioUsers.id, BigInt(body.portfolioUserId))); + + return NextResponse.json( + { success: true, portfolioUserId: body.portfolioUserId, role: body.role }, + { status: 200 } + ); + } catch (err) { + console.error("PUT /colloborators error:", err); + return NextResponse.json( + { error: "Failed to update role" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/portfolio/[portfolioId]/route.ts b/src/app/api/portfolio/[portfolioId]/route.ts index 9572f89..749c7bd 100644 --- a/src/app/api/portfolio/[portfolioId]/route.ts +++ b/src/app/api/portfolio/[portfolioId]/route.ts @@ -164,39 +164,3 @@ export async function DELETE(request: NextRequest, props: { params: Promise<{ po } -// Get colloborators (users) that have access to the portfolio -export async function GET( - _req: NextRequest, - props: { params: Promise<{ portfolioId: string }> } -) { - const { portfolioId } = await props.params; - - try { - const rows = await db - .select({ - userId: portfolioUsers.userId, - role: portfolioUsers.role, - name: user.firstName, - email: user.email, - }) - .from(portfolioUsers) - .leftJoin(user, eq(user.id, portfolioUsers.userId)) - .where(eq(portfolioUsers.portfolioId, BigInt(portfolioId))); - - // Explicitly normalize BigInts to strings - const collaborators = rows.map((r) => ({ - userId: r.userId ? r.userId.toString() : null, - role: r.role, - name: r.name ?? null, - email: r.email ?? "", - })); - - return NextResponse.json({ users: collaborators }, { status: 200 }); - } catch (err) { - console.error("GET /users error:", err); - return NextResponse.json( - { error: "Failed to fetch users" }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/UsersPermissionsCard.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/UsersPermissionsCard.tsx index 8b00d8e..387bf1f 100644 --- a/src/app/portfolio/[slug]/(portfolio)/settings/UsersPermissionsCard.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/settings/UsersPermissionsCard.tsx @@ -13,23 +13,24 @@ import { Button } from "@/app/shadcn_components/ui/button"; import { useState, useEffect } from "react"; import { Role, RoleDropdown, Collaborator } from "@/app/portfolio/[slug]/(portfolio)/settings/roles"; -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; + async function getPortfolioUsers(portfolioId: string): Promise { - const res = await fetch(`/api/portfolio/${portfolioId}`, { + 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 users = Array.isArray(json) ? json : json.users; // support both shapes - // Guard + shape to Collaborator[] return Array.isArray(users) ? users .filter((u: any) => u.role !== "creator") // 👈 filter out creator .map((u: any) => ({ + portfolioUserId: String(u.portfolioUserId), userId: String(u.userId), name: u.name ?? null, email: u.email ?? "", @@ -38,10 +39,28 @@ async function getPortfolioUsers(portfolioId: string): Promise { : []; } +async function updatePortfolioUserRole( + portfolioId: string, + portfolioUserId: string, + role: Role +): Promise { + const res = await fetch(`/api/portfolio/${portfolioId}/colloborators`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ portfolioUserId, role }), + }); + if (!res.ok) { + const msg = await res.text().catch(() => ""); + throw new Error(msg || "Failed to update role"); + } +} + export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) { const [inviteEmail, setInviteEmail] = useState(""); const [inviteRole, setInviteRole] = useState("read"); + const queryClient = useQueryClient(); + const { data: collaborators = [], isLoading, @@ -60,18 +79,59 @@ export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) { }, }); + const changeRoleMutation = useMutation({ + mutationFn: ({ portfolioUserId, role }: { portfolioUserId: string; role: Role }) => + updatePortfolioUserRole(portfolioId, portfolioUserId, role), + + // Optimistic update + onMutate: async ({ portfolioUserId, role }) => { + await queryClient.cancelQueries({ queryKey: ["portfolioUsers", portfolioId] }); + const previous = queryClient.getQueryData(["portfolioUsers", portfolioId]); + + // Optimistically update cache + queryClient.setQueryData( + ["portfolioUsers", portfolioId], + (old) => + (old ?? []).map((c) => + c.portfolioUserId === portfolioUserId ? { ...c, role } : c + ) + ); + + // Return context to rollback on error + return { previous }; + }, + + // Rollback on error + onError: (err, _vars, context) => { + if (context?.previous) { + queryClient.setQueryData(["portfolioUsers", portfolioId], context.previous); + } + console.error("Failed to update role:", err); + }, + + // Always revalidate after success/error + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ["portfolioUsers", portfolioId] }); + }, + }); + + function handleInvite() { - console.log("handle invite"); + console.log("Inivte email", inviteEmail); + console.log("Inivte Role", inviteRole); // TODO: POST invite -> then refetch() } - function onChangeRole(email: string, role: Role) { - console.log(`on change role ${email} ${role}`); + function onChangeRole(portfolioUserId: string, role: Role) { + console.log(`Change portfolioUserId ${portfolioUserId} to ${role}`); + changeRoleMutation.mutate({ portfolioUserId, role }); + // TODO: PATCH role -> then refetch() } - function onRemove(email: string) { - console.log(`remove user ${email}`); + function onRemove(portfolioUserId: string) { + console.log(`This button will delete the row portoflioUserId ${portfolioUserId}`); + console.log("This was not implemented as Jun-te wanted to avoid Delete via drizzle before Database integrirty") // TODO: DELETE user -> then refetch() } @@ -151,10 +211,10 @@ export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) { {c.name || "—"} {c.email} - onChangeRole(c.email, r)} /> + onChangeRole(c.portfolioUserId, r)} /> - diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/roles.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/roles.tsx index bd74b6c..feeb223 100644 --- a/src/app/portfolio/[slug]/(portfolio)/settings/roles.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/settings/roles.tsx @@ -8,10 +8,11 @@ import { } from "@/app/shadcn_components/ui/select"; // Roles you support in your app (adjust as needed) -const ROLE_OPTIONS = ["read", "write"] as const; +export const ROLE_OPTIONS = ["read", "write"] as const; export type Role = typeof ROLE_OPTIONS[number]; export type Collaborator = { + portfolioUserId: string; userId: string; name?: string | null; email: string;