Skip to content

Commit 12891d8

Browse files
authored
Merge pull request #92 from refactor-group/88-feature-add-member-management-page-add-users
88 feature add member management page add users
2 parents 08055ca + 255b89e commit 12891d8

File tree

7 files changed

+146
-75
lines changed

7 files changed

+146
-75
lines changed

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import "@/styles/globals.css";
33
import { siteConfig } from "@/site.config.ts";
44

55
import { SiteHeader } from "@/components/ui/site-header";
6+
import { AppSidebar } from "@/components/ui/app-sidebar";
7+
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
68
export const metadata: Metadata = {
79
title: siteConfig.name,
810
description: "Manage coaching members",
@@ -14,11 +16,14 @@ export default function MembersLayout({
1416
children: React.ReactNode;
1517
}>) {
1618
return (
17-
// Ensure that SiteHeader has enough vertical space to stay sticky at the top
18-
// of the page by using "min-h-screen" in the parent div
19-
<div className="min-h-screen">
20-
<SiteHeader />
21-
<main>{children}</main>
22-
</div>
19+
<SidebarProvider>
20+
<div className="flex min-h-screen min-w-full">
21+
<AppSidebar />
22+
<SidebarInset>
23+
<SiteHeader />
24+
<main className="flex-1 p-6">{children}</main>
25+
</SidebarInset>
26+
</div>
27+
</SidebarProvider>
2328
);
2429
}

src/components/ui/app-sidebar.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
SidebarRail,
2121
SidebarSeparator,
2222
} from "@/components/ui/sidebar";
23+
import { useOrganizationStateStore } from "@/lib/providers/organization-state-store-provider";
2324

2425
// Custom styles for menu buttons to ensure consistent centering
2526
const menuButtonStyles = {
@@ -30,6 +31,8 @@ const menuButtonStyles = {
3031
};
3132

3233
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
34+
const { currentOrganizationId } = useOrganizationStateStore((state) => state);
35+
3336
return (
3437
<Sidebar collapsible="icon" {...props}>
3538
<SidebarHeader className="h-16 flex flex-col justify-between pb-0">
@@ -101,7 +104,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
101104
menuButtonStyles.buttonCollapsed
102105
)}
103106
>
104-
<a href="/members">
107+
<a href={`/organizations/${currentOrganizationId}/members`}>
105108
<span className={menuButtonStyles.iconWrapper}>
106109
<Users className="h-4 w-4" />
107110
</span>

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

Lines changed: 111 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
DialogContent,
1010
DialogHeader,
1111
DialogTitle,
12+
DialogDescription,
13+
DialogFooter,
1214
} from "@/components/ui/dialog";
1315
import { Label } from "@/components/ui/label";
1416
import { Input } from "@/components/ui/input";
@@ -39,8 +41,12 @@ export function AddMemberDialog({
3941
lastName: "",
4042
displayName: "",
4143
email: "",
44+
password: "",
45+
confirmPassword: "", // For validation
4246
});
4347

48+
const [passwordError, setPasswordError] = useState("");
49+
4450
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
4551
const { name, value } = e.target;
4652
setFormData((prev) => ({
@@ -52,80 +58,128 @@ export function AddMemberDialog({
5258
const handleSubmit = async (e: React.FormEvent) => {
5359
e.preventDefault();
5460

61+
// Validate passwords match
62+
// we may want to add other validations in the future
63+
if (formData.password !== formData.confirmPassword) {
64+
setPasswordError("Passwords do not match");
65+
return;
66+
}
67+
5568
const newUser: NewUser = {
5669
first_name: formData.firstName,
5770
last_name: formData.lastName,
5871
display_name: formData.displayName,
5972
email: formData.email,
73+
password: formData.password, // Add password to the NewUser type
6074
};
6175

62-
await createUserNested(currentOrganizationId, newUser);
63-
setFormData({
64-
firstName: "",
65-
lastName: "",
66-
displayName: "",
67-
email: "",
68-
});
69-
// SWR Refresh
70-
onMemberAdded();
71-
onOpenChange(false);
76+
try {
77+
await createUserNested(currentOrganizationId, newUser);
78+
setFormData({
79+
firstName: "",
80+
lastName: "",
81+
displayName: "",
82+
email: "",
83+
password: "",
84+
confirmPassword: "",
85+
});
86+
setPasswordError("");
87+
onMemberAdded();
88+
onOpenChange(false);
89+
} catch (error) {
90+
console.error("Error creating user:", error);
91+
// Handle error appropriately
92+
}
7293
};
7394

7495
return (
7596
<Dialog open={open} onOpenChange={onOpenChange}>
7697
<DialogContent>
7798
<DialogHeader>
7899
<DialogTitle>Add New Member</DialogTitle>
100+
<DialogDescription>
101+
Create a new member account. The member will be able to change their
102+
password after first login.
103+
</DialogDescription>
79104
</DialogHeader>
80-
<form onSubmit={handleSubmit} className="space-y-4 pt-4">
81-
<div className="space-y-2">
82-
<Label htmlFor="firstName">First Name</Label>
83-
<Input
84-
id="firstName"
85-
name="firstName"
86-
value={formData.firstName}
87-
onChange={handleInputChange}
88-
placeholder="Enter first name"
89-
required
90-
/>
91-
</div>
92-
<div className="space-y-2">
93-
<Label htmlFor="lastName">Last Name</Label>
94-
<Input
95-
id="lastName"
96-
name="lastName"
97-
value={formData.lastName}
98-
onChange={handleInputChange}
99-
placeholder="Enter last name"
100-
required
101-
/>
102-
</div>
103-
<div className="space-y-2">
104-
<Label htmlFor="displayName">Display Name</Label>
105-
<Input
106-
id="displayName"
107-
name="displayName"
108-
value={formData.displayName}
109-
onChange={handleInputChange}
110-
placeholder="Enter display name"
111-
required
112-
/>
105+
<form onSubmit={handleSubmit}>
106+
<div className="space-y-4">
107+
<div className="space-y-2">
108+
<Label htmlFor="firstName">First Name</Label>
109+
<Input
110+
id="firstName"
111+
name="firstName"
112+
value={formData.firstName}
113+
onChange={handleInputChange}
114+
placeholder="Enter first name"
115+
required
116+
/>
117+
</div>
118+
<div className="space-y-2">
119+
<Label htmlFor="lastName">Last Name</Label>
120+
<Input
121+
id="lastName"
122+
name="lastName"
123+
value={formData.lastName}
124+
onChange={handleInputChange}
125+
placeholder="Enter last name"
126+
required
127+
/>
128+
</div>
129+
<div className="space-y-2">
130+
<Label htmlFor="displayName">Display Name</Label>
131+
<Input
132+
id="displayName"
133+
name="displayName"
134+
value={formData.displayName}
135+
onChange={handleInputChange}
136+
placeholder="Enter display name"
137+
required
138+
/>
139+
</div>
140+
<div className="space-y-2">
141+
<Label htmlFor="email">Email</Label>
142+
<Input
143+
id="email"
144+
name="email"
145+
type="email"
146+
value={formData.email}
147+
onChange={handleInputChange}
148+
placeholder="Enter email address"
149+
required
150+
/>
151+
</div>
152+
<div className="grid gap-4">
153+
<Label htmlFor="password">Initial Password</Label>
154+
<Input
155+
id="password"
156+
name="password"
157+
type="password"
158+
value={formData.password}
159+
onChange={handleInputChange}
160+
required
161+
/>
162+
</div>
163+
<div className="grid gap-4">
164+
<Label htmlFor="confirmPassword">Confirm Password</Label>
165+
<Input
166+
id="confirmPassword"
167+
name="confirmPassword"
168+
type="password"
169+
value={formData.confirmPassword}
170+
onChange={handleInputChange}
171+
required
172+
/>
173+
</div>
174+
{passwordError && (
175+
<div className="text-red-500 text-sm">{passwordError}</div>
176+
)}
113177
</div>
114-
<div className="space-y-2">
115-
<Label htmlFor="email">Email</Label>
116-
<Input
117-
id="email"
118-
name="email"
119-
type="email"
120-
value={formData.email}
121-
onChange={handleInputChange}
122-
placeholder="Enter email address"
123-
required
124-
/>
178+
<div className="pt-4">
179+
<DialogFooter>
180+
<Button type="submit">Create Member</Button>
181+
</DialogFooter>
125182
</div>
126-
<Button type="submit" className="w-full">
127-
Add Member
128-
</Button>
129183
</form>
130184
</DialogContent>
131185
</Dialog>

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ export function MemberContainer({
1919
onRefresh,
2020
isLoading,
2121
}: MemberContainerProps) {
22+
// Check if current user is a coach in any relationship
23+
const isCoachInAnyRelationship = relationships.some(
24+
(rel) => rel.coach_id === userSession.id
25+
);
26+
2227
// Find relationships where current user is either coach or coachee
2328
const userRelationships = relationships.filter(
2429
(rel) =>
@@ -45,7 +50,12 @@ export function MemberContainer({
4550
<div className="space-y-6">
4651
<div className="flex justify-between items-center">
4752
<h2 className="text-2xl font-semibold">Members</h2>
48-
<AddMemberButton onMemberAdded={onRefresh} />
53+
{/* Only show the button if user is a coach to _some_ user within the
54+
scope of the organization. We may come back and add this directly to user
55+
data. */}
56+
{isCoachInAnyRelationship && (
57+
<AddMemberButton onMemberAdded={onRefresh} />
58+
)}
4959
</div>
5060
<MemberList users={associatedUsers} />
5161
</div>

src/components/ui/organization-selector.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,17 @@ function OrganizationsList({ userId }: { userId: Id }) {
3030

3131
// Be sure to cache the list of current organizations in the OrganizationStateStore
3232
useEffect(() => {
33-
if (!organizations.length) return;
33+
if (!organizations?.length) return;
3434
console.debug(
3535
`organizations (useEffect): ${JSON.stringify(organizations)}`
3636
);
3737
setCurrentOrganizations(organizations);
38-
}, [organizations]);
38+
}, [organizations, setCurrentOrganizations]);
3939

4040
if (isLoading) return <div>Loading...</div>;
4141
if (isError) return <div>Error loading organizations</div>;
4242
if (!organizations?.length) return <div>No organizations found</div>;
4343

44-
console.debug(`organizations: ${JSON.stringify(organizations)}`);
45-
4644
return (
4745
<>
4846
{organizations.map((org) => (
@@ -61,7 +59,7 @@ export default function OrganizationSelector({
6159
}: OrganizationSelectorProps) {
6260
const {
6361
currentOrganizationId,
64-
setCurrentOrganizationId,
62+
setCurrentOrganization,
6563
getCurrentOrganization,
6664
} = useOrganizationStateStore((state) => state);
6765
const { resetCoachingSessionState } = useCoachingSessionStateStore(
@@ -72,7 +70,8 @@ export default function OrganizationSelector({
7270
);
7371

7472
const handleSetOrganization = (organizationId: Id) => {
75-
setCurrentOrganizationId(organizationId);
73+
const organization = getCurrentOrganization(organizationId);
74+
setCurrentOrganization(organization);
7675
console.info("Resetting Coaching Session State");
7776
resetCoachingRelationshipState();
7877
resetCoachingSessionState();

src/lib/api/entity-api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,8 +372,8 @@ export namespace EntityApi {
372372
api: {
373373
create: (entity: T) => Promise<U>;
374374
createNested: (id: Id, entity: T) => Promise<U>;
375-
update: (id: Id, entity: T) => Promise<T>;
376-
delete: (id: Id) => Promise<T>;
375+
update: (id: Id, entity: T) => Promise<U>;
376+
delete: (id: Id) => Promise<U>;
377377
}
378378
) => {
379379
const [isLoading, setIsLoading] = useState(false);

src/types/user.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ export interface User {
1111
}
1212

1313
export interface NewUser {
14-
email: string;
15-
password?: string;
1614
first_name: string;
1715
last_name: string;
1816
display_name: string;
17+
email: string;
18+
password: string;
1919
}
2020

2121
export function parseUser(data: unknown): User {

0 commit comments

Comments
 (0)