Skip to content

feat: update roster to add shifts in holidays and leaves#4123

Open
sarathibalamurugan wants to merge 1 commit intofrappe:developfrom
aerele:feat/issue-3615
Open

feat: update roster to add shifts in holidays and leaves#4123
sarathibalamurugan wants to merge 1 commit intofrappe:developfrom
aerele:feat/issue-3615

Conversation

@sarathibalamurugan
Copy link
Contributor

@sarathibalamurugan sarathibalamurugan commented Feb 12, 2026

Description:
Currently in Roster we can't assign shifts in holidays and leave. Now we can assign shifts in holidays and leaves seamlessly.

ref: 49986

Before:

RosterBefore.webm

After:

RosterAfter.webm

Backport needed for: v15, v16

Summary by CodeRabbit

  • Bug Fixes

    • Day cells now block actions only when there are no shifts, improving drag-and-drop and interaction reliability.
  • Refactor

    • Unified rendering of holidays, leaves, and shifts into a single per-day container for consistent visuals.
    • Event mapping rewritten so per-day holiday, leave, and multiple shifts are initialized, collected, sorted, and displayed reliably with safer null-checks and fallbacks.

@sarathibalamurugan sarathibalamurugan marked this pull request as ready for review February 12, 2026 10:27
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

MonthViewTable.vue was refactored to use per-date DateEvents objects { holiday?, leave?, shift?[] } stored on mappedEvents[employee][key]. mapEventsToDates initializes these objects and assigns holiday/leave fields; shifts are ensured into a .shift array via handleShifts, pushed, and sorted. The isHolidayOrLeave helper was removed and drag/drop blocking now only checks for absence of shifts. Rendering consolidated into a single day-cell container reading events.data?.[employee]?.[day.date].

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: enabling shifts to be added during holidays and leaves, which is the primary feature objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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


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
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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
roster/src/components/MonthViewTable.vue (3)

373-380: ⚠️ Potential issue | 🔴 Critical

Bug: hasSameShift checks the wrong data path after the refactor.

After the restructure, events.data[employee][day] is an object { holiday?, leave?, shift? }, not an array. Array.isArray(...) will always return false, so hasSameShift will never detect a duplicate shift — meaning drag-and-drop onto a cell with an identical shift will no longer be blocked.

🐛 Proposed fix
 const hasSameShift = (employee: string, day: string) =>
-	Array.isArray(events.data?.[employee]?.[day]) &&
-	events.data?.[employee]?.[day].some(
+	Array.isArray(events.data?.[employee]?.[day]?.shift) &&
+	events.data?.[employee]?.[day].shift.some(
 		(shift: Shift) =>
 			shift.shift_type === hoveredCell.value.shift_type &&
 			shift.shift_location === hoveredCell.value.shift_location &&
 			shift.status === hoveredCell.value.shift_status,
 	);

502-507: ⚠️ Potential issue | 🔴 Critical

Bug: sortShiftsByStartTime operates on the date entry object instead of the .shift array.

Same root cause as hasSameShift: after the refactor, mappedEvents[employee][key] is { holiday?, leave?, shift? }. The Array.isArray check always fails, so shifts are never sorted.

🐛 Proposed fix
 const sortShiftsByStartTime = (mappedEvents: MappedEvents, employee: string, key: string) => {
-	if (Array.isArray(mappedEvents[employee][key]))
-		mappedEvents[employee][key].sort((a: Shift, b: Shift) =>
+	const entry = mappedEvents[employee][key];
+	if (entry && typeof entry === "object" && "shift" in entry && Array.isArray(entry.shift))
+		entry.shift.sort((a: Shift, b: Shift) =>
 			a.start_time.localeCompare(b.start_time),
 		);
 };

316-316: 🛠️ Refactor suggestion | 🟠 Major

MappedEvents type no longer matches the actual runtime structure.

The type is Record<string, Record<string, Holiday | Leave | Shift[]>>, but after the refactor each date entry is an object with optional holiday, leave, and shift keys. This type mismatch suppresses useful compiler checks and is the likely reason the two bugs above weren't caught.

♻️ Suggested type update
-type MappedEvents = Record<string, Record<string, Holiday | Leave | Shift[]>>;
+interface DateEvents {
+	holiday?: Holiday;
+	leave?: Leave;
+	shift?: Shift[];
+}
+type MappedEvents = Record<string, Record<string, DateEvents>>;

@sarathibalamurugan sarathibalamurugan marked this pull request as draft February 12, 2026 10:31
@sarathibalamurugan sarathibalamurugan marked this pull request as ready for review February 12, 2026 11:08
Copy link
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: 1

🤖 Fix all issues with AI agents
In `@roster/src/components/MonthViewTable.vue`:
- Around line 108-115: The template uses v-html to render
events.data[employee.name][day.date].holiday.description which opens an XSS
vector; replace the v-html usage in MonthViewTable.vue with safe text
interpolation and conditional rendering: remove v-html and render
holiday.description inside {{ }} (or a sanitized text helper) and render the
weekly_off marker as a separate conditional element (e.g., a <strong>WO</strong>
shown via v-if/v-else) so no raw HTML from holiday.description is injected;
ensure you reference the same keys (events.data, employee.name, day.date,
holiday.description, weekly_off) when updating the template and, if HTML must be
allowed, run the description through a sanitizer utility before rendering.
🧹 Nitpick comments (1)
roster/src/components/MonthViewTable.vue (1)

118-124: Leave indicator is suppressed when a holiday is also present on the same date.

Using v-else-if means a date with both a holiday and a leave only shows the holiday badge. If this is intentional (holiday takes precedence), it's fine — just calling it out for confirmation.

Copy link
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.

🧹 Nitpick comments (1)
roster/src/components/MonthViewTable.vue (1)

507-511: Overly defensive guards in sortShiftsByStartTime.

Given that mappedEvents[employee][key] is always initialized as {} on line 443, the entry && typeof entry === "object" checks are redundant. The Array.isArray(entry.shift) check alone suffices. This is a minor nit.

Simplified guard
 const sortShiftsByStartTime = (mappedEvents: MappedEvents, employee: string, key: string) => {
-	const entry = mappedEvents[employee][key];
-	if (entry && typeof entry === "object" && "shift" in entry && Array.isArray(entry.shift))
-		entry.shift.sort((a: Shift, b: Shift) => a.start_time.localeCompare(b.start_time));
+	const shifts = mappedEvents[employee][key]?.shift;
+	if (Array.isArray(shifts))
+		shifts.sort((a: Shift, b: Shift) => a.start_time.localeCompare(b.start_time));
 };

Copy link
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: 1

🤖 Fix all issues with AI agents
In `@roster/src/components/MonthViewTable.vue`:
- Around line 93-99: The swap_shift handler currently allows assigning shifts on
dates that may be holidays or in an employee's approved leave; update swap_shift
to validate against those business rules before performing any operation and to
return a clear error when conflicts exist. Specifically, before calling
break_shift or insert_shift, query the employee's holiday calendar and any
approved leave/applications for the target date(s) and if any match, abort and
return a 4xx error (with a message like "cannot assign shift during
holiday/approved leave"); ensure break_shift and insert_shift are not executed
when a conflict is detected and add corresponding unit/error-path handling so
callers receive the rejection.
🧹 Nitpick comments (2)
roster/src/components/MonthViewTable.vue (2)

378-385: Inconsistent optional chaining in hasSameShift.

Line 379 uses ?.[day]?.shift but Line 380 drops to ?.[day].shift.some(...). Safe due to && short-circuiting, but inconsistent and fragile if the expression is ever refactored (e.g., extracted or reordered).

Suggested fix
 const hasSameShift = (employee: string, day: string) =>
 	Array.isArray(events.data?.[employee]?.[day]?.shift) &&
-	events.data?.[employee]?.[day].shift.some(
+	events.data?.[employee]?.[day]?.shift?.some(
 		(shift: Shift) =>
 			shift.shift_type === hoveredCell.value.shift_type &&
 			shift.shift_location === hoveredCell.value.shift_location &&
 			shift.status === hoveredCell.value.shift_status,
 	);

507-511: Overly defensive guard in sortShiftsByStartTime.

Given mappedEvents[employee][key] is always initialized as {} on Line 443 and .shift is only ever set as an array, the typeof entry === "object" and "shift" in entry checks are redundant. The Array.isArray check alone suffices.

Simplified version
 const sortShiftsByStartTime = (mappedEvents: MappedEvents, employee: string, key: string) => {
-	const entry = mappedEvents[employee][key];
-	if (entry && typeof entry === "object" && "shift" in entry && Array.isArray(entry.shift))
-		entry.shift.sort((a: Shift, b: Shift) => a.start_time.localeCompare(b.start_time));
+	const shifts = mappedEvents[employee][key]?.shift;
+	if (Array.isArray(shifts))
+		shifts.sort((a: Shift, b: Shift) => a.start_time.localeCompare(b.start_time));
 };

Copy link
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: 1

🧹 Nitpick comments (1)
roster/src/components/MonthViewTable.vue (1)

444-444: Object.values() called on an array — use direct iteration instead.

data[employee] is typed as (HolidayWithDate | LeaveApplication | ShiftAssignment)[]. Wrapping it in Object.values() works (returns the same elements for dense arrays) but is unidiomatic, adds a needless allocation, and loses direct array-type inference.

♻️ Proposed refactor
-		for (const event of Object.values(data[employee])) {
+		for (const event of data[employee]) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@roster/src/components/MonthViewTable.vue` at line 444, The loop currently
iterates with Object.values(data[employee]) which is unnecessary because
data[employee] is already an array; change the iteration to iterate directly
over the array (use data[employee] in the for...of) so you avoid the extra
allocation and preserve array-type inference for types like HolidayWithDate |
LeaveApplication | ShiftAssignment; update the loop in MonthViewTable.vue where
the code uses Object.values(data[employee]) to use direct iteration (for (const
event of data[employee])) and ensure any downstream code relying on the previous
Object.values result still works with an array element (event) as typed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@roster/src/components/MonthViewTable.vue`:
- Around line 119-124: The template currently uses v-else-if on the leave block
so a leave is hidden when a holiday is present; update the template to render
both indicators by removing the exclusive v-else-if and using independent
conditionals (e.g., change
v-else-if="events.data?.[employee.name]?.[day.date]?.leave" to v-if or a
separate element) so that events.data[employee.name][day.date].holiday and
events.data[employee.name][day.date].leave can both display; ensure any CSS for
.blocked-cell or the new element keeps the layout readable.

---

Duplicate comments:
In `@roster/src/components/MonthViewTable.vue`:
- Around line 108-115: The template currently uses v-html with
events.data[employee.name][day.date].holiday.description which is an XSS risk;
replace the v-html binding with a safe text binding (mustache interpolation or
v-text) so the description is rendered as escaped plain text, e.g. bind to
events.data[employee.name][day.date].holiday.description via {{ ... }} or
v-text; if HTML rendering is required, instead pass the description through a
sanitizer utility (e.g., DOMPurify) in a computed or method like
sanitizeHolidayDescription(description) and bind the sanitized result,
referencing the same events.data[...]holiday.description symbol.
- Around line 93-99: The swap_shift flow removed the client-side
isHolidayOrLeave guard but the swap_shift endpoint still lacks server-side
validation, so add a validation in the swap_shift request handler to reject
swaps that would place a shift on an employee's approved leave or a holiday:
call the existing isHolidayOrLeave (or the domain function that checks
leaves/holidays) for the target employee/date inside the swap_shift handler
before applying changes, return a 4xx error with a clear message when it
conflicts, and ensure any callers (e.g., the frontend swapShift.submit() path)
surface that error to the user; also keep hasSameShift logic as a short-circuit
to avoid unnecessary validation if no change is happening.

---

Nitpick comments:
In `@roster/src/components/MonthViewTable.vue`:
- Line 444: The loop currently iterates with Object.values(data[employee]) which
is unnecessary because data[employee] is already an array; change the iteration
to iterate directly over the array (use data[employee] in the for...of) so you
avoid the extra allocation and preserve array-type inference for types like
HolidayWithDate | LeaveApplication | ShiftAssignment; update the loop in
MonthViewTable.vue where the code uses Object.values(data[employee]) to use
direct iteration (for (const event of data[employee])) and ensure any downstream
code relying on the previous Object.values result still works with an array
element (event) as typed.

@sarathibalamurugan sarathibalamurugan force-pushed the feat/issue-3615 branch 5 times, most recently from 56b58f8 to bdb79bd Compare February 25, 2026 07:12
@sarathibalamurugan sarathibalamurugan force-pushed the feat/issue-3615 branch 2 times, most recently from 4914973 to 3b6e0e6 Compare March 4, 2026 04:59
@sarathibalamurugan sarathibalamurugan force-pushed the feat/issue-3615 branch 2 times, most recently from d865a76 to dcf2362 Compare March 10, 2026 07:50
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.

1 participant