Skip to content

Commit 551b3e4

Browse files
authored
Merge pull request #212 from refactor-group/185-user_role-ui-updates
User Role UI Updates
2 parents 841bf18 + 86722ef commit 551b3e4

File tree

12 files changed

+428
-114
lines changed

12 files changed

+428
-114
lines changed

__tests__/components/ui/coaching-sessions/editor-cache-context.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ vi.mock('@/lib/providers/auth-store-provider', () => ({
1414

1515
vi.mock('@/lib/hooks/use-current-relationship-role', () => ({
1616
useCurrentRelationshipRole: vi.fn(() => ({
17-
relationship_role: 'coach'
17+
relationship_role: 'Coach'
1818
}))
1919
}))
2020

__tests__/components/ui/presence-indicator.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { render, screen } from '@testing-library/react';
22
import { PresenceIndicator } from '@/components/ui/presence-indicator';
33
import { createConnectedPresence, createDisconnectedPresence } from '@/types/presence';
4+
import { RelationshipRole } from '@/types/relationship-role';
45

56
describe('PresenceIndicator', () => {
67
const mockConnectedPresence = createConnectedPresence({
78
userId: 'user1',
89
name: 'Test User',
9-
relationship_role: 'coach',
10+
relationship_role: RelationshipRole.Coach,
1011
color: '#ffcc00'
1112
});
1213

src/app/organizations/[id]/members/page.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
"use client";
22

33
import { use, useEffect, useState } from "react";
4-
import { useSearchParams } from "next/navigation";
4+
import { useSearchParams, redirect } from "next/navigation";
55
import { Card, CardContent } from "@/components/ui/card";
66
import { useAuthStore } from "@/lib/providers/auth-store-provider";
77
import { useCoachingRelationshipList } from "@/lib/api/coaching-relationships";
88
import { useUserList } from "@/lib/api/organizations/users";
99
import { useCurrentOrganization } from "@/lib/hooks/use-current-organization";
10+
import { useCurrentUserRole } from "@/lib/hooks/use-current-user-role";
1011
import { Id } from "@/types/general";
1112
import { MemberContainer } from "@/components/ui/members/member-container";
1213
import { PageContainer } from "@/components/ui/page-container";
14+
import { toast } from "sonner";
1315

1416
export default function MembersPage({
1517
params,
@@ -22,11 +24,21 @@ export default function MembersPage({
2224
);
2325

2426
const organizationId = use(params).id;
25-
const { setCurrentOrganizationId } = useCurrentOrganization();
27+
const { currentOrganizationId, setCurrentOrganizationId } = useCurrentOrganization();
28+
const currentUserRoleState = useCurrentUserRole();
2629

2730
useEffect(() => {
28-
setCurrentOrganizationId(organizationId);
29-
}, [organizationId, setCurrentOrganizationId]);
31+
// Only sync if different to prevent conflicts with OrganizationSwitcher
32+
if (currentOrganizationId !== organizationId) {
33+
setCurrentOrganizationId(organizationId);
34+
}
35+
}, [organizationId, currentOrganizationId, setCurrentOrganizationId]);
36+
37+
// Access control: redirect if user doesn't have access to this organization
38+
if (currentOrganizationId === organizationId && currentUserRoleState.status === 'no_access') {
39+
toast.error("You don't have access to this organization");
40+
redirect('/dashboard');
41+
}
3042

3143
const {
3244
relationships,

src/components/ui/app-sidebar.tsx

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import type * as React from "react";
44
import Link from "next/link";
5+
import { useRouter, usePathname } from "next/navigation";
56
import { BarChart3, Gift, Home, Settings, Users } from "lucide-react";
67

78
import { OrganizationSwitcher } from "./organization-switcher";
@@ -22,6 +23,7 @@ import {
2223
} from "@/components/ui/sidebar";
2324
import { SidebarCollapsible } from "@/types/sidebar";
2425
import { useCurrentOrganization } from "@/lib/hooks/use-current-organization";
26+
import type { Id } from "@/types/general";
2527

2628
// Custom styles for menu buttons to ensure consistent centering
2729
const menuButtonStyles = {
@@ -33,6 +35,22 @@ const menuButtonStyles = {
3335

3436
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
3537
const { currentOrganizationId } = useCurrentOrganization();
38+
const router = useRouter();
39+
const pathname = usePathname();
40+
41+
const handleOrgChange = (newOrgId: Id) => {
42+
// Check if current path is organization-scoped
43+
const orgRouteMatch = pathname?.match(/^\/organizations\/[^\/]+(.*)$/);
44+
45+
if (orgRouteMatch) {
46+
// Preserve everything after the org ID and reconstruct with new org ID
47+
const routeSuffix = orgRouteMatch[1] || '';
48+
router.push(`/organizations/${newOrgId}${routeSuffix}`);
49+
} else {
50+
// Not on an org-scoped route, go to dashboard
51+
router.push('/dashboard');
52+
}
53+
};
3654

3755
return (
3856
<Sidebar collapsible={SidebarCollapsible.Icon} {...props}>
@@ -67,7 +85,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
6785
<SidebarGroupContent>
6886
<SidebarMenu>
6987
<SidebarMenuItem>
70-
<OrganizationSwitcher />
88+
<OrganizationSwitcher onSelect={handleOrgChange} />
7189
</SidebarMenuItem>
7290
</SidebarMenu>
7391
</SidebarGroupContent>
@@ -88,12 +106,12 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
88106
menuButtonStyles.buttonCollapsed
89107
)}
90108
>
91-
<a href="/dashboard">
109+
<Link href="/dashboard">
92110
<span className={menuButtonStyles.iconWrapper}>
93111
<Home className="h-4 w-4" />
94112
</span>
95113
<span>Dashboard</span>
96-
</a>
114+
</Link>
97115
</SidebarMenuButton>
98116
</SidebarMenuItem>
99117
<SidebarMenuItem>
@@ -105,12 +123,12 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
105123
menuButtonStyles.buttonCollapsed
106124
)}
107125
>
108-
<a href={`/organizations/${currentOrganizationId}/members`}>
126+
<Link href={`/organizations/${currentOrganizationId}/members`}>
109127
<span className={menuButtonStyles.iconWrapper}>
110128
<Users className="h-4 w-4" />
111129
</span>
112130
<span>Members</span>
113-
</a>
131+
</Link>
114132
</SidebarMenuButton>
115133
</SidebarMenuItem>
116134
</SidebarMenu>
@@ -130,12 +148,12 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
130148
menuButtonStyles.buttonCollapsed
131149
)}
132150
>
133-
<a href="/referrals">
151+
<Link href="/referrals">
134152
<span className={menuButtonStyles.iconWrapper}>
135153
<Gift className="h-4 w-4" />
136154
</span>
137155
<span>Referrals</span>
138-
</a>
156+
</Link>
139157
</SidebarMenuButton>
140158
</SidebarMenuItem>
141159
<SidebarMenuItem>
@@ -147,12 +165,12 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
147165
menuButtonStyles.buttonCollapsed
148166
)}
149167
>
150-
<a href="/settings">
168+
<Link href="/settings">
151169
<span className={menuButtonStyles.iconWrapper}>
152170
<Settings className="h-4 w-4" />
153171
</span>
154172
<span>Organization settings</span>
155-
</a>
173+
</Link>
156174
</SidebarMenuButton>
157175
</SidebarMenuItem>
158176
</SidebarMenu>
@@ -172,12 +190,12 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
172190
menuButtonStyles.buttonCollapsed
173191
)}
174192
>
175-
<a href="/status">
193+
<Link href="/status">
176194
<span className={menuButtonStyles.iconWrapper}>
177195
<BarChart3 className="h-4 w-4" />
178196
</span>
179197
<span>System status</span>
180-
</a>
198+
</Link>
181199
</SidebarMenuButton>
182200
</SidebarMenuItem>
183201
</SidebarMenu>

src/components/ui/members/member-card.tsx

Lines changed: 56 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { useState } from "react";
22
import { useCurrentOrganization } from "@/lib/hooks/use-current-organization";
33
import { useAuthStore } from "@/lib/providers/auth-store-provider";
4-
import { useCurrentUserRole } from "@/lib/hooks/use-current-user-role";
54
import { useUserMutation } from "@/lib/api/organizations/users";
5+
import { getUserDisplayRoles, getUserCoaches } from "@/lib/utils/user-roles";
66
import { Button } from "@/components/ui/button";
77
import {
88
DropdownMenu,
@@ -30,19 +30,18 @@ import {
3030
import { CoachingRelationshipWithUserNames } from "@/types/coaching_relationship";
3131
import { AuthStore } from "@/lib/stores/auth-store";
3232
import { Id } from "@/types/general";
33-
import { User, Role, isAdminOrSuperAdmin } from "@/types/user";
33+
import { User, isAdminOrSuperAdmin, UserRoleState } from "@/types/user";
3434
import { RelationshipRole } from "@/types/relationship-role";
3535
import { useCoachingRelationshipMutation } from "@/lib/api/coaching-relationships";
3636
import { toast } from "sonner";
3737

3838
interface MemberCardProps {
39-
firstName: string;
40-
lastName: string;
41-
email: string;
42-
userId: Id;
39+
user: User;
40+
currentUserId: Id;
4341
userRelationships: CoachingRelationshipWithUserNames[];
4442
onRefresh: () => void;
4543
users: User[];
44+
currentUserRoleState: UserRoleState;
4645
}
4746

4847
interface Member {
@@ -52,17 +51,24 @@ interface Member {
5251
}
5352

5453
export function MemberCard({
55-
firstName,
56-
lastName,
57-
email,
58-
userId,
54+
user,
55+
currentUserId,
5956
userRelationships,
6057
onRefresh,
6158
users,
59+
currentUserRoleState,
6260
}: MemberCardProps) {
6361
const { currentOrganizationId } = useCurrentOrganization();
6462
const { isACoach, userSession } = useAuthStore((state: AuthStore) => state);
65-
const currentUserRoleState = useCurrentUserRole();
63+
64+
// Extract user properties
65+
const { id: userId, first_name: firstName, last_name: lastName, email } = user;
66+
67+
// Get display roles for this user
68+
const displayRoles = getUserDisplayRoles(user, currentOrganizationId, userRelationships);
69+
70+
// Get coaches for this user
71+
const coaches = getUserCoaches(userId, userRelationships);
6672
const { error: deleteError, deleteNested: deleteUser } = useUserMutation(
6773
currentOrganizationId
6874
);
@@ -71,17 +77,11 @@ export function MemberCard({
7177

7278
console.log("is a coach", isACoach);
7379

74-
// Check if current user is a coach in any of this user's relationships
75-
// and make sure we can't delete ourselves. Admins can delete any user.
80+
// Only admins and super admins can delete users (but not themselves)
7681
const canDeleteUser =
7782
currentUserRoleState.hasAccess &&
78-
(
79-
(userRelationships?.some(
80-
(rel) => rel.coach_id === userSession.id && userId !== userSession.id
81-
) ||
82-
currentUserRoleState.role === Role.Admin) &&
83-
userSession.id !== userId
84-
);
83+
userSession.id !== userId &&
84+
isAdminOrSuperAdmin(currentUserRoleState);
8585

8686
const handleDelete = async () => {
8787
if (!confirm("Are you sure you want to delete this member?")) {
@@ -117,46 +117,55 @@ export function MemberCard({
117117
const [selectedMember, setSelectedMember] = useState<Member | null>(null);
118118
const [assignedMember, setAssignedMember] = useState<Member | null>(null);
119119

120-
const handleCreateCoachingRelationship = () => {
120+
const handleCreateCoachingRelationship = async () => {
121121
if (!selectedMember || !assignedMember) return;
122122

123-
if (assignMode === RelationshipRole.Coach) {
124-
console.log("Assign", selectedMember.id, "as coach for", userId);
125-
createRelationship(currentOrganizationId, {
126-
coach_id: assignedMember.id,
127-
coachee_id: selectedMember.id,
128-
});
129-
} else {
130-
console.log("Assign", selectedMember.id, "as coachee for", userId);
131-
createRelationship(currentOrganizationId, {
132-
coach_id: selectedMember.id,
133-
coachee_id: assignedMember.id,
134-
});
135-
}
123+
try {
124+
if (assignMode === RelationshipRole.Coach) {
125+
console.log("Assign", selectedMember.id, "as coach for", userId);
126+
await createRelationship(currentOrganizationId, {
127+
coach_id: assignedMember.id,
128+
coachee_id: selectedMember.id,
129+
});
130+
} else {
131+
console.log("Assign", selectedMember.id, "as coachee for", userId);
132+
await createRelationship(currentOrganizationId, {
133+
coach_id: selectedMember.id,
134+
coachee_id: assignedMember.id,
135+
});
136+
}
136137

137-
if (createError) {
138+
toast.success(
139+
`Successfully assigned ${assignedMember.first_name} ${assignedMember.last_name} as ${assignMode} for ${selectedMember.first_name} ${selectedMember.last_name}`
140+
);
141+
onRefresh();
142+
setAssignDialogOpen(false);
143+
setSelectedMember(null);
144+
setAssignedMember(null);
145+
} catch (error) {
138146
toast.error(`Error assigning ${assignMode}`);
139-
return;
147+
console.error("Error creating coaching relationship:", error);
140148
}
141-
142-
toast.success(
143-
`Successfully assigned ${assignedMember.first_name} ${assignedMember.last_name} as ${assignMode} for ${selectedMember.first_name} ${selectedMember.last_name}`
144-
);
145-
onRefresh();
146-
setAssignDialogOpen(false);
147-
setSelectedMember(null);
148-
setAssignedMember(null);
149149
};
150150

151151
return (
152152
<div className="flex items-center p-4 hover:bg-accent/50 transition-colors">
153153
<div className="flex-1">
154154
<h3 className="font-medium">
155155
{firstName} {lastName}
156+
{userId === currentUserId && " (You)"}
156157
</h3>
157158
{email && <p className="text-sm text-muted-foreground">{email}</p>}
159+
{displayRoles.length > 0 && (
160+
<p className="text-sm text-muted-foreground">
161+
<span className="font-medium">Roles:</span> {displayRoles.join(', ')}
162+
</p>
163+
)}
164+
<p className="text-sm text-muted-foreground">
165+
<span className="font-medium">Coaches:</span> {coaches.length > 0 ? coaches.join(', ') : 'None'}
166+
</p>
158167
</div>
159-
{(isACoach || isAdminOrSuperAdmin(currentUserRoleState)) && (
168+
{isAdminOrSuperAdmin(currentUserRoleState) && (
160169
<DropdownMenu>
161170
<DropdownMenuTrigger asChild>
162171
<Button
@@ -200,7 +209,7 @@ export function MemberCard({
200209
)}
201210
{canDeleteUser && (
202211
<>
203-
<DropdownMenuSeparator />
212+
{userId !== currentUserId && <DropdownMenuSeparator />}
204213
<DropdownMenuItem
205214
onClick={handleDelete}
206215
className="text-destructive focus:text-destructive"
@@ -222,7 +231,7 @@ export function MemberCard({
222231
</DialogTitle>
223232
<DialogDescription>
224233
Select a member to be their{" "}
225-
{assignMode === RelationshipRole.Coach ? "coach" : "coachee"}
234+
{assignMode.toLowerCase()}
226235
</DialogDescription>
227236
</DialogHeader>
228237
<Select

0 commit comments

Comments
 (0)