From 458fe18f2c6ba0f17a5dc151d0f5c4a0a36582eb Mon Sep 17 00:00:00 2001 From: Levi Gomes Date: Fri, 10 Jul 2026 01:00:32 -0300 Subject: [PATCH 1/3] feat(users): add role types, badge variants and updateRole API method --- src/components/ui/badge.tsx | 2 ++ src/services/api.ts | 3 +++ src/types/index.ts | 2 ++ src/types/user.ts | 8 ++++++++ 4 files changed, 15 insertions(+) diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index c636ffe..d36597b 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -13,6 +13,8 @@ const badgeVariants = cva( active: "bg-verde-claro/20 text-verde-claro", inactive: "bg-areia/30 text-verde", admin: "bg-telha/15 text-telha border border-telha/30", + manager: "bg-azul/25 text-verde border border-azul/60", + member: "bg-areia/30 text-verde border border-areia", }, }, defaultVariants: { diff --git a/src/services/api.ts b/src/services/api.ts index d0f3a8c..90bc07c 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -8,6 +8,7 @@ import type { MyManagedOrgsResponse, UserListResponse, UserUpdate, + UserRoleUpdate, UserRoleResponse, AppResponse, AppCreate, @@ -145,6 +146,8 @@ export const usersAPI = { get: (userId: string) => api.get(`/users/${userId}`), update: (userId: string, data: UserUpdate) => api.patch(`/users/${userId}`, data), + updateRole: (userId: string, data: UserRoleUpdate) => + api.put(`/users/${userId}/role`, data), delete: (userId: string) => api.delete(`/users/${userId}`), listRoles: (userId: string) => api.get(`/users/${userId}/roles`), diff --git a/src/types/index.ts b/src/types/index.ts index e343aa0..1c0bb4b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -9,8 +9,10 @@ export type { } from "./auth" export type { + UserRole, UserListResponse, UserUpdate, + UserRoleUpdate, UserRoleResponse, } from "./user" diff --git a/src/types/user.ts b/src/types/user.ts index 42c8b33..012c4a1 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -1,3 +1,5 @@ +export type UserRole = "member" | "manager" | "platform_admin"; + export interface UserListResponse { id: string email: string @@ -5,6 +7,7 @@ export interface UserListResponse { avatar_url: string | null is_active: boolean is_platform_admin: boolean + role?: UserRole; created_at: string } @@ -14,6 +17,11 @@ export interface UserUpdate { avatar_url?: string | null } +export interface UserRoleUpdate { + role: UserRole; + project_ids?: string[]; +} + export interface UserRoleResponse { app_key: string role_key: string From 030b4f8aeec1309493849b42b81f0acc62328a12 Mon Sep 17 00:00:00 2001 From: Levi Gomes Date: Fri, 10 Jul 2026 01:00:32 -0300 Subject: [PATCH 2/3] feat(users): manage user role from the user detail page --- src/components/pages/UserDetailPage.tsx | 230 +++++++++++++++++++++--- 1 file changed, 202 insertions(+), 28 deletions(-) diff --git a/src/components/pages/UserDetailPage.tsx b/src/components/pages/UserDetailPage.tsx index 4e3a1a0..8350de2 100644 --- a/src/components/pages/UserDetailPage.tsx +++ b/src/components/pages/UserDetailPage.tsx @@ -1,13 +1,26 @@ import { useEffect, useState } from "react" import { useParams, useNavigate } from "react-router" -import { ArrowLeft, KeyRound, Plus, Trash2, User as UserIcon } from "lucide-react" +import { + ArrowLeft, + KeyRound, + Plus, + Shield, + Trash2, + User as UserIcon, + UserCog, + type LucideIcon, +} from "lucide-react" import { toast } from "sonner" -import { usersAPI, appsAPI, rolesAPI } from "@/services/api" +import { usersAPI, appsAPI, projectsAPI, rolesAPI } from "@/services/api" +import { useAuth } from "@/contexts/AuthContext" import type { UserListResponse, + UserRole, + UserRoleUpdate, UserRoleResponse, AppResponse, AppRoleResponse, + ProjectResponse, } from "@/types" import { card } from "@/styles" import { Button } from "@/components/ui/button" @@ -35,17 +48,43 @@ import { ConfirmDialog } from "@/components/common/ConfirmDialog" import { formatDate } from "@/utils/format" +const roleBadge: Record< + UserRole, + { variant: "admin" | "manager" | "member"; icon: LucideIcon; label: string } +> = { + platform_admin: { variant: "admin", icon: Shield, label: "Admin" }, + manager: { variant: "manager", icon: UserCog, label: "Manager" }, + member: { variant: "member", icon: UserIcon, label: "Member" }, +} + +const roleOptions: { value: UserRole; label: string }[] = [ + { value: "member", label: "Member" }, + { value: "manager", label: "Manager" }, + { value: "platform_admin", label: "Platform Admin" }, +] + +function getUserRole(user: UserListResponse): UserRole { + return user.role ?? (user.is_platform_admin ? "platform_admin" : "member") +} + function UserInfoCard({ user, + isSelf, + roleSaving, + onRoleSelect, onToggleActive, - onToggleAdmin, onDelete, }: { user: UserListResponse + isSelf: boolean + roleSaving: boolean + onRoleSelect: (role: UserRole) => void onToggleActive: () => void - onToggleAdmin: () => void onDelete: () => void }) { + const role = getUserRole(user) + const { variant, icon: RoleIcon, label } = roleBadge[role] + return (
@@ -85,18 +124,36 @@ function UserInfoCard({
- Platform Admin - + Role: + - {user.is_platform_admin ? ( - Yes - ) : ( - No - )} - + + + {label} + +
@@ -204,6 +261,7 @@ function RolesTable({ export default function UserDetailPage() { const { userId } = useParams<{ userId: string }>() const navigate = useNavigate() + const { user: currentUser } = useAuth() const [user, setUser] = useState(null) const [userLoading, setUserLoading] = useState(true) @@ -223,6 +281,13 @@ export default function UserDetailPage() { ) const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false) + const [managerDialogOpen, setManagerDialogOpen] = useState(false) + const [projects, setProjects] = useState([]) + const [projectsLoading, setProjectsLoading] = useState(false) + const [selectedProjectIds, setSelectedProjectIds] = useState([]) + const [roleSaving, setRoleSaving] = useState(false) + const [memberConfirmOpen, setMemberConfirmOpen] = useState(false) + async function fetchUser() { if (!userId) return try { @@ -268,23 +333,68 @@ export default function UserDetailPage() { } } - async function handleToggleAdmin() { - if (!userId || !user) return + async function applyRoleUpdate(update: UserRoleUpdate) { + if (!userId) return false + setRoleSaving(true) try { - const { data } = await usersAPI.update(userId, { - is_platform_admin: !user.is_platform_admin, - }) - setUser(data) - toast.success( - data.is_platform_admin - ? "User is now a platform admin" - : "Platform admin removed", - ) - } catch { - toast.error("Failed to update user") + await usersAPI.updateRole(userId, update) + const roleLabel = roleOptions.find((o) => o.value === update.role)?.label + toast.success(`User role updated to ${roleLabel}`) + await fetchUser() + return true + } catch (err: unknown) { + const detail = (err as { response?: { data?: { detail?: string } } }) + ?.response?.data?.detail + toast.error(detail || "Failed to update role") + return false + } finally { + setRoleSaving(false) + } + } + + async function openManagerDialog() { + setSelectedProjectIds([]) + setManagerDialogOpen(true) + if (projects.length === 0) { + setProjectsLoading(true) + try { + const { data } = await projectsAPI.list() + setProjects(data) + } catch { + toast.error("Failed to load projects") + } finally { + setProjectsLoading(false) + } + } + } + + function handleRoleSelect(role: UserRole) { + if (!user || role === getUserRole(user)) return + if (role === "platform_admin") { + applyRoleUpdate({ role: "platform_admin" }) + } else if (role === "manager") { + openManagerDialog() + } else { + setMemberConfirmOpen(true) } } + function toggleProjectSelection(projectId: string) { + setSelectedProjectIds((prev) => + prev.includes(projectId) + ? prev.filter((id) => id !== projectId) + : [...prev, projectId], + ) + } + + async function handleConfirmManager() { + const ok = await applyRoleUpdate({ + role: "manager", + project_ids: selectedProjectIds, + }) + if (ok) setManagerDialogOpen(false) + } + async function handleDeleteUser() { if (!userId) return try { @@ -398,8 +508,10 @@ export default function UserDetailPage() { setDeleteConfirmOpen(true)} /> @@ -489,6 +601,68 @@ export default function UserDetailPage() { + + + + Select projects this user will manage + + Managers oversee specific projects. Select at least one project + to grant this user the manager role. + + + {projectsLoading ? ( +

Loading projects...

+ ) : projects.length === 0 ? ( +

+ No projects available. Create a project before assigning a + manager. +

+ ) : ( +
+ {projects.map((project) => ( + + ))} +
+ )} + + + + +
+
+ + applyRoleUpdate({ role: "member" })} + /> + { From 2bdb4b8e48820729d3414f70b8fece93d099399c Mon Sep 17 00:00:00 2001 From: Levi Gomes Date: Wed, 15 Jul 2026 17:00:38 -0300 Subject: [PATCH 3/3] chore(ci): empty commit to re-trigger CI checks Co-Authored-By: Claude Opus 4.8 (1M context)