Skip to content

Commit c626022

Browse files
authored
Merge pull request #124 from refactor-group/add_admin_role
Add the concept of user roles.
2 parents a71886d + 1196b12 commit c626022

16 files changed

+269
-70
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { siteConfig } from "@/site.config.ts";
55
import { SiteHeader } from "@/components/ui/site-header";
66
import { AppSidebar } from "@/components/ui/app-sidebar";
77
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
8+
import { Toaster } from "@/components/ui/sonner";
89
export const metadata: Metadata = {
910
title: siteConfig.name,
1011
description: "Manage coaching members",
@@ -22,6 +23,7 @@ export default function MembersLayout({
2223
<SidebarInset>
2324
<SiteHeader />
2425
<main className="flex-1 p-6">{children}</main>
26+
<Toaster />
2527
</SidebarInset>
2628
</div>
2729
</SidebarProvider>

src/components/ui/coaching-relationship-selector.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export default function CoachingRelationshipSelector({
7575
(action) => action
7676
);
7777

78-
const { setIsCoach } = useAuthStore((state) => state);
78+
const { setIsCurrentCoach } = useAuthStore((state) => state);
7979

8080
const handleSetCoachingRelationship = (relationshipId: Id) => {
8181
setCurrentCoachingRelationshipId(relationshipId);
@@ -91,7 +91,7 @@ export default function CoachingRelationshipSelector({
9191
? getCurrentCoachingRelationship(currentCoachingRelationshipId)
9292
: null;
9393
if (currentRelationship) {
94-
setIsCoach(currentRelationship.coach_id);
94+
setIsCurrentCoach(currentRelationship.coach_id);
9595
}
9696
}, [currentCoachingRelationshipId]);
9797

src/components/ui/coaching-session.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const CoachingSession: React.FC<CoachingSessionProps> = ({
3232
const { setCurrentCoachingSessionId } = useCoachingSessionStateStore(
3333
(state) => state
3434
);
35-
const { isCoach } = useAuthStore((state) => state);
35+
const { isCurrentCoach } = useAuthStore((state) => state);
3636

3737
return (
3838
<Card>
@@ -64,7 +64,7 @@ const CoachingSession: React.FC<CoachingSessionProps> = ({
6464
<DropdownMenuItem onClick={onUpdate}>
6565
Edit
6666
</DropdownMenuItem>
67-
{isCoach && (
67+
{isCurrentCoach && (
6868
<DropdownMenuItem onClick={onDelete} className="text-destructive">
6969
Delete
7070
</DropdownMenuItem>

src/components/ui/dashboard/add-entities.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default function AddEntities({
1818
}: AddEntitiesProps) {
1919
const router = useRouter();
2020
const { currentOrganizationId } = useOrganizationStateStore((state) => state);
21-
const { isCoach } = useAuthStore((state) => state);
21+
const { isCurrentCoach } = useAuthStore((state) => state);
2222

2323
const onMemberButtonClicked = () => {
2424
router.push(`/organizations/${currentOrganizationId}/members`);
@@ -31,12 +31,12 @@ export default function AddEntities({
3131
</h3>
3232
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
3333
<AddCoachingSessionButton
34-
disabled={!isCoach || !currentOrganizationId}
34+
disabled={!isCurrentCoach || !currentOrganizationId}
3535
onClick={onCreateSession}
3636
/>
3737

3838
<AddMemberButton
39-
disabled={!isCoach || !currentOrganizationId}
39+
disabled={!isCurrentCoach || !currentOrganizationId}
4040
onClick={onMemberButtonClicked}
4141
/>
4242
</div>

src/components/ui/members/add-member-dialog.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { Input } from "@/components/ui/input";
1717
import { useUserMutation } from "@/lib/api/organizations/users";
1818
import { NewUser } from "@/types/user";
1919
import { useOrganizationStateStore } from "@/lib/providers/organization-state-store-provider";
20+
import { toast } from "sonner";
2021

2122
interface AddMemberDialogProps {
2223
open: boolean;
@@ -85,10 +86,11 @@ export function AddMemberDialog({
8586
});
8687
setPasswordError("");
8788
onMemberAdded();
89+
toast.success(`New Member ${formData.firstName} ${formData.lastName} added successfully`);
8890
onOpenChange(false);
8991
} catch (error) {
9092
console.error("Error creating user:", error);
91-
// Handle error appropriately
93+
toast.error("There was an error adding the member");
9294
}
9395
};
9496

Lines changed: 174 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,38 @@
1+
import { useState } from "react";
12
import { useOrganizationStateStore } from "@/lib/providers/organization-state-store-provider";
23
import { useAuthStore } from "@/lib/providers/auth-store-provider";
34
import { useUserMutation } from "@/lib/api/organizations/users";
45
import { Button } from "@/components/ui/button";
5-
import { Trash2 } from "lucide-react";
6-
import { CoachingRelationshipWithUserNames } from "@/types/coaching_relationship_with_user_names";
6+
import {
7+
DropdownMenu,
8+
DropdownMenuTrigger,
9+
DropdownMenuContent,
10+
DropdownMenuItem,
11+
DropdownMenuSeparator,
12+
} from "@/components/ui/dropdown-menu";
13+
import { MoreHorizontal, Trash2 } from "lucide-react";
14+
import {
15+
Dialog,
16+
DialogContent,
17+
DialogDescription,
18+
DialogHeader,
19+
DialogFooter,
20+
DialogTitle,
21+
} from "@/components/ui/dialog";
22+
import {
23+
Select,
24+
SelectTrigger,
25+
SelectValue,
26+
SelectContent,
27+
SelectItem,
28+
} from "@/components/ui/select";
29+
import { CoachingRelationshipWithUserNames } from "@/types/coaching_relationship";
730
import { OrganizationStateStore } from "@/lib/stores/organization-state-store";
831
import { AuthStore } from "@/lib/stores/auth-store";
932
import { Id } from "@/types/general";
33+
import { User, Role } from "@/types/user";
34+
import { useCoachingRelationshipMutation } from "@/lib/api/coaching-relationships";
35+
import { toast } from "sonner";
1036

1137
interface MemberCardProps {
1238
firstName: string;
@@ -15,6 +41,13 @@ interface MemberCardProps {
1541
userId: Id;
1642
userRelationships: CoachingRelationshipWithUserNames[];
1743
onRefresh: () => void;
44+
users: User[];
45+
}
46+
47+
interface Member {
48+
id: Id;
49+
first_name: string;
50+
last_name: string;
1851
}
1952

2053
export function MemberCard({
@@ -24,31 +57,84 @@ export function MemberCard({
2457
userId,
2558
userRelationships,
2659
onRefresh,
60+
users,
2761
}: MemberCardProps) {
2862
const currentOrganizationId = useOrganizationStateStore(
2963
(state: OrganizationStateStore) => state.currentOrganizationId
3064
);
31-
const { userSession } = useAuthStore((state: AuthStore) => state);
32-
const { deleteNested: deleteUser } = useUserMutation(currentOrganizationId);
65+
const { isACoach, userSession } = useAuthStore((state: AuthStore) => state);
66+
const { error: deleteError, deleteNested: deleteUser } = useUserMutation(currentOrganizationId);
67+
const { error: createError, createNested: createRelationship } = useCoachingRelationshipMutation(currentOrganizationId);
68+
69+
console.log("is a coach", isACoach);
3370

3471
// Check if current user is a coach in any of this user's relationships
35-
// and make sure we can't delete ourselves
36-
const canDeleteUser = userRelationships.some(
72+
// and make sure we can't delete ourselves. Admins can delete any user.
73+
const canDeleteUser = (userRelationships?.some(
3774
(rel) => rel.coach_id === userSession.id && userId !== userSession.id
38-
);
75+
) || (userSession.role === Role.Admin)) && userSession.id !== userId;
3976

4077
const handleDelete = async () => {
4178
if (!confirm("Are you sure you want to delete this member?")) {
4279
return;
4380
}
81+
await deleteUser(currentOrganizationId, userId);
82+
onRefresh();
4483

45-
try {
46-
await deleteUser(currentOrganizationId, userId);
84+
if (deleteError) {
85+
console.error("Error deleting member:", deleteError);
86+
toast.error("Error deleting member");
4787
onRefresh();
48-
} catch (error) {
49-
console.error("Error deleting user:", error);
50-
// TODO: Show an error toast here once we start using toasts for showing operation results.
88+
return;
89+
}
90+
toast.success("Member deleted successfully");
91+
onRefresh();
92+
};
93+
94+
const handleAssignMember = (val: string) => {
95+
const user = users.find((m) => m.id === val);
96+
if (!user) return;
97+
const member: Member = {
98+
id: user.id,
99+
first_name: user.first_name,
100+
last_name: user.last_name,
101+
};
102+
setAssignedMember(member);
103+
};
104+
105+
// Placeholder – actual UI flows will be implemented later
106+
const [assignDialogOpen, setAssignDialogOpen] = useState(false);
107+
const [assignMode, setAssignMode] = useState<"coach" | "coachee">("coach");
108+
const [selectedMember, setSelectedMember] = useState<Member | null>(null);
109+
const [assignedMember, setAssignedMember] = useState<Member | null>(null);
110+
111+
const handleCreateCoachingRelationship = () => {
112+
if (!selectedMember || !assignedMember) return;
113+
114+
if (assignMode === "coach") {
115+
console.log("Assign", selectedMember.id, "as coach for", userId);
116+
createRelationship(currentOrganizationId, {
117+
coach_id: assignedMember.id,
118+
coachee_id: selectedMember.id,
119+
});
120+
} else {
121+
console.log("Assign", selectedMember.id, "as coachee for", userId);
122+
createRelationship(currentOrganizationId, {
123+
coach_id: selectedMember.id,
124+
coachee_id: assignedMember.id,
125+
});
51126
}
127+
128+
if (createError) {
129+
toast.error(`Error assigning ${assignMode}`);
130+
return;
131+
}
132+
133+
toast.success(`Successfully assigned ${assignedMember.first_name} ${assignedMember.last_name} as ${assignMode} for ${selectedMember.first_name} ${selectedMember.last_name}`);
134+
onRefresh();
135+
setAssignDialogOpen(false);
136+
setSelectedMember(null);
137+
setAssignedMember(null);
52138
};
53139

54140
return (
@@ -59,16 +145,82 @@ export function MemberCard({
59145
</h3>
60146
{email && <p className="text-sm text-muted-foreground">{email}</p>}
61147
</div>
62-
{canDeleteUser && (
63-
<Button
64-
variant="ghost"
65-
size="icon"
66-
onClick={handleDelete}
67-
className="text-destructive hover:text-destructive"
68-
>
69-
<Trash2 className="h-4 w-4" />
70-
</Button>
71-
)}
148+
{(isACoach || userSession.role === Role.Admin) && (
149+
<DropdownMenu>
150+
<DropdownMenuTrigger asChild>
151+
<Button variant="ghost" size="icon" className="text-muted-foreground">
152+
<MoreHorizontal className="h-4 w-4" />
153+
</Button>
154+
</DropdownMenuTrigger>
155+
<DropdownMenuContent align="end">
156+
{userSession.role === Role.Admin && (
157+
<>
158+
<DropdownMenuItem
159+
onClick={() => {
160+
setAssignMode("coach");
161+
setAssignDialogOpen(true);
162+
setSelectedMember({id: userId, first_name: firstName, last_name: lastName});
163+
}}
164+
>
165+
Assign Coach
166+
</DropdownMenuItem>
167+
<DropdownMenuItem
168+
onClick={() => {
169+
setAssignMode("coachee");
170+
setAssignDialogOpen(true);
171+
setSelectedMember({id: userId, first_name: firstName, last_name: lastName});
172+
}}
173+
>
174+
Assign Coachee
175+
</DropdownMenuItem>
176+
</>
177+
)}
178+
{canDeleteUser && (
179+
<>
180+
<DropdownMenuSeparator />
181+
<DropdownMenuItem
182+
onClick={handleDelete}
183+
className="text-destructive focus:text-destructive"
184+
>
185+
<Trash2 className="mr-2 h-4 w-4" /> Delete
186+
</DropdownMenuItem>
187+
</>
188+
)}
189+
</DropdownMenuContent>
190+
</DropdownMenu>
191+
)}
192+
193+
{/* Assign Coach/Coachee Modal */}
194+
<Dialog open={assignDialogOpen} onOpenChange={setAssignDialogOpen}>
195+
<DialogContent>
196+
<DialogHeader>
197+
<DialogTitle>
198+
{assignMode === "coach" ? "Assign Coach" : "Assign Coachee"}
199+
</DialogTitle>
200+
<DialogDescription>
201+
Select a member to be their {assignMode === "coach" ? "coach" : "coachee"}
202+
</DialogDescription>
203+
</DialogHeader>
204+
<Select
205+
onValueChange={(val) => handleAssignMember(val)}
206+
value={assignedMember?.id?.toString()}
207+
>
208+
<SelectTrigger className="w-full">
209+
<SelectValue placeholder="Select a member" />
210+
</SelectTrigger>
211+
<SelectContent>
212+
{users
213+
.filter((m) => m.id !== userId)
214+
.map((m) => (
215+
<SelectItem key={m.id} value={m.id.toString()}>{`${m.first_name} ${m.last_name}`}</SelectItem>
216+
))}
217+
</SelectContent>
218+
</Select>
219+
<DialogFooter>
220+
<Button onClick={handleCreateCoachingRelationship}>{assignMode === "coach" ? "Assign as Coach" : "Assign as Coachee"}</Button>
221+
</DialogFooter>
222+
</DialogContent>
223+
</Dialog>
72224
</div>
73225
);
74226
}

0 commit comments

Comments
 (0)