Skip to content

created scheduling actions#59

Merged
Berny-ft merged 4 commits intodevelopfrom
feature/47-scheduling-actions
May 5, 2026
Merged

created scheduling actions#59
Berny-ft merged 4 commits intodevelopfrom
feature/47-scheduling-actions

Conversation

@Berny-ft
Copy link
Copy Markdown
Contributor

@Berny-ft Berny-ft commented May 2, 2026

Closes #47

Overview

selectAvailabilities — Sets available time slots ([{start, end}]) on an existing coaching_sessions row.
selectTimeSlot — Picks one of those slots, setting scheduled_at and confirming the session.

Testing

Manual testing: Used Drizzle Studio (pnpm db:studio) to create a test coaching session, verified that selected_time_slots, scheduled_at, and status columns update correctly through the full scheduling flow.

Unit testing: Wrote 18 Jest tests covering auth rejection, input validation (Zod), authorization checks, and success paths for both coach and user roles. All passing.

Checklist

  • Code is neat, readable, and works
  • Code is commented where appropriate and well-documented
  • Commit messages follow our guidelines
  • Issue number is linked
  • Branch is linked
  • Reviewers are assigned (one of your tech leads)

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds server actions to support scheduling flows for existing coaching_sessions rows: setting available time slots and confirming a chosen slot, with auth + authorization checks and Zod validation.

Changes:

  • Introduces selectAvailabilities to store/replace selected_time_slots for a session.
  • Introduces selectTimeSlot to confirm a chosen slot by setting scheduled_at and status = confirmed.
  • Adds shared helpers/schemas for auth gating and session authorization.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/scheduling/actions.ts
Comment on lines +214 to +218
.set({
scheduledAt: new Date(start),
status: "confirmed",
updatedAt: new Date(),
})
Comment thread app/scheduling/actions.ts
await db
.update(coachingSessions)
.set({
selectedTimeSlots: timeSlots,
Comment thread app/scheduling/actions.ts
Comment on lines +88 to +91
export async function selectAvailabilities(
_prev: SchedulingActionState,
formData: FormData,
): Promise<SchedulingActionState> {
Comment thread app/scheduling/actions.ts
Comment on lines +156 to +160
export async function selectTimeSlot(
_prev: SchedulingActionState,
formData: FormData,
): Promise<SchedulingActionState> {
// 1. Auth gate
Copy link
Copy Markdown
Contributor

@martin0024 martin0024 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good job!

Comment thread app/scheduling/actions.ts Outdated
message?: string;
} | null;

const iso8601 = z.string().datetime({ message: "Must be an ISO 8601 datetime" });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const iso8601 = z.string().datetime({ message: "Must be an ISO 8601 datetime" });
const iso8601 = z
.string()
.datetime({ offset: true, message: "Must be an ISO 8601 datetime with timezone" });

Comment thread app/scheduling/actions.ts
Comment on lines +27 to +32
const selectAvailabilitiesSchema = z.object({
session_id: z.string().uuid("Invalid session ID"),
time_slots: z
.array(timeSlotSchema)
.min(1, "At least one time slot is required"),
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const selectAvailabilitiesSchema = z.object({
session_id: z.string().uuid("Invalid session ID"),
time_slots: z
.array(timeSlotSchema)
.min(1, "At least one time slot is required"),
});
const selectAvailabilitiesSchema = z.object({
session_id: z.string().uuid("Invalid session ID"),
time_slots: z
.array(timeSlotSchema)
.min(1, "At least one time slot is required")
.max(20, "You can select up to 20 time slots"),
});

add a limit

Comment thread app/scheduling/actions.ts Outdated
let sessionId: string;
let timeSlots: TimeSlot[];
try {
const rawSlots = formData.get("time_slots")?.toString() ?? "";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would isolate the parsing.

Suggested change
const rawSlots = formData.get("time_slots")?.toString() ?? "";
function parseJsonField<T>(formData: FormData, key: string): T | null {
const value = formData.get(key);
if (typeof value !== "string") {
return null;
}
try {
return JSON.parse(value) as T;
} catch {
return null;
}
}
const rawTimeSlots = parseJsonField<TimeSlot[]>(formData, "time_slots");
if (!rawTimeSlots) {
return { errors: { time_slots: ["Invalid time slots format"] } };
}

Comment thread app/scheduling/actions.ts Outdated
}

sessionId = parsed.data.session_id;
timeSlots = parsed.data.time_slots as TimeSlot[];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
timeSlots = parsed.data.time_slots as TimeSlot[];
timeSlots = parsed.data.time_slots.map(normalizeSlot);
function normalizeSlot(slot: TimeSlot): TimeSlot {
  return {
    start: new Date(slot.start).toISOString(),
    end: new Date(slot.end).toISOString(),
  };
}
const normalizedStart = new Date(start).toISOString();
const normalizedEnd = new Date(end).toISOString();

normalize the slots before storing, if you store "2026-05-03T10:00:00+02:00" and then later compare against "2026-05-03T08:00:00.000Z" then s.start === start && s.end === end this will fail?

Comment thread app/scheduling/actions.ts Outdated
Comment on lines +230 to +237
await db
.update(coachingSessions)
.set({
scheduledAt: new Date(start),
status: "confirmed",
updatedAt: new Date(),
})
.where(eq(coachingSessions.id, session_id));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

race condition issue between the SELECT and the UPDATE another request could confirm the session.

Suggested change
await db
.update(coachingSessions)
.set({
scheduledAt: new Date(start),
status: "confirmed",
updatedAt: new Date(),
})
.where(eq(coachingSessions.id, session_id));
const updatedRows = await db
.update(coachingSessions)
.set({
scheduledAt: new Date(normalizedStart),
status: "confirmed",
updatedAt: new Date(),
})
.where(
and(
eq(coachingSessions.id, session_id),
eq(coachingSessions.status, "pending"),
),
)
.returning({ id: coachingSessions.id });
if (updatedRows.length === 0) {
return {
errors: {
_form: ["This session is no longer available for confirmation"],
},
};
}

Comment thread app/scheduling/actions.ts
* Both the assigned coach and the session user can call this action.
* The payload is an array of `{ start, end }` ISO 8601 strings.
*/
export async function selectAvailabilities(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

selectAvailabilities -> coach only
selectTimeSlot -> user only

? if so we can add a helper.

function isCoach(session: typeof coachingSessions.$inferSelect, userId: string) {
  return session.coachId === userId;
}

function isSessionUser(session: typeof coachingSessions.$inferSelect, userId: string) {
  return session.userId === userId;
}

if (!isCoach(result.session, callerId)) {
  return {
    errors: {
      _form: ["Only the coach can set availabilities"],
    },
  };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, both have to be callable by both roles. This is because we might change the flow to allow both parties to have a back and forth where they tell each other's availabilities.

Comment thread app/scheduling/actions.ts
Comment on lines +145 to +151
errors: {
_form: [
e instanceof Error
? e.message
: "Could not update availabilities",
],
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid sending e.message to the client it could leak sql schema details.

Suggested change
errors: {
_form: [
e instanceof Error
? e.message
: "Could not update availabilities",
],
},
console.error("[SELECT_AVAILABILITIES]", e);
return {
errors: {
_form: ["Could not update availabilities"],
},
};

Comment thread app/scheduling/actions.ts Outdated
time_slots: z
.array(timeSlotSchema)
.min(1, "At least one time slot is required")
.max(20,"You can select up to 20 time slots")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

20 is too low, make it 50

Comment thread app/scheduling/actions.ts
* Both the assigned coach and the session user can call this action.
* The payload is an array of `{ start, end }` ISO 8601 strings.
*/
export async function selectAvailabilities(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, both have to be callable by both roles. This is because we might change the flow to allow both parties to have a back and forth where they tell each other's availabilities.

Copy link
Copy Markdown
Contributor

@RenaudBernier RenaudBernier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work Berny

@Berny-ft Berny-ft merged commit 0363f8d into develop May 5, 2026
1 check passed
@Berny-ft Berny-ft deleted the feature/47-scheduling-actions branch May 5, 2026 19:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants