Skip to content

[BP] Room View Dashboard#1970

Open
vrindajoshi wants to merge 53 commits intomainfrom
bp-room-view
Open

[BP] Room View Dashboard#1970
vrindajoshi wants to merge 53 commits intomainfrom
bp-room-view

Conversation

@vrindajoshi
Copy link
Copy Markdown
Contributor

@vrindajoshi vrindajoshi commented Mar 17, 2026

Notes

Summary by Sourcery

Implement a revamped shelter operator dashboard with room-level views, shared table components, and updated routing/layout, while standardizing buttons and typography across UI primitives.

New Features:

  • Add reusable ShelterTable and RoomTable components backed by a generic Table/Row abstraction to display shelters and rooms with actions, filtering, and sorting affordances.
  • Introduce a tabbed ShelterDashboardPage with slider-style navigation between overview, rooms, beds, occupancy, and label views, including a hardcoded Rooms tab with room management affordances.
  • Add an EditRoomModal for editing room details and a NavBar component that replaces the old operator header with breadcrumbs, org selection, and create-shelter actions.
  • Expose operator routing via a feature-flagged operatorRoute hook and switch the web app to use a data-driven router configuration with createBrowserRouter.

Enhancements:

  • Update the main operator Dashboard to use the new ShelterTable with improved search, pagination, empty/loading states, and a floating Reserve button on the root operator page.
  • Refactor operator layout to use the new NavBar and user context instead of a custom header and organization dropdown implementation.
  • Unify buttons, dropdowns, modals, toasts, and chips styling to use shared Button and Text primitives and add a smaller primary button variant and additional semantic color schemes.
  • Normalize shelter and room capacity/tag presentation, including availableBeds on the Shelter type and visual capacity bars in the shelter table.
  • Minor layout and typography tweaks in shared layouts and base UI components to align with the new design system.

Build:

  • Add a placeholder shared services entrypoint to satisfy TypeScript path aliases and project references.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 17, 2026

Reviewer's Guide

Introduces a new operator shelter dashboard UX built on reusable generic Table/Row components (ShelterTable, RoomTable), adds a tabbed shelter detail page with room management (including edit modal and floating actions), modernizes operator layout/navigation (NavBar, Router changes, feature-flagged operator route), and standardizes several base UI elements (buttons, dropdowns, modals, toasts) on shared Button/Text primitives.

Sequence diagram for room deletion with confirmation modal

sequenceDiagram
  actor Operator
  participant RoomTable
  participant ConfirmationModal
  participant ParentPage as RoomsTabContent

  Operator->>RoomTable: click Delete on room row
  RoomTable-->>RoomTable: setDeleteConfirmation(isOpen=true, roomId)
  RoomTable->>ConfirmationModal: render isOpen=true with primaryAction

  Operator->>ConfirmationModal: click Delete (primaryAction)
  ConfirmationModal->>RoomTable: primaryAction.onClick()
  RoomTable-->>RoomTable: check roomIds or roomId
  RoomTable->>ParentPage: onDeleteRoom(roomId)
  RoomTable-->>RoomTable: clear selectedRoomIds
  RoomTable-->>RoomTable: closeDeleteConfirmation()
  RoomTable->>ConfirmationModal: render isOpen=false
Loading

Class diagram for new table abstractions and related UI components

classDiagram
  direction LR

  class Table~TItem,TRowObject~ {
    +TableColumn~TItem~[] columns
    +TItem[] rows
    +function getRowKey(item TItem, index number) string
    +function getRowObject(item TItem, index number) TRowObject
    +function getTrailingContent(rowObject TRowObject, item TItem, index number) ReactNode
    +function onRowClick(rowObject TRowObject, rowIndex number) void
    +function onDelete(rowObject TRowObject, rowIndex number) void
    +boolean loading
    +ReactNode loadingState
    +ReactNode emptyState
  }

  class TableColumn~TItem~ {
    +string key
    +ReactNode label
    +string width
    +string headerClassName
    +string cellClassName
    +function render(item TItem) ReactNode
  }

  class Row~TRowObject~ {
    +RowCell[] cells
    +TRowObject rowObject
    +number rowIndex
    +string templateColumns
    +ReactNode trailingContent
    +function onRowClick(rowObject TRowObject, rowIndex number) void
    +function onDelete(rowObject TRowObject, rowIndex number) void
  }

  class RowCell {
    +string key
    +ReactNode content
    +string className
  }

  class Shelter {
    +string id
    +string name
    +string address
    +number totalBeds
    +number availableBeds
    +string[] tags
  }

  class Room {
    +string id
    +string name
    +RoomStatus status
    +string[] tags
  }

  class RoomStatus {
    <<enumeration>>
    available
    occupied
    out_of_service
    reserved
  }

  class ShelterRowObject {
    +string id
    +Shelter shelter
    +string name
    +string address
    +number totalBeds
    +number reservedBeds
    +string[] tags
  }

  class RoomRowObject {
    +string id
    +Room room
  }

  class ShelterTable {
    +Shelter[] rows
    +function renderTags(tags string[]) ReactNode
    +function onRowClick(rowObject ShelterRowObject, rowIndex number) void
    +function onSearchChange(value string) void
    +function onDeleteShelter(shelterId string) void
  }

  class RoomTable {
    +Room[] rows
    +string[] selectedRoomIds
    +function onRowClick(rowObject RoomRowObject, rowIndex number) void
    +function onSearchChange(value string) void
    +function onDeleteRoom(roomId string) void
    +function onDeleteRooms(roomIds string[]) void
  }

  class EditRoomModal {
    +boolean isOpen
    +Room room
    +function onClose() void
    +function onSave(updatedRoom Room) void
  }

  class ShelterDashboardPage {
    +ShelterTab tab
    +function render() ReactElement
  }

  class ShelterTabContent {
    +function ShelterTabContent(tab ShelterTab, shelter Shelter) ReactElement
    +function RoomsTabContent() ReactElement
  }

  class SliderTabs {
    +SliderTabItem[] items
    +string activePathSuffix
    +string basePath
    +function buildTabPath(basePath string, pathSuffix string) string
  }

  class SliderTabItem {
    +string label
    +string pathSuffix
  }

  class NavBar {
    +string organizationName
    +string shelterName
    +string pageTitle
    +boolean showCreateButton
  }

  class OperatorLayout {
    +function render() ReactElement
  }

  class Button {
    +ButtonVariant variant
    +ButtonColor color
    +ReactNode leftIcon
    +ReactNode rightIcon
    +function onClick(event MouseEvent) void
  }

  class ButtonVariant {
    <<enumeration>>
    floating
    primary
    primary_sm
    trash
    edit
    right_arrow
  }

  class ButtonColor {
    <<enumeration>>
    blue
    red
    green
    yellow
  }

  class ConfirmationModal {
    +string title
    +string description
    +string variant
    +function primaryAction.onClick() void
    +function secondaryAction.onClick() void
  }

  class Toast {
    +string title
    +string description
    +TToastStatus status
  }

  class toastVariantConfig {
    +ButtonColor actionButtonClass
  }

  TableColumn --> Table : used_by
  Row --> Table : used_by
  RowCell --> Row : used_by

  ShelterTable --> Table : composes
  RoomTable --> Table : composes

  ShelterTable --> Shelter : renders
  ShelterTable --> ShelterRowObject : maps_to
  RoomTable --> Room : renders
  RoomTable --> RoomRowObject : maps_to

  Room --> RoomStatus

  ShelterDashboardPage --> SliderTabs : uses
  ShelterDashboardPage --> ShelterTabContent : uses

  ShelterTabContent --> RoomTable : uses
  ShelterTabContent --> EditRoomModal : uses

  NavBar --> Button : uses
  OperatorLayout --> NavBar : uses

  Button ..> ButtonVariant
  Button ..> ButtonColor
  ConfirmationModal --> Button : uses
  Toast --> Button : uses
  toastVariantConfig --> Toast : config_for
Loading

File-Level Changes

Change Details Files
Replace the simple dashboard list with the new ShelterTable component, update pagination/search behavior, and add a floating reservation action on the operator root.
  • Increase PAGE_SIZE and rename search state to tableSearchInput with debounced updates tied to the new table search API.
  • Map GraphQL shelter results to include availableBeds and pass shelters into ShelterTable with loading/empty/error states.
  • Wire row clicks to navigate into the new tabbed shelter dashboard route, and reset pagination when the active organization changes.
  • Show a floating 'Reserve' button on the operator root route only.
libs/react/shelter-operator/src/lib/pages/dashboard/Dashboard.tsx
Introduce reusable Table/Row abstractions and a RoomTable with selection, search, and delete confirmation flows, plus a simple EditRoomModal and shelter tab content wiring.
  • Create a generic Table and Row component that render arbitrary columns, trailing actions, and support row click/delete handlers.
  • Add RoomTable with local search (name/status/tags), row selection with bulk delete confirmation via ConfirmationModal, and per-row edit/duplicate/delete actions using the shared Button/Text primitives.
  • Add an EditRoomModal wrapper around the base Modal components for editing a room (currently stubbed to call onSave and close).
  • Implement ShelterTabContent and RoomsTabContent that render the RoomTable, wire row clicks to open EditRoomModal, and include a floating 'Create Room' action button.
libs/react/shelter-operator/src/lib/components/Table.tsx
libs/react/shelter-operator/src/lib/components/Row.tsx
libs/react/shelter-operator/src/lib/components/RoomTable.tsx
libs/react/shelter-operator/src/lib/components/rooms/EditRoomModal.tsx
libs/react/shelter-operator/src/lib/pages/dashboard/components/ShelterTabContent.tsx
Add a tabbed shelter detail view (ShelterDashboardPage) and route it via OperatorApp with per-tab URLs.
  • Create ShelterDashboardPage that shows shelter name/address, header actions (Settings/Reserve), a SliderTabs control, and delegates body rendering to ShelterTabContent based on the active tab prop.
  • Add SliderTabs component that renders horizontal tabs with an animated bottom slider and builds links from a base path plus path suffix, preserving route state for the shelter object.
  • Update OperatorApp routing to mount ShelterDashboardPage at /operator/shelter/:id and additional tabbed subroutes (/rooms, /beds, /occupancy, /label) by passing the appropriate tab prop, while keeping the sign-in route outside the layout wrapper.
libs/react/shelter-operator/src/lib/pages/dashboard/ShelterDashboardPage.tsx
libs/react/shelter-operator/src/lib/pages/dashboard/components/SliderTabs.tsx
libs/react/shelter-operator/src/lib/OperatorApp.tsx
Refactor operator layout/header into a NavBar component that integrates org context, breadcrumbs, and the create-shelter CTA while hiding the public layout around operator routes via router changes.
  • Replace the inline header in OperatorLayout with a NavBar that renders the logo, dynamic title/breadcrumbs, optional org dropdown (using useActiveOrg + Dropdown), and action buttons (Create Shelter, account).
  • Use location state and user org data in OperatorLayout to compute organizationName, shelterName, and pageTitle for NavBar depending on whether the route is dashboard, reservation, or shelter detail.
  • Refactor shelter-web routing to separate publicRoutes and operatorRoute, then build the app router via createBrowserRouter with MainLayout wrapping public routes and OperatorApp mounted separately based on a feature-flagged useOperatorRoute hook; remove BrowserRouter from main.tsx in favor of RouterProvider inside App.
  • Add a small placeholder index.ts to libs/expo/shared/services to satisfy TS path aliases.
libs/react/shelter-operator/src/lib/components/layout/OperatorLayout.tsx
libs/react/shelter-operator/src/lib/components/NavBar.tsx
apps/shelter-web/src/app/router/buildShelterRoutes.tsx
apps/shelter-web/src/app/router/useOperatorRoute.tsx
apps/shelter-web/src/app/router/index.ts
apps/shelter-web/src/app/App.tsx
apps/shelter-web/src/main.tsx
libs/expo/shared/services/src/index.ts
Standardize base UI primitives (buttons, modals, dropdowns, toasts) on shared Button/Text components and extend button variants/colors to support new flows.
  • Refactor the Button component into libs/react/shelter-operator/src/lib/components/base-ui/buttons/buttons.tsx, add a primary-sm variant, support additional color schemes (red/green/yellow), and adjust icon defaults and text rendering to use the Text component; expose it via a new index file and update references (WizardNavigation, WizardProgressBar, ShelterTable, etc.).
  • Update ConfirmationModal to use the new Button color enums for variant styling and render title/description via Text; adjust toastVariantConfig to return ButtonColor values and update Toast to use Button with primary-sm actions and Text for title/description.
  • Refine Dropdown, DropdownMenu, and DropdownChips to use Text for labels/options/chips, tweak styling of the footer actions (Clear all/Done), and keep behavior the same.
  • Update Modal stories to use typed args, the shared Button/Text components, and add a PrimarySmall story for the new button variant; also make minor housekeeping tweaks (e.g., LONG_CONTENT_PARAGRAPHS formatting, HorizontalLayout style).
libs/react/shelter-operator/src/lib/components/base-ui/buttons/buttons.tsx
libs/react/shelter-operator/src/lib/components/base-ui/buttons/buttons.stories.tsx
libs/react/shelter-operator/src/lib/components/base-ui/buttons/index.ts
libs/react/shelter-operator/src/lib/components/base-ui/modal/ConfirmationModal.tsx
libs/react/shelter-operator/src/lib/components/base-ui/modal/Modal.stories.tsx
libs/react/shelter-operator/src/lib/components/base-ui/toast/Toast.tsx
libs/react/shelter-operator/src/lib/components/base-ui/toast/constants.tsx
libs/react/shelter-operator/src/lib/components/base-ui/dropdown/Dropdown.tsx
libs/react/shelter-operator/src/lib/components/base-ui/dropdown/DropdownMenu.tsx
libs/react/shelter-operator/src/lib/components/base-ui/dropdown/DropdownChips.tsx
libs/react/shelter-operator/src/lib/components/layout/WizardNavigation.tsx
libs/react/shelter-operator/src/lib/components/layout/WizardProgressBar.tsx
libs/react/shelter/src/lib/layout/HorizontalLayout.tsx
Adjust shared types and minor utilities to support the new dashboard data model and usage.
  • Extend the Shelter type with an optional availableBeds field used in capacity calculations and table rendering.
  • Ensure Dashboard maps GraphQL results to include availableBeds (currently null) to keep the shape consistent with Room/Shelter tables.
  • Add/adjust imports and types (e.g., type-only imports, ButtonColor in toast constants) to keep TS happy and improve tree-shaking.
libs/react/shelter-operator/src/lib/types/shelter.ts
libs/react/shelter-operator/src/lib/pages/dashboard/Dashboard.tsx
libs/react/shelter-operator/src/lib/components/base-ui/toast/constants.tsx
apps/shelter-web/src/app/router/buildShelterRoutes.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 4 issues, and left some high level feedback:

  • There are a couple of debug-style console.log calls left in shared components (e.g., Row and HorizontalLayout) that will fire on every click/render; consider removing these or gating them behind a debug flag to avoid noisy logs in production.
  • Several places rely on useOutletContext() as any for passing organization-related data (e.g., in Dashboard and ShelterTabContent); introducing a shared context type/interface and using useOutletContext<ThatType>() would make these flows safer and easier to maintain.
  • RoomTable both does its own local filtering based on searchInput and still calls the parent onSearchChange, which may lead to redundant or confusing filtering behavior when wired to real data; consider clearly separating ‘local-only’ search from ‘server-driven’ search, or letting the parent fully control search state.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- There are a couple of debug-style console.log calls left in shared components (e.g., Row and HorizontalLayout) that will fire on every click/render; consider removing these or gating them behind a debug flag to avoid noisy logs in production.
- Several places rely on `useOutletContext() as any` for passing organization-related data (e.g., in Dashboard and ShelterTabContent); introducing a shared context type/interface and using `useOutletContext<ThatType>()` would make these flows safer and easier to maintain.
- RoomTable both does its own local filtering based on `searchInput` and still calls the parent `onSearchChange`, which may lead to redundant or confusing filtering behavior when wired to real data; consider clearly separating ‘local-only’ search from ‘server-driven’ search, or letting the parent fully control search state.

## Individual Comments

### Comment 1
<location path="libs/react/shelter-operator/src/lib/components/Row.tsx" line_range="38-39" />
<code_context>
+  onRowClick,
+  onClick,
+}: RowProps<TRowObject>) {
+  const handleRowClick = () => {
+    console.log('[ShelterOperator][Row click]', rowObject);
+    onRowClick?.(rowObject, rowIndex);
+    onClick?.();
</code_context>
<issue_to_address>
**issue:** Avoid logging every row click from the shared Row component.

Because this is a shared Row component, this `console.log('[ShelterOperator][Row click]', rowObject);` will fire for every row click app-wide, creating noisy logs and potentially exposing row data in the console. Please remove it or move any necessary logging into the specific caller that needs the diagnostics.
</issue_to_address>

### Comment 2
<location path="libs/react/shelter-operator/src/lib/components/RoomTable.tsx" line_range="96-105" />
<code_context>
+  onDeleteRoom,
+  onDeleteRooms,
+}: RoomTableProps) {
+  const [searchInput, setSearchInput] = useState('');
+  const [selectedRoomIds, setSelectedRoomIds] = useState<string[]>([]);
+  const [deleteConfirmation, setDeleteConfirmation] = useState<{
+    isOpen: boolean;
+    roomIds?: string[];
+    roomId?: string;
+    roomName?: string;
+  }>({ isOpen: false });
+
+  useEffect(() => {
+    onSearchChange?.(searchInput);
+  }, [onSearchChange, searchInput]);
+
</code_context>
<issue_to_address>
**suggestion:** Clarify whether RoomTable should perform local filtering when onSearchChange is provided.

Because `searchInput` is used both to compute local `filteredRows` and to trigger `onSearchChange`, a consumer that uses `onSearchChange` for server-side filtering would effectively get client-side filtering layered on top of already-filtered data. This can lead to unexpected results (e.g., mismatched search semantics or pagination). Consider either only doing local filtering when `onSearchChange` is undefined, or making local filtering explicitly opt-in via a prop so consumers can decide where filtering should occur.

Suggested implementation:

```typescript
  emptyState,
  wrapperClassName,
  headerClassName,
  rowClassName,
  tableStyle,
  headerStyle,
  rowStyle,
  onDeleteRoom,
  onDeleteRooms,
  enableLocalFiltering,
}: RoomTableProps) {
  const [searchInput, setSearchInput] = useState('');
  const [selectedRoomIds, setSelectedRoomIds] = useState<string[]>([]);
  const [deleteConfirmation, setDeleteConfirmation] = useState<{
    isOpen: boolean;
    roomIds?: string[];
    roomId?: string;
    roomName?: string;
  }>({ isOpen: false });

  // When enableLocalFiltering is undefined, default to local filtering only if no onSearchChange is provided.
  const localFilteringEnabled = enableLocalFiltering ?? !onSearchChange;

  useEffect(() => {
    onSearchChange?.(searchInput);
  }, [onSearchChange, searchInput]);

// REPLACE WITH ACTUAL QUERIED DATA
export type RoomStatus =

```

1. Ensure `useState` and `useEffect` are imported at the top of this file, e.g.:
   `import React, { useEffect, useState } from 'react';` (or adjust to match existing import style).
2. Update the `RoomTableProps` type/interface to include the new optional prop:
   `enableLocalFiltering?: boolean;` and the existing callback: `onSearchChange?: (value: string) => void;` if it is not already declared.
3. Wherever local filtering is currently computed (likely something like `const filteredRows = rows.filter(...)` using `searchInput`), gate that logic with `localFilteringEnabled`. For example:
   - If `localFilteringEnabled` is `true`, compute `filteredRows` based on `searchInput`.
   - If `localFilteringEnabled` is `false`, skip the client-side filtering and use the provided `rows` directly.
4. If `filteredRows` is passed to the table component, ensure you switch between `filteredRows` and `rows` based on `localFilteringEnabled` to avoid layering client-side filtering on top of server-side filtering.
</issue_to_address>

### Comment 3
<location path="libs/react/shelter-operator/src/lib/components/RoomTable.tsx" line_range="50-52" />
<code_context>
+  tableStyle?: CSSProperties;
+  headerStyle?: CSSProperties;
+  rowStyle?: CSSProperties;
+  onCreateRoom?: () => void;
+  onDeleteRoom?: (roomId: string) => void;
+  onDeleteRooms?: (roomIds: string[]) => void;
+};
+
</code_context>
<issue_to_address>
**suggestion:** Wire up or remove the unused onCreateRoom prop to keep the API consistent.

`RoomTableProps` includes `onCreateRoom`, but the component never uses it—the "Create Room" button currently lives in `RoomsTabContent`. This makes the prop misleading for consumers. Either wire `onCreateRoom` through to a create button inside `RoomTable`, or remove the prop until `RoomTable` is actually responsible for room creation.

Suggested implementation:

```typescript
  wrapperClassName?: string;
  headerClassName?: string;
  rowClassName?: string;
  tableStyle?: CSSProperties;
  headerStyle?: CSSProperties;
  rowStyle?: CSSProperties;
  onDeleteRoom?: (roomId: string) => void;
  onDeleteRooms?: (roomIds: string[]) => void;
};

```

1. Remove `onCreateRoom` from the `RoomTable` component props destructuring/signature, if it appears there.
2. Remove any usages of `onCreateRoom` passed into `<RoomTable ... />` from parent components (e.g., `RoomsTabContent`), since the prop is no longer supported.
3. If types are re-exported (e.g., `RoomTableProps` from an index file), ensure there are no references or documentation mentioning `onCreateRoom`.
</issue_to_address>

### Comment 4
<location path="libs/react/shelter-operator/src/lib/components/RoomTable.tsx" line_range="39" />
<code_context>
+  rows: Room[];
+  getRowKey?: (item: Room, index: number) => string;
+  onRowClick?: (rowObject: RoomRowObject, rowIndex: number) => void;
+  onSearchChange?: (value: string) => void;
+  searchPlaceholder?: string;
+  loading?: boolean;
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying RoomTable by choosing a single owner for search state and extracting the status UI into a reusable RoomStatusTag component to keep the table configuration declarative and easier to read.

You can simplify this component meaningfully without losing any functionality by:

### 1. Remove the split-brain search handling

Right now `RoomTable` both:

- Keeps `searchInput` locally and filters with `filteredRows`
- Calls `onSearchChange?.(searchInput)` so the parent might *also* filter

That makes it unclear who owns filtering. If the table is already doing the filtering, you can drop the callback and the extra effect and keep `rows` as the single source of truth:

```tsx
type RoomTableProps = {
  rows: Room[];
  // remove this if parent is not expected to filter:
  // onSearchChange?: (value: string) => void;
  // ...
};

export function RoomTable({
  rows,
  // onSearchChange,
  // ...
}: RoomTableProps) {
  const [searchInput, setSearchInput] = useState('');

  // remove this effect
  // useEffect(() => {
  //   onSearchChange?.(searchInput);
  // }, [onSearchChange, searchInput]);

  const filteredRows = useMemo(() => {
    const query = searchInput.trim().toLowerCase();
    if (!query) return rows;

    return rows.filter((room) => {
      const normalizedStatus = STATUS_LABEL[room.status].toLowerCase();
      const matchesName = room.name.toLowerCase().includes(query);
      const matchesStatus = normalizedStatus.includes(query);
      const matchesTags = room.tags.some((tag) =>
        tag.toLowerCase().includes(query)
      );
      return matchesName || matchesStatus || matchesTags;
    });
  }, [rows, searchInput]);

  // ...
}
```

If you *do* want the parent to own filtering instead, invert it:

- Expose `searchInput` and `onSearchChange` as controlled props
- Remove `filteredRows` and just render `rows` (already filtered by parent)

```tsx
type RoomTableProps = {
  rows: Room[];
  searchValue: string;
  onSearchChange: (value: string) => void;
  // ...
};

export function RoomTable({
  rows,
  searchValue,
  onSearchChange,
  // ...
}: RoomTableProps) {
  // no local search state
  // no filteredRows useMemo

  return (
    <>
      <input
        type="text"
        value={searchValue}
        onChange={(e) => onSearchChange(e.target.value)}
        // ...
      />
      <Table
        rows={rows}
        // ...
      />
    </>
  );
}
```

Picking one model eliminates confusion and duplicated search logic.

---

### 2. Extract a `RoomStatusTag` to simplify column definitions

The status column currently embeds styling maps and inline JSX. You can extract that into a tiny presentational component and keep the column config declarative:

```tsx
// RoomStatusTag.tsx
const STATUS_STYLE: Record<RoomStatus, string> = {
  available: 'bg-[#D7F5DF]',
  occupied: 'bg-[#DCEEFF]',
  'out-of-service': 'bg-[#FFE5E0]',
  reserved: 'bg-[#FFEBCB]',
};

const STATUS_TEXT_STYLE: Record<RoomStatus, string> = {
  available: 'text-[#0F8F2F] font-medium',
  occupied: 'text-[#1F6FC7] font-medium',
  'out-of-service': 'text-[#D7332A] font-medium',
  reserved: 'text-[#CC6F00] font-medium',
};

const STATUS_LABEL: Record<RoomStatus, string> = {
  available: 'Available',
  occupied: 'Occupied',
  'out-of-service': 'Out of Service',
  reserved: 'Reserved',
};

export function RoomStatusTag({ status }: { status: RoomStatus }) {
  return (
    <span
      className={['inline-flex rounded-full px-3 py-1 leading-none', STATUS_STYLE[status]].join(
        ' '
      )}
    >
      <Text variant="tag-sm" className={STATUS_TEXT_STYLE[status]}>
        {STATUS_LABEL[status]}
      </Text>
    </span>
  );
}
```

Then your column definition becomes much easier to scan:

```tsx
const columns: TableColumn<Room>[] = useMemo(
  () => [
    // ...
    {
      key: 'status',
      label: 'Status',
      width: '1.5fr',
      render: (room) => <RoomStatusTag status={room.status} />,
    },
    // ...
  ],
  [selectedRoomIds]
);
```

This removes the status-specific styling logic from `RoomTable`, makes the columns more readable, and gives you a reusable status tag for other parts of the UI.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +96 to +105
const [searchInput, setSearchInput] = useState('');
const [selectedRoomIds, setSelectedRoomIds] = useState<string[]>([]);
const [deleteConfirmation, setDeleteConfirmation] = useState<{
isOpen: boolean;
roomIds?: string[];
roomId?: string;
roomName?: string;
}>({ isOpen: false });

useEffect(() => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Clarify whether RoomTable should perform local filtering when onSearchChange is provided.

Because searchInput is used both to compute local filteredRows and to trigger onSearchChange, a consumer that uses onSearchChange for server-side filtering would effectively get client-side filtering layered on top of already-filtered data. This can lead to unexpected results (e.g., mismatched search semantics or pagination). Consider either only doing local filtering when onSearchChange is undefined, or making local filtering explicitly opt-in via a prop so consumers can decide where filtering should occur.

Suggested implementation:

  emptyState,
  wrapperClassName,
  headerClassName,
  rowClassName,
  tableStyle,
  headerStyle,
  rowStyle,
  onDeleteRoom,
  onDeleteRooms,
  enableLocalFiltering,
}: RoomTableProps) {
  const [searchInput, setSearchInput] = useState('');
  const [selectedRoomIds, setSelectedRoomIds] = useState<string[]>([]);
  const [deleteConfirmation, setDeleteConfirmation] = useState<{
    isOpen: boolean;
    roomIds?: string[];
    roomId?: string;
    roomName?: string;
  }>({ isOpen: false });

  // When enableLocalFiltering is undefined, default to local filtering only if no onSearchChange is provided.
  const localFilteringEnabled = enableLocalFiltering ?? !onSearchChange;

  useEffect(() => {
    onSearchChange?.(searchInput);
  }, [onSearchChange, searchInput]);

// REPLACE WITH ACTUAL QUERIED DATA
export type RoomStatus =
  1. Ensure useState and useEffect are imported at the top of this file, e.g.:
    import React, { useEffect, useState } from 'react'; (or adjust to match existing import style).
  2. Update the RoomTableProps type/interface to include the new optional prop:
    enableLocalFiltering?: boolean; and the existing callback: onSearchChange?: (value: string) => void; if it is not already declared.
  3. Wherever local filtering is currently computed (likely something like const filteredRows = rows.filter(...) using searchInput), gate that logic with localFilteringEnabled. For example:
    • If localFilteringEnabled is true, compute filteredRows based on searchInput.
    • If localFilteringEnabled is false, skip the client-side filtering and use the provided rows directly.
  4. If filteredRows is passed to the table component, ensure you switch between filteredRows and rows based on localFilteringEnabled to avoid layering client-side filtering on top of server-side filtering.

Comment on lines +50 to +52
onCreateRoom?: () => void;
onDeleteRoom?: (roomId: string) => void;
onDeleteRooms?: (roomIds: string[]) => void;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Wire up or remove the unused onCreateRoom prop to keep the API consistent.

RoomTableProps includes onCreateRoom, but the component never uses it—the "Create Room" button currently lives in RoomsTabContent. This makes the prop misleading for consumers. Either wire onCreateRoom through to a create button inside RoomTable, or remove the prop until RoomTable is actually responsible for room creation.

Suggested implementation:

  wrapperClassName?: string;
  headerClassName?: string;
  rowClassName?: string;
  tableStyle?: CSSProperties;
  headerStyle?: CSSProperties;
  rowStyle?: CSSProperties;
  onDeleteRoom?: (roomId: string) => void;
  onDeleteRooms?: (roomIds: string[]) => void;
};
  1. Remove onCreateRoom from the RoomTable component props destructuring/signature, if it appears there.
  2. Remove any usages of onCreateRoom passed into <RoomTable ... /> from parent components (e.g., RoomsTabContent), since the prop is no longer supported.
  3. If types are re-exported (e.g., RoomTableProps from an index file), ensure there are no references or documentation mentioning onCreateRoom.

rows: Room[];
getRowKey?: (item: Room, index: number) => string;
onRowClick?: (rowObject: RoomRowObject, rowIndex: number) => void;
onSearchChange?: (value: string) => void;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (complexity): Consider simplifying RoomTable by choosing a single owner for search state and extracting the status UI into a reusable RoomStatusTag component to keep the table configuration declarative and easier to read.

You can simplify this component meaningfully without losing any functionality by:

1. Remove the split-brain search handling

Right now RoomTable both:

  • Keeps searchInput locally and filters with filteredRows
  • Calls onSearchChange?.(searchInput) so the parent might also filter

That makes it unclear who owns filtering. If the table is already doing the filtering, you can drop the callback and the extra effect and keep rows as the single source of truth:

type RoomTableProps = {
  rows: Room[];
  // remove this if parent is not expected to filter:
  // onSearchChange?: (value: string) => void;
  // ...
};

export function RoomTable({
  rows,
  // onSearchChange,
  // ...
}: RoomTableProps) {
  const [searchInput, setSearchInput] = useState('');

  // remove this effect
  // useEffect(() => {
  //   onSearchChange?.(searchInput);
  // }, [onSearchChange, searchInput]);

  const filteredRows = useMemo(() => {
    const query = searchInput.trim().toLowerCase();
    if (!query) return rows;

    return rows.filter((room) => {
      const normalizedStatus = STATUS_LABEL[room.status].toLowerCase();
      const matchesName = room.name.toLowerCase().includes(query);
      const matchesStatus = normalizedStatus.includes(query);
      const matchesTags = room.tags.some((tag) =>
        tag.toLowerCase().includes(query)
      );
      return matchesName || matchesStatus || matchesTags;
    });
  }, [rows, searchInput]);

  // ...
}

If you do want the parent to own filtering instead, invert it:

  • Expose searchInput and onSearchChange as controlled props
  • Remove filteredRows and just render rows (already filtered by parent)
type RoomTableProps = {
  rows: Room[];
  searchValue: string;
  onSearchChange: (value: string) => void;
  // ...
};

export function RoomTable({
  rows,
  searchValue,
  onSearchChange,
  // ...
}: RoomTableProps) {
  // no local search state
  // no filteredRows useMemo

  return (
    <>
      <input
        type="text"
        value={searchValue}
        onChange={(e) => onSearchChange(e.target.value)}
        // ...
      />
      <Table
        rows={rows}
        // ...
      />
    </>
  );
}

Picking one model eliminates confusion and duplicated search logic.


2. Extract a RoomStatusTag to simplify column definitions

The status column currently embeds styling maps and inline JSX. You can extract that into a tiny presentational component and keep the column config declarative:

// RoomStatusTag.tsx
const STATUS_STYLE: Record<RoomStatus, string> = {
  available: 'bg-[#D7F5DF]',
  occupied: 'bg-[#DCEEFF]',
  'out-of-service': 'bg-[#FFE5E0]',
  reserved: 'bg-[#FFEBCB]',
};

const STATUS_TEXT_STYLE: Record<RoomStatus, string> = {
  available: 'text-[#0F8F2F] font-medium',
  occupied: 'text-[#1F6FC7] font-medium',
  'out-of-service': 'text-[#D7332A] font-medium',
  reserved: 'text-[#CC6F00] font-medium',
};

const STATUS_LABEL: Record<RoomStatus, string> = {
  available: 'Available',
  occupied: 'Occupied',
  'out-of-service': 'Out of Service',
  reserved: 'Reserved',
};

export function RoomStatusTag({ status }: { status: RoomStatus }) {
  return (
    <span
      className={['inline-flex rounded-full px-3 py-1 leading-none', STATUS_STYLE[status]].join(
        ' '
      )}
    >
      <Text variant="tag-sm" className={STATUS_TEXT_STYLE[status]}>
        {STATUS_LABEL[status]}
      </Text>
    </span>
  );
}

Then your column definition becomes much easier to scan:

const columns: TableColumn<Room>[] = useMemo(
  () => [
    // ...
    {
      key: 'status',
      label: 'Status',
      width: '1.5fr',
      render: (room) => <RoomStatusTag status={room.status} />,
    },
    // ...
  ],
  [selectedRoomIds]
);

This removes the status-specific styling logic from RoomTable, makes the columns more readable, and gives you a reusable status tag for other parts of the UI.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 17, 2026

🔍 [shelter-web] Preview available at: https://shelter.dev.betterangels.la/branches/bp-room-view

Last updated: 2026-04-02T00:01:02.672Z

@vrindajoshi vrindajoshi marked this pull request as draft March 20, 2026 00:54
…ow Navbar

Co-authored-by: jennifer shi <jennifershi255@gmail.com>
@vrindajoshi vrindajoshi marked this pull request as ready for review March 24, 2026 00:43
Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • Search state is being managed both in the parent (Dashboard via tableSearchInput/debouncedSearch) and again inside ShelterTable and RoomTable (local searchInput + onSearchChange), which makes behavior harder to reason about; consider consolidating search control either entirely in the parent (and passing the value/handler down) or entirely in the table components and dropping the extra effect in the parent.
  • In NavBar, organizationName for shelter pages is derived from user.organizations[0] instead of the activeOrg context, which can show the wrong organization when multiple orgs exist; it would be more robust to pull the displayed organization name from useActiveOrg for consistency.
  • renderTags in ShelterTable falls back to a hardcoded tag list (Women Only, Shared, etc.) when tags is empty, which will silently display incorrect tags for real shelters; if this is just placeholder, consider gating it behind a feature flag or removing the fallback before shipping.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Search state is being managed both in the parent (Dashboard via `tableSearchInput`/`debouncedSearch`) and again inside `ShelterTable` and `RoomTable` (local `searchInput` + `onSearchChange`), which makes behavior harder to reason about; consider consolidating search control either entirely in the parent (and passing the value/handler down) or entirely in the table components and dropping the extra effect in the parent.
- In `NavBar`, `organizationName` for shelter pages is derived from `user.organizations[0]` instead of the `activeOrg` context, which can show the wrong organization when multiple orgs exist; it would be more robust to pull the displayed organization name from `useActiveOrg` for consistency.
- `renderTags` in `ShelterTable` falls back to a hardcoded tag list (`Women Only`, `Shared`, etc.) when `tags` is empty, which will silently display incorrect tags for real shelters; if this is just placeholder, consider gating it behind a feature flag or removing the fallback before shipping.

## Individual Comments

### Comment 1
<location path="libs/react/shelter-operator/src/lib/components/ShelterTable.tsx" line_range="135-144" />
<code_context>
+        label: 'Capacity',
+        width: '1.2fr',
+        cellClassName: 'whitespace-nowrap',
+        render: (shelter) => {
+          const totalBeds = shelter.totalBeds ?? 0;
+          const hasCapacity = totalBeds > 0;
+          const reservedBeds = hasCapacity
+            ? Math.min(
+                Math.max(totalBeds - (shelter.availableBeds ?? 0), 0),
+                totalBeds
+              )
+            : null;
+          const progressPct =
+            hasCapacity && reservedBeds !== null
+              ? (reservedBeds / totalBeds) * 100
</code_context>
<issue_to_address>
**issue (bug_risk):** Treat `availableBeds === null` as unknown capacity instead of "0 available" to avoid showing shelters as fully reserved when data is missing.

The current `shelter.availableBeds ?? 0` logic makes `reservedBeds = totalBeds` when `availableBeds` is `null`, so shelters with unknown availability appear 100% reserved. Instead, treat `availableBeds == null` as an "unknown" state (e.g., show `N/A` or hide the progress bar) and only compute `reservedBeds`/`progressPct` when `availableBeds != null`.
</issue_to_address>

### Comment 2
<location path="libs/react/shelter-operator/src/lib/components/RoomTable.tsx" line_range="50" />
<code_context>
+  tableStyle?: CSSProperties;
+  headerStyle?: CSSProperties;
+  rowStyle?: CSSProperties;
+  onCreateRoom?: () => void;
+  onDeleteRoom?: (roomId: string) => void;
+  onDeleteRooms?: (roomIds: string[]) => void;
</code_context>
<issue_to_address>
**suggestion:** Remove or wire up the unused `onCreateRoom` prop to keep the RoomTable API lean.

`RoomTableProps` exposes `onCreateRoom`, but the component never uses it—the "Create Room" button in `RoomsTabContent` bypasses this prop. To avoid a misleading API, either remove `onCreateRoom` from `RoomTableProps` or move the creation button into `RoomTable` and call `onCreateRoom` there for a consistent integration point.

Suggested implementation:

```typescript
  wrapperClassName?: string;
  headerClassName?: string;
  rowClassName?: string;
  tableStyle?: CSSProperties;
  headerStyle?: CSSProperties;
  rowStyle?: CSSProperties;
  onDeleteRoom?: (roomId: string) => void;
  onDeleteRooms?: (roomIds: string[]) => void;

```

1. In the same file, remove `onCreateRoom` from the RoomTable component props destructuring, e.g. change:
   `function RoomTable({ ..., onCreateRoom, onDeleteRoom, onDeleteRooms }: RoomTableProps)` to remove `onCreateRoom` from the list.
2. Remove any remaining references to `onCreateRoom` in `RoomTable.tsx` if they exist (TypeScript will flag these after the type removal).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +135 to +144
render: (shelter) => {
const totalBeds = shelter.totalBeds ?? 0;
const hasCapacity = totalBeds > 0;
const reservedBeds = hasCapacity
? Math.min(
Math.max(totalBeds - (shelter.availableBeds ?? 0), 0),
totalBeds
)
: null;
const progressPct =
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Treat availableBeds === null as unknown capacity instead of "0 available" to avoid showing shelters as fully reserved when data is missing.

The current shelter.availableBeds ?? 0 logic makes reservedBeds = totalBeds when availableBeds is null, so shelters with unknown availability appear 100% reserved. Instead, treat availableBeds == null as an "unknown" state (e.g., show N/A or hide the progress bar) and only compute reservedBeds/progressPct when availableBeds != null.

tableStyle?: CSSProperties;
headerStyle?: CSSProperties;
rowStyle?: CSSProperties;
onCreateRoom?: () => void;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Remove or wire up the unused onCreateRoom prop to keep the RoomTable API lean.

RoomTableProps exposes onCreateRoom, but the component never uses it—the "Create Room" button in RoomsTabContent bypasses this prop. To avoid a misleading API, either remove onCreateRoom from RoomTableProps or move the creation button into RoomTable and call onCreateRoom there for a consistent integration point.

Suggested implementation:

  wrapperClassName?: string;
  headerClassName?: string;
  rowClassName?: string;
  tableStyle?: CSSProperties;
  headerStyle?: CSSProperties;
  rowStyle?: CSSProperties;
  onDeleteRoom?: (roomId: string) => void;
  onDeleteRooms?: (roomIds: string[]) => void;
  1. In the same file, remove onCreateRoom from the RoomTable component props destructuring, e.g. change:
    function RoomTable({ ..., onCreateRoom, onDeleteRoom, onDeleteRooms }: RoomTableProps) to remove onCreateRoom from the list.
  2. Remove any remaining references to onCreateRoom in RoomTable.tsx if they exist (TypeScript will flag these after the type removal).

# Conflicts:
#	apps/shelter-web/src/app/App.tsx
#	apps/shelter-web/src/app/router/buildShelterRoutes.tsx
#	apps/shelter-web/src/app/router/useShelterRoutes.tsx
#	apps/shelter-web/src/main.tsx
#	libs/react/shelter-operator/src/lib/OperatorApp.tsx
#	libs/react/shelter-operator/src/lib/ShelterDashboardPage.tsx
#	libs/react/shelter-operator/src/lib/components/NavBar.tsx
#	libs/react/shelter-operator/src/lib/components/Row.tsx
#	libs/react/shelter-operator/src/lib/components/ShelterTable.tsx
#	libs/react/shelter-operator/src/lib/components/Table.tsx
#	libs/react/shelter-operator/src/lib/components/base-ui/buttons/buttons.tsx
#	libs/react/shelter-operator/src/lib/components/layout/OperatorLayout.tsx
#	libs/react/shelter-operator/src/lib/pages/dashboard/Dashboard.tsx
#	libs/react/shelter-operator/src/lib/types/shelter.ts
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.

3 participants