Repository: DSP3-apps/ncea-frontend
Pull request #60: NCEA-239 Wrong information 'Natural Capital' tab of dataset detail page
Author: login: rishabmehra1990
Labels: name: released
Created: 2025-03-26T15:08:32Z | Merged: 2025-03-27T16:07:28Z

### What one thing this PR does?
In this PR, it will add the mock data for Natural Capital data based upon the mock data provided.

A sample one-liner about this task.

### Task details

- [NCEA-239 - Wrong information 'Natural Capital' tab of dataset detail page](https://dsp-support.atlassian.net/browse/NCEA-239)

### How do these changes look like?
![Screenshot 2025-03-26 200246](https://github.com/user-attachments/assets/a374456b-d0dd-4fca-8a56-4ce871022423)


### Dev-tested in

1. Local environment

- [More Info Search Result](http://localhost:3000/natural-capital-ecosystem-assessment/search/fae1c657-64bb-414c-a1e9-e992b2e7ec03?q=disease&jry=qs&pg=1&rpp=20&srt=most_relevant#natural)


Diff:
diff --git a/__tests__/utils/formatSearchResponse.spec.ts b/__tests__/utils/formatSearchResponse.spec.ts
index e21326b9..f9c7859c 100644
--- a/__tests__/utils/formatSearchResponse.spec.ts
+++ b/__tests__/utils/formatSearchResponse.spec.ts
@@ -1,5 +1,6 @@
 import { formatSearchResponse } from '../../src/utils/formatSearchResponse';
 import { MORE_INFO_MOCK_DATA } from '../../src/services/handlers/mocks/more-info-response';
+import { naturalTabStaticData } from '../../src/utils/constants';
 
 describe('Format the search response', () => {
   it('should format the search response correctly', async () => {
@@ -94,19 +95,9 @@ describe('Format the search response', () => {
       available_formats: '',
       frequency_of_update: 'unknown',
       character_encoding: 'utf8',
-      Natural_capital_title: 'Exotic Notifible Disease Report Case Reason for Submission Vocabulary',
-      Natural_capital_description:
-        "This dataset provides a codelist for the reason why a sample resulting in Non Negative Lab Result was submitted  that resulted an Exotic Notifiable Disease investigations carried out by the Animal and Plant Health Agency. Investigations that follow the report of an exotic notifiable disease are called 'report cases'. Notifiable diseases in this context are animal diseases that an animal owner/keeper is under legal obligation to report to the Animal and Plant Health Agency (APHA), even if there is only a suspicion that an animal may be affected. 'Exotic diseases' are defined as diseases that are not currently present within the United Kingdom. Reason for Submission is the reason why a sample resulting in Non Negative Lab Result was submitted this could be a result of: \n" +
-        'Active Surveillance These are investigations where a surveillance program is in place that actively samples certain premises in order to provide assurance of national disease freedom.  \n' +
-        'Testing to Exclude: Testing for exclusion is specifically intended for use in cases where NAD or West Nile Fever is not formally suspected, but cannot be excluded from the differential diagnosis of a health or production problem. This service may help to detect disease at the earliest opportunity in those cases where the clinical signs do not give rise to a level of suspicion of a NAD or West Nile Fever that justifies either a consultation case or a statutory notification and official inquiry in the first instance. For more information on testing for exclusion scheme for NAD please see http://ahvla.defra.gov.uk/documents/nad/vr-534.3.full_.pdf  \n' +
-        'Pre Export: Samples submitted as part of import controls\n' +
-        'Post Import: Samples submitted as part of import controls\n' +
-        'Private Samples: Samples collected for private diagnostic work\n' +
-        'Pre Breeding: Samples submitted before an animal breeds\n' +
-        'Not Disclosed: Samples which are submitted for undeclared reason.',
-      Natural_capital_displayData: '',
-      Natural_capital_no_data: '',
-      Natural_capital_glossary_link: '',
+      Natural_capital_title: naturalTabStaticData.title,
+      Natural_capital_description: naturalTabStaticData.description,
+      Natural_capital_displayData: expect.any(String),
       spatialDataService: '',
       spatialRepresentationService: 'vector',
       spatialReferencingSystem: 'http://www.opengis.net/def/crs/EPSG/0/27700',
diff --git a/__tests__/utils/getNaturalCapitalTab.spec.ts b/__tests__/utils/getNaturalCapitalTab.spec.ts
index 81f3aa62..a8adcf6b 100644
--- a/__tests__/utils/getNaturalCapitalTab.spec.ts
+++ b/__tests__/utils/getNaturalCapitalTab.spec.ts
@@ -1,16 +1,82 @@
-import { getNaturalTab } from '../../src/utils/getNaturalCapitalTab';
+import { getNaturalTab, generateClassifierTable } from '../../src/utils/getNaturalCapitalTab';
 import { MORE_INFO_MOCK_DATA } from '../../src/services/handlers/mocks/more-info-response';
+import { naturalTabStaticData } from '../../src/utils/constants';
 
 describe('getNaturalTab', () => {
   it('should return the correct structure when searchItem contains OrgNceaClassifiers', () => {
     const result = getNaturalTab(MORE_INFO_MOCK_DATA);
 
-    expect(result).toEqual({
-      Natural_capital_title: MORE_INFO_MOCK_DATA.title,
-      Natural_capital_description: MORE_INFO_MOCK_DATA.abstract,
-      Natural_capital_displayData: '',
-      Natural_capital_no_data: '',
-      Natural_capital_glossary_link: '',
-    });
+    expect(result.Natural_capital_title).toStrictEqual(naturalTabStaticData.title);
+    expect(result.Natural_capital_description).toStrictEqual(naturalTabStaticData.description);
+    expect(result.Natural_capital_displayData).toContain('<table class="details-table-full">');
+  });
+});
+
+describe('generateClassifierTable', () => {
+  it('should return a properly formatted HTML table when given data with themes, categories, and subcategories', () => {
+    const data = [
+      {
+        name: 'Theme 1',
+        naturalCapitalCategory: [
+          {
+            name: 'Category 1',
+            naturalCapitalSubCategory: [{ name: 'Subcategory 1' }, { name: 'Subcategory 2' }],
+          },
+          {
+            name: 'Category 2',
+            naturalCapitalSubCategory: [],
+          },
+        ],
+      },
+    ];
+
+    const result = generateClassifierTable(data);
+
+    expect(result).toContain('<table class="details-table-full">');
+    expect(result).toContain('<td>Theme 1</td>');
+    expect(result).toContain('<td>Category 1</td>');
+    expect(result).toContain('<td>Subcategory 1</td>');
+    expect(result).toContain('<td>Subcategory 2</td>');
+  });
+
+  it('should return an empty string when data is empty', () => {
+    const result = generateClassifierTable([]);
+
+    expect(result).toBe('');
+  });
+
+  it('should handle theme only if no category and sub category provided', () => {
+    const data = [
+      {
+        name: 'Theme 1',
+        naturalCapitalCategory: [],
+      },
+    ];
+
+    const result = generateClassifierTable(data);
+
+    expect(result).toContain('<td>Theme 1</td>');
+    expect(result).not.toContain('<th width="35%">Category</th>');
+    expect(result).not.toContain('<th width="35%">Subcategory</th>');
+  });
+
+  it('should handle theme and category only if no sub category provided', () => {
+    const data = [
+      {
+        name: 'Theme 1',
+        naturalCapitalCategory: [
+          {
+            id: 'lv2-001',
+            name: 'Terrestrial and freshwater habitats',
+          },
+        ],
+      },
+    ];
+
+    const result = generateClassifierTable(data);
+
+    expect(result).toContain('<td>Theme 1</td>');
+    expect(result).toContain('<td>Terrestrial and freshwater habitats</td>');
+    expect(result).not.toContain('<th width="35%">Subcategory</th>');
   });
 });
diff --git a/src/interfaces/searchResponse.interface.ts b/src/interfaces/searchResponse.interface.ts
index 1a6cbb8a..a749bf7e 100644
--- a/src/interfaces/searchResponse.interface.ts
+++ b/src/interfaces/searchResponse.interface.ts
@@ -298,3 +298,20 @@ export interface IMoreInfoSearchItem extends IGeographyItem, IQualityItem, IGene
   license: ILicenseItem;
   organisation?: string;
 }
+
+export interface NaturalCapitalSubCategory {
+  id?: string;
+  name?: string;
+}
+
+export interface NaturalCapitalCategory {
+  id?: string;
+  name?: string;
+  naturalCapitalSubCategory?: NaturalCapitalSubCategory[] | null | undefined;
+}
+
+export interface NaturalCapitalTableItems {
+  id?: string;
+  name?: string;
+  naturalCapitalCategory?: NaturalCapitalCategory[] | null | undefined;
+}
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 4817b7b3..c2522c2f 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -205,8 +205,8 @@ export const detailsTabOptions: TabOptions = {
     Natural_capital_title: 'Natural_capital_title',
     Natural_capital_description: 'Natural_capital_description',
     Natural_capital_displayData: 'Natural_capital_displayData',
-    Natural_capital_glossary_link: 'Natural_capital_glossary_link',
-    Natural_capital_no_data: 'Natural_capital_no_data',
+    // Natural_capital_glossary_link: 'Natural_capital_glossary_link',
+    // Natural_capital_no_data: 'Natural_capital_no_data',
   },
   governance: {
     Role: 'role',
@@ -377,3 +377,27 @@ export const cookiePolicyUrl = 'https://environment.data.gov.uk/help/cookies';
 export const termsAndConditionsUrl = 'https://environment.data.gov.uk/support/faqs/275811163/275811177';
 export const privacyPolicyUrl = 'https://environment.data.gov.uk/support/faqs/275811163/275811229';
 export const supportUrl = 'https://environment.data.gov.uk/support';
+export const nceaClassifiersMockTableData = {
+  isDatasetRelevantToANaturalCapitalApproach: true,
+  nceaContribution: 'Supported',
+  nceaClassifiers: {
+    naturalCapitalThemes: [
+      {
+        id: 'lv-1',
+        name: 'Natural asset',
+        naturalCapitalCategory: [
+          {
+            id: 'lv2-001',
+            name: 'Terrestrial and freshwater habitats',
+            naturalCapitalSubCategory: [
+              {
+                id: 'lv3-003',
+                name: 'Boundary and linear features',
+              },
+            ],
+          },
+        ],
+      },
+    ],
+  },
+};
diff --git a/src/utils/getNaturalCapitalTab.ts b/src/utils/getNaturalCapitalTab.ts
index 8cc0e3f9..5131d7f4 100644
--- a/src/utils/getNaturalCapitalTab.ts
+++ b/src/utils/getNaturalCapitalTab.ts
@@ -1,13 +1,133 @@
 'use strict';
 
-import { INatural, INaturalItem } from '../interfaces/searchResponse.interface';
-
-const getNaturalTab = (payload: INaturalItem): INatural => ({
-  Natural_capital_title: payload?.title ?? '',
-  Natural_capital_description: payload?.abstract ?? '',
-  Natural_capital_displayData: '', // There is no data available from AGM side, so decided to leave as empty
-  Natural_capital_no_data: '', // There is no data available from AGM side, so decided to leave as empty
-  Natural_capital_glossary_link: '', // There is no data available from AGM side, so decided to leave as empty
+import {
+  INatural,
+  NaturalCapitalCategory,
+  NaturalCapitalSubCategory,
+  NaturalCapitalTableItems,
+} from '../interfaces/searchResponse.interface';
+import { naturalTabStaticData, nceaClassifiersMockTableData } from '../utils/constants';
+
+export const generateClassifierTable = (data: NaturalCapitalTableItems[]): string => {
+  if (!Array.isArray(data) || data.length === 0) {
+    return '';
+  }
+
+  const { hasCategory, hasSubcategory } = analyzeClassifiers(data);
+
+  const tableHTML = `
+    <table class="details-table-full">
+      ${generateTableHeader(hasCategory, hasSubcategory)}
+      <tbody>
+        ${generateTableRows(data, hasCategory, hasSubcategory)}
+      </tbody>
+    </table>
+  `;
+
+  return tableHTML;
+};
+
+const analyzeClassifiers = (data: NaturalCapitalTableItems[]) => {
+  let hasCategory = false;
+  let hasSubcategory = false;
+
+  data.forEach((item: NaturalCapitalTableItems) => {
+    if (Array.isArray(item.naturalCapitalCategory) && item.naturalCapitalCategory.length > 0) {
+      hasCategory = true;
+      item.naturalCapitalCategory.forEach((category: NaturalCapitalCategory) => {
+        if (Array.isArray(category.naturalCapitalSubCategory) && category.naturalCapitalSubCategory.length > 0) {
+          hasSubcategory = true;
+        }
+      });
+    }
+  });
+
+  return { hasCategory, hasSubcategory };
+};
+
+const generateTableHeader = (hasCategory: boolean, hasSubcategory: boolean): string => {
+  const categoryWidth = hasSubcategory ? '35%' : '70%';
+  const subcategoryWidth = '35%';
+  const themeWidth = hasCategory || hasSubcategory ? '30%' : '100%';
+
+  return `
+    <thead>
+      <tr>
+        <th width="${themeWidth}">Theme</th>
+        ${hasCategory ? `<th width="${categoryWidth}">Category</th>` : ''}
+        ${hasSubcategory ? `<th width="${subcategoryWidth}">Subcategory</th>` : ''}
+      </tr>
+      <tr><td colspan="${1 + (hasCategory ? 1 : 0) + (hasSubcategory ? 1 : 0)}" class="details-table-hr"></td></tr>
+    </thead>
+  `;
+};
+
+const generateTableRows = (data: NaturalCapitalTableItems[], hasCategory: boolean, hasSubcategory: boolean): string => {
+  return data.map((item) => generateTableRow(item, hasCategory, hasSubcategory)).join('');
+};
+
+const generateTableRow = (item: NaturalCapitalTableItems, hasCategory: boolean, hasSubcategory: boolean): string => {
+  const themeName = item?.name ?? '';
+  let rows = '';
+
+  if (Array.isArray(item.naturalCapitalCategory) && item.naturalCapitalCategory.length > 0) {
+    item.naturalCapitalCategory.forEach((category: NaturalCapitalCategory, index: number) => {
+      const categoryName = category?.name ?? '';
+      if (Array.isArray(category.naturalCapitalSubCategory) && category.naturalCapitalSubCategory.length > 0) {
+        category.naturalCapitalSubCategory.forEach((subcategory: NaturalCapitalSubCategory, subIndex: number) => {
+          const subcategoryName = subcategory?.name ?? '';
+          rows += createRow(themeName, categoryName, subcategoryName, index, subIndex, hasCategory, hasSubcategory);
+        });
+      } else {
+        rows += createRow(themeName, categoryName, '', index, 0, hasCategory, hasSubcategory);
+      }
+    });
+  } else {
+    rows += createRow(themeName, '', '', 0, 0, hasCategory, hasSubcategory);
+  }
+
+  const colspan = 1 + (hasCategory ? 1 : 0) + (hasSubcategory ? 1 : 0);
+  rows += `<tr><td colspan="${colspan}" class="details-table-hr"></td></tr>`;
+
+  return rows;
+};
+
+const createRow = (
+  themeName: string,
+  categoryName: string,
+  subcategoryName: string,
+  index: number,
+  subIndex: number,
+  hasCategory: boolean,
+  hasSubcategory: boolean,
+): string => {
+  const themeCell = index === 0 && subIndex === 0 ? `<td>${themeName}</td>` : '<td></td>';
+
+  let categoryCell = '';
+  if (hasCategory && subIndex === 0) {
+    categoryCell = `<td>${categoryName}</td>`;
+  } else if (hasCategory) {
+    categoryCell = '<td></td>';
+  }
+
+  const subcategoryCell = hasSubcategory ? `<td>${subcategoryName}</td>` : '';
+
+  return `
+    <tr>
+      ${themeCell}
+      ${categoryCell}
+      ${subcategoryCell}
+    </tr>
+  `;
+};
+
+const getNaturalTab = (payload): INatural => ({
+  Natural_capital_title: naturalTabStaticData.title,
+  Natural_capital_description: naturalTabStaticData.description,
+  Natural_capital_displayData: generateClassifierTable(
+    payload?.nceaClassifiers?.naturalCapitalThemes ??
+      nceaClassifiersMockTableData?.nceaClassifiers?.naturalCapitalThemes,
+  ),
 });
 
 export { getNaturalTab };


Reviews:
user: login: marksmall
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-26T17:20:14Z
user: login: rishabmehra1990
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T06:32:49Z
user: login: rishabmehra1990
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T06:33:13Z
user: login: rishabmehra1990
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T06:42:29Z
user: login: marksmall
state: APPROVED
author_association: NONE
submitted_at: 2025-03-27T16:07:14Z

Review comments:
diff_hunk: @@ -94,17 +95,9 @@ describe('Format the search response', () => {
       available_formats: '',
       frequency_of_update: 'unknown',
       character_encoding: 'utf8',
-      Natural_capital_title: 'Exotic Notifible Disease Report Case Reason for Submission Vocabulary',
-      Natural_capital_description:
-        "This dataset provides a codelist for the reason why a sample resulting in Non Negative Lab Result was submitted  that resulted an Exotic Notifiable Disease investigations carried out by the Animal and Plant Health Agency. Investigations that follow the report of an exotic notifiable disease are called 'report cases'. Notifiable diseases in this context are animal diseases that an animal owner/keeper is under legal obligation to report to the Animal and Plant Health Agency (APHA), even if there is only a suspicion that an animal may be affected. 'Exotic diseases' are defined as diseases that are not currently present within the United Kingdom. Reason for Submission is the reason why a sample resulting in Non Negative Lab Result was submitted this could be a result of: \n" +
-        'Active Surveillance These are investigations where a surveillance program is in place that actively samples certain premises in order to provide assurance of national disease freedom.  \n' +
-        'Testing to Exclude: Testing for exclusion is specifically intended for use in cases where NAD or West Nile Fever is not formally suspected, but cannot be excluded from the differential diagnosis of a health or production problem. This service may help to detect disease at the earliest opportunity in those cases where the clinical signs do not give rise to a level of suspicion of a NAD or West Nile Fever that justifies either a consultation case or a statutory notification and official inquiry in the first instance. For more information on testing for exclusion scheme for NAD please see http://ahvla.defra.gov.uk/documents/nad/vr-534.3.full_.pdf  \n' +
-        'Pre Export: Samples submitted as part of import controls\n' +
-        'Post Import: Samples submitted as part of import controls\n' +
-        'Private Samples: Samples collected for private diagnostic work\n' +
-        'Pre Breeding: Samples submitted before an animal breeds\n' +
-        'Not Disclosed: Samples which are submitted for undeclared reason.',
-      Natural_capital_displayData: '',
+      Natural_capital_title: naturalTabStaticData.title,
+      Natural_capital_description: naturalTabStaticData.description,
path: __tests__/utils/formatSearchResponse.spec.ts
user: login: marksmall
body: Should the `title` and `description` be in config, it is never going to change no matter what category data is displayed, seems pointless to read and re-render for each, when it could just be a bit of static content? Or have I misunderstood the purpose of the title and description?
created_at: 2025-03-26T15:21:31Z
author_association: NONE
diff_hunk: @@ -1,13 +1,131 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
 'use strict';
 
-import { INatural, INaturalItem } from '../interfaces/searchResponse.interface';
+import { INatural } from '../interfaces/searchResponse.interface';
+import { naturalTabStaticData, nceaClassifiersMockTableData } from '../utils/constants';
 
-const getNaturalTab = (payload: INaturalItem): INatural => ({
-  Natural_capital_title: payload?.title ?? '',
-  Natural_capital_description: payload?.abstract ?? '',
-  Natural_capital_displayData: '', // There is no data available from AGM side, so decided to leave as empty
-  Natural_capital_no_data: '', // There is no data available from AGM side, so decided to leave as empty
-  Natural_capital_glossary_link: '', // There is no data available from AGM side, so decided to leave as empty
+export const generateClassifierTable = (data: Record<string, any>[]): string => {
+  if (!Array.isArray(data) || data.length === 0) {
+    return '';
+  }
+
+  const { hasCategory, hasSubcategory } = analyzeClassifiers(data);
+
+  const tableHTML = `
+    <table class="details-table-full">
+      ${generateTableHeader(hasCategory, hasSubcategory)}
+      <tbody>
+        ${generateTableRows(data, hasCategory, hasSubcategory)}
+      </tbody>
+    </table>
+  `;
+
+  return tableHTML;
+};
+
+const analyzeClassifiers = (data: Record<string, any>[]) => {
+  let hasCategory = false;
+  let hasSubcategory = false;
+
+  data.forEach((item: Record<string, any>) => {
+    if (Array.isArray(item.naturalCapitalCategory) && item.naturalCapitalCategory.length > 0) {
+      hasCategory = true;
+      item.naturalCapitalCategory.forEach((category: Record<string, any>) => {
+        if (Array.isArray(category.naturalCapitalSubCategory) && category.naturalCapitalSubCategory.length > 0) {
+          hasSubcategory = true;
+        }
+      });
+    }
+  });
+
+  return { hasCategory, hasSubcategory };
+};
+
+const generateTableHeader = (hasCategory: boolean, hasSubcategory: boolean): string => {
+  const categoryWidth = hasSubcategory ? '35%' : '70%';
+  const subcategoryWidth = '35%';
+  const themeWidth = hasCategory || hasSubcategory ? '30%' : '100%';
+
+  return `
+    <thead>
+      <tr>
+        <th width="${themeWidth}">Theme</th>
+        ${hasCategory ? `<th width="${categoryWidth}">Category</th>` : ''}
+        ${hasSubcategory ? `<th width="${subcategoryWidth}">Subcategory</th>` : ''}
+      </tr>
+      <tr><td colspan="${1 + (hasCategory ? 1 : 0) + (hasSubcategory ? 1 : 0)}" class="details-table-hr"></td></tr>
+    </thead>
+  `;
+};
+
+const generateTableRows = (data: Record<string, any>[], hasCategory: boolean, hasSubcategory: boolean): string => {
+  return data.map((item) => generateTableRow(item, hasCategory, hasSubcategory)).join('');
+};
+
+const generateTableRow = (item: Record<string, any>, hasCategory: boolean, hasSubcategory: boolean): string => {
+  const themeName = item?.name ?? '';
+  let rows = '';
+
+  if (Array.isArray(item.naturalCapitalCategory) && item.naturalCapitalCategory.length > 0) {
+    item.naturalCapitalCategory.forEach((category: Record<string, any>, index: number) => {
+      const categoryName = category?.name ?? '';
+      if (Array.isArray(category.naturalCapitalSubCategory) && category.naturalCapitalSubCategory.length > 0) {
+        category.naturalCapitalSubCategory.forEach((subcategory: Record<string, any>, subIndex: number) => {
+          const subcategoryName = subcategory?.name ?? '';
+          rows += createRow(themeName, categoryName, subcategoryName, index, subIndex, hasCategory, hasSubcategory);
+        });
+      } else {
+        rows += createRow(themeName, categoryName, '', index, 0, hasCategory, hasSubcategory);
+      }
+    });
+  } else {
+    rows += createRow(themeName, '', '', 0, 0, hasCategory, hasSubcategory);
+  }
+
+  const colspan = 1 + (hasCategory ? 1 : 0) + (hasSubcategory ? 1 : 0);
+  rows += `<tr><td colspan="${colspan}" class="details-table-hr"></td></tr>`;
+
+  return rows;
+};
+
+const createRow = (
+  themeName: string,
+  categoryName: string,
+  subcategoryName: string,
+  index: number,
+  subIndex: number,
+  hasCategory: boolean,
+  hasSubcategory: boolean,
+): string => {
+  const themeCell = index === 0 && subIndex === 0 ? `<td>${themeName}</td>` : '<td></td>';
+
+  let categoryCell = '';
+  if (hasCategory && subIndex === 0) {
+    categoryCell = `<td>${categoryName}</td>`;
+  } else if (hasCategory) {
+    categoryCell = '<td></td>';
+  }
+
+  const subcategoryCell = hasSubcategory ? `<td>${subcategoryName}</td>` : '';
+
+  return `
+    <tr>
+      ${themeCell}
+      ${categoryCell}
+      ${subcategoryCell}
+    </tr>
+  `;
+};
+
+const getNaturalTab = (payload): INatural => ({
+  Natural_capital_title: naturalTabStaticData.title,
+  Natural_capital_description: naturalTabStaticData.description,
+  Natural_capital_displayData: generateClassifierTable(
+    payload?.nceaClassifiers?.naturalCapitalThemes ??
+      nceaClassifiersMockTableData?.nceaClassifiers?.naturalCapitalThemes,
+  ),
+  Natural_capital_no_data: '',
+  Natural_capital_glossary_link: '',
path: src/utils/getNaturalCapitalTab.ts
user: login: marksmall
body: What are these used for? If never used, should they really remain?
created_at: 2025-03-26T15:23:45Z
author_association: NONE
diff_hunk: @@ -1,13 +1,131 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
 'use strict';
 
-import { INatural, INaturalItem } from '../interfaces/searchResponse.interface';
+import { INatural } from '../interfaces/searchResponse.interface';
+import { naturalTabStaticData, nceaClassifiersMockTableData } from '../utils/constants';
 
-const getNaturalTab = (payload: INaturalItem): INatural => ({
-  Natural_capital_title: payload?.title ?? '',
-  Natural_capital_description: payload?.abstract ?? '',
-  Natural_capital_displayData: '', // There is no data available from AGM side, so decided to leave as empty
-  Natural_capital_no_data: '', // There is no data available from AGM side, so decided to leave as empty
-  Natural_capital_glossary_link: '', // There is no data available from AGM side, so decided to leave as empty
+export const generateClassifierTable = (data: Record<string, any>[]): string => {
+  if (!Array.isArray(data) || data.length === 0) {
+    return '';
+  }
+
+  const { hasCategory, hasSubcategory } = analyzeClassifiers(data);
+
+  const tableHTML = `
+    <table class="details-table-full">
+      ${generateTableHeader(hasCategory, hasSubcategory)}
+      <tbody>
+        ${generateTableRows(data, hasCategory, hasSubcategory)}
+      </tbody>
+    </table>
+  `;
+
+  return tableHTML;
+};
+
+const analyzeClassifiers = (data: Record<string, any>[]) => {
path: src/utils/getNaturalCapitalTab.ts
user: login: marksmall
body: Get out of the habit of using `any`, using this negates the point in using TypeScript in the first place. If you really don't know what it will be, then use `unknown`, but realistically you should know what it is no? I would have thought it would always be either a basic type e.g. `string`|`number`|`boolean`|Object.
created_at: 2025-03-26T15:28:52Z
author_association: NONE
diff_hunk: @@ -1,13 +1,131 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
 'use strict';
 
-import { INatural, INaturalItem } from '../interfaces/searchResponse.interface';
+import { INatural } from '../interfaces/searchResponse.interface';
+import { naturalTabStaticData, nceaClassifiersMockTableData } from '../utils/constants';
 
-const getNaturalTab = (payload: INaturalItem): INatural => ({
-  Natural_capital_title: payload?.title ?? '',
-  Natural_capital_description: payload?.abstract ?? '',
-  Natural_capital_displayData: '', // There is no data available from AGM side, so decided to leave as empty
-  Natural_capital_no_data: '', // There is no data available from AGM side, so decided to leave as empty
-  Natural_capital_glossary_link: '', // There is no data available from AGM side, so decided to leave as empty
+export const generateClassifierTable = (data: Record<string, any>[]): string => {
+  if (!Array.isArray(data) || data.length === 0) {
+    return '';
+  }
+
+  const { hasCategory, hasSubcategory } = analyzeClassifiers(data);
+
+  const tableHTML = `
+    <table class="details-table-full">
+      ${generateTableHeader(hasCategory, hasSubcategory)}
+      <tbody>
+        ${generateTableRows(data, hasCategory, hasSubcategory)}
+      </tbody>
+    </table>
+  `;
+
+  return tableHTML;
+};
+
+const analyzeClassifiers = (data: Record<string, any>[]) => {
+  let hasCategory = false;
+  let hasSubcategory = false;
+
+  data.forEach((item: Record<string, any>) => {
+    if (Array.isArray(item.naturalCapitalCategory) && item.naturalCapitalCategory.length > 0) {
+      hasCategory = true;
+      item.naturalCapitalCategory.forEach((category: Record<string, any>) => {
+        if (Array.isArray(category.naturalCapitalSubCategory) && category.naturalCapitalSubCategory.length > 0) {
+          hasSubcategory = true;
+        }
+      });
+    }
+  });
+
+  return { hasCategory, hasSubcategory };
+};
+
+const generateTableHeader = (hasCategory: boolean, hasSubcategory: boolean): string => {
+  const categoryWidth = hasSubcategory ? '35%' : '70%';
+  const subcategoryWidth = '35%';
+  const themeWidth = hasCategory || hasSubcategory ? '30%' : '100%';
path: src/utils/getNaturalCapitalTab.ts
user: login: marksmall
body: this seems iffy to me, I just expected a 3 column table always, whether `category` or `sub-category` exists or not. I could easily be wrong on this of course, but this kind of code always scares me. It's one of those cases that unless your developing, it is easy to think is bad code, but there are good reasons, maybe we can chat over why this is necessary.
created_at: 2025-03-26T15:31:53Z
author_association: NONE
diff_hunk: @@ -1,13 +1,131 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
 'use strict';
 
-import { INatural, INaturalItem } from '../interfaces/searchResponse.interface';
+import { INatural } from '../interfaces/searchResponse.interface';
+import { naturalTabStaticData, nceaClassifiersMockTableData } from '../utils/constants';
 
-const getNaturalTab = (payload: INaturalItem): INatural => ({
-  Natural_capital_title: payload?.title ?? '',
-  Natural_capital_description: payload?.abstract ?? '',
-  Natural_capital_displayData: '', // There is no data available from AGM side, so decided to leave as empty
-  Natural_capital_no_data: '', // There is no data available from AGM side, so decided to leave as empty
-  Natural_capital_glossary_link: '', // There is no data available from AGM side, so decided to leave as empty
+export const generateClassifierTable = (data: Record<string, any>[]): string => {
+  if (!Array.isArray(data) || data.length === 0) {
+    return '';
+  }
+
+  const { hasCategory, hasSubcategory } = analyzeClassifiers(data);
+
+  const tableHTML = `
+    <table class="details-table-full">
+      ${generateTableHeader(hasCategory, hasSubcategory)}
+      <tbody>
+        ${generateTableRows(data, hasCategory, hasSubcategory)}
+      </tbody>
+    </table>
+  `;
+
+  return tableHTML;
+};
+
+const analyzeClassifiers = (data: Record<string, any>[]) => {
+  let hasCategory = false;
+  let hasSubcategory = false;
+
+  data.forEach((item: Record<string, any>) => {
+    if (Array.isArray(item.naturalCapitalCategory) && item.naturalCapitalCategory.length > 0) {
+      hasCategory = true;
+      item.naturalCapitalCategory.forEach((category: Record<string, any>) => {
+        if (Array.isArray(category.naturalCapitalSubCategory) && category.naturalCapitalSubCategory.length > 0) {
+          hasSubcategory = true;
+        }
+      });
+    }
+  });
+
+  return { hasCategory, hasSubcategory };
+};
+
+const generateTableHeader = (hasCategory: boolean, hasSubcategory: boolean): string => {
+  const categoryWidth = hasSubcategory ? '35%' : '70%';
+  const subcategoryWidth = '35%';
+  const themeWidth = hasCategory || hasSubcategory ? '30%' : '100%';
path: src/utils/getNaturalCapitalTab.ts
user: login: rishabmehra1990
body: The method generateTableHeader(), its a old code written which is setting the width of the columns.
created_at: 2025-03-27T06:32:49Z
author_association: NONE
diff_hunk: @@ -1,13 +1,131 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
 'use strict';
 
-import { INatural, INaturalItem } from '../interfaces/searchResponse.interface';
+import { INatural } from '../interfaces/searchResponse.interface';
+import { naturalTabStaticData, nceaClassifiersMockTableData } from '../utils/constants';
 
-const getNaturalTab = (payload: INaturalItem): INatural => ({
-  Natural_capital_title: payload?.title ?? '',
-  Natural_capital_description: payload?.abstract ?? '',
-  Natural_capital_displayData: '', // There is no data available from AGM side, so decided to leave as empty
-  Natural_capital_no_data: '', // There is no data available from AGM side, so decided to leave as empty
-  Natural_capital_glossary_link: '', // There is no data available from AGM side, so decided to leave as empty
+export const generateClassifierTable = (data: Record<string, any>[]): string => {
+  if (!Array.isArray(data) || data.length === 0) {
+    return '';
+  }
+
+  const { hasCategory, hasSubcategory } = analyzeClassifiers(data);
+
+  const tableHTML = `
+    <table class="details-table-full">
+      ${generateTableHeader(hasCategory, hasSubcategory)}
+      <tbody>
+        ${generateTableRows(data, hasCategory, hasSubcategory)}
+      </tbody>
+    </table>
+  `;
+
+  return tableHTML;
+};
+
+const analyzeClassifiers = (data: Record<string, any>[]) => {
path: src/utils/getNaturalCapitalTab.ts
user: login: rishabmehra1990
body: Done, I have removed the any.
created_at: 2025-03-27T06:33:13Z
author_association: NONE
diff_hunk: @@ -94,17 +95,9 @@ describe('Format the search response', () => {
       available_formats: '',
       frequency_of_update: 'unknown',
       character_encoding: 'utf8',
-      Natural_capital_title: 'Exotic Notifible Disease Report Case Reason for Submission Vocabulary',
-      Natural_capital_description:
-        "This dataset provides a codelist for the reason why a sample resulting in Non Negative Lab Result was submitted  that resulted an Exotic Notifiable Disease investigations carried out by the Animal and Plant Health Agency. Investigations that follow the report of an exotic notifiable disease are called 'report cases'. Notifiable diseases in this context are animal diseases that an animal owner/keeper is under legal obligation to report to the Animal and Plant Health Agency (APHA), even if there is only a suspicion that an animal may be affected. 'Exotic diseases' are defined as diseases that are not currently present within the United Kingdom. Reason for Submission is the reason why a sample resulting in Non Negative Lab Result was submitted this could be a result of: \n" +
-        'Active Surveillance These are investigations where a surveillance program is in place that actively samples certain premises in order to provide assurance of national disease freedom.  \n' +
-        'Testing to Exclude: Testing for exclusion is specifically intended for use in cases where NAD or West Nile Fever is not formally suspected, but cannot be excluded from the differential diagnosis of a health or production problem. This service may help to detect disease at the earliest opportunity in those cases where the clinical signs do not give rise to a level of suspicion of a NAD or West Nile Fever that justifies either a consultation case or a statutory notification and official inquiry in the first instance. For more information on testing for exclusion scheme for NAD please see http://ahvla.defra.gov.uk/documents/nad/vr-534.3.full_.pdf  \n' +
-        'Pre Export: Samples submitted as part of import controls\n' +
-        'Post Import: Samples submitted as part of import controls\n' +
-        'Private Samples: Samples collected for private diagnostic work\n' +
-        'Pre Breeding: Samples submitted before an animal breeds\n' +
-        'Not Disclosed: Samples which are submitted for undeclared reason.',
-      Natural_capital_displayData: '',
+      Natural_capital_title: naturalTabStaticData.title,
+      Natural_capital_description: naturalTabStaticData.description,
path: __tests__/utils/formatSearchResponse.spec.ts
user: login: rishabmehra1990
body: yes @marksmall, you are right it should be in some configuration. But the UI is rendering based upon the collection of objects being parsed. [code](https://github.com/DSP3-apps/ncea-frontend/blob/main/src/utils/constants.ts#L204)

created_at: 2025-03-27T06:42:29Z
author_association: NONE

PR comments:
user: login: marksmall
created_at: 2025-03-26T15:18:56Z
body: Looks good, but I think you need to make your mock more complex than this. So you have the mock include one hierarchy i.e. `theme` -> `Category` -> `Sub-Category`, but what about if there is one that only has a `theme` or a `theme` and `Category`?

Update your mock to have an entry with each possible permutation, so we can see it can handle them, so that would be:

* Just a theme
* Just a theme and category
* A theme, category and sub-category
author_association: NONE
user: login: rishabmehra1990
created_at: 2025-03-27T06:27:05Z
body: Hi @marksmall 
Here are the scenarios along with screenshots. I have also added the test suites to cover this scenarios 

Just a theme
![Screenshot 2025-03-27 114124](https://github.com/user-attachments/assets/6ae41912-3162-47d4-b09a-887d4c44b9af)

Just a theme and category
![Screenshot 2025-03-27 114322](https://github.com/user-attachments/assets/394790db-86b2-4df8-8b22-b38b4e05cc33)

A theme, category and sub-category
![Screenshot 2025-03-26 200246](https://github.com/user-attachments/assets/d3d5ff40-1c08-4e25-bc8e-9503a47e7bcf)


author_association: NONE
user: login: dspgithub-ops
created_at: 2025-04-01T16:21:45Z
body: :tada: This PR is included in version 1.18.1 :tada:

The release is available on [GitHub release](https://github.com/DSP3-apps/ncea-frontend/releases/tag/v1.18.1)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: NONE