mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
allow roles to be updated
This commit is contained in:
parent
4044665c96
commit
c60f09e302
4 changed files with 172 additions and 47 deletions
100
src/app/api/portfolio/[portfolioId]/colloborators/route.ts
Normal file
100
src/app/api/portfolio/[portfolioId]/colloborators/route.ts
Normal file
|
|
@ -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<typeof bodySchema>;
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Collaborator[]> {
|
||||
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<Collaborator[]> {
|
|||
: [];
|
||||
}
|
||||
|
||||
async function updatePortfolioUserRole(
|
||||
portfolioId: string,
|
||||
portfolioUserId: string,
|
||||
role: Role
|
||||
): Promise<void> {
|
||||
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<Role>("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<Collaborator[]>(["portfolioUsers", portfolioId]);
|
||||
|
||||
// Optimistically update cache
|
||||
queryClient.setQueryData<Collaborator[]>(
|
||||
["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 }) {
|
|||
<TableCell>{c.name || "—"}</TableCell>
|
||||
<TableCell>{c.email}</TableCell>
|
||||
<TableCell className="min-w-40">
|
||||
<RoleDropdown value={c.role} onChange={(r) => onChangeRole(c.email, r)} />
|
||||
<RoleDropdown value={c.role} onChange={(r) => onChangeRole(c.portfolioUserId, r)} />
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="destructive" className="bg-red-700" onClick={() => onRemove(c.email)}>
|
||||
<Button variant="destructive" className="bg-red-700" onClick={() => onRemove(c.portfolioUserId)}>
|
||||
Remove
|
||||
</Button>
|
||||
</TableCell>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue