diff --git a/backend/app/api/v1/routes_admin.py b/backend/app/api/v1/routes_admin.py
index f7bc80a..54949ce 100644
--- a/backend/app/api/v1/routes_admin.py
+++ b/backend/app/api/v1/routes_admin.py
@@ -62,7 +62,8 @@ async def list_users(
role=user_doc["role"],
auth_provider=user_doc.get("auth_provider", "local"),
is_active=user_doc["is_active"],
- created_at=user_doc.get("created_at", datetime.utcnow()).isoformat()
+ created_at=user_doc.get("created_at", datetime.utcnow()).isoformat(),
+ pm_client_ids=user_doc.get("pm_client_ids", []),
))
return UserListResponse(
@@ -94,7 +95,8 @@ async def get_user(
role=user_doc["role"],
auth_provider=user_doc.get("auth_provider", "local"),
is_active=user_doc["is_active"],
- created_at=user_doc.get("created_at", datetime.utcnow()).isoformat()
+ created_at=user_doc.get("created_at", datetime.utcnow()).isoformat(),
+ pm_client_ids=user_doc.get("pm_client_ids", []),
)
@@ -141,7 +143,8 @@ async def create_user(
role=user_data.role,
auth_provider="local",
is_active=True,
- created_at=user_doc["created_at"].isoformat()
+ created_at=user_doc["created_at"].isoformat(),
+ pm_client_ids=[],
)
@@ -198,7 +201,8 @@ async def update_user(
role=result["role"],
auth_provider=result.get("auth_provider", "local"),
is_active=result["is_active"],
- created_at=result.get("created_at", datetime.utcnow()).isoformat()
+ created_at=result.get("created_at", datetime.utcnow()).isoformat(),
+ pm_client_ids=result.get("pm_client_ids", []),
)
diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py
index 11e6e1a..7d6bd11 100644
--- a/backend/app/schemas/auth.py
+++ b/backend/app/schemas/auth.py
@@ -53,6 +53,7 @@ class UserResponse(BaseModel):
auth_provider: AuthProvider
is_active: bool
created_at: Optional[str] = None
+ pm_client_ids: list[str] = []
class UserListResponse(BaseModel):
diff --git a/frontend/src/hooks/useClients.ts b/frontend/src/hooks/useClients.ts
index 6045cec..905239e 100644
--- a/frontend/src/hooks/useClients.ts
+++ b/frontend/src/hooks/useClients.ts
@@ -15,7 +15,7 @@ export function useClient(clientId: string) {
return useQuery({
queryKey: ['clients', clientId],
queryFn: () => apiClient.getClient(clientId),
- enabled: !!clientId,
+ enabled: !!clientId && clientId !== 'undefined',
});
}
@@ -39,6 +39,58 @@ export function useUpdateClient(clientId: string) {
});
}
+export function useTeamsForClient(clientId: string | null) {
+ return useQuery({
+ queryKey: ['clients', clientId, 'teams'],
+ queryFn: () => apiClient.listTeams(clientId!),
+ enabled: !!clientId,
+ });
+}
+
+export function useAssignPMAny() {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: ({ clientId, userId }: { clientId: string; userId: string }) =>
+ apiClient.assignPM(clientId, userId),
+ onSuccess: (_, { clientId, userId }) => {
+ qc.invalidateQueries({ queryKey: ['clients', clientId, 'pm'] });
+ qc.invalidateQueries({ queryKey: ['users', userId] });
+ qc.invalidateQueries({ queryKey: ['users'] });
+ },
+ });
+}
+
+export function useRemovePMAny() {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: ({ clientId, userId }: { clientId: string; userId: string }) =>
+ apiClient.removePM(clientId, userId),
+ onSuccess: (_, { clientId, userId }) => {
+ qc.invalidateQueries({ queryKey: ['clients', clientId, 'pm'] });
+ qc.invalidateQueries({ queryKey: ['users', userId] });
+ qc.invalidateQueries({ queryKey: ['users'] });
+ },
+ });
+}
+
+export function useAddTeamMemberAny() {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: ({ clientId, teamId, userId }: { clientId: string; teamId: string; userId: string }) =>
+ apiClient.addTeamMember(clientId, teamId, userId),
+ onSuccess: (_, { clientId }) => qc.invalidateQueries({ queryKey: ['clients', clientId, 'teams'] }),
+ });
+}
+
+export function useRemoveTeamMemberAny() {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: ({ clientId, teamId, userId }: { clientId: string; teamId: string; userId: string }) =>
+ apiClient.removeTeamMember(clientId, teamId, userId),
+ onSuccess: (_, { clientId }) => qc.invalidateQueries({ queryKey: ['clients', clientId, 'teams'] }),
+ });
+}
+
// ── PMs ──────────────────────────────────────────────────────────────────────
export function usePMs(clientId: string) {
diff --git a/frontend/src/routes/admin/UserDetail.tsx b/frontend/src/routes/admin/UserDetail.tsx
index 0021703..e2f9713 100644
--- a/frontend/src/routes/admin/UserDetail.tsx
+++ b/frontend/src/routes/admin/UserDetail.tsx
@@ -1,8 +1,17 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { useUser, useUpdateUser, useResetUserPassword } from '../../hooks/useUsers';
+import {
+ useClients,
+ useTeamsForClient,
+ useAssignPMAny,
+ useRemovePMAny,
+ useAddTeamMemberAny,
+ useRemoveTeamMemberAny,
+} from '../../hooks/useClients';
import { useToastContext } from '../../contexts/ToastContext';
-import type { UserRole, UpdateUserRequest } from '../../types/api';
+import { useQueryClient } from '@tanstack/react-query';
+import type { UserRole, UpdateUserRequest, User, Client, Team } from '../../types/api';
export function UserDetail() {
const { id } = useParams<{ id: string }>();
@@ -20,7 +29,6 @@ export function UserDetail() {
is_active: true,
});
- // Initialize form when user data loads
useEffect(() => {
if (user) {
setFormData({
@@ -166,6 +174,7 @@ export function UserDetail() {
+
@@ -246,6 +255,9 @@ export function UserDetail() {
+ {/* Assignments Card */}
+
No clients found.
+ ) : ( +No teams for this client.
+ ) : ( +