Skip to content

Commit 893bf65

Browse files
committed
refactor(services): rename coach references to coordinator and update related logic in actions, tests, and components
1 parent 818f2a3 commit 893bf65

15 files changed

Lines changed: 132 additions & 125 deletions

File tree

__tests__/services-actions.test.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ jest.mock("next/cache", () => ({
5454
updateTag: jest.fn(),
5555
}));
5656

57-
const COACH_A = "11111111-1111-1111-1111-111111111111";
58-
const COACH_B = "22222222-2222-2222-2222-222222222222";
57+
const COORDINATOR_A = "11111111-1111-1111-1111-111111111111";
58+
const COORDINATOR_B = "22222222-2222-2222-2222-222222222222";
5959
const SERVICE_ID = "33333333-3333-3333-3333-333333333333";
6060

6161
function fd(obj: Record<string, string>): FormData {
@@ -75,7 +75,7 @@ beforeEach(() => {
7575
});
7676

7777
describe("createService", () => {
78-
it("rejects a private lesson without a coach", async () => {
78+
it("rejects a private lesson without a coordinator", async () => {
7979
const result = await createService(
8080
null,
8181
fd({
@@ -87,12 +87,12 @@ describe("createService", () => {
8787
}),
8888
);
8989

90-
expect(result?.errors?.coach_id).toBeDefined();
90+
expect(result?.errors?.coordinator_id).toBeDefined();
9191
expect(insert).not.toHaveBeenCalled();
9292
expect(createProduct).not.toHaveBeenCalled();
9393
});
9494

95-
it("persists the coach when creating a private lesson", async () => {
95+
it("persists the coordinator when creating a private lesson", async () => {
9696
const result = await createService(
9797
null,
9898
fd({
@@ -101,17 +101,17 @@ describe("createService", () => {
101101
type: "private_lessons",
102102
duration_minutes: "60",
103103
price_cad: "50.00",
104-
coach_id: COACH_A,
104+
coordinator_id: COORDINATOR_A,
105105
}),
106106
);
107107

108108
expect(result).toEqual({ message: "Service created." });
109109
expect(insertValues).toHaveBeenCalledWith(
110-
expect.objectContaining({ type: "private_lessons", coachId: COACH_A }),
110+
expect.objectContaining({ type: "private_lessons", coordinatorId: COORDINATOR_A }),
111111
);
112112
});
113113

114-
it("creates a program with no coach", async () => {
114+
it("creates a program with no coordinator", async () => {
115115
const result = await createService(
116116
null,
117117
fd({
@@ -128,13 +128,13 @@ describe("createService", () => {
128128

129129
expect(result).toEqual({ message: "Service created." });
130130
expect(insertValues).toHaveBeenCalledWith(
131-
expect.objectContaining({ type: "programs", coachId: null }),
131+
expect.objectContaining({ type: "programs", coordinatorId: null }),
132132
);
133133
});
134134
});
135135

136136
describe("updateService", () => {
137-
it("reassigns the coach on a private lesson", async () => {
137+
it("reassigns the coordinator on a private lesson", async () => {
138138
selectLimit.mockResolvedValue([
139139
{
140140
id: SERVICE_ID,
@@ -146,12 +146,12 @@ describe("updateService", () => {
146146

147147
const result = await updateService(
148148
null,
149-
fd({ service_id: SERVICE_ID, coach_id: COACH_B }),
149+
fd({ service_id: SERVICE_ID, coordinator_id: COORDINATOR_B }),
150150
);
151151

152152
expect(result).toEqual({ message: "Service updated." });
153153
expect(updateSet).toHaveBeenCalledWith(
154-
expect.objectContaining({ coachId: COACH_B }),
154+
expect.objectContaining({ coordinatorId: COORDINATOR_B }),
155155
);
156156
});
157157

@@ -168,7 +168,7 @@ describe("updateService", () => {
168168

169169
const result = await updateService(
170170
null,
171-
fd({ service_id: SERVICE_ID, coach_id: COACH_B, price_cad: "50.00" }),
171+
fd({ service_id: SERVICE_ID, coordinator_id: COORDINATOR_B, price_cad: "50.00" }),
172172
);
173173

174174
expect(result).toEqual({ message: "Service updated." });
@@ -197,7 +197,7 @@ describe("updateService", () => {
197197
expect(createPrice).toHaveBeenCalledWith("prod_1", 7500);
198198
});
199199

200-
it("rejects clearing the coach on a private lesson", async () => {
200+
it("rejects clearing the coordinator on a private lesson", async () => {
201201
selectLimit.mockResolvedValue([
202202
{
203203
id: SERVICE_ID,
@@ -209,10 +209,10 @@ describe("updateService", () => {
209209

210210
const result = await updateService(
211211
null,
212-
fd({ service_id: SERVICE_ID, coach_id: "" }),
212+
fd({ service_id: SERVICE_ID, coordinator_id: "" }),
213213
);
214214

215-
expect(result?.errors?.coach_id).toBeDefined();
215+
expect(result?.errors?.coordinator_id).toBeDefined();
216216
expect(updateSet).not.toHaveBeenCalled();
217217
});
218218
});

__tests__/users-actions.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jest.mock("@/lib/stripe", () => ({
3737
grantComplimentarySubscription: jest.fn(),
3838
}));
3939

40-
const COACH_ID = "11111111-1111-1111-1111-111111111111";
40+
const COORDINATOR_ID = "11111111-1111-1111-1111-111111111111";
4141

4242
function fd(obj: Record<string, string>): FormData {
4343
const f = new FormData();
@@ -56,16 +56,16 @@ describe("deleteUserAdmin", () => {
5656
it("returns Unauthorized when the caller is not an admin", async () => {
5757
requireAdmin.mockRejectedValue(new Error("Forbidden"));
5858

59-
const result = await deleteUserAdmin(null, fd({ user_id: COACH_ID }));
59+
const result = await deleteUserAdmin(null, fd({ user_id: COORDINATOR_ID }));
6060

6161
expect(result).toEqual({ errors: { _form: ["Unauthorized"] } });
6262
expect(deleteUser).not.toHaveBeenCalled();
6363
});
6464

65-
it("blocks deletion when the coach is assigned to a private lesson", async () => {
65+
it("blocks deletion when the coordinator is assigned to a private lesson", async () => {
6666
selectLimit.mockResolvedValue([{ id: "svc-1" }]);
6767

68-
const result = await deleteUserAdmin(null, fd({ user_id: COACH_ID }));
68+
const result = await deleteUserAdmin(null, fd({ user_id: COORDINATOR_ID }));
6969

7070
expect(result?.errors?._form?.[0]).toMatch(/private lesson/i);
7171
expect(deleteUser).not.toHaveBeenCalled();
@@ -74,16 +74,16 @@ describe("deleteUserAdmin", () => {
7474
it("deletes the user when not assigned to any private lesson", async () => {
7575
selectLimit.mockResolvedValue([]);
7676

77-
const result = await deleteUserAdmin(null, fd({ user_id: COACH_ID }));
77+
const result = await deleteUserAdmin(null, fd({ user_id: COORDINATOR_ID }));
7878

7979
expect(result).toEqual({ message: "User deleted." });
80-
expect(deleteUser).toHaveBeenCalledWith(COACH_ID);
80+
expect(deleteUser).toHaveBeenCalledWith(COORDINATOR_ID);
8181
});
8282

8383
it("surfaces an auth error from Supabase", async () => {
8484
deleteUser.mockResolvedValue({ error: { message: "boom" } });
8585

86-
const result = await deleteUserAdmin(null, fd({ user_id: COACH_ID }));
86+
const result = await deleteUserAdmin(null, fd({ user_id: COORDINATOR_ID }));
8787

8888
expect(result).toEqual({ errors: { _form: ["boom"] } });
8989
});

app/(authenticated)/services/actions.ts

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -118,16 +118,18 @@ function parseProgramSchedule(formData: FormData): ParseResult<ProgramSchedule>
118118
};
119119
}
120120

121-
function parseCoachId(formData: FormData): ParseResult<string> {
122-
const raw = field(formData, "coach_id");
121+
function parseCoordinatorId(formData: FormData): ParseResult<string> {
122+
const raw = field(formData, "coordinator_id");
123123
if (!raw)
124124
return {
125125
ok: false,
126-
errors: { coach_id: ["A coach is required for private lessons"] },
126+
errors: {
127+
coordinator_id: ["A coordinator is required for private lessons"],
128+
},
127129
};
128130
const result = z.string().uuid().safeParse(raw);
129131
if (!result.success) {
130-
return { ok: false, errors: { coach_id: ["Invalid coach"] } };
132+
return { ok: false, errors: { coordinator_id: ["Invalid coordinator"] } };
131133
}
132134
return { ok: true, value: result.data };
133135
}
@@ -156,7 +158,7 @@ export async function createService(
156158
}
157159

158160
// Validate price format independently so its error reports alongside
159-
// schedule/coach errors instead of in a separate round-trip.
161+
// schedule/coordinator errors instead of in a separate round-trip.
160162
const priceRaw = formData.get("price_cad")?.toString() ?? "";
161163
let cents: number | null = null;
162164
if (priceRaw && !errors.price_cad) {
@@ -166,11 +168,11 @@ export async function createService(
166168
}
167169
}
168170

169-
// Schedule / coach checks key off the submitted type, not parsed.data,
171+
// Schedule / coordinator checks key off the submitted type, not parsed.data,
170172
// so they still run when baseFields fails on unrelated fields.
171173
const typeRaw = formData.get("type")?.toString();
172174
let scheduledAtValue: ProgramSchedule | null = null;
173-
let coachIdValue: string | null = null;
175+
let coordinatorIdValue: string | null = null;
174176
if (typeRaw === "programs") {
175177
const result = parseProgramSchedule(formData);
176178
if (!result.ok) {
@@ -179,9 +181,9 @@ export async function createService(
179181
scheduledAtValue = result.value;
180182
}
181183
} else if (typeRaw === "private_lessons") {
182-
const coach = parseCoachId(formData);
183-
if (!coach.ok) Object.assign(errors, coach.errors);
184-
else coachIdValue = coach.value;
184+
const coordinator = parseCoordinatorId(formData);
185+
if (!coordinator.ok) Object.assign(errors, coordinator.errors);
186+
else coordinatorIdValue = coordinator.value;
185187
}
186188

187189
if (Object.keys(errors).length > 0) {
@@ -210,7 +212,7 @@ export async function createService(
210212
slots: scheduledAtValue?.slots ?? null,
211213
durationMinutes: duration_minutes,
212214
stripeProductId: productId,
213-
coachId: coachIdValue,
215+
coordinatorId: coordinatorIdValue,
214216
status: "active",
215217
});
216218
} catch (e) {
@@ -306,20 +308,23 @@ export async function updateService(
306308
}
307309

308310
let scheduledAtValue: ProgramSchedule | undefined;
309-
let coachIdValue: string | undefined;
311+
let coordinatorIdValue: string | undefined;
310312
if (row.type === "programs" && formData.has("start_date")) {
311313
const result = parseProgramSchedule(formData);
312314
if (!result.ok) {
313315
Object.assign(errors, result.errors);
314316
} else {
315317
scheduledAtValue = result.value;
316318
}
317-
} else if (row.type === "private_lessons" && formData.has("coach_id")) {
318-
// Private lessons can be reassigned to a different coach, but the coach
319-
// remains mandatory: an empty/invalid value is rejected.
320-
const coach = parseCoachId(formData);
321-
if (!coach.ok) Object.assign(errors, coach.errors);
322-
else coachIdValue = coach.value;
319+
} else if (
320+
row.type === "private_lessons" &&
321+
formData.has("coordinator_id")
322+
) {
323+
// Private lessons can be reassigned to a different coordinator, but the
324+
// coordinator remains mandatory: an empty/invalid value is rejected.
325+
const coordinator = parseCoordinatorId(formData);
326+
if (!coordinator.ok) Object.assign(errors, coordinator.errors);
327+
else coordinatorIdValue = coordinator.value;
323328
}
324329

325330
if (Object.keys(errors).length > 0) {
@@ -350,13 +355,13 @@ export async function updateService(
350355

351356
const dbPatch: Partial<typeof services.$inferInsert> = {};
352357
if (duration_minutes !== undefined) dbPatch.durationMinutes = duration_minutes;
353-
if (coachIdValue !== undefined) dbPatch.coachId = coachIdValue;
358+
if (coordinatorIdValue !== undefined)
359+
dbPatch.coordinatorId = coordinatorIdValue;
354360
if (scheduledAtValue !== undefined) {
355361
dbPatch.startDate = scheduledAtValue.startDate;
356362
dbPatch.endDate = scheduledAtValue.endDate;
357363
dbPatch.slots = scheduledAtValue.slots;
358364
}
359-
if (coachIdValue !== undefined) dbPatch.coachId = coachIdValue;
360365

361366
if (Object.keys(dbPatch).length > 0) {
362367
dbPatch.updatedAt = new Date();
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import { listCoaches, listServices } from "./queries";
1+
import { listCoordinators, listServices } from "./queries";
22
import { ServicesTable } from "./services-table";
33

44
export default async function ServicesPage() {
5-
const [services, coaches] = await Promise.all([
5+
const [services, coordinators] = await Promise.all([
66
listServices(),
7-
listCoaches(),
7+
listCoordinators(),
88
]);
99

1010
return (
1111
<main className="flex min-h-screen flex-col gap-6 p-8">
1212
<h1 className="text-3xl font-bold">Services</h1>
13-
<ServicesTable services={services} coaches={coaches} />
13+
<ServicesTable services={services} coordinators={coordinators} />
1414
</main>
1515
);
1616
}

app/(authenticated)/services/queries.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const SERVICES_TAG = "services";
99

1010
const UUID_RE =
1111
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
12-
const COACHES_TAG = "coaches";
12+
const COORDINATORS_TAG = "coordinators";
1313

1414
export type ServiceStatus = "active" | "disabled" | "archived" | "deleted";
1515
export type ServiceType = "private_lessons" | "programs";
@@ -21,7 +21,7 @@ export type ServiceView = {
2121
durationMinutes: number;
2222
status: ServiceStatus;
2323
stripeProductId: string;
24-
coachId: string | null;
24+
coordinatorId: string | null;
2525
createdAt: Date;
2626
updatedAt: Date;
2727
title: string | null;
@@ -52,7 +52,7 @@ async function buildServiceView(
5252
durationMinutes: row.durationMinutes,
5353
status: row.status,
5454
stripeProductId: row.stripeProductId,
55-
coachId: row.coachId,
55+
coordinatorId: row.coordinatorId,
5656
createdAt: row.createdAt,
5757
updatedAt: row.updatedAt,
5858
title: stripeData?.title ?? null,
@@ -111,21 +111,21 @@ export async function getService(id: string): Promise<ServiceView | null> {
111111
return buildServiceView(row);
112112
}
113113

114-
export type CoachOption = {
114+
export type CoordinatorOption = {
115115
id: string;
116116
firstName: string;
117117
lastName: string;
118118
};
119119

120120
/**
121-
* List all profiles with the `coach` role, ordered by name.
121+
* List all profiles with the `coordinator` role, ordered by name.
122122
*
123-
* Cached via Next Cache Components; bust via the `coaches` tag when
124-
* coach assignments change.
123+
* Cached via Next Cache Components; bust via the `coordinators` tag when
124+
* coordinator assignments change.
125125
*/
126-
export async function listCoaches(): Promise<CoachOption[]> {
126+
export async function listCoordinators(): Promise<CoordinatorOption[]> {
127127
"use cache";
128-
cacheTag(COACHES_TAG);
128+
cacheTag(COORDINATORS_TAG);
129129

130130
return db
131131
.select({
@@ -134,6 +134,6 @@ export async function listCoaches(): Promise<CoachOption[]> {
134134
lastName: profiles.lastName,
135135
})
136136
.from(profiles)
137-
.where(eq(profiles.role, "coach"))
137+
.where(eq(profiles.role, "coordinator"))
138138
.orderBy(asc(profiles.firstName), asc(profiles.lastName));
139139
}

0 commit comments

Comments
 (0)