Skip to content

feat(interview): prevent interviewer double-booking and scheduling co…#326

Merged
Sachinchaurasiya360 merged 1 commit into
Sachinchaurasiya360:mainfrom
sanjaysaini383:feat/interview-conflict-prevention
May 21, 2026
Merged

feat(interview): prevent interviewer double-booking and scheduling co…#326
Sachinchaurasiya360 merged 1 commit into
Sachinchaurasiya360:mainfrom
sanjaysaini383:feat/interview-conflict-prevention

Conversation

@sanjaysaini383
Copy link
Copy Markdown
Contributor

@sanjaysaini383 sanjaysaini383 commented May 18, 2026

Description

This PR addresses the issue #314 where recruiters could accidentally double-book an interviewer for two overlapping interviews, resulting in scheduling conflicts that had to be resolved manually.

Changes Made

Backend:

  • interview.service.ts: Added a checkSchedulingConflict helper method. Before an interview is created or updated, the system fetches any existing interviews on the same day for the requested interviewerIds. It performs an in-memory check to ensure the new [scheduledAt, scheduledAt + durationMinutes] timeframe does not overlap with any active schedules.
  • interview.controller.ts: Updated the create and update endpoints to catch the specific conflict error message and return a standard 409 Conflict HTTP status code instead of a generic 500 Internal Server Error.

Frontend:

  • InterviewsPage.tsx:
    • Added an "Interviewer IDs" field to the interview scheduling form, allowing recruiters to assign interviewers directly via comma-separated IDs.
    • Implemented error handling within the createMutation. If the API responds with a 409 Conflict, the UI gracefully alerts the recruiter via react-hot-toast with a clear message: "Scheduling conflict: One or more interviewers are already booked at this time."

How to Test

  1. Log in as a Recruiter and navigate to the HR Dashboard -> Interviews.
  2. Schedule a new interview, assigning it to Interviewer ID: 1 at 10:00 AM with a duration of 60 minutes.
  3. Attempt to schedule a second interview and assign it to Interviewer ID: 1 at 10:30 AM.
  4. Observe the 409 Conflict API response and verify that the red Toast notification appears in the UI preventing the action.

Checklist

  • Tested locally against Postgres and client
  • Code follows CONTRIBUTING.md conventions
  • No breaking changes to existing database schema or endpoints

Summary by CodeRabbit

Release Notes

  • New Features

    • Schedule interviews with multiple interviewers in a single booking
    • Automatic conflict detection prevents scheduling interviewers during overlapping time slots
  • Improvements

    • Enhanced error notifications for scheduling conflicts and other scheduling issues

Review Change Stack

@sanjaysaini383
Copy link
Copy Markdown
Contributor Author

Hi @Sachinchaurasiya360
label this PR as gssoc26:approved level:Intermediate and assign it to me .

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

📝 Walkthrough

Walkthrough

This PR adds multi-interviewer interview scheduling with real-time conflict detection across the backend and client. The service layer validates scheduling conflicts by comparing time windows; the controller maps that error to HTTP 409; and the client form accepts comma-separated interviewer IDs with appropriate success and error notifications.

Changes

Multi-Interviewer Scheduling Conflict Detection

Layer / File(s) Summary
Service-level conflict validation
server/src/module/interview/interview.service.ts
Introduces checkSchedulingConflict() helper that queries existing interviews for the same day and relevant interviewers, excludes non-actionable statuses, and throws on time window overlap. Both create() and update() now invoke this check with effective interviewer IDs, duration, and (for update) the current interview ID exclusion before persisting.
HTTP 409 error response
server/src/module/interview/interview.controller.ts
create() and update() error handlers now differentiate the scheduling conflict error message and return HTTP 409 with the error details instead of a generic 500.
Client form submission and error handling
client/src/module/recruiter/hr/InterviewsPage.tsx
Form adds interviewerIds state field; submission parses comma-separated values into a filtered numeric array and includes it in the POST body; success path resets form and shows success toast; HTTP 409 errors trigger a conflict-specific toast, while other errors show a generic failure toast. UI includes a new "Interviewer IDs" text input in the create modal with comma-separated hint.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related issues

  • #314: This PR directly implements the backend scheduling conflict detection in InterviewService and the corresponding HTTP 409 response in the controller, along with frontend form and error-toast handling, which aligns with the proposed changes in that issue.

Poem

🐰 Multiple interviewers, schedules aligned,
No more double-booked from an oversight blind!
Conflict detection keeps calendars clean,
Toast notifications make status well-seen,
Harmony in hiring, a schedule refined! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: preventing interviewer double-booking and scheduling conflicts, which aligns with the PR's core objective across all three modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
client/src/module/recruiter/hr/InterviewsPage.tsx (1)

700-711: ⚡ Quick win

Use a standard text size token for the new field copy.

The new label/help text introduces text-[10px]. Please switch to the closest standard scale class instead of another arbitrary size.

As per coding guidelines, client/src/**/*.{tsx,ts}: Do not use arbitrary bracket sizes like \text-[17px]`, use standard scale classes instead`.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/module/recruiter/hr/InterviewsPage.tsx` around lines 700 - 711,
The label and help text in InterviewsPage.tsx use an arbitrary size class
`text-[10px]`; replace those with the nearest standard Tailwind scale (e.g.,
`text-xs`) to comply with the project rule against arbitrary bracket
sizes—update the label class on the element that currently contains
`text-[10px]` and the help paragraph that follows (both related to the
`interviewerIds` input and the `form.interviewerIds` state) so they use
`text-xs` (or another approved scale token) instead of `text-[10px]`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/src/module/recruiter/hr/InterviewsPage.tsx`:
- Around line 119-131: The current parsing of form.interviewerIds into parsedIds
silently drops non-numeric tokens (e.g. "12, abc" -> [12]) which can submit
fewer interviewers or an empty array and bypass checks; update the submit flow
that builds parsedIds to validate each comma-separated token from
form.interviewerIds and if any token is not a valid integer or the resulting
parsedIds is empty, reject the submit (return/throw and surface a UI error)
instead of filtering them out; update the code paths around parsedIds and the
api.post("/hr/interviews") call so you only call api.post when all tokens are
valid and include validation error messages for the user explaining which tokens
are invalid.

In `@server/src/module/interview/interview.service.ts`:
- Around line 33-37: The read-then-write TOCTOU in interview.service.ts (the
call to checkSchedulingConflict(...) before saving) must be made atomic: wrap
the conflict check and the subsequent insert/update for the interviewer slots
inside a single DB transaction and lock the affected interviewer rows/slots
(e.g. SELECT ... FOR UPDATE or use SERIALIZABLE/isolation-level locking provided
by your ORM) so concurrent requests cannot both pass the check; apply the same
pattern in the update(...) path as well (acquire locks for data.interviewerIds,
perform the conflict check inside the transaction, then perform the write and
commit).
- Around line 182-190: The current day-bounded conflict query (in
InterviewService where potentialConflicts is built using
prisma.interview.findMany) misses interviews that start before midnight and end
after it; instead compute the proposed slot's start and end
(proposedStart/proposedEnd) and query for any interviews with overlapping
intervals for the same interviewers by replacing the date-range filter with an
overlap filter: interviewerIds: { hasSome: interviewerIds } and
(existingInterviewEnd > proposedStart AND existingInterviewStart < proposedEnd)
— use the actual fields in your model (scheduledAt and scheduledEnd or
scheduledAt + duration) when constructing the prisma.where clause so
cross-midnight overlaps are caught.
- Around line 115-120: In update(), avoid re-running scheduling validation when
the interview's effective new status becomes an inactive state (CANCELLED,
COMPLETED, NO_SHOW): compute the effective status by merging incoming update
fields with the existing interview.status, and only call
this.checkSchedulingConflict(newInterviewerIds, newScheduledAt, newDuration,
interview.id) when that effective status is not one of the inactive statuses;
i.e., add a guard around the checkSchedulingConflict call that skips it for
CANCELLED/COMPLETED/NO_SHOW.

---

Nitpick comments:
In `@client/src/module/recruiter/hr/InterviewsPage.tsx`:
- Around line 700-711: The label and help text in InterviewsPage.tsx use an
arbitrary size class `text-[10px]`; replace those with the nearest standard
Tailwind scale (e.g., `text-xs`) to comply with the project rule against
arbitrary bracket sizes—update the label class on the element that currently
contains `text-[10px]` and the help paragraph that follows (both related to the
`interviewerIds` input and the `form.interviewerIds` state) so they use
`text-xs` (or another approved scale token) instead of `text-[10px]`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dfe67cd3-f11d-4d5f-9967-96e6f42d3ce4

📥 Commits

Reviewing files that changed from the base of the PR and between 3552efa and 5c74495.

📒 Files selected for processing (3)
  • client/src/module/recruiter/hr/InterviewsPage.tsx
  • server/src/module/interview/interview.controller.ts
  • server/src/module/interview/interview.service.ts

Comment on lines +119 to 131
const parsedIds = form.interviewerIds
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0)
.map(Number)
.filter((n) => !isNaN(n));

await api.post("/hr/interviews", {
...form,
applicationId: Number(form.applicationId),
durationMinutes: Number(form.durationMinutes),
interviewerIds: parsedIds,
});
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't silently drop invalid interviewer IDs.

"12, abc" becomes [12], and "abc" becomes [], so this can submit fewer interviewers than the user entered and even bypass conflict checking entirely when the parsed array ends up empty. Reject invalid tokens instead of filtering them out.

💡 One way to validate before submit
-      const parsedIds = form.interviewerIds
-        .split(",")
-        .map((s) => s.trim())
-        .filter((s) => s.length > 0)
-        .map(Number)
-        .filter((n) => !isNaN(n));
+      const rawIds = form.interviewerIds
+        .split(",")
+        .map((s) => s.trim())
+        .filter((s) => s.length > 0);
+
+      if (rawIds.some((id) => !/^\d+$/.test(id))) {
+        throw new Error("Please enter only numeric interviewer IDs.");
+      }
+
+      const parsedIds = rawIds.map(Number);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const parsedIds = form.interviewerIds
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0)
.map(Number)
.filter((n) => !isNaN(n));
await api.post("/hr/interviews", {
...form,
applicationId: Number(form.applicationId),
durationMinutes: Number(form.durationMinutes),
interviewerIds: parsedIds,
});
const rawIds = form.interviewerIds
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
if (rawIds.some((id) => !/^\d+$/.test(id))) {
throw new Error("Please enter only numeric interviewer IDs.");
}
const parsedIds = rawIds.map(Number);
await api.post("/hr/interviews", {
...form,
applicationId: Number(form.applicationId),
durationMinutes: Number(form.durationMinutes),
interviewerIds: parsedIds,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/module/recruiter/hr/InterviewsPage.tsx` around lines 119 - 131,
The current parsing of form.interviewerIds into parsedIds silently drops
non-numeric tokens (e.g. "12, abc" -> [12]) which can submit fewer interviewers
or an empty array and bypass checks; update the submit flow that builds
parsedIds to validate each comma-separated token from form.interviewerIds and if
any token is not a valid integer or the resulting parsedIds is empty, reject the
submit (return/throw and surface a UI error) instead of filtering them out;
update the code paths around parsedIds and the api.post("/hr/interviews") call
so you only call api.post when all tokens are valid and include validation error
messages for the user explaining which tokens are invalid.

Comment on lines +33 to +37
await this.checkSchedulingConflict(
data.interviewerIds ?? [],
new Date(data.scheduledAt),
data.durationMinutes ?? 60
);
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.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Make the conflict check and write atomic.

This is still a TOCTOU flow: two concurrent requests can both pass checkSchedulingConflict() and then both insert/update the same interviewer slot. The same read-then-write gap exists in update(), so the double-booking guarantee is not reliable under concurrent traffic. Use a transaction with DB-level serialization/locking for the affected interviewer IDs before the final write.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/module/interview/interview.service.ts` around lines 33 - 37, The
read-then-write TOCTOU in interview.service.ts (the call to
checkSchedulingConflict(...) before saving) must be made atomic: wrap the
conflict check and the subsequent insert/update for the interviewer slots inside
a single DB transaction and lock the affected interviewer rows/slots (e.g.
SELECT ... FOR UPDATE or use SERIALIZABLE/isolation-level locking provided by
your ORM) so concurrent requests cannot both pass the check; apply the same
pattern in the update(...) path as well (acquire locks for data.interviewerIds,
perform the conflict check inside the transaction, then perform the write and
commit).

Comment on lines +115 to +120
await this.checkSchedulingConflict(
newInterviewerIds,
newScheduledAt,
newDuration,
interview.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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Skip conflict validation when the effective new status is inactive.

update() re-runs scheduling validation even when this change is turning the interview into CANCELLED, COMPLETED, or NO_SHOW. That can block cancelling or closing legacy/conflicting records even though the resulting state is no longer schedulable.

💡 Narrow fix
+    const newStatus = data.status ?? interview.status;
     const newScheduledAt = (updateData["scheduledAt"] as Date) ?? interview.scheduledAt;
     const newDuration = data.durationMinutes ?? interview.durationMinutes;
     const newInterviewerIds = data.interviewerIds ?? interview.interviewerIds;
 
-    await this.checkSchedulingConflict(
-      newInterviewerIds,
-      newScheduledAt,
-      newDuration,
-      interview.id
-    );
+    if (!["CANCELLED", "COMPLETED", "NO_SHOW"].includes(newStatus)) {
+      await this.checkSchedulingConflict(
+        newInterviewerIds,
+        newScheduledAt,
+        newDuration,
+        interview.id
+      );
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await this.checkSchedulingConflict(
newInterviewerIds,
newScheduledAt,
newDuration,
interview.id
);
const newStatus = data.status ?? interview.status;
const newScheduledAt = (updateData["scheduledAt"] as Date) ?? interview.scheduledAt;
const newDuration = data.durationMinutes ?? interview.durationMinutes;
const newInterviewerIds = data.interviewerIds ?? interview.interviewerIds;
if (!["CANCELLED", "COMPLETED", "NO_SHOW"].includes(newStatus)) {
await this.checkSchedulingConflict(
newInterviewerIds,
newScheduledAt,
newDuration,
interview.id
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/module/interview/interview.service.ts` around lines 115 - 120, In
update(), avoid re-running scheduling validation when the interview's effective
new status becomes an inactive state (CANCELLED, COMPLETED, NO_SHOW): compute
the effective status by merging incoming update fields with the existing
interview.status, and only call this.checkSchedulingConflict(newInterviewerIds,
newScheduledAt, newDuration, interview.id) when that effective status is not one
of the inactive statuses; i.e., add a guard around the checkSchedulingConflict
call that skips it for CANCELLED/COMPLETED/NO_SHOW.

Comment on lines +182 to +190
const startOfDay = new Date(scheduledAt);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(scheduledAt);
endOfDay.setHours(23, 59, 59, 999);

const potentialConflicts = await prisma.interview.findMany({
where: {
interviewerIds: { hasSome: interviewerIds },
scheduledAt: { gte: startOfDay, lte: endOfDay },
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

The day-bounded lookup misses overlaps that cross midnight.

This only loads interviews whose scheduledAt starts on the same calendar day as the proposed slot. An interview starting at 11:30 PM and running past midnight will not be seen when scheduling another interview at 12:15 AM the next day, even though the intervals overlap.

Also applies to: 199-205

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/module/interview/interview.service.ts` around lines 182 - 190, The
current day-bounded conflict query (in InterviewService where potentialConflicts
is built using prisma.interview.findMany) misses interviews that start before
midnight and end after it; instead compute the proposed slot's start and end
(proposedStart/proposedEnd) and query for any interviews with overlapping
intervals for the same interviewers by replacing the date-range filter with an
overlap filter: interviewerIds: { hasSome: interviewerIds } and
(existingInterviewEnd > proposedStart AND existingInterviewStart < proposedEnd)
— use the actual fields in your model (scheduledAt and scheduledEnd or
scheduledAt + duration) when constructing the prisma.where clause so
cross-midnight overlaps are caught.

@Sachinchaurasiya360 Sachinchaurasiya360 added good first issue Good for newcomers gssoc: intermediate gssoc:approved Approved for GSSoC scoring gssoc:ai-slop Low-effort AI-generated contribution labels May 21, 2026
@Sachinchaurasiya360 Sachinchaurasiya360 merged commit 08214d7 into Sachinchaurasiya360:main May 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

good first issue Good for newcomers gssoc:ai-slop Low-effort AI-generated contribution gssoc:approved Approved for GSSoC scoring gssoc: intermediate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants