Skip to content

Comments

RU-T47 Fixing role assignments issue#229

Merged
ucswift merged 2 commits intomasterfrom
develop
Feb 22, 2026
Merged

RU-T47 Fixing role assignments issue#229
ucswift merged 2 commits intomasterfrom
develop

Conversation

@ucswift
Copy link
Member

@ucswift ucswift commented Feb 22, 2026

Summary by CodeRabbit

  • New Features

    • Modal-based user selection for role assignments with search, highlighting, and an "Unassigned" option
  • Bug Fixes

    • Auto-unassignment on reassignment to prevent users appearing in multiple roles
  • Style

    • Dark-mode support and refined visual indicators for assignment rows and user status
  • Tests

    • Comprehensive unit tests for role selection modal and assignment flows
  • Chores

    • Consolidated role/user fetching into a single unified call
    • Removed an Android foreground media playback permission

@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Walkthrough

Replaces select-based role assignment with a modal-driven UI, adds a searchable RoleUserSelectionModal, consolidates role/user fetching into a new fetchAllForUnit API, and implements pending-assignment logic to prevent users from being assigned to multiple roles. Tests updated accordingly across role-related suites and sidebar/accounting.

Changes

Cohort / File(s) Summary
Android Configuration
app.config.ts
Removed android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK from Android permissions.
Role Assignment UI
src/components/roles/role-assignment-item.tsx, src/components/roles/role-assignment-item.test.tsx
Replaced Select flow with Pressable row that opens RoleUserSelectionModal; added dark-mode styling, status/avatar visuals, new handlers and props (optional roleName in currentAssignments). Tests updated for modal interactions and accessibility.
Role User Selection Modal
src/components/roles/role-user-selection-modal.tsx, src/components/roles/__tests__/role-user-selection-modal.test.tsx
New searchable modal component showing users, Unassigned option, cross-role assignment indicators, status/staffing chips; comprehensive tests for search, filtering, selection, highlight, and empty states.
Roles Store API
src/stores/roles/store.ts
Added fetchAllForUnit(unitId) to concurrently load roles, unitRoleAssignments, and users; initialization switched to parallel Promise.all fetches.
Role Management Logic
src/components/roles/roles-modal.tsx, src/components/roles/roles-bottom-sheet.tsx, src/components/roles/__tests__/roles-modal.test.tsx, src/components/roles/__tests__/roles-bottom-sheet.test.tsx
Replaced separate fetchRolesForUnit/fetchUsers calls with fetchAllForUnit. Added pending-assignment handling that auto-unassigns conflicting roles and computes effectiveAssignments to merge server and pending state for UI and save operations. Tests adapted to new API and logic.
Sidebar Roles Counter
src/components/sidebar/roles-sidebar.tsx, src/components/sidebar/__tests__/roles-sidebar.test.tsx
totalCount now derived from roles filtered by active unit; tests updated to include mock roles and validate counts alongside assignments.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as RoleAssignmentItem
    participant Modal as RoleUserSelectionModal
    participant Store as RolesStore
    participant Server

    User->>UI: Press row to assign user
    UI->>Modal: open(isOpen=true, props: roleId, users, currentAssignments)
    Modal->>Store: read users / currentAssignments
    User->>Modal: select user (or Unassigned)
    Modal->>UI: onSelectUser(userId or undefined)
    UI->>Store: handleAssignUser(roleId, userId)
    Store->>Store: update pendingAssignments (auto-unassign conflicting roles)
    Store->>Store: compute effectiveAssignments (server + pending)
    UI->>UI: re-render with effectiveAssignments
    User->>UI: Press Save
    UI->>Store: save pendingAssignments
    Store->>Server: persist assignments
    Server-->>Store: confirm
    Store->>Store: clear pending state / refresh
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • Unit#172: Modifies the same role-management components and testing paths (role-assignment-item, roles-modal, roles-bottom-sheet) and introduces/tests consolidated fetch behavior.
  • Unit#214: Also updates Android permissions in app.config.ts, changing entries in the android.permissions array.

Poem

🐰 I hopped in to swap Select for a modal bright,

Users searchable, one role per light,
Pending hops chase conflicts away,
FetchAll gathers roles in a single day,
Now every bunny finds the right play.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'RU-T47 Fixing role assignments issue' is vague and generic, using non-descriptive terms like 'Fixing role assignments issue' that don't clearly convey the specific nature or scope of the changes. Replace with a more specific title that describes the actual changes, such as 'Add role user selection modal with cross-role assignment handling' or 'Refactor role assignment UI to prevent multi-role user conflicts'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

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

@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

Caution

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

⚠️ Outside diff range comments (3)
src/components/roles/roles-bottom-sheet.tsx (1)

40-46: ⚠️ Potential issue | 🟡 Minor

No guard against concurrent fetchAllForUnit invocations on rapid open/close

Every isOpen → true transition fires fetchAllForUnit, which launches three concurrent API calls. If the sheet is closed and reopened before the first batch resolves, a second batch starts in parallel. Because there is no abort/cancellation, whichever batch resolves last wins, and that could be the older (stale) one.

A lightweight guard—similar to what init already does—prevents the duplicate in-flight fetches:

🛡️ Suggested guard
  React.useEffect(() => {
    if (isOpen && activeUnit) {
+     if (useRolesStore.getState().isLoading) return;
      useRolesStore.getState().fetchAllForUnit(activeUnit.UnitId);
      setPendingAssignments([]);
    }
  }, [isOpen, activeUnit]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/roles/roles-bottom-sheet.tsx` around lines 40 - 46, The effect
currently calls useRolesStore.getState().fetchAllForUnit on every isOpen true
transition which can start overlapping fetch batches; add a lightweight guard in
the effect to avoid starting a second fetch when one is already in-flight for
the same unit (e.g. query a store flag like
useRolesStore.getState().isFetchingForUnit(activeUnit.UnitId) or compare
useRolesStore.getState().currentFetchToken/unitId before calling
fetchAllForUnit), or update fetchAllForUnit to accept/return a cancel token and
store the active token so the effect can skip invocation if a token exists;
modify the effect around React.useEffect(...) so it checks that guard (and still
calls setPendingAssignments([]) when opening).
src/components/roles/roles-modal.tsx (2)

162-162: ⚠️ Potential issue | 🟡 Minor

Error message is not wrapped in t() for internationalization.

The error string from the store is displayed directly. All user-facing text should be wrapped in t(). As per coding guidelines, "Ensure all text is wrapped in t() from react-i18next for translations."

♻️ Suggested change
-            <Text className="py-4 text-center text-red-500">{error}</Text>
+            <Text className="py-4 text-center text-red-500">{t('common.error', error ?? 'An error occurred')}</Text>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/roles/roles-modal.tsx` at line 162, The displayed error string
must be internationalized: import and call useTranslation (const { t } =
useTranslation()) inside the RolesModal component and replace the raw {error} in
the Text element with a translated value (e.g., {t(error)} if error is a
translation key, or {t('roles.error', { message: error })} if error is a raw
message and you add a template key), updating the Text node referenced in
roles-modal.tsx accordingly.

144-144: ⚠️ Potential issue | 🟡 Minor

Toast error message is not internationalized.

Hardcoded English string passed to showToast. As per coding guidelines, "Ensure all text is wrapped in t() from react-i18next for translations."

♻️ Suggested change
-      useToastStore.getState().showToast('error', 'Error saving role assignments');
+      useToastStore.getState().showToast('error', t('roles.errors.saveFailed', 'Error saving role assignments'));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/roles/roles-modal.tsx` at line 144, The toast error message in
roles-modal.tsx currently passes a hardcoded English string to
useToastStore.getState().showToast; replace that literal with a translated
string using react-i18next (e.g., call t('roles.saveError') or similar) and
ensure the translation function is available in the component by importing
useTranslation and doing const { t } = useTranslation(); then pass t('...') to
useToastStore.getState().showToast('error', t('roles.saveError')) so the message
is internationalized.
🧹 Nitpick comments (12)
src/components/roles/role-user-selection-modal.tsx (1)

64-70: Redundant key prop on Pressable inside renderUserItem

renderUserItem is called as a plain function inside filteredUsers.map(...), where the wrapping React.Fragment already carries key={user.UserId} (line 178). The key on the inner Pressable (line 66) is unused by the reconciler.

♻️ Suggested cleanup
-       <Pressable
-         key={item.UserId}
-         onPress={() => handleSelect(item.UserId)}
+       <Pressable
+         onPress={() => handleSelect(item.UserId)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/roles/role-user-selection-modal.tsx` around lines 64 - 70, The
inner Pressable in renderUserItem is given a redundant key prop
(key={item.UserId}) even though the outer React.Fragment rendered in
filteredUsers.map already provides key={user.UserId}; remove the unused key on
the Pressable inside renderUserItem (the element created in the renderUserItem
function) so only the fragment's key remains, leaving onPress, className, testID
and accessibilityRole unchanged.
src/components/roles/__tests__/roles-modal.test.tsx (1)

289-365: "Empty RoleId prevention" tests verify inline logic, not the component's handleSave

Tests like "should allow unassignments by including roles with valid RoleId but empty UserId" (lines 316–332) and "should find role assignments without UnitId filter" (lines 343–364) exercise copy-pasted filter/find lambdas directly. If the component's handleSave diverges from these inline snippets, the tests continue to pass while the real behavior is broken.

Consider either:

  • Replacing inline-logic tests with integration-style tests that press the Save button on a rendered <RolesModal> and assert mockAssignRoles was called with the expected payload, or
  • Moving the shared filter logic to a pure utility function and unit-testing that utility directly.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/roles/__tests__/roles-modal.test.tsx` around lines 289 - 365,
The tests under "Empty RoleId prevention" exercise inline filter/find lambdas
rather than the component's actual save logic, so they can pass even if
RolesModal.handleSave is broken; change the tests to either (a) be integration
tests that render <RolesModal isOpen onClose={mockOnClose} />, simulate user
actions and press the Save button, then assert mockAssignRoles was called with
the expected payload (verifying unassignments where RoleId is present but UserId
is empty and that UnitId is not required), or (b) extract the filtering/find
logic into a pure utility (e.g., a new function like filterValidRoleAssignments
or findAssignmentByUnitRoleId) and unit-test that utility directly instead of
re-implementing lambdas in the test file so behavior and implementation stay in
sync with RolesModal.handleSave.
src/components/roles/__tests__/role-user-selection-modal.test.tsx (2)

292-307: Empty-state test doesn't verify the fallback text is rendered

The test confirms user items disappear but doesn't assert that the "No users found" message appears. A search that matches nothing could silently render nothing if the empty state branch is broken.

🧪 Suggested addition
  fireEvent.changeText(screen.getByTestId('user-search-input'), 'zzzzz');

  expect(screen.queryByTestId('user-item-user1')).toBeNull();
  expect(screen.queryByTestId('user-item-user2')).toBeNull();
+ expect(screen.getByText('No users found')).toBeTruthy();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/roles/__tests__/role-user-selection-modal.test.tsx` around
lines 292 - 307, The test "shows empty state when no users match search" for
RoleUserSelectionModal currently only checks that user items ('user-item-user1',
'user-item-user2') are gone; update the test to also assert the empty-state
fallback is rendered by checking for the "No users found" message (e.g. via
screen.getByText('No users found') or the modal's empty-state test id) after
firing changeText on 'user-search-input' so the empty-state branch is explicitly
verified.

96-325: Missing coverage for currentAssignments / "assigned elsewhere" badge

The new currentAssignments + currentRoleId props drive the amber "assigned elsewhere" badge in the component, but no test exercises that path. Given the latent bug described in the component review below, a dedicated test here would both document the expected behavior and catch regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/roles/__tests__/role-user-selection-modal.test.tsx` around
lines 96 - 325, Add a test for RoleUserSelectionModal that covers the
currentAssignments/currentRoleId path: render RoleUserSelectionModal with isOpen
true, users mockUsers, currentRoleId set to a different role than the
assignment, and currentAssignments containing an entry mapping one of the UserId
values (e.g., 'user2') to an object indicating they are assigned to another
role; then assert that the "assigned elsewhere" (amber) badge is present for
that user's test id (e.g., 'user-item-user2' or a badge test id), and verify
selection behavior still works (tapping the user still calls
onSelectUser/onClose or is prevented as expected by the component). Ensure you
reference the component props currentAssignments and currentRoleId and the
RoleUserSelectionModal render used in existing tests.
src/stores/roles/store.ts (1)

101-119: fetchAllForUnit lacks a concurrency guard unlike init

init guards against concurrent runs via the isLoading / isInitialized check, but fetchAllForUnit starts unconditionally. If called while still in-flight (e.g., the sheet opened twice in quick succession), two concurrent Promise.all batches race to overwrite the store, and the earlier response may land last.

♻️ Suggested guard
  fetchAllForUnit: async (unitId: string) => {
+   if (useRolesStore.getState().isLoading) return;
    set({ isLoading: true, error: null });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/roles/store.ts` around lines 101 - 119, fetchAllForUnit currently
always starts a fetch and can run concurrently with another call; guard it like
init by reading the store state (via get()) and returning early if a fetch is
already in progress (e.g., if get().isLoading is true), then proceed to set
isLoading and run the Promise.all of getAllUnitRolesAndAssignmentsForDepartment,
getRoleAssignmentsForUnit, and getAllPersonnelInfos; also ensure you clear
isLoading in a finally block so concurrent callers don't leave the store stuck.
Use the existing symbols fetchAllForUnit, getRoleAssignmentsForUnit,
getAllUnitRolesAndAssignmentsForDepartment, getAllPersonnelInfos, set and get to
implement the guard and robust loading lifecycle.
src/components/roles/__tests__/role-assignment-item.test.tsx (2)

27-31: mockModalOnSelectUser is assigned but never read in any test — dead code.

The module-level variable is set on every render of the mock (line 31) and reset in beforeEach (line 129), but no test ever accesses it. All interactions go through fireEvent.press instead. Consider removing it to reduce noise.

♻️ Suggested change
-let mockModalOnSelectUser: ((userId?: string) => void) | undefined;
 jest.mock('../role-user-selection-modal', () => ({
   RoleUserSelectionModal: ({ isOpen, onClose, roleName, selectedUserId, users, onSelectUser }: any) => {
     const { View, Text, TouchableOpacity } = require('react-native');
-    mockModalOnSelectUser = onSelectUser;
     if (!isOpen) return null;

And in beforeEach:

   beforeEach(() => {
     jest.clearAllMocks();
-    mockModalOnSelectUser = undefined;
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/roles/__tests__/role-assignment-item.test.tsx` around lines 27
- 31, The module-level variable mockModalOnSelectUser is assigned inside the
RoleUserSelectionModal mock but never read in tests, so remove the dead variable
and its resets in beforeEach; update the mock (RoleUserSelectionModal) to simply
accept props and render without assigning onSelectUser to mockModalOnSelectUser,
and delete any beforeEach lines that set mockModalOnSelectUser = undefined to
keep tests clean.

29-29: Mock uses any for props typing.

Per coding guidelines: "Avoid using any; strive for precise types." The mock's props parameter uses any. While this is in a test mock and pragmatically acceptable, you could import or inline the relevant prop types for better type safety.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/roles/__tests__/role-assignment-item.test.tsx` at line 29, The
test mock for RoleUserSelectionModal currently types its props as any; replace
that with the real prop type to avoid any—import or inline the component's prop
interface (e.g., RoleUserSelectionModalProps or the prop type used by
RoleUserSelectionModal) and use it in the mock signature (props:
RoleUserSelectionModalProps) so isOpen, onClose, roleName, selectedUserId,
users, and onSelectUser are strongly typed.
src/components/roles/role-assignment-item.tsx (1)

99-99: Consider breaking the long RoleUserSelectionModal prop list across multiple lines for readability.

This single line is very wide with 8 props. Spreading them across lines would improve readability and make diffs cleaner.

♻️ Suggested formatting
-      <RoleUserSelectionModal isOpen={isModalOpen} onClose={handleCloseModal} roleName={role.Name} selectedUserId={assignedUser?.UserId} users={availableUsers} onSelectUser={handleSelectUser} currentAssignments={currentAssignments} currentRoleId={role.UnitRoleId} />
+      <RoleUserSelectionModal
+        isOpen={isModalOpen}
+        onClose={handleCloseModal}
+        roleName={role.Name}
+        selectedUserId={assignedUser?.UserId}
+        users={availableUsers}
+        onSelectUser={handleSelectUser}
+        currentAssignments={currentAssignments}
+        currentRoleId={role.UnitRoleId}
+      />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/roles/role-assignment-item.tsx` at line 99, The
RoleUserSelectionModal JSX invocation is a single very long line; split the
props onto multiple lines to improve readability: break the
<RoleUserSelectionModal ... /> call so each prop (isOpen, onClose, roleName,
selectedUserId, users, onSelectUser, currentAssignments, currentRoleId) appears
on its own or grouped across lines, keeping the component name and closing tag
aligned and preserving the existing prop order and values (e.g.,
isOpen={isModalOpen}, onClose={handleCloseModal},
selectedUserId={assignedUser?.UserId}, currentRoleId={role.UnitRoleId}).
src/components/roles/roles-modal.tsx (4)

35-41: activeUnit is an object reference — consider depending on its UnitId instead.

If useCoreStore returns a new object with the same UnitId on re-render, this effect will re-fire and re-fetch unnecessarily. Depending on activeUnit?.UnitId and guarding inside the effect is more stable.

♻️ Suggested change
  React.useEffect(() => {
    if (isOpen && activeUnit) {
      useRolesStore.getState().fetchAllForUnit(activeUnit.UnitId);
      // Reset pending assignments when modal opens
      setPendingAssignments([]);
    }
-  }, [isOpen, activeUnit]);
+  }, [isOpen, activeUnit?.UnitId]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/roles/roles-modal.tsx` around lines 35 - 41, The effect
currently depends on the object reference activeUnit which can trigger
unnecessary re-runs; change the dependency to activeUnit?.UnitId and inside the
effect guard on isOpen and activeUnit?.UnitId (or store UnitId in a local
variable) so you call
useRolesStore.getState().fetchAllForUnit(activeUnit.UnitId) and
setPendingAssignments([]) only when isOpen is true and a UnitId exists; this
ensures fetchAllForUnit and setPendingAssignments are triggered only when the
unit identity changes rather than when a new object reference is returned.

106-146: Save doesn't reset pendingAssignments on success before closing.

On success (line 135-136), the modal closes but pendingAssignments isn't cleared until the next open (line 39). If there's any re-render between onClose() and the next isOpen=true, the stale pending state could briefly produce incorrect effectiveAssignments. Consider resetting explicitly on successful save.

♻️ Suggested change
      // Refresh role assignments after all updates
      await useRolesStore.getState().fetchRolesForUnit(activeUnit.UnitId);
+     setPendingAssignments([]);
      onClose();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/roles/roles-modal.tsx` around lines 106 - 146, The save flow
in handleSave successfully updates remote roles but never clears the local
pendingAssignments before calling onClose, leaving stale pending state; after
the await useRolesStore.getState().fetchRolesForUnit(activeUnit.UnitId) and
before onClose(), explicitly reset the pendingAssignments state (e.g., call the
setter that manages pendingAssignments or dispatch the clear action) so
pendingAssignments is emptied on success; reference handleSave,
pendingAssignments, useRolesStore.getState().fetchRolesForUnit, and onClose when
making this change.

43-67: The auto-unassign swap logic is complex — consider adding inline comments for each branch.

The logic is functionally correct: when assigning a user to a new role, it checks both server-side and pending assignments to automatically unassign the user from their previous role. However, the multiple nested conditionals and array manipulations make this hard to follow during future maintenance.

One concrete concern: the pendingForUser branch (lines 55-61) removes the pending entry for the user on the other role and, if there was a server assignment, replaces it with an explicit unassignment. But if the user was pending-assigned to two other roles (an unlikely edge case from rapid interactions), find on line 51 only catches the first one.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/roles/roles-modal.tsx` around lines 43 - 67,
handleAssignUser's auto-unassign swap logic is hard to follow and may miss cases
where a user has multiple pending assignments; add concise inline comments
explaining each branch (the initial filter, serverAssignment detection via
unitRoleAssignments, pendingForUser handling, and why we add explicit
unassignment entries), and change the pendingForUser discovery from a single
find to handling all matching pending assignments (e.g., iterate/filter pending
entries for userId rather than using pendingForUser = updated.find(...)) so
every pending assignment for that user is removed and, for each removed role
that had a server assignment (check via unitRoleAssignments), add the explicit
unassignment entry; reference the handleAssignUser callback, pendingAssignments
state updater, unitRoleAssignments, pendingForUser logic, and the code paths
that append { roleId, userId } or { roleId, userId: undefined } when applying
these fixes.

166-181: Anonymous closure in map callback creates new function reference per role on every render.

Line 179 creates a new arrow function (userId) => handleAssignUser(role.UnitRoleId, userId) for each role on every render. While this is a ScrollView (not a FlatList), it still causes every RoleAssignmentItem to receive a new onAssignUser prop each render, defeating React.memo if it were applied. This is acceptable for a small list of roles but worth noting. As per coding guidelines, "Avoid anonymous functions in renderItem or event handlers to prevent re-renders."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/roles/roles-modal.tsx` around lines 166 - 181, The inline
arrow passed to RoleAssignmentItem (onAssignUser={(userId) =>
handleAssignUser(role.UnitRoleId, userId)}) creates a new function per role
every render; fix by building stable callbacks once and reusing them: create a
memoized map of handlers (e.g., onAssignUserMap) with useMemo that iterates
filteredRoles and stores a function for each role.UnitRoleId that calls
handleAssignUser(roleId, userId), then pass
onAssignUser={onAssignUserMap.get(role.UnitRoleId)} into RoleAssignmentItem so
the onAssignUser reference is stable across renders unless filteredRoles or
handleAssignUser change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/roles/role-user-selection-modal.tsx`:
- Around line 57-63: The logic in renderUserItem marks everyone "In another
role" when currentRoleId is undefined; update the
otherAssignment/isAssignedElsewhere check to only compare roleId when
currentRoleId is defined—i.e., when searching currentAssignments in
renderUserItem, include a guard that currentRoleId != null (or !== undefined)
before evaluating a.roleId !== currentRoleId so users assigned to the shown role
are not incorrectly flagged; reference renderUserItem, currentAssignments,
currentRoleId, otherAssignment, and isAssignedElsewhere when making the change.

---

Outside diff comments:
In `@src/components/roles/roles-bottom-sheet.tsx`:
- Around line 40-46: The effect currently calls
useRolesStore.getState().fetchAllForUnit on every isOpen true transition which
can start overlapping fetch batches; add a lightweight guard in the effect to
avoid starting a second fetch when one is already in-flight for the same unit
(e.g. query a store flag like
useRolesStore.getState().isFetchingForUnit(activeUnit.UnitId) or compare
useRolesStore.getState().currentFetchToken/unitId before calling
fetchAllForUnit), or update fetchAllForUnit to accept/return a cancel token and
store the active token so the effect can skip invocation if a token exists;
modify the effect around React.useEffect(...) so it checks that guard (and still
calls setPendingAssignments([]) when opening).

In `@src/components/roles/roles-modal.tsx`:
- Line 162: The displayed error string must be internationalized: import and
call useTranslation (const { t } = useTranslation()) inside the RolesModal
component and replace the raw {error} in the Text element with a translated
value (e.g., {t(error)} if error is a translation key, or {t('roles.error', {
message: error })} if error is a raw message and you add a template key),
updating the Text node referenced in roles-modal.tsx accordingly.
- Line 144: The toast error message in roles-modal.tsx currently passes a
hardcoded English string to useToastStore.getState().showToast; replace that
literal with a translated string using react-i18next (e.g., call
t('roles.saveError') or similar) and ensure the translation function is
available in the component by importing useTranslation and doing const { t } =
useTranslation(); then pass t('...') to
useToastStore.getState().showToast('error', t('roles.saveError')) so the message
is internationalized.

---

Nitpick comments:
In `@src/components/roles/__tests__/role-assignment-item.test.tsx`:
- Around line 27-31: The module-level variable mockModalOnSelectUser is assigned
inside the RoleUserSelectionModal mock but never read in tests, so remove the
dead variable and its resets in beforeEach; update the mock
(RoleUserSelectionModal) to simply accept props and render without assigning
onSelectUser to mockModalOnSelectUser, and delete any beforeEach lines that set
mockModalOnSelectUser = undefined to keep tests clean.
- Line 29: The test mock for RoleUserSelectionModal currently types its props as
any; replace that with the real prop type to avoid any—import or inline the
component's prop interface (e.g., RoleUserSelectionModalProps or the prop type
used by RoleUserSelectionModal) and use it in the mock signature (props:
RoleUserSelectionModalProps) so isOpen, onClose, roleName, selectedUserId,
users, and onSelectUser are strongly typed.

In `@src/components/roles/__tests__/role-user-selection-modal.test.tsx`:
- Around line 292-307: The test "shows empty state when no users match search"
for RoleUserSelectionModal currently only checks that user items
('user-item-user1', 'user-item-user2') are gone; update the test to also assert
the empty-state fallback is rendered by checking for the "No users found"
message (e.g. via screen.getByText('No users found') or the modal's empty-state
test id) after firing changeText on 'user-search-input' so the empty-state
branch is explicitly verified.
- Around line 96-325: Add a test for RoleUserSelectionModal that covers the
currentAssignments/currentRoleId path: render RoleUserSelectionModal with isOpen
true, users mockUsers, currentRoleId set to a different role than the
assignment, and currentAssignments containing an entry mapping one of the UserId
values (e.g., 'user2') to an object indicating they are assigned to another
role; then assert that the "assigned elsewhere" (amber) badge is present for
that user's test id (e.g., 'user-item-user2' or a badge test id), and verify
selection behavior still works (tapping the user still calls
onSelectUser/onClose or is prevented as expected by the component). Ensure you
reference the component props currentAssignments and currentRoleId and the
RoleUserSelectionModal render used in existing tests.

In `@src/components/roles/__tests__/roles-modal.test.tsx`:
- Around line 289-365: The tests under "Empty RoleId prevention" exercise inline
filter/find lambdas rather than the component's actual save logic, so they can
pass even if RolesModal.handleSave is broken; change the tests to either (a) be
integration tests that render <RolesModal isOpen onClose={mockOnClose} />,
simulate user actions and press the Save button, then assert mockAssignRoles was
called with the expected payload (verifying unassignments where RoleId is
present but UserId is empty and that UnitId is not required), or (b) extract the
filtering/find logic into a pure utility (e.g., a new function like
filterValidRoleAssignments or findAssignmentByUnitRoleId) and unit-test that
utility directly instead of re-implementing lambdas in the test file so behavior
and implementation stay in sync with RolesModal.handleSave.

In `@src/components/roles/role-assignment-item.tsx`:
- Line 99: The RoleUserSelectionModal JSX invocation is a single very long line;
split the props onto multiple lines to improve readability: break the
<RoleUserSelectionModal ... /> call so each prop (isOpen, onClose, roleName,
selectedUserId, users, onSelectUser, currentAssignments, currentRoleId) appears
on its own or grouped across lines, keeping the component name and closing tag
aligned and preserving the existing prop order and values (e.g.,
isOpen={isModalOpen}, onClose={handleCloseModal},
selectedUserId={assignedUser?.UserId}, currentRoleId={role.UnitRoleId}).

In `@src/components/roles/role-user-selection-modal.tsx`:
- Around line 64-70: The inner Pressable in renderUserItem is given a redundant
key prop (key={item.UserId}) even though the outer React.Fragment rendered in
filteredUsers.map already provides key={user.UserId}; remove the unused key on
the Pressable inside renderUserItem (the element created in the renderUserItem
function) so only the fragment's key remains, leaving onPress, className, testID
and accessibilityRole unchanged.

In `@src/components/roles/roles-modal.tsx`:
- Around line 35-41: The effect currently depends on the object reference
activeUnit which can trigger unnecessary re-runs; change the dependency to
activeUnit?.UnitId and inside the effect guard on isOpen and activeUnit?.UnitId
(or store UnitId in a local variable) so you call
useRolesStore.getState().fetchAllForUnit(activeUnit.UnitId) and
setPendingAssignments([]) only when isOpen is true and a UnitId exists; this
ensures fetchAllForUnit and setPendingAssignments are triggered only when the
unit identity changes rather than when a new object reference is returned.
- Around line 106-146: The save flow in handleSave successfully updates remote
roles but never clears the local pendingAssignments before calling onClose,
leaving stale pending state; after the await
useRolesStore.getState().fetchRolesForUnit(activeUnit.UnitId) and before
onClose(), explicitly reset the pendingAssignments state (e.g., call the setter
that manages pendingAssignments or dispatch the clear action) so
pendingAssignments is emptied on success; reference handleSave,
pendingAssignments, useRolesStore.getState().fetchRolesForUnit, and onClose when
making this change.
- Around line 43-67: handleAssignUser's auto-unassign swap logic is hard to
follow and may miss cases where a user has multiple pending assignments; add
concise inline comments explaining each branch (the initial filter,
serverAssignment detection via unitRoleAssignments, pendingForUser handling, and
why we add explicit unassignment entries), and change the pendingForUser
discovery from a single find to handling all matching pending assignments (e.g.,
iterate/filter pending entries for userId rather than using pendingForUser =
updated.find(...)) so every pending assignment for that user is removed and, for
each removed role that had a server assignment (check via unitRoleAssignments),
add the explicit unassignment entry; reference the handleAssignUser callback,
pendingAssignments state updater, unitRoleAssignments, pendingForUser logic, and
the code paths that append { roleId, userId } or { roleId, userId: undefined }
when applying these fixes.
- Around line 166-181: The inline arrow passed to RoleAssignmentItem
(onAssignUser={(userId) => handleAssignUser(role.UnitRoleId, userId)}) creates a
new function per role every render; fix by building stable callbacks once and
reusing them: create a memoized map of handlers (e.g., onAssignUserMap) with
useMemo that iterates filteredRoles and stores a function for each
role.UnitRoleId that calls handleAssignUser(roleId, userId), then pass
onAssignUser={onAssignUserMap.get(role.UnitRoleId)} into RoleAssignmentItem so
the onAssignUser reference is stable across renders unless filteredRoles or
handleAssignUser change.

In `@src/stores/roles/store.ts`:
- Around line 101-119: fetchAllForUnit currently always starts a fetch and can
run concurrently with another call; guard it like init by reading the store
state (via get()) and returning early if a fetch is already in progress (e.g.,
if get().isLoading is true), then proceed to set isLoading and run the
Promise.all of getAllUnitRolesAndAssignmentsForDepartment,
getRoleAssignmentsForUnit, and getAllPersonnelInfos; also ensure you clear
isLoading in a finally block so concurrent callers don't leave the store stuck.
Use the existing symbols fetchAllForUnit, getRoleAssignmentsForUnit,
getAllUnitRolesAndAssignmentsForDepartment, getAllPersonnelInfos, set and get to
implement the guard and robust loading lifecycle.

@ucswift
Copy link
Member Author

ucswift commented Feb 22, 2026

Approve

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

This PR is approved.

@ucswift ucswift merged commit cf1956a into master Feb 22, 2026
19 of 20 checks passed
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