Repository: msupply-foundation/open-msupply
Pull request #7629: 7608 Remove confirm, strength and navigate to error when finalising
Author: login: andreievg
Labels: name: enhancement
description: New feature or request
name: needs triage
name: feature: R&R
Created: 2025-05-07T00:09:52Z | Merged: 2025-05-08T04:38:14Z

<!-- IMPORTANT!
  - Every PR must reference an issue; this helps to explain the intent of the PR
 -->

Fixes #7608

Carry over issue: https://github.com/msupply-foundation/open-msupply/issues/7635

# 👩🏻‍💻 What does this PR do?

* Remove confirm column
* Remove strength column
* Since confirm column is removed, error needs to be shown somehow, when finalised, there is an indication that there is an error and we navigate to that line
* Search changed slightly, for performance, and since we need to scroll to the list in other areas
* We now save only the lines that have changed and don't have error (there is a carry over for error handling, and other things in this issue)

## 💌 Any notes for the reviewer?

# 🧪 Testing

Confirm should not exist in RnR form, strength should not exist in RnR form. When there are errors you should be taken to the error when finalising.

# 📃 Documentation

- [ ] **Part of an epic**: documentation will be completed for the feature as a whole
- [ ] **No documentation required**: no user facing changes or a bug fix which isn't a change in behaviour
- [ ] **These areas should be updated or checked**: <!-- _(e.g.)_ New `issued` column in `Requisitions` indicates stock quantity already in shipments -->
  1.
  2.


# 📃 Reviewer Checklist

The PR Reviewer(s) should fill out this section before approving the PR

**Breaking Changes**
- [ ] No Breaking Changes in the Graphql API
- [ ] Technically some Breaking Changes but not expected to impact any integrations

**Issue Review**
- [ ] All requirements in original issue have been covered
- [ ] A follow up issue(s) have been created to cover additional requirements

**Tests Pass**
- [ ] Postgres
- [ ] SQLite
- [ ] Frontend



Linked issue #7608: RnR Form - Grouped Issues
<!-- NOTE: before adding something to your issue name or description, check that there does not already exist a label for it! -->

Epic: https://github.com/msupply-foundation/open-msupply-internal/issues/69

## Is your feature request related to a problem? Please describe 👀

See epic for more details;

Confirm button workflow ? (it gets focused, but no keyboard, how does user navigate to next line first column), do we need Confirm button, there are other ways to indicate to the user that pre-filled values were edited/touched.

Remove strength column ? (Louisa mentioned that it's not needed, confirmed with client too, see epic)

Error state when auto saving ? (Current error will pop up ever X seconds, and error field is highlighted, confirmation checkbox is disabled with msges)

Expiry date input ? (Much faster to just enter mm/dd, also what is this for ? I mean it's not accurate since we are dealing with batches)

## Describe the solution you'd like 🎁


### Describe alternatives you've considered 💭


### Additional context 💌

<!-- Add any other context or screenshots about the feature request here. -->

### Moneyworks Jobcode 🧰

<!-- Add the moneyworks jobcode for this change if you know what it is. -->

Linked issue comments: user: login: lache-melvin
created_at: 2025-05-12T20:10:32Z
body: Closed by #7629
author_association: CONTRIBUTOR

Diff:
diff --git a/client/packages/common/src/intl/locales/en/common.json b/client/packages/common/src/intl/locales/en/common.json
index be89caaf7ab..463151808ce 100644
--- a/client/packages/common/src/intl/locales/en/common.json
+++ b/client/packages/common/src/intl/locales/en/common.json
@@ -384,6 +384,7 @@
   "error.report-does-not-exist": "Report does not exist",
   "error.requisition-not-found": "Requisition not found",
   "error.return-not-found": "Return not found",
+  "error.rnr-has-errors": "Cannot finalise the R&R form while some lines have errors. You will be taken to the first error line",
   "error.rnr-not-found": "R&R Form not found",
   "error.scanner-not-connected": "Scanner is not connected. See the Admin > Devices section to connect a scanner.",
   "error.server-failed-to-start": "Error! The server failed to start",
@@ -1996,4 +1997,4 @@
   "description": {
     "suggested-quantity": "The number of units which the system has calculated should be ordered"
   }
-}
+}
\ No newline at end of file
diff --git a/client/packages/common/src/intl/locales/tet/common.json b/client/packages/common/src/intl/locales/tet/common.json
index 22df9603515..60a260d2ba0 100644
--- a/client/packages/common/src/intl/locales/tet/common.json
+++ b/client/packages/common/src/intl/locales/tet/common.json
@@ -123,6 +123,7 @@
   "error.provide-reason": "Tenke justifika razaun ba lina sira ne'ebe maka iha diferensia iha kalkulasaun numeru ba pakote nian.",
   "error.provide-valid-reason": "Ajustamentu razaun la han malu ho ajustamentu diresaun",
   "error.reduced-below-zero": "Lina Stock existe iha remessas sasan sai nian. Kuantidade labele reduz kiik liu husi zero.",
+  "error.rnr-has-errors": "Cannot finalise the R&R form while some lines have errors. You will be taken to the first error line",
   "error.server-failed-to-start": "Erru! Servedor falha atu Start",
   "error.server-not-found": "Hasoru problema wainhira atu lokaliza servedor mSupply Lokal nian",
   "error.shipment-not-found": "Remessa la hetan",
@@ -585,4 +586,4 @@
   "tools": "Ekipamentus",
   "warning.caps-lock": "Alerta: Caps Lock ativu hela",
   "warning.nothing-to-supply": "Laiha buat ida atu distribui!"
-}
+}
\ No newline at end of file
diff --git a/client/packages/common/src/ui/components/inputs/SearchBar/SearchBar.tsx b/client/packages/common/src/ui/components/inputs/SearchBar/SearchBar.tsx
index d1defe01959..ec8ea9d96a1 100644
--- a/client/packages/common/src/ui/components/inputs/SearchBar/SearchBar.tsx
+++ b/client/packages/common/src/ui/components/inputs/SearchBar/SearchBar.tsx
@@ -11,7 +11,8 @@ interface SearchBarProps {
   value: string;
   onChange: (value: string) => void;
   onClear?: () => void;
-  alwaysShowClear?: boolean;
+  onSearchIconClick?: () => void;
+  searchIconButtonLabel?: string;
   placeholder: string;
   isLoading?: boolean;
   debounceTime?: number;
@@ -48,12 +49,13 @@ const EndAdornment = ({
 export const SearchBar = ({
   value,
   onChange,
+  onSearchIconClick,
   onClear,
   placeholder,
   isLoading = false,
   debounceTime = 500,
   expandOnFocus = false,
-  alwaysShowClear = false,
+  searchIconButtonLabel = '',
   autoFocus = false,
 }: SearchBarProps) => {
   const [buffer, setBuffer] = useState(value);
@@ -78,6 +80,10 @@ export const SearchBar = ({
     setLoading(true);
   };
 
+  const searchIcon = (
+    <SearchIcon sx={{ color: 'gray.main', marginBottom: 1 }} fontSize="small" />
+  );
+
   return (
     <Box
       sx={{
@@ -85,10 +91,16 @@ export const SearchBar = ({
         alignItems: 'flex-end',
       }}
     >
-      <SearchIcon
-        sx={{ color: 'gray.main', marginBottom: 1 }}
-        fontSize="small"
-      />
+      {onSearchIconClick ? (
+        <IconButton
+          icon={searchIcon}
+          onClick={onSearchIconClick}
+          label={searchIconButtonLabel}
+        />
+      ) : (
+        searchIcon
+      )}
+
       <BasicTextInput
         autoFocus={autoFocus}
         slotProps={{
@@ -96,7 +108,7 @@ export const SearchBar = ({
             endAdornment: (
               <EndAdornment
                 isLoading={isLoading || loading}
-                hasValue={!!buffer || alwaysShowClear}
+                hasValue={!!buffer}
                 onClear={() => {
                   handleChange('');
                   if (onClear) onClear();
diff --git a/client/packages/requisitions/src/RnRForms/DetailView/ContentArea.tsx b/client/packages/requisitions/src/RnRForms/DetailView/ContentArea.tsx
index 0bb61f16a79..c374f05a13c 100644
--- a/client/packages/requisitions/src/RnRForms/DetailView/ContentArea.tsx
+++ b/client/packages/requisitions/src/RnRForms/DetailView/ContentArea.tsx
@@ -1,6 +1,7 @@
-import React, { useRef } from 'react';
+import React, { useEffect, useRef } from 'react';
 import {
   Box,
+  // Fade,
   GlobalStyles,
   InfoIcon,
   LocaleKey,
@@ -14,7 +15,7 @@ import {
 import { RnRFormLineFragment } from '../api/operations.generated';
 import { RnRFormLine } from './RnRFormLine';
 import { Search } from './Search';
-import { itemMatchesSearch } from '../utils';
+import { oneTime, useRnRFormContext } from '../api';
 
 interface ContentAreaProps {
   data: RnRFormLineFragment[];
@@ -56,17 +57,16 @@ export const ContentArea = ({
   const t = useTranslation();
   const containerRef = useRef<HTMLDivElement>(null);
   const listRef = useRef<ViewportListRef>(null);
+  const { setListRef } = useRnRFormContext(
+    oneTime(({ setListRef }) => ({
+      setListRef,
+    }))
+  );
 
-  const onSearch = (search: string) => {
-    const foundIndex = data.findIndex(l => itemMatchesSearch(search, l.item));
-
-    foundIndex > -1 &&
-      listRef.current?.scrollToIndex({
-        alignToTop: false,
-        // load 2 lines below the found line
-        index: foundIndex + 2,
-      });
-  };
+  useEffect(() => {
+    // Store the ref in Zustand state
+    setListRef(listRef);
+  }, [setListRef]);
 
   return data.length === 0 ? (
     <NothingHere body={t('error.no-items')} />
@@ -134,7 +134,7 @@ export const ContentArea = ({
           },
         }}
       >
-        <Headers onSearch={onSearch} />
+        <Headers />
 
         <tbody>
           <ViewportList
@@ -162,7 +162,7 @@ export const ContentArea = ({
   );
 };
 
-const Headers = ({ onSearch }: { onSearch: (value: string) => void }) => {
+const Headers = () => {
   const t = useTranslation();
   return (
     <thead>
@@ -180,11 +180,9 @@ const Headers = ({ onSearch }: { onSearch: (value: string) => void }) => {
             }}
           >
             {t('label.name')}
-            <Search onSearch={onSearch} />
+            <Search />
           </Box>
         </th>
-
-        <HeaderCell label="label.strength" width={85} />
         <HeaderCell label="label.unit" width={80} />
         <HeaderCell label="label.ven" width={55} />
         <HeaderCell
@@ -232,7 +230,6 @@ const Headers = ({ onSearch }: { onSearch: (value: string) => void }) => {
           tooltip="description.rnr-low-stock"
         />
         <HeaderCell label="label.comment" />
-        <HeaderCell label="label.confirmed" />
         <HeaderCell
           label="label.approved-quantity"
           tooltip="description.rnr-approved-quantity"
diff --git a/client/packages/requisitions/src/RnRForms/DetailView/DetailView.tsx b/client/packages/requisitions/src/RnRForms/DetailView/DetailView.tsx
index 23a4864de9f..cdfa0023d40 100644
--- a/client/packages/requisitions/src/RnRForms/DetailView/DetailView.tsx
+++ b/client/packages/requisitions/src/RnRForms/DetailView/DetailView.tsx
@@ -15,7 +15,7 @@ import { ActivityLogList } from '@openmsupply-client/system';
 import { Footer } from './Footer';
 import { AppBarButtons } from './AppBarButtons';
 import { ContentArea } from './ContentArea';
-import { RnRFormQuery, useRnRForm, useRnRFormContext } from '../api';
+import { oneTime, RnRFormQuery, useRnRForm, useRnRFormContext } from '../api';
 import { SidePanel } from './SidePanel';
 import { AutoSave } from './AutoSave';
 
@@ -23,7 +23,7 @@ export const RnRFormDetailView = () => {
   const { id = '' } = useParams();
   const [isInitialising, setIsInitialising] = useState(true);
 
-  const setInitial = useRnRFormContext(state => state.setInitial);
+  const setInitial = useRnRFormContext(oneTime(state => state.setInitial));
 
   const {
     query: { data, isLoading },
diff --git a/client/packages/requisitions/src/RnRForms/DetailView/Footer.tsx b/client/packages/requisitions/src/RnRForms/DetailView/Footer.tsx
index 31494c57b58..37ac54ea8cd 100644
--- a/client/packages/requisitions/src/RnRForms/DetailView/Footer.tsx
+++ b/client/packages/requisitions/src/RnRForms/DetailView/Footer.tsx
@@ -10,7 +10,12 @@ import {
   useConfirmationModal,
   useKeyboard,
 } from '@openmsupply-client/common';
-import { RnRFormQuery, useRnRForm, useRnRFormContext } from '../api';
+import {
+  RnRFormQuery,
+  useErrorLineIndex,
+  useRnRForm,
+  useRnRFormContext,
+} from '../api';
 import { useSaveAllLines } from './AutoSave';
 
 export const Footer = ({ data }: { data: RnRFormQuery }) => {
@@ -23,29 +28,27 @@ export const Footer = ({ data }: { data: RnRFormQuery }) => {
     finalise: { finalise, isFinalising },
   } = useRnRForm({ rnrFormId: data.id });
 
-  const { hasUnconfirmedLines, confirmUnconfirmedLines } = useRnRFormContext(
-    ({ hasUnconfirmedLines, confirmUnconfirmedLines }) => ({
-      hasUnconfirmedLines,
-      confirmUnconfirmedLines,
-    })
-  );
+  const errorLineIndex = useRnRFormContext(useErrorLineIndex);
+  const scrollToIndex = useRnRFormContext(state => state.scrollToIndex);
 
   const showFinaliseConfirmation = useConfirmationModal({
     onConfirm: async () => {
       try {
-        if (hasUnconfirmedLines()) {
-          confirmUnconfirmedLines();
+        if (errorLineIndex != -1) {
+          scrollToIndex(errorLineIndex);
+        } else {
           await saveAllLines();
+          await finalise();
+          success(t('label.finalised'))();
         }
-        await finalise();
-        success(t('label.finalised'))();
       } catch (e) {
         error((e as Error).message)();
       }
     },
-    message: hasUnconfirmedLines()
-      ? `${t('messages.rnr-not-all-lines-confirmed')}\n${t('messages.confirm-finalise-rnr')}`
-      : t('messages.confirm-finalise-rnr'),
+    message:
+      errorLineIndex != -1
+        ? t('error.rnr-has-errors')
+        : t('messages.confirm-finalise-rnr'),
     title: t('heading.are-you-sure'),
   });
 
diff --git a/client/packages/requisitions/src/RnRForms/DetailView/RnRFormLine.tsx b/client/packages/requisitions/src/RnRForms/DetailView/RnRFormLine.tsx
index ded27171a6d..f7914576c7c 100644
--- a/client/packages/requisitions/src/RnRForms/DetailView/RnRFormLine.tsx
+++ b/client/packages/requisitions/src/RnRForms/DetailView/RnRFormLine.tsx
@@ -2,8 +2,6 @@ import React from 'react';
 import {
   AlertIcon,
   BasicTextInput,
-  Checkbox,
-  CircleIcon,
   DatePicker,
   Formatter,
   LowStockStatus,
@@ -13,7 +11,6 @@ import {
   Tooltip,
   useAuthContext,
   useBufferState,
-  useNotification,
   useTheme,
   VenCategoryType,
 } from '@openmsupply-client/common';
@@ -27,18 +24,13 @@ export const RnRFormLine = ({
   lineId,
 }: {
   lineId: string;
-
   periodLength: number;
   disabled: boolean;
 }) => {
   const theme = useTheme();
   const { store } = useAuthContext();
-  const { error } = useNotification();
-
   const lineState = useRnRFormContext(useCachedRnRDraftLine(lineId));
 
-  // console.log('rendering', lineState?.line.id, lineId);
-
   if (!lineState) return null;
 
   const { line, setLine, highlight } = lineState;
@@ -117,6 +109,7 @@ export const RnRFormLine = ({
 
   const readOnlyBackgroundColor = theme.palette.background.drawer;
   const highlightColour = theme.palette.chart.cold.light;
+  const errorColour = theme.palette.chart.hot.light;
 
   const readOnlyColumn = {
     backgroundColor: readOnlyBackgroundColor,
@@ -125,7 +118,12 @@ export const RnRFormLine = ({
   };
   const itemDetailStyle = {
     ...readOnlyColumn,
-    backgroundColor: highlight ? highlightColour : readOnlyBackgroundColor,
+    backgroundColor:
+      line.finalBalance < 0
+        ? errorColour
+        : highlight
+          ? highlightColour
+          : readOnlyBackgroundColor,
   };
 
   return (
@@ -141,7 +139,6 @@ export const RnRFormLine = ({
       <td className="sticky-column second-column" style={itemDetailStyle}>
         {line.item.name}
       </td>
-      <td style={readOnlyColumn}>{line.item.strength}</td>
       <td style={readOnlyColumn}>{line.item.unitName}</td>
       <td style={{ ...readOnlyColumn, textAlign: 'center' }}>{venCategory}</td>
 
@@ -197,9 +194,10 @@ export const RnRFormLine = ({
 
       {/* Readonly calculated values */}
       <RnRNumberCell
-        backgroundColor={readOnlyBackgroundColor}
+        backgroundColor={
+          line.finalBalance < 0 ? errorColour : readOnlyBackgroundColor
+        }
         value={line.finalBalance}
-        error={line.finalBalance < 0}
         textColor={textColor}
       />
       <RnRNumberCell
@@ -278,34 +276,6 @@ export const RnRFormLine = ({
         />
       </td>
 
-      {/* Confirm the line */}
-      <td style={{ textAlign: 'center' }}>
-        {
-          <>
-            <Checkbox
-              tabIndex={-1}
-              checked={!!line.confirmed}
-              size="medium"
-              onClick={async () => {
-                if (line.finalBalance < 0) {
-                  error('Final balance should not be below 0')();
-                  return;
-                }
-                setLine({ ...line, confirmed: !line.confirmed });
-              }}
-              disabled={disabled}
-              sx={{ marginLeft: '10px' }}
-            />
-            <CircleIcon
-              sx={{
-                width: '10px',
-                visibility: line?.isDirty ? 'visible' : 'hidden',
-                color: 'secondary.main',
-              }}
-            />
-          </>
-        }
-      </td>
       {/* Readonly - populated from Response Requisition */}
       <RnRNumberCell
         backgroundColor={readOnlyBackgroundColor}
diff --git a/client/packages/requisitions/src/RnRForms/DetailView/Search.tsx b/client/packages/requisitions/src/RnRForms/DetailView/Search.tsx
index 05158e67f89..d85a9bb6db3 100644
--- a/client/packages/requisitions/src/RnRForms/DetailView/Search.tsx
+++ b/client/packages/requisitions/src/RnRForms/DetailView/Search.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
 import {
   Box,
   IconButton,
@@ -8,24 +8,32 @@ import {
 } from '@openmsupply-client/common';
 import { useRnRFormContext } from '../api';
 
-export const Search = (props: { onSearch: (value: string) => void }) => {
+export const Search = () => {
   const t = useTranslation();
 
-  const { search, setSearch } = useRnRFormContext(({ search, setSearch }) => ({
-    search,
-    setSearch,
-  }));
+  const { search, scrollToIndex, resetSearch } = useRnRFormContext(
+    ({ search, scrollToIndex, resetSearch }) => ({
+      search,
+      scrollToIndex,
+      resetSearch,
+    })
+  );
 
   const [showSearch, setShowSearch] = useState(false);
-  const [input, setInput] = useState(search);
+  const [input, setInput] = useState('');
+
+  useEffect(() => {
+    return () => resetSearch();
+  }, []);
 
   const onSearch = (value: string) => {
     setInput(value);
     // Only search when 3+ characters are entered
     if (value.length > 2) {
-      setSearch(value);
-      props.onSearch(value);
-    } else setSearch('');
+      let found = search(value);
+
+      found != -1 && scrollToIndex(found);
+    } else resetSearch();
   };
 
   return (
@@ -48,8 +56,8 @@ export const Search = (props: { onSearch: (value: string) => void }) => {
             onChange={onSearch}
             placeholder={t('placeholder.search-by-name-or-code')}
             onClear={() => setShowSearch(false)}
-            debounceTime={0}
-            alwaysShowClear
+            onSearchIconClick={() => setShowSearch(false)}
+            searchIconButtonLabel={t('button.close')}
             autoFocus
           />
         </Box>
diff --git a/client/packages/requisitions/src/RnRForms/api/hooks/useRnRForm.ts b/client/packages/requisitions/src/RnRForms/api/hooks/useRnRForm.ts
index 36379799f54..11e944ad6b8 100644
--- a/client/packages/requisitions/src/RnRForms/api/hooks/useRnRForm.ts
+++ b/client/packages/requisitions/src/RnRForms/api/hooks/useRnRForm.ts
@@ -53,15 +53,6 @@ export const useRnRForm = ({ rnrFormId }: { rnrFormId: string }) => {
     debouncedUpdateRnRForm(patch);
   };
 
-  // const confirmRemainingLines = async () => {
-  //   if (!query.data) return;
-
-  //   const lines = query.data.lines
-  //     .filter(line => !line.confirmed)
-  //     .map(line => ({ ...line, confirmed: true }));
-  //   await updateLines(lines);
-  // };
-
   return {
     query,
     finalise: { finalise, isFinalising, finaliseError },
diff --git a/client/packages/requisitions/src/RnRForms/api/hooks/useRnRFormContext.ts b/client/packages/requisitions/src/RnRForms/api/hooks/useRnRFormContext.ts
index 5c3fbd20a3a..51bd70ec50e 100644
--- a/client/packages/requisitions/src/RnRForms/api/hooks/useRnRFormContext.ts
+++ b/client/packages/requisitions/src/RnRForms/api/hooks/useRnRFormContext.ts
@@ -1,32 +1,41 @@
 import React from 'react';
 import { RnRFormLineFragment } from '../operations.generated';
-import { create, RecordWithId } from '@openmsupply-client/common';
+import {
+  create,
+  RecordWithId,
+  ViewportListRef,
+} from '@openmsupply-client/common';
 import keyBy from 'lodash/keyBy';
 import mapValues from 'lodash/mapValues';
 import { itemMatchesSearch } from '../../utils';
 
 type SetLine = (line: RecordWithId & Partial<RnRFormLineFragment>) => void;
 interface RnRFormContext {
+  listRef?: React.RefObject<ViewportListRef>;
   rnrFormId: string;
-  search: string;
+  foundIds: { [id: string]: boolean };
   baseLines: { [id: string]: RnRFormLineFragment };
+  baseLineIndexes: { [id: string]: number };
   draftLines: Record<
     string,
     RecordWithId & Partial<RnRFormLineFragment> & { isDirty?: boolean }
   >;
   draftLineIteration: Record<string, number>;
   setDraftLine: SetLine;
-  setSearch: (search: string) => void;
   clearAllDirtyLines: () => void;
-  getAllDirtyLines: () => RnRFormLineFragment[];
+  getAllDirtyLines: (ignoreError?: boolean) => RnRFormLineFragment[];
   setInitial: (rnrFormId: string, _: RnRFormLineFragment[]) => void;
-  hasUnconfirmedLines: () => boolean;
-  confirmUnconfirmedLines: () => void;
+  setListRef: (_: React.RefObject<ViewportListRef>) => void;
+  scrollToIndex: (_: number) => void;
+  search: (_: string) => number;
+  resetSearch: () => void;
 }
 
 export const useRnRFormContext = create<RnRFormContext>((set, get) => ({
-  search: '',
+  listRef: undefined,
+  foundIds: {},
   baseLines: {},
+  baseLineIndexes: {},
   draftLines: {},
   draftLineIteration: {},
   rnrFormId: '',
@@ -43,12 +52,6 @@ export const useRnRFormContext = create<RnRFormContext>((set, get) => ({
       },
     }));
   },
-  setSearch: search => {
-    set(state => ({
-      ...state,
-      search,
-    }));
-  },
   clearAllDirtyLines: () =>
     set(state => ({
       ...state,
@@ -65,15 +68,26 @@ export const useRnRFormContext = create<RnRFormContext>((set, get) => ({
     set(state => ({
       ...state,
       baseLines: keyBy(baseLines, 'id'),
+      baseLineIndexes: mapValues(
+        keyBy(
+          baseLines.map(({ id }, index) => ({ id, index })),
+          'id'
+        ),
+        ({ index }) => index
+      ),
       rnrFormId,
       draftLines: {},
       draftLineIteration: {},
     }));
   },
-  getAllDirtyLines: () => {
+  getAllDirtyLines: (ignoreError = true) => {
     const { baseLines, draftLines } = get();
     return Object.values(draftLines).flatMap(draftLine => {
-      if (!draftLine.isDirty) return [];
+      if (
+        !draftLine.isDirty ||
+        (ignoreError && (draftLine?.finalBalance || 0) < 0)
+      )
+        return [];
       const baseLine = baseLines[draftLine.id];
       if (!baseLine) return [];
 
@@ -91,29 +105,30 @@ export const useRnRFormContext = create<RnRFormContext>((set, get) => ({
       return baseLine.confirmed;
     });
   },
-  confirmUnconfirmedLines: () => {
-    const { baseLines, draftLines } = get();
-    const toBeConfirmed = Object.values(baseLines).flatMap(baseLine => {
-      const draftLine = draftLines[baseLine.id] || { id: baseLine.id };
-      const line = { ...baseLine, ...draftLine };
-      if (!line.confirmed)
-        return [{ ...draftLine, confirmed: true, isDirty: true }];
+  setListRef: listRef => set(state => ({ ...state, listRef })),
+  scrollToIndex: index =>
+    get().listRef?.current?.scrollToIndex({
+      alignToTop: false,
+      // load 2 lines below the found line
+      index: index + 2,
+    }),
+  resetSearch: () => set(state => ({ ...state, foundIds: {} })),
+  search: term => {
+    let found = Object.values(get().baseLines).filter(l =>
+      itemMatchesSearch(term, l.item)
+    );
+    let first = found[0];
+    if (!first) {
+      set(state => ({ ...state, foundIds: {} }));
+      return -1;
+    }
 
-      return [];
-    });
-
-    const toBeConfirmedById = keyBy(toBeConfirmed, 'id');
     set(state => ({
       ...state,
-      draftLines: { ...state.draftLines, ...toBeConfirmedById },
-      draftLineIteration: {
-        ...state.draftLineIteration,
-        ...mapValues(
-          toBeConfirmedById,
-          ({ id }) => (state.draftLineIteration[id] || 0) + 1
-        ),
-      },
+      foundIds: mapValues(keyBy(found, 'id'), () => true),
     }));
+
+    return get().baseLineIndexes[first.id] || -1;
   },
 }));
 
@@ -135,15 +150,22 @@ export const useRnRDraft = (id: string) => {
   };
 };
 
-// export function useShallow<S, U>(selector: (state: S) => U): (state: S) => U {
-//   const prev = React.useRef<U | undefined>(undefined);
-//   return state => {
-//     const next = selector(state);
-//     return shallow(prev.current, next)
-//       ? (prev.current as U)
-//       : (prev.current = next);
-//   };
-// }
+export function oneTime<S, U>(selector: (state: S) => U): (state: S) => U {
+  const once = React.useRef<U | undefined>(undefined);
+  return state => {
+    const next = selector(state);
+    return !!once.current ? (once.current as U) : (once.current = next);
+  };
+}
+
+export const useErrorLineIndex = (state: RnRFormContext) => {
+  const firstErrorLine = Object.values(state.draftLines).find(
+    draftLine => (draftLine?.finalBalance || 0) < 0
+  );
+
+  if (!firstErrorLine) return -1;
+  return state.baseLineIndexes[firstErrorLine.id] || -1;
+};
 
 export const useCachedRnRDraftLine = (id: string) => {
   const prevIteration = React.useRef(-1);
@@ -165,8 +187,7 @@ export const useCachedRnRDraftLine = (id: string) => {
 
     const line = { ...baseLine, ...(state.draftLines[id] || {}) };
 
-    const highlight =
-      !!state.search && itemMatchesSearch(state.search, line.item);
+    const highlight = state.foundIds[id] || false;
 
     const shouldUpdate =
       previousIteration !== (state.draftLineIteration[id] ?? 0) ||


Reviews:
user: login: andreievg
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T00:11:49Z
user: login: andreievg
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T00:12:14Z
user: login: andreievg
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T00:12:46Z
user: login: andreievg
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T00:13:08Z
user: login: andreievg
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T00:14:14Z
user: login: andreievg
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T00:15:53Z
user: login: andreievg
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T02:38:35Z
user: login: lache-melvin
body: Looking gooood 🔥 
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T03:18:12Z
user: login: andreievg
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T03:40:24Z
user: login: andreievg
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T03:43:14Z
user: login: andreievg
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T04:09:35Z
user: login: lache-melvin
body: Sweeeet thanks for changes 🚀 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T04:12:51Z

Review comments:
diff_hunk: @@ -56,17 +57,16 @@ export const ContentArea = ({
   const t = useTranslation();
   const containerRef = useRef<HTMLDivElement>(null);
   const listRef = useRef<ViewportListRef>(null);
+  const { setListRef } = useRnRFormContext(
path: client/packages/requisitions/src/RnRForms/DetailView/ContentArea.tsx
user: login: andreievg
body: As per PR description, mainly relocated search logic because need to scroll to the error record on finalise, so that all got moved and i decided to also move search functionality to the context
created_at: 2025-05-07T00:11:49Z
author_association: COLLABORATOR
diff_hunk: @@ -23,29 +28,27 @@ export const Footer = ({ data }: { data: RnRFormQuery }) => {
     finalise: { finalise, isFinalising },
   } = useRnRForm({ rnrFormId: data.id });
 
-  const { hasUnconfirmedLines, confirmUnconfirmedLines } = useRnRFormContext(
-    ({ hasUnconfirmedLines, confirmUnconfirmedLines }) => ({
-      hasUnconfirmedLines,
-      confirmUnconfirmedLines,
-    })
-  );
+  const errorLineIndex = useRnRFormContext(useErrorLineIndex);
+  const scrollToIndex = useRnRFormContext(state => state.scrollToIndex);
 
   const showFinaliseConfirmation = useConfirmationModal({
     onConfirm: async () => {
       try {
-        if (hasUnconfirmedLines()) {
-          confirmUnconfirmedLines();
+        if (errorLineIndex != -1) {
+          scrollToIndex(errorLineIndex);
+        } else {
           await saveAllLines();
+          await finalise();
+          success(t('label.finalised'))();
         }
-        await finalise();
-        success(t('label.finalised'))();
       } catch (e) {
         error((e as Error).message)();
       }
     },
-    message: hasUnconfirmedLines()
-      ? `${t('messages.rnr-not-all-lines-confirmed')}\n${t('messages.confirm-finalise-rnr')}`
-      : t('messages.confirm-finalise-rnr'),
+    message:
+      errorLineIndex != -1
+        ? `You will be taken to first error line`
path: client/packages/requisitions/src/RnRForms/DetailView/Footer.tsx
user: login: andreievg
body: Needs to be translated will add to the carry over issue
created_at: 2025-05-07T00:12:14Z
author_association: COLLABORATOR
diff_hunk: @@ -50,7 +58,6 @@ export const Search = (props: { onSearch: (value: string) => void }) => {
             onClear={() => setShowSearch(false)}
             onSearchIconClick={() => setShowSearch(false)}
             searchIconButtonLabel={t('button.close')}
-            debounceTime={0}
path: client/packages/requisitions/src/RnRForms/DetailView/Search.tsx
user: login: andreievg
body: I think we do need debounce here
created_at: 2025-05-07T00:12:45Z
author_association: COLLABORATOR
diff_hunk: @@ -8,24 +8,32 @@ import {
 } from '@openmsupply-client/common';
 import { useRnRFormContext } from '../api';
 
-export const Search = (props: { onSearch: (value: string) => void }) => {
+export const Search = () => {
   const t = useTranslation();
 
-  const { search, setSearch } = useRnRFormContext(({ search, setSearch }) => ({
-    search,
-    setSearch,
-  }));
+  const { search, scrollToIndex, resetSearch } = useRnRFormContext(
path: client/packages/requisitions/src/RnRForms/DetailView/Search.tsx
user: login: andreievg
body: More comments about this in the context
created_at: 2025-05-07T00:13:08Z
author_association: COLLABORATOR
diff_hunk: @@ -1,32 +1,41 @@
 import React from 'react';
 import { RnRFormLineFragment } from '../operations.generated';
-import { create, RecordWithId } from '@openmsupply-client/common';
+import {
path: client/packages/requisitions/src/RnRForms/api/hooks/useRnRFormContext.ts
user: login: andreievg
body: Needed to scroll in footer, so added ref and scrolling here.

Also wanted cached rnr draft to be very lean, so created foundbyids
created_at: 2025-05-07T00:14:13Z
author_association: COLLABORATOR
diff_hunk: @@ -56,17 +57,16 @@ export const ContentArea = ({
   const t = useTranslation();
   const containerRef = useRef<HTMLDivElement>(null);
   const listRef = useRef<ViewportListRef>(null);
+  const { setListRef } = useRnRFormContext(
path: client/packages/requisitions/src/RnRForms/DetailView/ContentArea.tsx
user: login: andreievg
body: Why oneTime ? I noticed that ContentArea was refreshed when some other state in RnRFormContext was changed, didn't investigate further but oneTime works (also added to DetailView, just in case)
created_at: 2025-05-07T00:15:53Z
author_association: COLLABORATOR
diff_hunk: @@ -15,15 +15,15 @@ import { ActivityLogList } from '@openmsupply-client/system';
 import { Footer } from './Footer';
 import { AppBarButtons } from './AppBarButtons';
 import { ContentArea } from './ContentArea';
-import { RnRFormQuery, useRnRForm, useRnRFormContext } from '../api';
+import { oneTime, RnRFormQuery, useRnRForm, useRnRFormContext } from '../api';
 import { SidePanel } from './SidePanel';
 import { AutoSave } from './AutoSave';
 
 export const RnRFormDetailView = () => {
   const { id = '' } = useParams();
   const [isInitialising, setIsInitialising] = useState(true);
 
-  const setInitial = useRnRFormContext(state => state.setInitial);
+  const setInitial = useRnRFormContext(oneTime(state => state.setInitial));
path: client/packages/requisitions/src/RnRForms/DetailView/DetailView.tsx
user: login: andreievg
body: hmm, so i noticed that content area was refreshing due to some changes i've added, so added oneTime just in case
created_at: 2025-05-07T02:38:35Z
author_association: COLLABORATOR
diff_hunk: @@ -23,29 +28,27 @@ export const Footer = ({ data }: { data: RnRFormQuery }) => {
     finalise: { finalise, isFinalising },
   } = useRnRForm({ rnrFormId: data.id });
 
-  const { hasUnconfirmedLines, confirmUnconfirmedLines } = useRnRFormContext(
-    ({ hasUnconfirmedLines, confirmUnconfirmedLines }) => ({
-      hasUnconfirmedLines,
-      confirmUnconfirmedLines,
-    })
-  );
+  const errorLineIndex = useRnRFormContext(useErrorLineIndex);
+  const scrollToIndex = useRnRFormContext(state => state.scrollToIndex);
 
   const showFinaliseConfirmation = useConfirmationModal({
     onConfirm: async () => {
       try {
-        if (hasUnconfirmedLines()) {
-          confirmUnconfirmedLines();
+        if (errorLineIndex != -1) {
+          scrollToIndex(errorLineIndex);
+        } else {
           await saveAllLines();
+          await finalise();
+          success(t('label.finalised'))();
         }
-        await finalise();
-        success(t('label.finalised'))();
       } catch (e) {
         error((e as Error).message)();
       }
     },
-    message: hasUnconfirmedLines()
-      ? `${t('messages.rnr-not-all-lines-confirmed')}\n${t('messages.confirm-finalise-rnr')}`
-      : t('messages.confirm-finalise-rnr'),
+    message:
+      errorLineIndex != -1
+        ? `You will be taken to first error line`
path: client/packages/requisitions/src/RnRForms/DetailView/Footer.tsx
user: login: lache-melvin
body: Quick to add to common.json, can't we just do now?

I think would be helpful to say something more like: `Cannot finalise the R&R form while some lines have errors. You will be taken to the first error line.`

Or maybe even, `..would you like to be taken to the first error line?` idk
created_at: 2025-05-07T02:59:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,29 +28,27 @@ export const Footer = ({ data }: { data: RnRFormQuery }) => {
     finalise: { finalise, isFinalising },
   } = useRnRForm({ rnrFormId: data.id });
 
-  const { hasUnconfirmedLines, confirmUnconfirmedLines } = useRnRFormContext(
-    ({ hasUnconfirmedLines, confirmUnconfirmedLines }) => ({
-      hasUnconfirmedLines,
-      confirmUnconfirmedLines,
-    })
-  );
+  const errorLineIndex = useRnRFormContext(useErrorLineIndex);
+  const scrollToIndex = useRnRFormContext(state => state.scrollToIndex);
 
   const showFinaliseConfirmation = useConfirmationModal({
     onConfirm: async () => {
       try {
-        if (hasUnconfirmedLines()) {
-          confirmUnconfirmedLines();
+        if (errorLineIndex != -1) {
+          scrollToIndex(errorLineIndex);
path: client/packages/requisitions/src/RnRForms/DetailView/Footer.tsx
user: login: lache-melvin
body: Noticed that there was no way to tell which line you had been scrolled to? Error cell no longer highlighted (as final balance is disabled field, so just rendering <p>) and even if it was, it's usually too far right to be view...

janky first pass to highlight when error state ... 

https://github.com/msupply-foundation/open-msupply/compare/7608-RnR-form-remove-confirm,-strength,-navigate-to-error-on-finalise...7608-RnR-form-remove-confirm,-strength,-navigate-to-error-on-finalise(highlight-error-line)

Probably would want to delay highlighting, they might be about to enter other columns that solve it? And probably want to share the "row is error" logic, in case we add more things than finalbalance < 0....

Though, now that you aren't attempting to save the error rows, probably want feedback to user fairly quickly about error row, because you removed the blocking navigation? They would lose the data they entered for any error rows, with no warning. Maybe we put check for error rows in confirm on leaving hook??
created_at: 2025-05-07T03:17:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,29 +28,27 @@ export const Footer = ({ data }: { data: RnRFormQuery }) => {
     finalise: { finalise, isFinalising },
   } = useRnRForm({ rnrFormId: data.id });
 
-  const { hasUnconfirmedLines, confirmUnconfirmedLines } = useRnRFormContext(
-    ({ hasUnconfirmedLines, confirmUnconfirmedLines }) => ({
-      hasUnconfirmedLines,
-      confirmUnconfirmedLines,
-    })
-  );
+  const errorLineIndex = useRnRFormContext(useErrorLineIndex);
+  const scrollToIndex = useRnRFormContext(state => state.scrollToIndex);
 
   const showFinaliseConfirmation = useConfirmationModal({
     onConfirm: async () => {
       try {
-        if (hasUnconfirmedLines()) {
-          confirmUnconfirmedLines();
+        if (errorLineIndex != -1) {
+          scrollToIndex(errorLineIndex);
+        } else {
           await saveAllLines();
+          await finalise();
+          success(t('label.finalised'))();
         }
-        await finalise();
-        success(t('label.finalised'))();
       } catch (e) {
         error((e as Error).message)();
       }
     },
-    message: hasUnconfirmedLines()
-      ? `${t('messages.rnr-not-all-lines-confirmed')}\n${t('messages.confirm-finalise-rnr')}`
-      : t('messages.confirm-finalise-rnr'),
+    message:
+      errorLineIndex != -1
+        ? `You will be taken to first error line`
path: client/packages/requisitions/src/RnRForms/DetailView/Footer.tsx
user: login: andreievg
body: Agree, thanks: ee39aa5b2bd6c12cdd66ee8854d27eaba7e0c92d, asked @louisaw123 for translation for this
created_at: 2025-05-08T03:40:24Z
author_association: COLLABORATOR
diff_hunk: @@ -23,29 +28,27 @@ export const Footer = ({ data }: { data: RnRFormQuery }) => {
     finalise: { finalise, isFinalising },
   } = useRnRForm({ rnrFormId: data.id });
 
-  const { hasUnconfirmedLines, confirmUnconfirmedLines } = useRnRFormContext(
-    ({ hasUnconfirmedLines, confirmUnconfirmedLines }) => ({
-      hasUnconfirmedLines,
-      confirmUnconfirmedLines,
-    })
-  );
+  const errorLineIndex = useRnRFormContext(useErrorLineIndex);
+  const scrollToIndex = useRnRFormContext(state => state.scrollToIndex);
 
   const showFinaliseConfirmation = useConfirmationModal({
     onConfirm: async () => {
       try {
-        if (hasUnconfirmedLines()) {
-          confirmUnconfirmedLines();
+        if (errorLineIndex != -1) {
+          scrollToIndex(errorLineIndex);
path: client/packages/requisitions/src/RnRForms/DetailView/Footer.tsx
user: login: andreievg
body: > Though, now that you aren't attempting to save the error rows, probably want feedback to user fairly quickly about error row, because you removed the blocking navigation? They would lose the data they entered for any error rows, with no warning. Maybe we put check for error rows in confirm on leaving hook??

Yep, added to carry over issue, I am really tempted to make another quick PR with removing auto save, and persisting zustand state to local storage
created_at: 2025-05-08T03:43:14Z
author_association: COLLABORATOR
diff_hunk: @@ -23,29 +28,27 @@ export const Footer = ({ data }: { data: RnRFormQuery }) => {
     finalise: { finalise, isFinalising },
   } = useRnRForm({ rnrFormId: data.id });
 
-  const { hasUnconfirmedLines, confirmUnconfirmedLines } = useRnRFormContext(
-    ({ hasUnconfirmedLines, confirmUnconfirmedLines }) => ({
-      hasUnconfirmedLines,
-      confirmUnconfirmedLines,
-    })
-  );
+  const errorLineIndex = useRnRFormContext(useErrorLineIndex);
+  const scrollToIndex = useRnRFormContext(state => state.scrollToIndex);
 
   const showFinaliseConfirmation = useConfirmationModal({
     onConfirm: async () => {
       try {
-        if (hasUnconfirmedLines()) {
-          confirmUnconfirmedLines();
+        if (errorLineIndex != -1) {
+          scrollToIndex(errorLineIndex);
path: client/packages/requisitions/src/RnRForms/DetailView/Footer.tsx
user: login: andreievg
body: Thanks for the suggested change, look and works great, it's ok to go red as soon as there is problem with balance, since if user does things in sequence they will have an error on the last edit
created_at: 2025-05-08T04:09:34Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-07T00:14:53Z
body: <!-- GITHUB_ACTION_COMMENT_PR -->
## Bundle size difference
Comparing this PR to `main`

|Old size      | New size     | Diff                     |
|:------------ |:-------------|:-------------------------|
|6.09 MB         |6.09 MB         | -200 B (-0.00%) |

author_association: CONTRIBUTOR
user: login: lache-melvin
created_at: 2025-05-08T04:13:36Z
body: Let me know when we have a consolidated PR with all of these changes ? with frontend compilation passing etc? 🫠 
author_association: CONTRIBUTOR