Repository: Enterprise-CMCS/macpro-mdct-mcr
Pull request #12120: Plan compliance PDF part 5: Calculate exceptions and non-compliance counts
Author: login: gmrabian
Labels: name: ready for review
description: Ready for all the reviews!
Created: 2025-04-16T18:09:05Z | Merged: 2025-04-17T18:56:07Z

### Description
<!-- Detailed description of changes and related context -->
- Calculate exceptions and non-compliance counts for display on PDF page
- Move two functions from `PlanComplianceTableOverlay` to shared `naaarPlanCompliance` util for reusability

### Related ticket(s)
<!-- Link to related ticket(s) or issue(s) -->
<!-- Hint: Type CMDCT-<ticket-number> for autolinking -->
CMDCT-4467 part 5

---
### How to test
<!-- Step-by-step instructions on how to test, if necessary -->
Already set up in [deployed env](https://d9x1dau963fsg.cloudfront.net/)
- Log in as a state user
- Open the created NAAAR and view the plan compliance section of the PDF
- There are four plans created, each representing a different level of completion and answer path
- Verify the display matches [Figma](https://www.figma.com/design/QcoVlfZ2adUBnzcHiI0G0S/MDCT-MCR-NAAAR?node-id=1356-27098&t=PbV1aWE2f3Kn15Ng-4) for each case

---
### Notes
There is a console error for unique keys, specifically noticed it after I had selected "No" on a 438.68 assurance with two plans.
I have `plan.id` set as the key on the top-level `Box` when mapping plans, so I'm not sure where else to look. However, I don't see that key coming up in the browser inspect either. I tried `Fragment` as well and the key didn't show.

---
### Pre-review checklist
<!-- Complete the following steps before opening for review -->
- [x] I have added [thorough](https://shorturl.at/aejkF) tests, if necessary
- [ ] I have updated relevant documentation, if necessary
- [x] I have performed a self-review of my code
- [x] I have manually tested this PR in the deployed cloud environment

---
### Pre-merge checklist
<!-- Complete the following steps before merging -->

#### Review
- [x] Design: This work has been reviewed and approved by design, if necessary
- [x] Product: This work has been reviewed and approved by product owner, if necessary
---



Diff:
diff --git a/logs/todos.log b/logs/todos.log
index 0cd325d0b..03361946e 100644
--- a/logs/todos.log
+++ b/logs/todos.log
@@ -2,8 +2,6 @@
 
 ./services/app-api/handlers/reports/fetch.ts:69: TODO: strict typing
 ./services/app-api/handlers/reports/fetch.ts:80: TODO: strict typing
-./services/ui-src/src/components/export/ExportedPlanOverlayReportSection.tsx:64: TODO: calculate counts */}
-./services/ui-src/src/components/export/ExportedPlanOverlayReportSection.tsx:69: TODO: calculate counts */}
 ./services/ui-src/src/components/layout/Timeout.tsx:36: TODO: When autosave is implemented, set up a callback function to listen to calls to update in authLifecycle
 ./services/ui-src/src/components/reports/ModalOverlayReportPage.test.tsx:170:todo Write a test to make sure admins can't click/change details?
 ./services/ui-src/src/utils/autosave/autosave.ts:193: TODO: modify DynamicField hydrationValue lifecycle so we can implement a stricter check,
diff --git a/services/ui-src/src/components/export/ExportedPlanOverlayReportSection.test.tsx b/services/ui-src/src/components/export/ExportedPlanOverlayReportSection.test.tsx
index 816a35e59..39f5f3b02 100644
--- a/services/ui-src/src/components/export/ExportedPlanOverlayReportSection.test.tsx
+++ b/services/ui-src/src/components/export/ExportedPlanOverlayReportSection.test.tsx
@@ -56,16 +56,17 @@ describe("<ExportedPlanOverlayReportSection />", () => {
     }
   });
 
-  test("ExportedPlanOverlayReportSection displays compliance tables", () => {
+  test("ExportedPlanOverlayReportSection displays compliance tables with plans but no response", () => {
+    const notAnsweredText = "Not answered";
     render(exportedPlanOverlaySectionComponent);
     expect(
       screen.getAllByRole("row", {
-        name: `${mockFormHeading1} ${mockFormHint1}`,
+        name: `${mockFormHeading1} ${mockFormHint1} ${notAnsweredText}`,
       })[0]
     ).toBeInTheDocument();
     expect(
       screen.getAllByRole("row", {
-        name: `${mockFormHeading2} ${mockFormHint2}`,
+        name: `${mockFormHeading2} ${mockFormHint2} ${notAnsweredText}`,
       })[0]
     ).toBeInTheDocument();
   });
diff --git a/services/ui-src/src/components/export/ExportedPlanOverlayReportSection.tsx b/services/ui-src/src/components/export/ExportedPlanOverlayReportSection.tsx
index 78ba19d45..d1d599552 100644
--- a/services/ui-src/src/components/export/ExportedPlanOverlayReportSection.tsx
+++ b/services/ui-src/src/components/export/ExportedPlanOverlayReportSection.tsx
@@ -8,7 +8,12 @@ import { exportTableSx } from "./ExportedReportFieldTable";
 // types
 import { EntityShape, PlanOverlayReportPageShape } from "types";
 // utils
-import { parseCustomHtml, useStore } from "utils";
+import {
+  getExceptionsNonComplianceCounts,
+  getExceptionsNonComplianceKeys,
+  parseCustomHtml,
+  useStore,
+} from "utils";
 
 /*
  * Designed originally for the plan compliance portion of the NAAAR report
@@ -18,6 +23,7 @@ export const ExportedPlanOverlayReportSection = ({ section }: Props) => {
   const { report } = useStore();
 
   const plans = report?.fieldData?.plans;
+  const standards = report?.fieldData?.standards ?? [];
 
   if (!plans) {
     return null;
@@ -37,11 +43,18 @@ export const ExportedPlanOverlayReportSection = ({ section }: Props) => {
 
   const displayPlansList = () => {
     return plans.map((plan: EntityShape) => {
-      const answer43868 = plan?.planCompliance43868_assurance?.[0]?.value ?? "";
-      const answer438206 =
-        plan?.planCompliance438206_assurance?.[0]?.value ?? "";
+      const answer43868 = plan?.planCompliance43868_assurance?.[0]?.value;
+      const answer438206 = plan?.planCompliance438206_assurance?.[0]?.value;
       const isNotCompliant43868 = answer43868 === nonCompliantLabel;
 
+      // counts
+      const exceptionsNonComplianceKeys = getExceptionsNonComplianceKeys(plan);
+      const { exceptionsCount, nonComplianceCount } =
+        getExceptionsNonComplianceCounts(exceptionsNonComplianceKeys);
+      const standardsTotalCount = standards.length;
+      const exceptionsCountText = `Total: ${exceptionsCount} of ${standardsTotalCount}`;
+      const nonComplianceCountText = `Total: ${nonComplianceCount} of ${standardsTotalCount}`;
+
       return (
         <Box key={plan.id}>
           <Heading as="h3" sx={sx.planNameHeading}>
@@ -61,13 +74,11 @@ export const ExportedPlanOverlayReportSection = ({ section }: Props) => {
               <Heading as="h5" sx={sx.h5}>
                 Non-compliant standards for 438.68
               </Heading>
-              {/* TODO: calculate counts */}
-              <Text sx={sx.count}>Total: 1 of 2</Text>
+              <Text sx={sx.count}>{nonComplianceCountText}</Text>
               <Heading as="h5" sx={sx.h5}>
                 Exceptions standards for 438.68
               </Heading>
-              {/* TODO: calculate counts */}
-              <Text sx={sx.count}>Total: 1 of 2</Text>
+              <Text sx={sx.count}>{exceptionsCountText}</Text>
             </>
           )}
           {complianceTable(
@@ -119,7 +130,11 @@ const complianceTable = (
             <Text sx={sx.fieldLabel}>{heading}</Text>
             <Text sx={sx.fieldHint}>{parseCustomHtml(hint)}</Text>
           </Td>
-          <Td>{answer}</Td>
+          <Td sx={sx.answerCell}>
+            <Text sx={!answer ? sx.notAnsweredStyling : {}}>
+              {answer ?? "Not answered"}
+            </Text>
+          </Td>
         </Tr>
       </Table>
     </>
@@ -140,6 +155,12 @@ const sx = {
     lineHeight: "lg",
     color: "palette.gray_medium",
   },
+  answerCell: {
+    width: "50%",
+  },
+  notAnsweredStyling: {
+    color: "palette.error_darker",
+  },
   count: {
     color: "palette.gray_medium",
     fontWeight: "bold",
diff --git a/services/ui-src/src/components/overlays/PlanComplianceTableOverlay.tsx b/services/ui-src/src/components/overlays/PlanComplianceTableOverlay.tsx
index acc95156a..44292ebe5 100644
--- a/services/ui-src/src/components/overlays/PlanComplianceTableOverlay.tsx
+++ b/services/ui-src/src/components/overlays/PlanComplianceTableOverlay.tsx
@@ -35,6 +35,8 @@ import {
   addAnalysisMethods,
   addExceptionsNonComplianceStatus,
   addStandardId,
+  getExceptionsNonComplianceCounts,
+  getExceptionsNonComplianceKeys,
   hasComplianceDetails,
   mapNaaarStandardEntity,
   mapNaaarStandardsData,
@@ -133,30 +135,17 @@ export const PlanComplianceTableOverlay = ({
 
     useEffect(() => {
       if (selectedEntity) {
-        const updatedExceptionsNonCompliance = Object.keys(
-          selectedEntity
-        ).filter(
-          (key) =>
-            key.startsWith(`${standardKeyPrefix}-`) &&
-            selectedEntity[key] !== undefined
+        setExceptionsNonCompliance(
+          getExceptionsNonComplianceKeys(selectedEntity)
         );
-        setExceptionsNonCompliance(updatedExceptionsNonCompliance);
       }
-    }, [selectedEntity, standardKeyPrefix]);
+    }, [selectedEntity]);
 
     useEffect(() => {
-      const { updatedExceptionsCount, updatedNonComplianceCount } =
-        exceptionsNonCompliance.reduce(
-          (obj, key) => {
-            if (key.endsWith("exceptionsDescription")) {
-              obj.updatedExceptionsCount++;
-            } else if (key.endsWith("nonComplianceDescription")) {
-              obj.updatedNonComplianceCount++;
-            }
-            return obj;
-          },
-          { updatedExceptionsCount: 0, updatedNonComplianceCount: 0 }
-        );
+      const {
+        exceptionsCount: updatedExceptionsCount,
+        nonComplianceCount: updatedNonComplianceCount,
+      } = getExceptionsNonComplianceCounts(exceptionsNonCompliance);
 
       setExceptionsCount(updatedExceptionsCount);
       setNonComplianceCount(updatedNonComplianceCount);
diff --git a/services/ui-src/src/utils/forms/naaarPlanCompliance.test.ts b/services/ui-src/src/utils/forms/naaarPlanCompliance.test.ts
index 4608d1575..0213a777a 100644
--- a/services/ui-src/src/utils/forms/naaarPlanCompliance.test.ts
+++ b/services/ui-src/src/utils/forms/naaarPlanCompliance.test.ts
@@ -1,14 +1,20 @@
 // constants
-import { exceptionsStatus, nonComplianceStatus } from "../../constants";
+import {
+  exceptionsStatus,
+  nonComplianceStatus,
+  planComplianceStandardKey,
+} from "../../constants";
 // types
 import { NaaarStandardsTableShape } from "components/tables/SortableNaaarStandardsTable";
-import { FormJson } from "types";
+import { EntityShape, FormJson } from "types";
 // utils
 import {
   addAnalysisMethods,
   addExceptionsNonComplianceStatus,
   addStandardId,
   exceptionsNonComplianceStatus,
+  getExceptionsNonComplianceCounts,
+  getExceptionsNonComplianceKeys,
   hasComplianceDetails,
 } from "utils";
 
@@ -239,4 +245,43 @@ describe("utils/forms/naaarPlanCompliance", () => {
       ]);
     });
   });
+
+  describe("getExceptionsNonComplianceKeys()", () => {
+    const keyPrefix = planComplianceStandardKey;
+    const expectedKey = `${keyPrefix}-mock`;
+    const entity: EntityShape = {
+      id: "mockEntityId1",
+      [expectedKey]: "Mock Value",
+      "mock-nonmatching-key": "mock value",
+    };
+
+    const exceptionsNonCompliance = [expectedKey];
+
+    test("returns array of keys matching plan compliance constant", () => {
+      expect(getExceptionsNonComplianceKeys(entity)).toEqual(
+        exceptionsNonCompliance
+      );
+    });
+  });
+
+  describe("getExceptionsNonComplianceCounts()", () => {
+    const mockExceptionComplianceKeys = [
+      "mock-key-1",
+      "mock-exceptionsDescription",
+      "mock-nonComplianceDescription",
+    ];
+
+    test("returns counts of zero for empty array", () => {
+      expect(getExceptionsNonComplianceCounts([])).toEqual({
+        exceptionsCount: 0,
+        nonComplianceCount: 0,
+      });
+    });
+
+    test("returns counts of 1 for exception key and 1 for no non-compliance key", () => {
+      expect(
+        getExceptionsNonComplianceCounts(mockExceptionComplianceKeys)
+      ).toEqual({ exceptionsCount: 1, nonComplianceCount: 1 });
+    });
+  });
 });
diff --git a/services/ui-src/src/utils/forms/naaarPlanCompliance.ts b/services/ui-src/src/utils/forms/naaarPlanCompliance.ts
index 3bf3fdca7..e43d11468 100644
--- a/services/ui-src/src/utils/forms/naaarPlanCompliance.ts
+++ b/services/ui-src/src/utils/forms/naaarPlanCompliance.ts
@@ -1,5 +1,9 @@
 // constants
-import { exceptionsStatus, nonComplianceStatus } from "../../constants";
+import {
+  exceptionsStatus,
+  nonComplianceStatus,
+  planComplianceStandardKey,
+} from "../../constants";
 // types
 import { NaaarStandardsTableShape } from "components/tables/SortableNaaarStandardsTable";
 import { AnyObject, EntityShape, FormJson } from "types";
@@ -204,3 +208,27 @@ export const addExceptionsNonComplianceStatus = (
       exceptionsNonCompliance: exceptionsOrNonCompliance,
     };
   });
+
+export const getExceptionsNonComplianceKeys = (selectedEntity: EntityShape) => {
+  return Object.keys(selectedEntity).filter(
+    (key) =>
+      key.startsWith(`${planComplianceStandardKey}-`) &&
+      selectedEntity[key] !== undefined
+  );
+};
+
+export const getExceptionsNonComplianceCounts = (
+  exceptionsNonComplianceKeys: string[]
+) => {
+  return exceptionsNonComplianceKeys.reduce(
+    (obj, key) => {
+      if (key.endsWith("exceptionsDescription")) {
+        obj.exceptionsCount++;
+      } else if (key.endsWith("nonComplianceDescription")) {
+        obj.nonComplianceCount++;
+      }
+      return obj;
+    },
+    { exceptionsCount: 0, nonComplianceCount: 0 }
+  );
+};


Reviews:
user: login: qlty-cloud-legacy[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-17T12:58:41Z
user: login: britt-mo
body: Looked good in deployed branch. 
Not coming up with anything helpful for the response column styling. Only thing that came to mind was to try to set a min-width that matches the figma?
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T16:46:21Z
user: login: jessabean
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:14:13Z
user: login: gmrabian
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:30:37Z
user: login: jessabean
body: 🙌🏻 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T18:53:33Z

Review comments:
diff_hunk: @@ -209,57 +209,58 @@ export interface Props {
   showHintText?: boolean;
 }
 
-const sx = {
-  root: {
-    "@media print": {
-      pageBreakInside: "avoid",
+export const exportTableSx = {
+  "@media print": {
+    pageBreakInside: "avoid",
+  },
+  marginBottom: "1rem",
+  "tr, th": {
+    verticalAlign: "top",
+    lineHeight: "base",
+    borderBottom: "1px solid",
+    borderColor: "palette.gray_lighter",
+  },
+  thead: {
+    //this will prevent generating a new header whenever the table spills over in another page
+    display: "table-row-group",
+  },
+  td: {
+    p: {
+      lineHeight: "1.25rem",
     },
-    marginBottom: "1rem",
-    "tr, th": {
-      verticalAlign: "top",
-      lineHeight: "base",
-      borderBottom: "1px solid",
-      borderColor: "palette.gray_lighter",
+    padding: "0.75rem 0.5rem",
+    borderStyle: "none",
+    fontWeight: "normal",
+    color: "palette.base",
+    ".shrink &": {
+      padding: "0.375rem 0rem",
     },
-    thead: {
-      //this will prevent generating a new header whenever the table spills over in another page
-      display: "table-row-group",
+    ".mobile &": {
+      fontSize: "xs",
     },
-    td: {
-      p: {
-        lineHeight: "1.25rem",
-      },
-      padding: "0.75rem 0.5rem",
-      borderStyle: "none",
-      fontWeight: "normal",
-      color: "palette.base",
-      ".shrink &": {
-        padding: "0.375rem 0rem",
-      },
-      ".mobile &": {
-        fontSize: "xs",
-      },
+  },
+  th: {
+    paddingBottom: "0.375rem",
+    fontWeight: "bold",
+    lineHeight: "lg",
+    color: "palette.gray_medium",
+    ".shrink &": {
+      padding: "0.375rem 0rem",
     },
-    th: {
-      paddingBottom: "0.375rem",
-      fontWeight: "bold",
-      lineHeight: "lg",
-      color: "palette.gray_medium",
-      ".shrink &": {
-        padding: "0.375rem 0rem",
-      },
-      "&:first-of-type": {
-        paddingLeft: 0,
-      },
+    "&:first-of-type": {
+      paddingLeft: 0,
     },
-    ".desktop &": {
-      "&.two-column": {
-        "th:first-of-type": {
-          paddingLeft: "6rem",
-        },
+  },
+  ".desktop &": {
+    "&.two-column": {
+      "th:first-of-type": {
+        paddingLeft: "6rem",
       },
     },
   },
path: services/ui-src/src/components/export/ExportedReportFieldTable.tsx
user: login: qlty-cloud-legacy[bot]
body: Found 50 lines of similar code in 2 locations \(mass = 133\) <i>[qlty:similar-code]</i><a href="#issue-ed6adab48d5d2a0a08634813d52271fb"></a>
created_at: 2025-04-17T12:58:41Z
author_association: NONE
diff_hunk: @@ -119,7 +130,11 @@ const complianceTable = (
             <Text sx={sx.fieldLabel}>{heading}</Text>
             <Text sx={sx.fieldHint}>{parseCustomHtml(hint)}</Text>
           </Td>
-          <Td>{answer}</Td>
+          <Td>
path: services/ui-src/src/components/export/ExportedPlanOverlayReportSection.tsx
user: login: jessabean
body: Idea for column width: can we add either an `sx` prop or inline style here to fix the width of this column? It looks like the width of the response column is about `50%`
created_at: 2025-04-17T17:14:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,7 +130,11 @@ const complianceTable = (
             <Text sx={sx.fieldLabel}>{heading}</Text>
             <Text sx={sx.fieldHint}>{parseCustomHtml(hint)}</Text>
           </Td>
-          <Td>{answer}</Td>
+          <Td>
path: services/ui-src/src/components/export/ExportedPlanOverlayReportSection.tsx
user: login: gmrabian
body: thank you!
created_at: 2025-04-17T17:30:37Z
author_association: CONTRIBUTOR

PR comments:
user: login: qlty-cloud-legacy[bot]
created_at: 2025-04-17T12:58:39Z
body: All good ✅
<sub>This is from Qlty Cloud, the successor to Code Climate Quality. <a href="https://qlty.sh/d/migration-overview">Learn more</a>.</sub>
<a href="#analysis-summary-comment"></a>
author_association: NONE
user: login: qlty-cloud-legacy[bot]
created_at: 2025-04-17T17:32:59Z
body: Code Climate has analyzed commit 4e611d44 and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **100.0%** (**90%** is the threshold).

This pull request will bring the total coverage in the repository to **97.1%** (**0.0%** change).

View more on [Code Climate](https://codeclimate.com/repos/644971a20bd7db00dfeaf21a/pull/12120).

author_association: NONE