From 1738d1e1b38919c5d2e8027adf6430da798a7156 Mon Sep 17 00:00:00 2001 From: sambokar Date: Fri, 15 Nov 2024 15:20:36 -0500 Subject: [PATCH 01/15] documentation updates --- frontend/documentation/topics/Selecting-Options.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/documentation/topics/Selecting-Options.md b/frontend/documentation/topics/Selecting-Options.md index a89333f5..10077478 100644 --- a/frontend/documentation/topics/Selecting-Options.md +++ b/frontend/documentation/topics/Selecting-Options.md @@ -17,10 +17,8 @@ will appear. A site describes an overall collection of plots in the same geographic location, and further sets a series of default parameters for the plots in that site: -- Subquadrat dimensions—sites are subdivided as follows: a site is a collection of plots, a - plot is a collection of **quadrats**, and a quadrat is a collection of **subquadrats**. -> **Subquadrats are not recorded as part of data collection, but are used to calculate -coordinates and other statistical data** +- Subquadrat dimensions—sites are subdivided as follows: a site is a collection of plots, and a + plot is a collection of **quadrats** - Double data entry—sites can be set to require double data entry. When this is enabled, measurements must be recorded **twice** for each **quadrat**, and will be held in temporary storage until an administrator reviews both sets of data and approves a single merged set. From 4ab9eca51e8e13dd2b6d63941642fb469972fd75 Mon Sep 17 00:00:00 2001 From: sambokar Date: Fri, 15 Nov 2024 15:26:46 -0500 Subject: [PATCH 02/15] fixing documentation workflow --- .github/workflows/build-docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index c4e6e37f..6b46aa61 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -13,8 +13,8 @@ permissions: pages: write env: - INSTANCE: 'Writerside/fad' # Use the Writerside instance ID as specified in your writerside.cfg - ARTIFACT: 'webHelpFAD-all.zip' # Update artifact name if needed + INSTANCE: 'documentation/fad' + ARTIFACT: 'webHelpFAD-all.zip' DOCKER_VERSION: '243.21565' # Writerside's recommended Docker version jobs: From 764e471cfcbd67e64d9407c63768c6c4d686d00f Mon Sep 17 00:00:00 2001 From: sambokar Date: Fri, 15 Nov 2024 15:27:49 -0500 Subject: [PATCH 03/15] technical change to trigger documentation workflow --- frontend/documentation/topics/Onboarding.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/documentation/topics/Onboarding.md b/frontend/documentation/topics/Onboarding.md index 105e5685..688cb4c3 100644 --- a/frontend/documentation/topics/Onboarding.md +++ b/frontend/documentation/topics/Onboarding.md @@ -1,6 +1,7 @@ # Onboarding for New Users -Thank you for using this application! This document will help you get started. +Thank you for using this application! +This document will provide you with an understanding of how the application works. ## Preparing Your Account From a603878e0b96e9e20d8e32b94f010566a4e3edc3 Mon Sep 17 00:00:00 2001 From: sambokar Date: Fri, 15 Nov 2024 15:34:10 -0500 Subject: [PATCH 04/15] workflow updates --- .github/workflows/build-docs.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 6b46aa61..696aed00 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -34,8 +34,13 @@ jobs: instance: ${{ env.INSTANCE }} artifact: ${{ env.ARTIFACT }} docker-version: ${{ env.DOCKER_VERSION }} + args: --verbose - # Step 3: Save build results + # Debug: List artifacts directory + - name: List artifacts directory + run: ls -la artifacts/ + + # Step 3: Save artifact with build results - name: Save artifact with build results uses: actions/upload-artifact@v4 with: From 7b2cd7f4cf94dc0d81af2f66e817c4eda674fdaa Mon Sep 17 00:00:00 2001 From: sambokar Date: Fri, 15 Nov 2024 15:34:50 -0500 Subject: [PATCH 05/15] triggering doc workflow --- frontend/documentation/topics/Onboarding.md | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/documentation/topics/Onboarding.md b/frontend/documentation/topics/Onboarding.md index 688cb4c3..f653d79a 100644 --- a/frontend/documentation/topics/Onboarding.md +++ b/frontend/documentation/topics/Onboarding.md @@ -3,6 +3,7 @@ Thank you for using this application! This document will provide you with an understanding of how the application works. + ## Preparing Your Account Before you can log into the application, you must complete the following steps: From f9abc896cf90b4cd9a4bb8ee3b19b86847beea20 Mon Sep 17 00:00:00 2001 From: sambokar Date: Fri, 15 Nov 2024 15:37:17 -0500 Subject: [PATCH 06/15] fixing artifact version --- .github/workflows/build-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 696aed00..b2e1cc47 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -14,7 +14,7 @@ permissions: env: INSTANCE: 'documentation/fad' - ARTIFACT: 'webHelpFAD-all.zip' + ARTIFACT: 'webHelpFAD2-all.zip' DOCKER_VERSION: '243.21565' # Writerside's recommended Docker version jobs: From 5846f60983d9005267ff3895a99f709eddd9a693 Mon Sep 17 00:00:00 2001 From: sambokar Date: Fri, 15 Nov 2024 15:37:38 -0500 Subject: [PATCH 07/15] triggering docs 2.0 --- frontend/documentation/topics/Selecting-Options.md | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/documentation/topics/Selecting-Options.md b/frontend/documentation/topics/Selecting-Options.md index 10077478..5e504b9b 100644 --- a/frontend/documentation/topics/Selecting-Options.md +++ b/frontend/documentation/topics/Selecting-Options.md @@ -6,7 +6,6 @@ select: 2. A plot 3. A census (if you need to start a new census, click on the Add New Census button!) - Once you select all of these, you will be redirected to the dashboard page, and a new interface will appear. From 95538e842018c659ac1603b5250a2780b8d2891f Mon Sep 17 00:00:00 2001 From: siddheshraze <81591724+siddheshraze@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:03:42 -0500 Subject: [PATCH 08/15] initial formatting fix. --- .../topics/After-Adding-Measurements.md | 27 ++++++------ .../topics/Navigable-Endpoints.md | 4 +- frontend/documentation/topics/Onboarding.md | 11 +++-- .../documentation/topics/Selecting-Options.md | 25 +++++------ .../documentation/topics/Submitting-Data.md | 42 ++++++++++--------- .../topics/Understanding-Supporting-Data.md | 26 ++++++------ frontend/package-lock.json | 39 ----------------- 7 files changed, 70 insertions(+), 104 deletions(-) diff --git a/frontend/documentation/topics/After-Adding-Measurements.md b/frontend/documentation/topics/After-Adding-Measurements.md index caca05d8..5313a574 100644 --- a/frontend/documentation/topics/After-Adding-Measurements.md +++ b/frontend/documentation/topics/After-Adding-Measurements.md @@ -1,25 +1,26 @@ # After Adding Measurements -Once you have successfully uploaded your supporting data, measurements, and successfully -validated your data, you can proceed in a number of ways. +Once you have successfully uploaded your supporting data, measurements, and successfully +validated your data, you can proceed in a number of ways. ## Post-Validation Statistics -After you have populated the measurements page, you can navigate to the post-validation -statistics page to perform statistics calculations on your census. +After you have populated the measurements page, you can navigate to the post-validation +statistics page to perform statistics calculations on your census. ### Interacting with the Post-Validation Statistics Page -After navigating to the page, you should see a table appear with a set of: -- SQL queries -- a brief explanation of their function -- historical data columns indicating: +After navigating to the page, you should see a table appear with a set of: + +- SQL queries +- a brief explanation of their function +- historical data columns indicating: - the last time they were run - - whether they were successful + - whether they were successful - what their successful run returned -Use the checkboxes to select the queries you want to run, and then click on the **Run -Statistics** button to run the queries. The queries should update momentarily. +Use the checkboxes to select the queries you want to run, and then click on the **Run +Statistics** button to run the queries. The queries should update momentarily. -You can also download or print the results of the queries by again selecting them via their -checkboxes and clicking on the **Download Statistics** or **Print Statistics** buttons. \ No newline at end of file +You can also download or print the results of the queries by again selecting them via their +checkboxes and clicking on the **Download Statistics** or **Print Statistics** buttons. diff --git a/frontend/documentation/topics/Navigable-Endpoints.md b/frontend/documentation/topics/Navigable-Endpoints.md index 0d95585a..1b63f18c 100644 --- a/frontend/documentation/topics/Navigable-Endpoints.md +++ b/frontend/documentation/topics/Navigable-Endpoints.md @@ -14,5 +14,5 @@ This a brief overview of the pages you can access in the app and their function! - Uploaded Files (review uploaded files here) - View All Historical Data (view all data, irrespective of census, here) - Validations (review validations, what they are examining, and toggle them on/off here) - > Toggling validations is an admin-only feature! -{style='warning'} \ No newline at end of file + > Toggling validations is an admin-only feature! + > {style='warning'} diff --git a/frontend/documentation/topics/Onboarding.md b/frontend/documentation/topics/Onboarding.md index f653d79a..7aecc13c 100644 --- a/frontend/documentation/topics/Onboarding.md +++ b/frontend/documentation/topics/Onboarding.md @@ -1,19 +1,18 @@ # Onboarding for New Users -Thank you for using this application! +Thank you for using this application! This document will provide you with an understanding of how the application works. - ## Preparing Your Account Before you can log into the application, you must complete the following steps: 1. You must have a personal MS or Office account. - > Support for existing SI accounts is still pending. Thank you for your patience! - {style='warning'} + > Support for existing SI accounts is still pending. Thank you for your patience! + > {style='warning'} 2. This account must be invited to the SIOCIORC tenant. -3. A user should be created for you and must be loaded into the website's `catalog` database. +3. A user should be created for you and must be loaded into the website's `catalog` database. 4. Your user should be assigned a user persona. 5. At least one site should be assigned to your account. -Once this process is completed, you should be able to successfully log in to the website! \ No newline at end of file +Once this process is completed, you should be able to successfully log in to the website! diff --git a/frontend/documentation/topics/Selecting-Options.md b/frontend/documentation/topics/Selecting-Options.md index 5e504b9b..f1c64249 100644 --- a/frontend/documentation/topics/Selecting-Options.md +++ b/frontend/documentation/topics/Selecting-Options.md @@ -1,34 +1,35 @@ # Selecting Options -Before you can properly access the website, you will need to **use the left-hand sidebar** to +Before you can properly access the website, you will need to **use the left-hand sidebar** to select: + 1. A site 2. A plot 3. A census (if you need to start a new census, click on the Add New Census button!) -Once you select all of these, you will be redirected to the dashboard page, and a new interface +Once you select all of these, you will be redirected to the dashboard page, and a new interface will appear. ## Understanding Sites, Plots, and Census ### What is a site? -A site describes an overall collection of plots in the same geographic location, and further sets +A site describes an overall collection of plots in the same geographic location, and further sets a series of default parameters for the plots in that site: -- Subquadrat dimensions—sites are subdivided as follows: a site is a collection of plots, and a +- Subquadrat dimensions—sites are subdivided as follows: a site is a collection of plots, and a plot is a collection of **quadrats** -- Double data entry—sites can be set to require double data entry. When this is enabled, - measurements must be recorded **twice** for each **quadrat**, and will be held in temporary - storage until an administrator reviews both sets of data and approves a single merged set. +- Double data entry—sites can be set to require double data entry. When this is enabled, + measurements must be recorded **twice** for each **quadrat**, and will be held in temporary + storage until an administrator reviews both sets of data and approves a single merged set. ### What is a plot? -A plot describes a specific region within a site being sampled, and is used to further -determine the overall coordinates of sampled stems within the plot. +A plot describes a specific region within a site being sampled, and is used to further +determine the overall coordinates of sampled stems within the plot. ### What is a census? -A census describes a date range for which sampled data is collected. This date range can vary -and is not a fixed interval. It is defined by the first and last measurements taken -during the sampling range. \ No newline at end of file +A census describes a date range for which sampled data is collected. This date range can vary +and is not a fixed interval. It is defined by the first and last measurements taken +during the sampling range. diff --git a/frontend/documentation/topics/Submitting-Data.md b/frontend/documentation/topics/Submitting-Data.md index 98e4780e..f78e7d8b 100644 --- a/frontend/documentation/topics/Submitting-Data.md +++ b/frontend/documentation/topics/Submitting-Data.md @@ -1,52 +1,54 @@ # Data Submission and Updating -There are several different ways you can add data to a census, and some data does overlap across -censuses. +There are several different ways you can add data to a census, and some data does overlap across +censuses. ## Adding Supporting Data ### Direct submission -Let's use the stem codes page as an example: -1. When you navigate to its data grid, you will see an **add row** button at the top of the grid. +Let's use the stem codes page as an example: + +1. When you navigate to its data grid, you will see an **add row** button at the top of the grid. 2. Clicking on it will generate a **single** empty row that can then be populated. -3. Once completed, click the save button to confirm and then submit your changes. -4. The grid will reload and you should see your new row. +3. Once completed, click the save button to confirm and then submit your changes. +4. The grid will reload and you should see your new row. ### Bulk submission Again, let's use the stem codes page as an example: + 1. Click on the **Manual Entry Form** button to open the bulk data input interface 2. This will open a popup with a new grid, with buttons to add new rows, save, and submit. 3. Add rows as needed and ensure that you save your changes before submitting. - 1. Note: Bulk data input follows the same header format as file upload. Please ensure you + 1. Note: Bulk data input follows the same header format as file upload. Please ensure you correctly enter your data. 4. The grid will reload and you should see your new rows. ### File upload Again, let's use the stem codes page as an example: + 1. Click on the **Upload** button to open the file upload interface -2. Follow the prompts to upload, review, and submit your file. - 1. Note: your file should be in CSV format and correctly match the header format outlined in - the first page of the interface. -3. After the upload completes, you should see your new rows in the grid. +2. Follow the prompts to upload, review, and submit your file. + 1. Note: your file should be in CSV format and correctly match the header format outlined in + the first page of the interface. +3. After the upload completes, you should see your new rows in the grid. ### Editing Existing Data -If needed, you can edit existing data by clicking on the pencil icon at the end of a row. The -row will then switch to Edit mode, allowing you to make changes. Once completed, click the save +If needed, you can edit existing data by clicking on the pencil icon at the end of a row. The +row will then switch to Edit mode, allowing you to make changes. Once completed, click the save icon to confirm and then submit your changes. -> When in the View Data (measurements upload) page, you should notice that Actions column is not -> present. This is a +> When in the View Data (measurements upload) page, you should notice that Actions column is not +> present. This is a > deliberate choice to ensure that data is preserved as-is. ## Adding Measurements Data -Uploading measurements data follows the same process as uploading supporting data, with the -exception that Direct Submission and Editing are **disabled**. Furthermore, addition of new rows -will automatically run validation checks on the rows to ensure they are correct. Rows that pass -validation will be highlighted green and have a checkmark icon, while rows that fail validation +Uploading measurements data follows the same process as uploading supporting data, with the +exception that Direct Submission and Editing are **disabled**. Furthermore, addition of new rows +will automatically run validation checks on the rows to ensure they are correct. Rows that pass +validation will be highlighted green and have a checkmark icon, while rows that fail validation will be highlighted red and have an exclamation mark icon. - diff --git a/frontend/documentation/topics/Understanding-Supporting-Data.md b/frontend/documentation/topics/Understanding-Supporting-Data.md index da1b2112..3955c8c9 100644 --- a/frontend/documentation/topics/Understanding-Supporting-Data.md +++ b/frontend/documentation/topics/Understanding-Supporting-Data.md @@ -1,12 +1,13 @@ # Understanding Supporting Data -Supporting data is descriptive information that needs to be added before you can submit any -measurements. +Supporting data is descriptive information that needs to be added before you can submit any +measurements. ## What is Supporting Data? -There are four types of supporting data that are **required** before any measurements can be +There are four types of supporting data that are **required** before any measurements can be submitted: + 1. Stem Codes 2. Personnel 3. Quadrats @@ -15,6 +16,7 @@ submitted: ### Stem Codes These are attributes that can be assigned to a stem, and are subdivided into seven **statuses**: + - alive - alive-not measured - dead @@ -23,23 +25,23 @@ These are attributes that can be assigned to a stem, and are subdivided into sev - omitted - missing -When you add a new stem code, you must specify a **unique** code, a description of the attribute -being represented, and a status from the aforementioned list. +When you add a new stem code, you must specify a **unique** code, a description of the attribute +being represented, and a status from the aforementioned list. ### Personnel (and Roles) -Personnel are the people who are involved in the collection of data. They are assigned by census -and can be assigned a role depending on their involvement. If their role is not present, you can -create it by clicking ont the **Roles** button, which will open a popup with a new table. +Personnel are the people who are involved in the collection of data. They are assigned by census +and can be assigned a role depending on their involvement. If their role is not present, you can +create it by clicking ont the **Roles** button, which will open a popup with a new table. ### Quadrats -Quadrats are the smallest **recorded** region of measurement in a plot. They record starting +Quadrats are the smallest **recorded** region of measurement in a plot. They record starting coordinates (and their units of measure), their dimensions (and their units of measure), their area (and its units of measure), and their rough shape (square, rectangular, circular, etc.) ### Species -The taxonomic information for the stems being measured. This can range from as broad as the -family designation, or as specific as the subspecies designation. However, at a minimum, the -species must be specified. \ No newline at end of file +The taxonomic information for the stems being measured. This can range from as broad as the +family designation, or as specific as the subspecies designation. However, at a minimum, the +species must be specified. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 97bb6c2a..c4612f89 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -3749,15 +3749,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/duplexer2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -5520,17 +5511,6 @@ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/space-separated-tokens": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", @@ -6082,15 +6062,6 @@ "stylis": "4.2.0" } }, - "node_modules/jszip/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/@mui/x-data-grid": { "version": "7.22.1", "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.22.1.tgz", @@ -6291,16 +6262,6 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/https-proxy-agent": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", From 8a34acffd087e3756aa93101a75017c8c371019f Mon Sep 17 00:00:00 2001 From: siddheshraze <81591724+siddheshraze@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:06:11 -0500 Subject: [PATCH 09/15] saving changes. --- .../app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts index 5b704531..37bdc3ff 100644 --- a/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts +++ b/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts @@ -18,7 +18,6 @@ export async function POST( if (!schema || schema === 'undefined' || !pageParam || pageParam === 'undefined' || !pageSizeParam || pageSizeParam === 'undefined') throw new Error('core slugs schema/page/pageSize not correctly received'); const { filterModel } = await request.json(); - console.log('route filter model: ', filterModel); if (!filterModel.items || !filterModel.quickFilterValues) throw new Error('filterModel is empty. filter API should not have triggered.'); const page = parseInt(pageParam); const pageSize = parseInt(pageSizeParam); From 38e76cf93be9e6525f375341b582553d92368e29 Mon Sep 17 00:00:00 2001 From: sambokar Date: Mon, 18 Nov 2024 13:21:30 -0500 Subject: [PATCH 10/15] updating package-lock --- frontend/package-lock.json | 41 +++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c4612f89..1bb9e08f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -3749,6 +3749,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -5511,6 +5520,17 @@ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/space-separated-tokens": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", @@ -6062,6 +6082,15 @@ "stylis": "4.2.0" } }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/@mui/x-data-grid": { "version": "7.22.1", "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.22.1.tgz", @@ -6262,6 +6291,16 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", @@ -13101,4 +13140,4 @@ "license": "MIT" } } -} +} \ No newline at end of file From 6a773c8e80026b2d5c2a39ab8df2e881c0da07d7 Mon Sep 17 00:00:00 2001 From: sambokar Date: Mon, 18 Nov 2024 15:17:00 -0500 Subject: [PATCH 11/15] saving changes --- .../[dataType]/[[...slugs]]/route.ts | 2 +- .../api/fixeddataerrors/[[...slugs]]/route.ts | 77 +++++++++++++++++++ .../[dataType]/[[...slugs]]/route.ts | 43 ++++++++--- 3 files changed, 109 insertions(+), 13 deletions(-) create mode 100644 frontend/app/api/fixeddataerrors/[[...slugs]]/route.ts diff --git a/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts index 68d4eceb..a20602e9 100644 --- a/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts +++ b/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts @@ -8,7 +8,7 @@ import ConnectionManager from '@/config/connectionmanager'; // slugs SHOULD CONT // slugs SHOULD CONTAIN AT MINIMUM: schema, page, pageSize, plotID, plotCensusNumber, (optional) quadratID, (optional) speciesID export async function GET( - request: NextRequest, + _request: NextRequest, { params }: { diff --git a/frontend/app/api/fixeddataerrors/[[...slugs]]/route.ts b/frontend/app/api/fixeddataerrors/[[...slugs]]/route.ts new file mode 100644 index 00000000..6d132edf --- /dev/null +++ b/frontend/app/api/fixeddataerrors/[[...slugs]]/route.ts @@ -0,0 +1,77 @@ +import { NextRequest, NextResponse } from 'next/server'; +import ConnectionManager from '@/config/connectionmanager'; +import { CMError } from '@/config/macros/uploadsystemmacros'; +import { HTTPResponses } from '@/config/macros'; + +export async function GET( + _request: NextRequest, + { + params + }: { + params: { slugs?: string[] }; + } +) { + if (!params.slugs || params.slugs.length < 5) throw new Error('slugs not received.'); + const [schema, pageParam, pageSizeParam, plotIDParam, plotCensusNumberParam, quadratIDParam, speciesIDParam] = params.slugs; + if (!schema || schema === 'undefined' || !pageParam || pageParam === 'undefined' || !pageSizeParam || pageSizeParam === 'undefined') + throw new Error('core slugs schema/page/pageSize not correctly received'); + const page = parseInt(pageParam); + const pageSize = parseInt(pageSizeParam); + const plotID = plotIDParam ? parseInt(plotIDParam) : undefined; + const plotCensusNumber = plotCensusNumberParam ? parseInt(plotCensusNumberParam) : undefined; + const quadratID = quadratIDParam ? parseInt(quadratIDParam) : undefined; + const speciesID = speciesIDParam ? parseInt(speciesIDParam) : undefined; + const connectionManager = new ConnectionManager(); + + try { + await connectionManager.beginTransaction(); + // Query to fetch existing validation errors + const validationErrorsQuery = ` + SELECT SQL_CALC_FOUND_ROWS + ms.*, + ( + SELECT GROUP_CONCAT(ve.ValidationID) + FROM ${schema}.cmverrors AS cve + JOIN catalog.validationprocedures AS ve ON cve.ValidationErrorID = ve.ValidationID + WHERE cve.CoreMeasurementID = ms.CoreMeasurementID + ) AS ValidationErrorIDs, + ( + SELECT GROUP_CONCAT(ve.Description) + FROM ${schema}.cmverrors AS cve + JOIN catalog.validationprocedures AS ve ON cve.ValidationErrorID = ve.ValidationID + WHERE cve.CoreMeasurementID = ms.CoreMeasurementID + ) AS ValidationErrorDescriptions + FROM + ${schema}.measurementssummary AS ms + JOIN ${schema}.census c ON ms.CensusID = c.CensusID AND ms.PlotID = c.PlotID + WHERE + ms.PlotID = ? + AND c.PlotCensusNumber = ? + LIMIT ?, ?;`; + const validationErrorsRows = await connectionManager.executeQuery(validationErrorsQuery); + + const parsedValidationErrors: CMError[] = validationErrorsRows.map((row: any) => ({ + coreMeasurementID: row.CoreMeasurementID, + validationErrorIDs: row.ValidationErrorIDs.split(',').map(Number), + descriptions: row.Descriptions.split(',') + })); + return new NextResponse( + JSON.stringify({ + failed: parsedValidationErrors + }), + { + status: HTTPResponses.OK, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } catch (error: any) { + await connectionManager.rollbackTransaction(); + return new NextResponse(JSON.stringify({ error: error.message }), { + status: 500 + }); + } finally { + await connectionManager.closeConnection(); + } +} diff --git a/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts index 37bdc3ff..d5acd8ee 100644 --- a/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts +++ b/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts @@ -4,6 +4,14 @@ import { escape } from 'mysql2'; import { format } from 'mysql2/promise'; import MapperFactory from '@/config/datamapper'; import { HTTPResponses } from '@/config/macros'; +import { capitalizeAndTransformField } from '@/config/utils'; +import { GridFilterItem, GridFilterModel } from '@mui/x-data-grid'; + +interface FilterItem { + field: string; + operator: string; + value: string | number | null; +} export async function POST( request: NextRequest, @@ -17,8 +25,9 @@ export async function POST( const [schema, pageParam, pageSizeParam, plotIDParam, plotCensusNumberParam, quadratIDParam, speciesIDParam] = params.slugs; if (!schema || schema === 'undefined' || !pageParam || pageParam === 'undefined' || !pageSizeParam || pageSizeParam === 'undefined') throw new Error('core slugs schema/page/pageSize not correctly received'); - const { filterModel } = await request.json(); - if (!filterModel.items || !filterModel.quickFilterValues) throw new Error('filterModel is empty. filter API should not have triggered.'); + const body = await request.json(); + const filterModel: GridFilterModel = body.filterModel; + if (!filterModel.items && !filterModel.quickFilterValues) throw new Error('filterModel is empty. filter API should not have triggered.'); const page = parseInt(pageParam); const pageSize = parseInt(pageSizeParam); const plotID = plotIDParam ? parseInt(plotIDParam) : undefined; @@ -30,6 +39,15 @@ export async function POST( let censusIDs; let pastCensusIDs: string | any[]; + const buildFilterModelStub = (filterModel: GridFilterModel, alias?: string) => + // the `field` here automatically references the column in the table. don't need to do other processing with it. + filterModel.items + .map((item: GridFilterItem) => { + const { field: field = capitalizeAndTransformField(item.field), operator, value } = item; + return `\`${alias ? `${alias}.` : ''}${field}\` ${operator} ${escape(`%${value}%`)}`; + }) + .join(` ${filterModel?.logicOperator?.toUpperCase()} `); + const buildSearchStub = (columns: any[], quickFilter: string[], alias?: string) => columns.map((column: any) => quickFilter.map(word => `\`${alias ? `${alias}.` : ''}${column}\` LIKE ${escape(`%${word}%`)}`).join(' OR ')).join(' OR '); @@ -46,12 +64,13 @@ export async function POST( console.log('error: ', e); throw new Error(e); } - const searchStub = buildSearchStub(columns, filterModel.quickFilterValues); + const searchStub = filterModel.quickFilterValues ? buildSearchStub(columns, filterModel.quickFilterValues) : ''; + const filterStub = filterModel.items ? buildFilterModelStub(filterModel) : ``; switch (params.dataType) { case 'validationprocedures': paginatedQuery = ` SELECT SQL_CALC_FOUND_ROWS * - FROM catalog.${params.dataType} WHERE ${searchStub}`; + FROM catalog.${params.dataType} WHERE ${searchStub} OR ${filterStub}`; queryParams.push(page * pageSize, pageSize); break; case 'specieslimits': @@ -65,7 +84,7 @@ export async function POST( case 'quadratpersonnel': case 'sitespecificvalidations': case 'roles': - paginatedQuery = `SELECT SQL_CALC_FOUND_ROWS * FROM ${schema}.${params.dataType} WHERE ${searchStub}`; + paginatedQuery = `SELECT SQL_CALC_FOUND_ROWS * FROM ${schema}.${params.dataType} WHERE (${searchStub} OR ${filterStub})`; queryParams.push(page * pageSize, pageSize); break; case 'personnel': @@ -74,7 +93,7 @@ export async function POST( FROM ${schema}.${params.dataType} p JOIN ${schema}.census c ON p.CensusID = c.CensusID WHERE c.PlotID = ? - AND c.PlotCensusNumber = ? AND ${searchStub}`; + AND c.PlotCensusNumber = ? AND (${searchStub} OR ${filterStub})`; console.log('paginated query: ', paginatedQuery); queryParams.push(plotID, plotCensusNumber, page * pageSize, pageSize); break; @@ -86,7 +105,7 @@ export async function POST( JOIN ${schema}.census c ON cq.CensusID = c.CensusID WHERE q.PlotID = ? AND c.PlotID = ? - AND c.PlotCensusNumber = ? AND ${searchStub}`; + AND c.PlotCensusNumber = ? AND (${searchStub} OR ${filterStub})`; queryParams.push(plotID, plotID, plotCensusNumber, page * pageSize, pageSize); break; case 'personnelrole': @@ -103,7 +122,7 @@ export async function POST( LEFT JOIN roles r ON p.RoleID = r.RoleID census c ON p.CensusID = c.CensusID - WHERE c.PlotID = ? AND c.PlotCensusNumber = ? AND ${searchStub}`; + WHERE c.PlotID = ? AND c.PlotCensusNumber = ? AND (${searchStub} OR ${filterStub})`; queryParams.push(plotID, plotCensusNumber, page * pageSize, pageSize); break; case 'measurementssummary': @@ -117,7 +136,7 @@ export async function POST( JOIN ${schema}.census c ON vft.PlotID = c.PlotID AND vft.CensusID = c.CensusID WHERE vft.PlotID = ? AND c.PlotID = ? - AND c.PlotCensusNumber = ? AND ${searchStub} + AND c.PlotCensusNumber = ? AND (${searchStub} OR ${filterStub}) ORDER BY vft.MeasurementDate ASC`; queryParams.push(plotID, plotID, plotCensusNumber, page * pageSize, pageSize); break; @@ -125,7 +144,7 @@ export async function POST( paginatedQuery = ` SELECT SQL_CALC_FOUND_ROWS * FROM ${schema}.census - WHERE PlotID = ? AND ${searchStub}`; + WHERE PlotID = ? AND (${searchStub} OR ${filterStub})`; queryParams.push(plotID, page * pageSize, pageSize); break; case 'coremeasurements': @@ -143,7 +162,7 @@ export async function POST( FROM ${schema}.${params.dataType} pdt JOIN ${schema}.census c ON pdt.CensusID = c.CensusID WHERE c.PlotID = ? - AND c.PlotCensusNumber = ? AND ${searchStub} + AND c.PlotCensusNumber = ? AND (${searchStub} OR ${filterStub}) ORDER BY pdt.MeasurementDate`; queryParams.push(plotID, plotCensusNumber, page * pageSize, pageSize); break; @@ -156,7 +175,7 @@ export async function POST( FROM ${schema}.${params.dataType} pdt JOIN ${schema}.census c ON sp.CensusID = c.CensusID WHERE c.PlotID = ? - AND c.CensusID IN (${censusIDs.map(() => '?').join(', ')}) AND ${searchStub} + AND c.CensusID IN (${censusIDs.map(() => '?').join(', ')}) AND (${searchStub} OR ${filterStub}) ORDER BY pdt.MeasurementDate ASC`; queryParams.push(plotID, ...censusIDs, page * pageSize, pageSize); break; From 371d11d07eef86f10584223295e825b12a1c8f93 Mon Sep 17 00:00:00 2001 From: sambokar Date: Mon, 25 Nov 2024 14:01:55 -0500 Subject: [PATCH 12/15] customized filtration system refined. error/valid/pending highlighting expanded to dynamically re-query data set when checked states change. --- .../[dataType]/[[...slugs]]/route.ts | 96 +--- .../api/fixeddataerrors/[[...slugs]]/route.ts | 77 --- .../[dataType]/[[...slugs]]/route.ts | 522 ++++++++++++------ .../procedures/[validationType]/route.ts | 9 +- .../validationerrordisplay/route.ts | 1 + .../api/validations/validationlist/route.ts | 2 +- .../components/client/datagridcolumns.tsx | 20 +- .../components/client/validationmodal.tsx | 318 +++++------ .../isolated/isolatedmsvstagingdatagrid.tsx | 4 +- .../datagrids/applications/msvdatagrid.tsx | 33 +- .../datagrids/measurementscommons.tsx | 490 ++++++++++------ .../processors/processorhelperfunctions.tsx | 94 ++-- .../segments/uploadvalidation.tsx | 158 +++--- frontend/config/datagridhelpers.ts | 27 +- .../config/sqlrdsdefinitions/validations.ts | 1 + frontend/config/sqlrdsdefinitions/views.ts | 4 +- 16 files changed, 980 insertions(+), 876 deletions(-) delete mode 100644 frontend/app/api/fixeddataerrors/[[...slugs]]/route.ts diff --git a/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts index a20602e9..1aadb314 100644 --- a/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts +++ b/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts @@ -8,7 +8,7 @@ import ConnectionManager from '@/config/connectionmanager'; // slugs SHOULD CONT // slugs SHOULD CONTAIN AT MINIMUM: schema, page, pageSize, plotID, plotCensusNumber, (optional) quadratID, (optional) speciesID export async function GET( - _request: NextRequest, + request: NextRequest, { params }: { @@ -16,19 +16,16 @@ export async function GET( } ): Promise> { if (!params.slugs || params.slugs.length < 5) throw new Error('slugs not received.'); - const [schema, pageParam, pageSizeParam, plotIDParam, plotCensusNumberParam, quadratIDParam, speciesIDParam] = params.slugs; + const [schema, pageParam, pageSizeParam, plotIDParam, plotCensusNumberParam, speciesIDParam] = params.slugs; if (!schema || schema === 'undefined' || !pageParam || pageParam === 'undefined' || !pageSizeParam || pageSizeParam === 'undefined') throw new Error('core slugs schema/page/pageSize not correctly received'); const page = parseInt(pageParam); const pageSize = parseInt(pageSizeParam); const plotID = plotIDParam ? parseInt(plotIDParam) : undefined; const plotCensusNumber = plotCensusNumberParam ? parseInt(plotCensusNumberParam) : undefined; - const quadratID = quadratIDParam ? parseInt(quadratIDParam) : undefined; const speciesID = speciesIDParam ? parseInt(speciesIDParam) : undefined; + const connectionManager = new ConnectionManager(); - let updatedMeasurementsExist = false; - let censusIDs; - let pastCensusIDs: string | any[]; try { let paginatedQuery = ``; @@ -92,21 +89,6 @@ export async function GET( WHERE c.PlotID = ? AND c.PlotCensusNumber = ? LIMIT ?, ?;`; queryParams.push(plotID, plotCensusNumber, page * pageSize, pageSize); break; - case 'measurementssummary': - case 'measurementssummary_staging': - case 'measurementssummaryview': - case 'viewfulltable': - case 'viewfulltableview': - paginatedQuery = ` - SELECT SQL_CALC_FOUND_ROWS vft.* - FROM ${schema}.${params.dataType} vft - JOIN ${schema}.census c ON vft.PlotID = c.PlotID AND vft.CensusID = c.CensusID - WHERE vft.PlotID = ? - AND c.PlotID = ? - AND c.PlotCensusNumber = ? - ORDER BY vft.MeasurementDate ASC LIMIT ?, ?;`; - queryParams.push(plotID, plotID, plotCensusNumber, page * pageSize, pageSize); - break; case 'census': paginatedQuery = ` SELECT SQL_CALC_FOUND_ROWS * @@ -114,41 +96,6 @@ export async function GET( WHERE PlotID = ? LIMIT ?, ?`; queryParams.push(plotID, page * pageSize, pageSize); break; - case 'coremeasurements': - // Retrieve multiple past CensusID for the given PlotCensusNumber - const censusQuery = ` - SELECT CensusID - FROM ${schema}.census - WHERE PlotID = ? - AND PlotCensusNumber = ? - ORDER BY StartDate DESC LIMIT 30 - `; - const censusResults = await connectionManager.executeQuery(format(censusQuery, [plotID, plotCensusNumber])); - if (censusResults.length < 2) { - paginatedQuery = ` - SELECT SQL_CALC_FOUND_ROWS pdt.* - FROM ${schema}.${params.dataType} pdt - JOIN ${schema}.census c ON pdt.CensusID = c.CensusID - WHERE c.PlotID = ? - AND c.PlotCensusNumber = ? - ORDER BY pdt.MeasurementDate LIMIT ?, ?`; - queryParams.push(plotID, plotCensusNumber, page * pageSize, pageSize); - break; - } else { - updatedMeasurementsExist = true; - censusIDs = censusResults.map((c: any) => c.CensusID); - pastCensusIDs = censusIDs.slice(1); - // Query to fetch paginated measurements from measurementssummaryview - paginatedQuery = ` - SELECT SQL_CALC_FOUND_ROWS pdt.* - FROM ${schema}.${params.dataType} pdt - JOIN ${schema}.census c ON sp.CensusID = c.CensusID - WHERE c.PlotID = ? - AND c.CensusID IN (${censusIDs.map(() => '?').join(', ')}) - ORDER BY pdt.MeasurementDate ASC LIMIT ?, ?`; - queryParams.push(plotID, ...censusIDs, page * pageSize, pageSize); - break; - } default: throw new Error(`Unknown dataType: ${params.dataType}`); } @@ -163,34 +110,15 @@ export async function GET( const totalRowsResult = await connectionManager.executeQuery(totalRowsQuery); const totalRows = totalRowsResult[0].totalRows; - if (updatedMeasurementsExist) { - // Separate deprecated and non-deprecated rows - const deprecated = paginatedResults.filter((row: any) => pastCensusIDs.includes(row.CensusID)); - - // Ensure deprecated measurements are duplicates - const uniqueKeys = ['PlotID', 'QuadratID', 'TreeID', 'StemID']; // Define unique keys that should match - const outputKeys = paginatedResults.map((row: any) => uniqueKeys.map(key => row[key]).join('|')); - const filteredDeprecated = deprecated.filter((row: any) => outputKeys.includes(uniqueKeys.map(key => row[key]).join('|'))); - return new NextResponse( - JSON.stringify({ - output: MapperFactory.getMapper(params.dataType).mapData(paginatedResults), - deprecated: MapperFactory.getMapper(params.dataType).mapData(filteredDeprecated), - totalCount: totalRows, - finishedQuery: format(paginatedQuery, queryParams) - }), - { status: HTTPResponses.OK } - ); - } else { - return new NextResponse( - JSON.stringify({ - output: MapperFactory.getMapper(params.dataType).mapData(paginatedResults), - deprecated: undefined, - totalCount: totalRows, - finishedQuery: format(paginatedQuery, queryParams) - }), - { status: HTTPResponses.OK } - ); - } + return new NextResponse( + JSON.stringify({ + output: MapperFactory.getMapper(params.dataType).mapData(paginatedResults), + deprecated: undefined, + totalCount: totalRows, + finishedQuery: format(paginatedQuery, queryParams) + }), + { status: HTTPResponses.OK } + ); } catch (error: any) { throw new Error(error); } finally { diff --git a/frontend/app/api/fixeddataerrors/[[...slugs]]/route.ts b/frontend/app/api/fixeddataerrors/[[...slugs]]/route.ts deleted file mode 100644 index 6d132edf..00000000 --- a/frontend/app/api/fixeddataerrors/[[...slugs]]/route.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import ConnectionManager from '@/config/connectionmanager'; -import { CMError } from '@/config/macros/uploadsystemmacros'; -import { HTTPResponses } from '@/config/macros'; - -export async function GET( - _request: NextRequest, - { - params - }: { - params: { slugs?: string[] }; - } -) { - if (!params.slugs || params.slugs.length < 5) throw new Error('slugs not received.'); - const [schema, pageParam, pageSizeParam, plotIDParam, plotCensusNumberParam, quadratIDParam, speciesIDParam] = params.slugs; - if (!schema || schema === 'undefined' || !pageParam || pageParam === 'undefined' || !pageSizeParam || pageSizeParam === 'undefined') - throw new Error('core slugs schema/page/pageSize not correctly received'); - const page = parseInt(pageParam); - const pageSize = parseInt(pageSizeParam); - const plotID = plotIDParam ? parseInt(plotIDParam) : undefined; - const plotCensusNumber = plotCensusNumberParam ? parseInt(plotCensusNumberParam) : undefined; - const quadratID = quadratIDParam ? parseInt(quadratIDParam) : undefined; - const speciesID = speciesIDParam ? parseInt(speciesIDParam) : undefined; - const connectionManager = new ConnectionManager(); - - try { - await connectionManager.beginTransaction(); - // Query to fetch existing validation errors - const validationErrorsQuery = ` - SELECT SQL_CALC_FOUND_ROWS - ms.*, - ( - SELECT GROUP_CONCAT(ve.ValidationID) - FROM ${schema}.cmverrors AS cve - JOIN catalog.validationprocedures AS ve ON cve.ValidationErrorID = ve.ValidationID - WHERE cve.CoreMeasurementID = ms.CoreMeasurementID - ) AS ValidationErrorIDs, - ( - SELECT GROUP_CONCAT(ve.Description) - FROM ${schema}.cmverrors AS cve - JOIN catalog.validationprocedures AS ve ON cve.ValidationErrorID = ve.ValidationID - WHERE cve.CoreMeasurementID = ms.CoreMeasurementID - ) AS ValidationErrorDescriptions - FROM - ${schema}.measurementssummary AS ms - JOIN ${schema}.census c ON ms.CensusID = c.CensusID AND ms.PlotID = c.PlotID - WHERE - ms.PlotID = ? - AND c.PlotCensusNumber = ? - LIMIT ?, ?;`; - const validationErrorsRows = await connectionManager.executeQuery(validationErrorsQuery); - - const parsedValidationErrors: CMError[] = validationErrorsRows.map((row: any) => ({ - coreMeasurementID: row.CoreMeasurementID, - validationErrorIDs: row.ValidationErrorIDs.split(',').map(Number), - descriptions: row.Descriptions.split(',') - })); - return new NextResponse( - JSON.stringify({ - failed: parsedValidationErrors - }), - { - status: HTTPResponses.OK, - headers: { - 'Content-Type': 'application/json' - } - } - ); - } catch (error: any) { - await connectionManager.rollbackTransaction(); - return new NextResponse(JSON.stringify({ error: error.message }), { - status: 500 - }); - } finally { - await connectionManager.closeConnection(); - } -} diff --git a/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts index d5acd8ee..9ff0325a 100644 --- a/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts +++ b/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts @@ -4,13 +4,14 @@ import { escape } from 'mysql2'; import { format } from 'mysql2/promise'; import MapperFactory from '@/config/datamapper'; import { HTTPResponses } from '@/config/macros'; -import { capitalizeAndTransformField } from '@/config/utils'; import { GridFilterItem, GridFilterModel } from '@mui/x-data-grid'; +import { handleError } from '@/utils/errorhandler'; +import { AllTaxonomiesViewQueryConfig, handleDeleteForSlices, handleUpsertForSlices } from '@/components/processors/processorhelperfunctions'; -interface FilterItem { - field: string; - operator: string; - value: string | number | null; +type VisibleFilter = 'valid' | 'errors' | 'pending'; + +interface ExtendedGridFilterModel extends GridFilterModel { + visible: VisibleFilter[]; } export async function POST( @@ -20,208 +21,379 @@ export async function POST( }: { params: { dataType: string; slugs?: string[] }; } -): Promise> { - if (!params.slugs || params.slugs.length < 5) throw new Error('slugs not received.'); - const [schema, pageParam, pageSizeParam, plotIDParam, plotCensusNumberParam, quadratIDParam, speciesIDParam] = params.slugs; - if (!schema || schema === 'undefined' || !pageParam || pageParam === 'undefined' || !pageSizeParam || pageSizeParam === 'undefined') - throw new Error('core slugs schema/page/pageSize not correctly received'); +) { + // trying to ensure that system correctly retains edit/add functionality -- not necessarily needed currently but better safe than sorry const body = await request.json(); - const filterModel: GridFilterModel = body.filterModel; - if (!filterModel.items && !filterModel.quickFilterValues) throw new Error('filterModel is empty. filter API should not have triggered.'); - const page = parseInt(pageParam); - const pageSize = parseInt(pageSizeParam); - const plotID = plotIDParam ? parseInt(plotIDParam) : undefined; - const plotCensusNumber = plotCensusNumberParam ? parseInt(plotCensusNumberParam) : undefined; - const quadratID = quadratIDParam ? parseInt(quadratIDParam) : undefined; - const speciesID = speciesIDParam ? parseInt(speciesIDParam) : undefined; - const connectionManager = new ConnectionManager(); - let updatedMeasurementsExist = false; - let censusIDs; - let pastCensusIDs: string | any[]; - - const buildFilterModelStub = (filterModel: GridFilterModel, alias?: string) => - // the `field` here automatically references the column in the table. don't need to do other processing with it. - filterModel.items - .map((item: GridFilterItem) => { - const { field: field = capitalizeAndTransformField(item.field), operator, value } = item; - return `\`${alias ? `${alias}.` : ''}${field}\` ${operator} ${escape(`%${value}%`)}`; - }) - .join(` ${filterModel?.logicOperator?.toUpperCase()} `); - - const buildSearchStub = (columns: any[], quickFilter: string[], alias?: string) => - columns.map((column: any) => quickFilter.map(word => `\`${alias ? `${alias}.` : ''}${column}\` LIKE ${escape(`%${word}%`)}`).join(' OR ')).join(' OR '); + if (body.newRow) { + console.log('newRow path'); + // required dynamic parameters: dataType (fixed),[ schema, gridID value] -> slugs + if (!params.slugs) throw new Error('slugs not provided'); + const [schema, gridID, plotIDParam, censusIDParam] = params.slugs; + if (!schema || !gridID) throw new Error('no schema or gridID provided'); + + const plotID = plotIDParam ? parseInt(plotIDParam) : undefined; + const censusID = censusIDParam ? parseInt(censusIDParam) : undefined; + + const connectionManager = new ConnectionManager(); + const { newRow } = await request.json(); + let insertIDs: { [key: string]: number } = {}; - try { - let paginatedQuery = ``; - const queryParams: any[] = []; - let columns: any[] = []; try { - const query = `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? - AND COLUMN_NAME NOT LIKE '%id%' AND COLUMN_NAME NOT LIKE '%uuid%' AND COLUMN_NAME NOT LIKE 'id%' AND COLUMN_NAME NOT LIKE '%_id' `; - const results = await connectionManager.executeQuery(query, [schema, params.dataType]); - columns = results.map((row: any) => row.COLUMN_NAME); - } catch (e: any) { - console.log('error: ', e); - throw new Error(e); + await connectionManager.beginTransaction(); + + if (Object.keys(newRow).includes('isNew')) delete newRow.isNew; + + const newRowData = MapperFactory.getMapper(params.dataType).demapData([newRow])[0]; + const demappedGridID = gridID.charAt(0).toUpperCase() + gridID.substring(1); + + // Handle SQL views with handleUpsertForSlices + if (params.dataType.includes('view')) { + let queryConfig; + switch (params.dataType) { + case 'alltaxonomiesview': + queryConfig = AllTaxonomiesViewQueryConfig; + break; + default: + throw new Error('Incorrect view call'); + } + + // Use handleUpsertForSlices and retrieve the insert IDs + insertIDs = await handleUpsertForSlices(connectionManager, schema, newRowData, queryConfig); + } + // Handle the case for 'attributes' + else if (params.dataType === 'attributes') { + const insertQuery = format('INSERT INTO ?? SET ?', [`${schema}.${params.dataType}`, newRowData]); + const results = await connectionManager.executeQuery(insertQuery); + insertIDs = { attributes: results.insertId }; // Standardize output with table name as key + } + // Handle all other cases + else { + delete newRowData[demappedGridID]; + if (params.dataType === 'plots') delete newRowData.NumQuadrats; + const insertQuery = format('INSERT INTO ?? SET ?', [`${schema}.${params.dataType}`, newRowData]); + const results = await connectionManager.executeQuery(insertQuery); + insertIDs = { [params.dataType]: results.insertId }; // Standardize output with table name as key + + // special handling needed for quadrats --> need to correlate incoming quadrats with current census + if (params.dataType === 'quadrats' && censusID) { + const cqQuery = format('INSERT INTO ?? SET ?', [`${schema}.censusquadrats`, { CensusID: censusID, QuadratID: insertIDs.quadrats }]); + const results = await connectionManager.executeQuery(cqQuery); + if (results.length === 0) throw new Error('Error inserting to censusquadrats'); + } + } + + return NextResponse.json({ message: 'Insert successful', createdIDs: insertIDs }, { status: HTTPResponses.OK }); + } catch (error: any) { + return handleError(error, connectionManager, newRow); + } finally { + await connectionManager.closeConnection(); } - const searchStub = filterModel.quickFilterValues ? buildSearchStub(columns, filterModel.quickFilterValues) : ''; - const filterStub = filterModel.items ? buildFilterModelStub(filterModel) : ``; - switch (params.dataType) { - case 'validationprocedures': - paginatedQuery = ` - SELECT SQL_CALC_FOUND_ROWS * - FROM catalog.${params.dataType} WHERE ${searchStub} OR ${filterStub}`; - queryParams.push(page * pageSize, pageSize); - break; - case 'specieslimits': - paginatedQuery = `SELECT SQL_CALC_FOUND_ROWS * FROM ${schema}.${params.dataType} pdt WHERE pdt.SpeciesID = ?`; - queryParams.push(speciesID, page * pageSize, pageSize); - break; - case 'attributes': - case 'species': - case 'stems': - case 'alltaxonomiesview': - case 'quadratpersonnel': - case 'sitespecificvalidations': - case 'roles': - paginatedQuery = `SELECT SQL_CALC_FOUND_ROWS * FROM ${schema}.${params.dataType} WHERE (${searchStub} OR ${filterStub})`; - queryParams.push(page * pageSize, pageSize); - break; - case 'personnel': - paginatedQuery = ` - SELECT SQL_CALC_FOUND_ROWS p.* - FROM ${schema}.${params.dataType} p - JOIN ${schema}.census c ON p.CensusID = c.CensusID - WHERE c.PlotID = ? - AND c.PlotCensusNumber = ? AND (${searchStub} OR ${filterStub})`; - console.log('paginated query: ', paginatedQuery); - queryParams.push(plotID, plotCensusNumber, page * pageSize, pageSize); - break; - case 'quadrats': - paginatedQuery = ` - SELECT SQL_CALC_FOUND_ROWS q.* - FROM ${schema}.quadrats q - JOIN ${schema}.censusquadrat cq ON q.QuadratID = cq.QuadratID - JOIN ${schema}.census c ON cq.CensusID = c.CensusID - WHERE q.PlotID = ? - AND c.PlotID = ? - AND c.PlotCensusNumber = ? AND (${searchStub} OR ${filterStub})`; - queryParams.push(plotID, plotID, plotCensusNumber, page * pageSize, pageSize); - break; - case 'personnelrole': - paginatedQuery = ` - SELECT SQL_CALC_FOUND_ROWS - p.PersonnelID, - p.CensusID, - p.FirstName, - p.LastName, - r.RoleName, - r.RoleDescription - FROM - personnel p - LEFT JOIN - roles r ON p.RoleID = r.RoleID - census c ON p.CensusID = c.CensusID - WHERE c.PlotID = ? AND c.PlotCensusNumber = ? AND (${searchStub} OR ${filterStub})`; - queryParams.push(plotID, plotCensusNumber, page * pageSize, pageSize); - break; - case 'measurementssummary': - case 'measurementssummary_staging': - case 'measurementssummaryview': - case 'viewfulltable': - case 'viewfulltableview': - paginatedQuery = ` + } else { + console.log('non new row path'); + const filterModel: ExtendedGridFilterModel = body.filterModel; + console.log('filter model: ', filterModel); + if (!params.slugs || params.slugs.length < 5) throw new Error('slugs not received.'); + const [schema, pageParam, pageSizeParam, plotIDParam, plotCensusNumberParam] = params.slugs; + if (!schema || schema === 'undefined' || !pageParam || pageParam === 'undefined' || !pageSizeParam || pageSizeParam === 'undefined') + throw new Error('core slugs schema/page/pageSize not correctly received'); + if (!filterModel || (!filterModel.items && !filterModel.quickFilterValues)) throw new Error('filterModel is empty. filter API should not have triggered.'); + const page = parseInt(pageParam); + const pageSize = parseInt(pageSizeParam); + const plotID = plotIDParam ? parseInt(plotIDParam) : undefined; + const plotCensusNumber = plotCensusNumberParam ? parseInt(plotCensusNumberParam) : undefined; + const connectionManager = new ConnectionManager(); + let updatedMeasurementsExist = false; + let censusIDs; + let pastCensusIDs: string | any[]; + + const buildFilterModelStub = (filterModel: GridFilterModel, alias?: string) => { + if (!filterModel.items || filterModel.items.length === 0) { + return ''; + } + + return filterModel.items + .map((item: GridFilterItem) => { + const { field, operator, value } = item; + const aliasedField = `${alias ? `${alias}.` : ''}${field}`; + const escapedValue = escape(`%${value}%`); // Handle escaping + return `${aliasedField} ${operator} ${escapedValue}`; + }) + .join(` ${filterModel?.logicOperator?.toUpperCase() || 'AND'} `); + }; + + const buildSearchStub = (columns: string[], quickFilter: string[], alias?: string) => { + if (!quickFilter || quickFilter.length === 0) { + return ''; // Return empty if no quick filters + } + + return columns + .map(column => { + const aliasedColumn = `${alias ? `${alias}.` : ''}${column}`; + return quickFilter.map(word => `${aliasedColumn} LIKE ${escape(`%${word}%`)}`).join(' OR '); + }) + .join(' OR '); + }; + + try { + let paginatedQuery = ``; + const queryParams: any[] = []; + let columns: any[] = []; + try { + const query = `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? + AND COLUMN_NAME NOT LIKE '%id%' AND COLUMN_NAME NOT LIKE '%uuid%' AND COLUMN_NAME NOT LIKE 'id%' AND COLUMN_NAME NOT LIKE '%_id' `; + const results = await connectionManager.executeQuery(query, [schema, params.dataType]); + columns = results.map((row: any) => row.COLUMN_NAME); + } catch (e: any) { + console.log('error: ', e); + throw new Error(e); + } + let searchStub = ''; + let filterStub = ''; + switch (params.dataType) { + case 'measurementssummary': + case 'measurementssummary_staging': + case 'measurementssummaryview': + case 'viewfulltable': + case 'viewfulltableview': + if (filterModel.quickFilterValues) { + searchStub = buildSearchStub(columns, filterModel.quickFilterValues, 'vft'); + } + + if (filterModel.items) { + filterStub = buildFilterModelStub(filterModel, 'vft'); + } + paginatedQuery = ` SELECT SQL_CALC_FOUND_ROWS vft.* FROM ${schema}.${params.dataType} vft JOIN ${schema}.census c ON vft.PlotID = c.PlotID AND vft.CensusID = c.CensusID WHERE vft.PlotID = ? AND c.PlotID = ? - AND c.PlotCensusNumber = ? AND (${searchStub} OR ${filterStub}) + AND c.PlotCensusNumber = ? + ${ + filterModel.visible.length > 0 + ? ` AND (${filterModel.visible + .map(v => { + switch (v) { + case 'valid': + return `vft.IsValidated = TRUE`; + case 'errors': + return `vft.IsValidated = FALSE`; + case 'pending': + return `vft.IsValidated IS NULL`; + default: + return null; + } + }) + .filter(Boolean) + .join(' OR ')})` + : '' + } + ${searchStub || filterStub ? ` AND (${[searchStub, filterStub].filter(Boolean).join(' OR ')})` : ''} ORDER BY vft.MeasurementDate ASC`; - queryParams.push(plotID, plotID, plotCensusNumber, page * pageSize, pageSize); - break; - case 'census': - paginatedQuery = ` - SELECT SQL_CALC_FOUND_ROWS * - FROM ${schema}.census - WHERE PlotID = ? AND (${searchStub} OR ${filterStub})`; - queryParams.push(plotID, page * pageSize, pageSize); - break; - case 'coremeasurements': - const censusQuery = ` + queryParams.push(plotID, plotID, plotCensusNumber, page * pageSize, pageSize); + break; + case 'coremeasurements': + if (filterModel.quickFilterValues) { + searchStub = buildSearchStub(columns, filterModel.quickFilterValues, 'vft'); + } + + if (filterModel.items) { + filterStub = buildFilterModelStub(filterModel, 'vft'); + } + const censusQuery = ` SELECT CensusID FROM ${schema}.census WHERE PlotID = ? AND PlotCensusNumber = ? ORDER BY StartDate DESC LIMIT 30 `; - const censusResults = await connectionManager.executeQuery(format(censusQuery, [plotID, plotCensusNumber])); - if (censusResults.length < 2) { - paginatedQuery = ` + const censusResults = await connectionManager.executeQuery(format(censusQuery, [plotID, plotCensusNumber])); + if (censusResults.length < 2) { + paginatedQuery = ` SELECT SQL_CALC_FOUND_ROWS pdt.* FROM ${schema}.${params.dataType} pdt JOIN ${schema}.census c ON pdt.CensusID = c.CensusID WHERE c.PlotID = ? - AND c.PlotCensusNumber = ? AND (${searchStub} OR ${filterStub}) + AND c.PlotCensusNumber = ? AND (${searchStub} ${filterStub !== '' ? `OR ${filterStub}` : ``}) ORDER BY pdt.MeasurementDate`; - queryParams.push(plotID, plotCensusNumber, page * pageSize, pageSize); - break; - } else { - updatedMeasurementsExist = true; - censusIDs = censusResults.map((c: any) => c.CensusID); - pastCensusIDs = censusIDs.slice(1); - paginatedQuery = ` + queryParams.push(plotID, plotCensusNumber, page * pageSize, pageSize); + break; + } else { + updatedMeasurementsExist = true; + censusIDs = censusResults.map((c: any) => c.CensusID); + pastCensusIDs = censusIDs.slice(1); + paginatedQuery = ` SELECT SQL_CALC_FOUND_ROWS pdt.* FROM ${schema}.${params.dataType} pdt JOIN ${schema}.census c ON sp.CensusID = c.CensusID WHERE c.PlotID = ? - AND c.CensusID IN (${censusIDs.map(() => '?').join(', ')}) AND (${searchStub} OR ${filterStub}) + AND c.CensusID IN (${censusIDs.map(() => '?').join(', ')}) + ${searchStub || filterStub ? ` AND (${[searchStub, filterStub].filter(Boolean).join(' OR ')})` : ''} ORDER BY pdt.MeasurementDate ASC`; - queryParams.push(plotID, ...censusIDs, page * pageSize, pageSize); - break; - } - default: - throw new Error(`Unknown dataType: ${params.dataType}`); + queryParams.push(plotID, ...censusIDs, page * pageSize, pageSize); + break; + } + default: + throw new Error(`Unknown dataType: ${params.dataType}`); + } + paginatedQuery += ` LIMIT ?, ?;`; + + if (paginatedQuery.match(/\?/g)?.length !== queryParams.length) { + throw new Error('Mismatch between query placeholders and parameters'); + } + console.log('completed query: ', format(paginatedQuery, queryParams)); + const paginatedResults = await connectionManager.executeQuery(format(paginatedQuery, queryParams)); + + const totalRowsQuery = 'SELECT FOUND_ROWS() as totalRows'; + const totalRowsResult = await connectionManager.executeQuery(totalRowsQuery); + const totalRows = totalRowsResult[0].totalRows; + + if (updatedMeasurementsExist) { + const deprecated = paginatedResults.filter((row: any) => pastCensusIDs.includes(row.CensusID)); + + const uniqueKeys = ['PlotID', 'QuadratID', 'TreeID', 'StemID']; + const outputKeys = paginatedResults.map((row: any) => uniqueKeys.map(key => row[key]).join('|')); + const filteredDeprecated = deprecated.filter((row: any) => outputKeys.includes(uniqueKeys.map(key => row[key]).join('|'))); + return new NextResponse( + JSON.stringify({ + output: MapperFactory.getMapper(params.dataType).mapData(paginatedResults), + deprecated: MapperFactory.getMapper(params.dataType).mapData(filteredDeprecated), + totalCount: totalRows, + finishedQuery: format(paginatedQuery, queryParams) + }), + { status: HTTPResponses.OK } + ); + } else { + return new NextResponse( + JSON.stringify({ + output: MapperFactory.getMapper(params.dataType).mapData(paginatedResults), + deprecated: undefined, + totalCount: totalRows, + finishedQuery: format(paginatedQuery, queryParams) + }), + { status: HTTPResponses.OK } + ); + } + } catch (error: any) { + throw new Error(error); + } finally { + await connectionManager.closeConnection(); } - paginatedQuery += ` LIMIT ?, ?;`; + } +} + +// slugs: schema, gridID +export async function PATCH(request: NextRequest, { params }: { params: { dataType: string; slugs?: string[] } }) { + if (!params.slugs) throw new Error('slugs not provided'); + const [schema, gridID] = params.slugs; + if (!schema || !gridID) throw new Error('no schema or gridID provided'); + + const connectionManager = new ConnectionManager(); + const demappedGridID = gridID.charAt(0).toUpperCase() + gridID.substring(1); + const { newRow, oldRow } = await request.json(); + let updateIDs: { [key: string]: number } = {}; + + try { + await connectionManager.beginTransaction(); - if (paginatedQuery.match(/\?/g)?.length !== queryParams.length) { - throw new Error('Mismatch between query placeholders and parameters'); + // Handle views with handleUpsertForSlices (applies to both insert and update logic) + if (params.dataType === 'alltaxonomiesview') { + let queryConfig; + switch (params.dataType) { + case 'alltaxonomiesview': + queryConfig = AllTaxonomiesViewQueryConfig; + break; + default: + throw new Error('Incorrect view call'); + } + + // Use handleUpsertForSlices for update operations as well (updates where needed) + updateIDs = await handleUpsertForSlices(connectionManager, schema, newRow, queryConfig); } - const paginatedResults = await connectionManager.executeQuery(format(paginatedQuery, queryParams)); - - const totalRowsQuery = 'SELECT FOUND_ROWS() as totalRows'; - const totalRowsResult = await connectionManager.executeQuery(totalRowsQuery); - const totalRows = totalRowsResult[0].totalRows; - - if (updatedMeasurementsExist) { - const deprecated = paginatedResults.filter((row: any) => pastCensusIDs.includes(row.CensusID)); - - const uniqueKeys = ['PlotID', 'QuadratID', 'TreeID', 'StemID']; - const outputKeys = paginatedResults.map((row: any) => uniqueKeys.map(key => row[key]).join('|')); - const filteredDeprecated = deprecated.filter((row: any) => outputKeys.includes(uniqueKeys.map(key => row[key]).join('|'))); - return new NextResponse( - JSON.stringify({ - output: MapperFactory.getMapper(params.dataType).mapData(paginatedResults), - deprecated: MapperFactory.getMapper(params.dataType).mapData(filteredDeprecated), - totalCount: totalRows, - finishedQuery: format(paginatedQuery, queryParams) - }), - { status: HTTPResponses.OK } - ); - } else { - return new NextResponse( - JSON.stringify({ - output: MapperFactory.getMapper(params.dataType).mapData(paginatedResults), - deprecated: undefined, - totalCount: totalRows, - finishedQuery: format(paginatedQuery, queryParams) - }), - { status: HTTPResponses.OK } + + // Handle non-view table updates + else { + const newRowData = MapperFactory.getMapper(params.dataType).demapData([newRow])[0]; + const { [demappedGridID]: gridIDKey, ...remainingProperties } = newRowData; + + // Construct the UPDATE query + const updateQuery = format( + `UPDATE ?? + SET ? + WHERE ?? = ?`, + [`${schema}.${params.dataType}`, remainingProperties, demappedGridID, gridIDKey] ); + + // Execute the UPDATE query + await connectionManager.executeQuery(updateQuery); + + // For non-view tables, standardize the response format + updateIDs = { [params.dataType]: gridIDKey }; } + + return NextResponse.json({ message: 'Update successful', updatedIDs: updateIDs }, { status: HTTPResponses.OK }); } catch (error: any) { - throw new Error(error); + return handleError(error, connectionManager, newRow); + } finally { + await connectionManager.closeConnection(); + } +} + +// slugs: schema, gridID +// body: full data row, only need first item from it this time though +export async function DELETE(request: NextRequest, { params }: { params: { dataType: string; slugs?: string[] } }) { + if (!params.slugs) throw new Error('slugs not provided'); + const [schema, gridID] = params.slugs; + if (!schema || !gridID) throw new Error('no schema or gridID provided'); + const connectionManager = new ConnectionManager(); + const demappedGridID = gridID.charAt(0).toUpperCase() + gridID.substring(1); + const { newRow } = await request.json(); + try { + await connectionManager.beginTransaction(); + + // Handle deletion for views + if (['alltaxonomiesview', 'measurementssummaryview'].includes(params.dataType)) { + const deleteRowData = MapperFactory.getMapper(params.dataType).demapData([newRow])[0]; + + // Prepare query configuration based on view + let queryConfig; + switch (params.dataType) { + case 'alltaxonomiesview': + queryConfig = AllTaxonomiesViewQueryConfig; + break; + default: + throw new Error('Incorrect view call'); + } + + // Use handleDeleteForSlices for handling deletion, taking foreign key constraints into account + await handleDeleteForSlices(connectionManager, schema, deleteRowData, queryConfig); + return NextResponse.json({ message: 'Delete successful' }, { status: HTTPResponses.OK }); + } + + // Handle deletion for tables + const deleteRowData = MapperFactory.getMapper(params.dataType).demapData([newRow])[0]; + const { [demappedGridID]: gridIDKey } = deleteRowData; + // for quadrats, censusquadrat needs to be cleared before quadrat can be deleted + if (params.dataType === 'quadrats') { + const qDeleteQuery = format(`DELETE FROM ?? WHERE ?? = ?`, [`${schema}.censusquadrat`, demappedGridID, gridIDKey]); + await connectionManager.executeQuery(qDeleteQuery); + } + const deleteQuery = format(`DELETE FROM ?? WHERE ?? = ?`, [`${schema}.${params.dataType}`, demappedGridID, gridIDKey]); + await connectionManager.executeQuery(deleteQuery); + return NextResponse.json({ message: 'Delete successful' }, { status: HTTPResponses.OK }); + } catch (error: any) { + if (error.code === 'ER_ROW_IS_REFERENCED_2') { + await connectionManager.rollbackTransaction(); + const referencingTableMatch = error.message.match(/CONSTRAINT `(.*?)` FOREIGN KEY \(`(.*?)`\) REFERENCES `(.*?)`/); + const referencingTable = referencingTableMatch ? referencingTableMatch[3] : 'unknown'; + return NextResponse.json( + { + message: 'Foreign key conflict detected', + referencingTable + }, + { status: HTTPResponses.FOREIGN_KEY_CONFLICT } + ); + } else return handleError(error, connectionManager, newRow); } finally { await connectionManager.closeConnection(); } diff --git a/frontend/app/api/validations/procedures/[validationType]/route.ts b/frontend/app/api/validations/procedures/[validationType]/route.ts index 43f23c52..512ee90b 100644 --- a/frontend/app/api/validations/procedures/[validationType]/route.ts +++ b/frontend/app/api/validations/procedures/[validationType]/route.ts @@ -2,12 +2,15 @@ import { NextRequest, NextResponse } from 'next/server'; import { runValidation } from '@/components/processors/processorhelperfunctions'; import { HTTPResponses } from '@/config/macros'; -export async function POST(request: NextRequest, { params }: { params: { validationProcedureName: string } }) { +export async function POST(request: NextRequest, { params }: { params: { validationType: string } }) { try { - const { schema, validationProcedureID, cursorQuery, p_CensusID, p_PlotID, minDBH, maxDBH, minHOM, maxHOM } = await request.json(); + if (!params.validationType) throw new Error('validationProcedureName not provided'); + const body = await request.json(); + const { schema, validationProcedureID, cursorQuery, p_CensusID, p_PlotID, minDBH, maxDBH, minHOM, maxHOM } = body; + console.log('body received from request: ', body); // Execute the validation procedure using the provided inputs - const validationResponse = await runValidation(validationProcedureID, params.validationProcedureName, schema, cursorQuery, { + const validationResponse = await runValidation(validationProcedureID, params.validationType, schema, cursorQuery, { p_CensusID, p_PlotID, minDBH, diff --git a/frontend/app/api/validations/validationerrordisplay/route.ts b/frontend/app/api/validations/validationerrordisplay/route.ts index 76ad985b..8804aa25 100644 --- a/frontend/app/api/validations/validationerrordisplay/route.ts +++ b/frontend/app/api/validations/validationerrordisplay/route.ts @@ -32,6 +32,7 @@ export async function GET(request: NextRequest) { validationErrorIDs: row.ValidationErrorIDs.split(',').map(Number), descriptions: row.Descriptions.split(',') })); + console.log('parsedValidationErrors: ', parsedValidationErrors); return new NextResponse( JSON.stringify({ failed: parsedValidationErrors diff --git a/frontend/app/api/validations/validationlist/route.ts b/frontend/app/api/validations/validationlist/route.ts index 55b04ab0..d77b426a 100644 --- a/frontend/app/api/validations/validationlist/route.ts +++ b/frontend/app/api/validations/validationlist/route.ts @@ -28,7 +28,7 @@ export async function GET(request: NextRequest): Promise { diff --git a/frontend/components/client/datagridcolumns.tsx b/frontend/components/client/datagridcolumns.tsx index 39a724d7..fb812ae1 100644 --- a/frontend/components/client/datagridcolumns.tsx +++ b/frontend/components/client/datagridcolumns.tsx @@ -311,9 +311,9 @@ export const StemTaxonomiesViewGridColumns: GridColDef[] = [ // note --> originally attempted to use GridValueFormatterParams, but this isn't exported by MUI X DataGrid anymore. replaced with for now. -const renderDBHCell = (params: GridRenderEditCellParams) => { +export const renderDBHCell = (params: GridRenderEditCellParams) => { const value = params.row.measuredDBH ? Number(params.row.measuredDBH).toFixed(2) : 'null'; - const units = params.row.dbhUnits || ''; + const units = params.row.dbhUnits ? (params.row.measuredDBH !== null ? params.row.dbhUnits : '') : ''; return ( @@ -323,7 +323,7 @@ const renderDBHCell = (params: GridRenderEditCellParams) => { ); }; -const renderEditDBHCell = (params: GridRenderEditCellParams) => { +export const renderEditDBHCell = (params: GridRenderEditCellParams) => { const apiRef = useGridApiRef(); const { id, row } = params; const [error, setError] = useState(false); @@ -388,12 +388,12 @@ const renderEditDBHCell = (params: GridRenderEditCellParams) => { const renderHOMCell = (params: GridRenderEditCellParams) => { const value = params.row.measuredHOM ? Number(params.row.measuredHOM).toFixed(2) : 'null'; - const units = params.row.homUnits || ''; + const units = params.row.homUnits ? (params.row.measuredHOM !== null ? params.row.homUnits : '') : ''; return ( - {value} - {units} + {value && {value}} + {units && {units}} ); }; @@ -543,7 +543,7 @@ export const MeasurementsSummaryViewGridColumns: GridColDef[] = [ editable: true }, { - field: 'localX', + field: 'stemLocalX', headerName: 'X', headerAlign: 'left', headerClassName: 'header', @@ -557,7 +557,7 @@ export const MeasurementsSummaryViewGridColumns: GridColDef[] = [ editable: true }, { - field: 'localY', + field: 'stemLocalY', headerName: 'Y', headerAlign: 'left', headerClassName: 'header', @@ -586,7 +586,7 @@ export const MeasurementsSummaryViewGridColumns: GridColDef[] = [ field: 'measuredDBH', headerName: 'DBH', headerClassName: 'header', - flex: 0.8, + flex: 0.5, align: 'right', editable: true, renderCell: renderDBHCell, @@ -607,7 +607,7 @@ export const MeasurementsSummaryViewGridColumns: GridColDef[] = [ field: 'description', headerName: 'Description', headerClassName: 'header', - flex: 1, + flex: 0.6, align: 'left', editable: true }, diff --git a/frontend/components/client/validationmodal.tsx b/frontend/components/client/validationmodal.tsx index a126d092..325899ba 100644 --- a/frontend/components/client/validationmodal.tsx +++ b/frontend/components/client/validationmodal.tsx @@ -3,37 +3,40 @@ import React, { useEffect, useState } from 'react'; import { Box, LinearProgress, Typography } from '@mui/material'; import CircularProgress from '@mui/joy/CircularProgress'; import { useOrgCensusContext, usePlotContext, useSiteContext } from '@/app/contexts/userselectionprovider'; -import { updateValidatedRows } from '@/components/processors/processorhelperfunctions'; -import { ValidationResponse } from '@/config/macros'; +import { Modal, ModalDialog } from '@mui/joy'; +import { CoreMeasurementsRDS } from '@/config/sqlrdsdefinitions/core'; type ValidationMessages = { - [key: string]: { description: string; definition: string }; + [key: string]: { id: number; description: string; definition: string }; }; -const ValidationModal: React.FC = () => { +interface VMProps { + isValidationModalOpen: boolean; + handleCloseValidationModal: () => Promise; +} + +function ValidationModal(props: VMProps) { + const { isValidationModalOpen, handleCloseValidationModal } = props; const [validationMessages, setValidationMessages] = useState({}); - const [validationResults, setValidationResults] = useState>({}); const [isValidationComplete, setIsValidationComplete] = useState(false); const [errorsFound, setErrorsFound] = useState(false); const [apiErrors, setApiErrors] = useState([]); const [validationProgress, setValidationProgress] = useState>({}); - const [countdown, setCountdown] = useState(5); - const [isUpdatingRows, setIsUpdatingRows] = useState(false); // New state for row update status + const [isUpdatingRows, setIsUpdatingRows] = useState(false); + const [rowsPassed, setRowsPassed] = useState([]); const currentSite = useSiteContext(); const currentPlot = usePlotContext(); const currentCensus = useOrgCensusContext(); - const schema = currentSite?.schemaName; const plotID = currentPlot?.plotID; useEffect(() => { - console.log('Loading validation procedures...'); - fetch('/api/validations/validationlist', { method: 'GET' }) + fetch(`/api/validations/validationlist?schema=${currentSite?.schemaName}`, { method: 'GET' }) .then(response => response.json()) .then(data => { - setValidationMessages(data); - const initialProgress = Object.keys(data).reduce((acc, api) => ({ ...acc, [api]: 0 }), {}); + setValidationMessages(data.coreValidations); + const initialProgress = Object.keys(data.coreValidations).reduce((acc, api) => ({ ...acc, [api]: 0 }), {}); setValidationProgress(initialProgress); }) .catch(error => { @@ -43,194 +46,175 @@ const ValidationModal: React.FC = () => { useEffect(() => { if (Object.keys(validationMessages).length > 0) { - performNextValidation(0, false).catch(console.error); + performValidations().catch(console.error); } }, [validationMessages]); - const performNextValidation = async (index: number, foundError: boolean = false) => { - if (index >= Object.keys(validationMessages).length) { + const performValidations = async () => { + try { + const validationProcedureNames = Object.keys(validationMessages); + + const results = await Promise.all( + validationProcedureNames.map(async procedureName => { + const { id: validationProcedureID, definition: cursorQuery } = validationMessages[procedureName]; + + try { + const response = await fetch(`/api/validations/procedures/${procedureName}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + schema: currentSite?.schemaName, + validationProcedureID, + cursorQuery, + p_CensusID: currentCensus?.dateRanges[0].censusID, + p_PlotID: plotID, + minDBH: null, + maxDBH: null, + minHOM: null, + maxHOM: null + }) + }); + + if (!response.ok) { + throw new Error(`Error executing ${procedureName}`); + } + + const result: boolean = await response.json(); + setValidationProgress(prevProgress => ({ + ...prevProgress, + [procedureName]: 100 + })); + + return { procedureName, hasError: result }; + } catch (error: any) { + console.error(`Error performing validation for ${procedureName}:`, error); + setApiErrors(prev => [...prev, `Failed to execute ${procedureName}: ${error.message}`]); + setValidationProgress(prevProgress => ({ + ...prevProgress, + [procedureName]: -1 + })); + return { procedureName, hasError: true }; + } + }) + ); + + const errorsExist = results.some(({ hasError }) => hasError); + try { - setIsUpdatingRows(true); // Indicate that the update is starting - await updateValidatedRows(schema!, { p_CensusID: currentCensus?.dateRanges[0]?.censusID, p_PlotID: currentPlot?.plotID }); // Call the updateValidatedRows - // function - // here - setIsUpdatingRows(false); // Indicate that the update is complete - setIsValidationComplete(true); - setErrorsFound(foundError); + setIsUpdatingRows(true); + const response = await fetch( + `/api/validations/updatepassedvalidations?schema=${currentSite?.schemaName}&plotID=${plotID}&censusID=${currentCensus?.dateRanges[0].censusID}`, + { method: 'GET' } + ); + setRowsPassed(await response.json()); + setErrorsFound(errorsExist); } catch (error: any) { console.error('Error in updating validated rows:', error); setApiErrors(prev => [...prev, `Failed to update validated rows: ${error.message}`]); - setIsUpdatingRows(false); // Ensure the flag is reset even on error + } finally { + setIsUpdatingRows(false); + setIsValidationComplete(true); } - return; - } - - const validationProcedureName = Object.keys(validationMessages)[index]; - const validationProcedureID = index + 1; // Assuming a 1-based index as an ID for simplicity; adjust as needed. - const cursorQuery = validationMessages[validationProcedureName].definition; - - try { - const { response, hasError } = await performValidation(validationProcedureName, validationProcedureID, cursorQuery); - setValidationResults(prevResults => ({ - ...prevResults, - [validationProcedureName]: response - })); - setValidationProgress(prevProgress => ({ ...prevProgress, [validationProcedureName]: 100 })); - await performNextValidation(index + 1, foundError || hasError); } catch (error) { - console.error(`Error in performNextValidation for ${validationProcedureName}:`, error); - } - }; - - const performValidation = async ( - validationProcedureName: string, - validationProcedureID: number, - cursorQuery: string - ): Promise<{ response: ValidationResponse; hasError: boolean }> => { - try { - const response = await fetch(`/api/validations/procedures/${validationProcedureName}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - schema, - validationProcedureID, - cursorQuery, - p_CensusID: currentCensus?.dateRanges[0].censusID, - p_PlotID: plotID, - minDBH: null, // Adjust these values as needed - maxDBH: null, - minHOM: null, - maxHOM: null - }) - }); - - if (!response.ok) { - throw new Error(`Error executing ${validationProcedureName}`); - } - - const result = await response.json(); - const hasError = result.failedRows > 0; - return { response: result, hasError }; - } catch (error: any) { - console.error(`Error performing validation for ${validationProcedureName}:`, error); - setApiErrors(prev => [...prev, `Failed to execute ${validationProcedureName}: ${error.message}`]); - setValidationProgress(prevProgress => ({ ...prevProgress, [validationProcedureName]: -1 })); - return { - response: { failedRows: 0, message: error.message, totalRows: 0 }, - hasError: false - }; + console.error('Error during validation process:', error); } }; const renderProgressBars = () => { return Object.keys(validationMessages).map(validationProcedureName => ( - {validationMessages[validationProcedureName]?.description || validationProcedureName} + {validationProcedureName} + {validationMessages[validationProcedureName]?.description} )); }; useEffect(() => { - let timer: number; - - if (isValidationComplete && countdown > 0) { - timer = window.setTimeout(() => setCountdown(countdown - 1), 1000) as unknown as number; - } else if (countdown === 0) { - // Automatically close modal or perform any final actions needed after validation - // Example: closeModal(); - } - - return () => clearTimeout(timer); - }, [countdown, isValidationComplete]); + if (isValidationComplete) handleCloseValidationModal().catch(console.error); + }, [isValidationComplete]); return ( - <> - {Object.keys(validationMessages).length > 0 && ( - - {!isValidationComplete ? ( - - Validating data... - {renderProgressBars()} - - ) : isUpdatingRows ? ( // Show updating message when update is in progress - - - Updating validated rows... - - ) : ( - + + + {Object.keys(validationMessages).length > 0 && ( + + {!isValidationComplete ? ( + + Validating data... + {renderProgressBars()} + + ) : isUpdatingRows ? ( - {countdown} seconds remaining + Updating validated rows... - Validation Results - {apiErrors.length > 0 && ( - - Some validations could not be performed: - {apiErrors.map(error => ( - - - {error} - + ) : ( + + Validation Results + {apiErrors.length > 0 && ( + + Some validations could not be performed: + {apiErrors.map(error => ( + + - {error} + + ))} + + )} + {rowsPassed.length > 0 && + rowsPassed.map(row => ( + + Updated Row: {row.coreMeasurementID} + ))} - - )} - {Object.entries(validationResults).map(([validationProcedureName, result]) => ( - - {validationProcedureName}: - {result.failedRows > 0 ? ( - <> - - {result.message} - Failed Core Measurement IDs: {result.failedCoreMeasurementIDs?.join(', ') ?? 'None'} - - ) : ( - - Processed Rows: {result.totalRows}, Errors Detected: {result.failedRows} - - )} - - ))} - - )} - - )} - + + )} + + )} + + ); -}; +} export default ValidationModal; diff --git a/frontend/components/datagrids/applications/isolated/isolatedmsvstagingdatagrid.tsx b/frontend/components/datagrids/applications/isolated/isolatedmsvstagingdatagrid.tsx index d2c16095..6ed934b6 100644 --- a/frontend/components/datagrids/applications/isolated/isolatedmsvstagingdatagrid.tsx +++ b/frontend/components/datagrids/applications/isolated/isolatedmsvstagingdatagrid.tsx @@ -27,8 +27,8 @@ export default function IsolatedMeasurementsSummaryDraftDataGrid() { speciesCode: '', treeTag: '', stemTag: '', - localX: 0, - localY: 0, + stemLocalX: 0, + stemLocalY: 0, coordinateUnits: '', measurementDate: null, measuredDBH: 0, diff --git a/frontend/components/datagrids/applications/msvdatagrid.tsx b/frontend/components/datagrids/applications/msvdatagrid.tsx index 7124031a..ab158349 100644 --- a/frontend/components/datagrids/applications/msvdatagrid.tsx +++ b/frontend/components/datagrids/applications/msvdatagrid.tsx @@ -1,7 +1,7 @@ 'use client'; -import { useOrgCensusContext, usePlotContext, useSiteContext } from '@/app/contexts/userselectionprovider'; -import React, { useEffect, useState } from 'react'; +import { useOrgCensusContext, usePlotContext } from '@/app/contexts/userselectionprovider'; +import React, { useState } from 'react'; import { GridRowModes, GridRowModesModel, GridRowsProp } from '@mui/x-data-grid'; import { randomId } from '@mui/x-data-grid-generator'; import { Snackbar } from '@mui/joy'; @@ -11,7 +11,6 @@ import { MeasurementsSummaryViewGridColumns } from '@/components/client/datagrid import { FormType } from '@/config/macros/formdetails'; import { MeasurementsSummaryRDS } from '@/config/sqlrdsdefinitions/views'; import MultilineModal from '@/components/datagrids/applications/multiline/multilinemodal'; -import { useLoading } from '@/app/contexts/loadingprovider'; import { Alert, AlertProps, AlertTitle, Collapse } from '@mui/material'; const initialMeasurementsSummaryViewRDSRow: MeasurementsSummaryRDS = { @@ -29,8 +28,8 @@ const initialMeasurementsSummaryViewRDSRow: MeasurementsSummaryRDS = { speciesCode: '', treeTag: '', stemTag: '', - localX: 0, - localY: 0, + stemLocalX: 0, + stemLocalY: 0, coordinateUnits: '', measurementDate: null, measuredDBH: 0, @@ -43,7 +42,6 @@ const initialMeasurementsSummaryViewRDSRow: MeasurementsSummaryRDS = { }; export default function MeasurementsSummaryViewDataGrid() { - const currentSite = useSiteContext(); const currentPlot = usePlotContext(); const currentCensus = useOrgCensusContext(); const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); @@ -63,32 +61,9 @@ export default function MeasurementsSummaryViewDataGrid() { }); const [isNewRowAdded, setIsNewRowAdded] = useState(false); const [shouldAddRowAfterFetch, setShouldAddRowAfterFetch] = useState(false); - const { setLoading } = useLoading(); - - async function reloadMSV() { - try { - setLoading(true, 'Refreshing Measurements Summary View...'); - const startTime = Date.now(); - const response = await fetch(`/api/refreshviews/measurementssummary/${currentSite?.schemaName ?? ''}`, { method: 'POST' }); - if (!response.ok) throw new Error('Measurements Summary View Refresh failure'); - setLoading(true, 'Processing data...'); - const duration = (Date.now() - startTime) / 1000; - setLoading(true, `Completed in ${duration.toFixed(2)} seconds.`); - await new Promise(resolve => setTimeout(resolve, 1000)); - } catch (e: any) { - console.error(e); - } finally { - setLoading(false); - } - } - - useEffect(() => { - reloadMSV().catch(console.error); - }, []); const addNewRowToGrid = () => { const id = randomId(); - // Define new row structure based on MeasurementsSummaryRDS type const newRow = { ...initialMeasurementsSummaryViewRDSRow, id: id, diff --git a/frontend/components/datagrids/measurementscommons.tsx b/frontend/components/datagrids/measurementscommons.tsx index cc7969fc..e0d00b1b 100644 --- a/frontend/components/datagrids/measurementscommons.tsx +++ b/frontend/components/datagrids/measurementscommons.tsx @@ -32,11 +32,9 @@ import { StyledDataGrid } from '@/config/styleddatagrid'; import { CellItemContainer, createDeleteQuery, - createFetchQuery, createPostPatchQuery, createQFFetchQuery, EditToolbarCustomProps, - errorMapping, filterColumns, getColumnVisibilityModel, getGridID, @@ -58,9 +56,12 @@ import { useSession } from 'next-auth/react'; import ConfirmationDialog from './confirmationdialog'; import ReEnterDataModal from './reentrydatamodal'; import { FormType, getTableHeaders } from '@/config/macros/formdetails'; -import { applyFilterToColumns, betweenOperator } from '@/components/datagrids/filtrationsystem'; +import { applyFilterToColumns } from '@/components/datagrids/filtrationsystem'; import { ClearIcon } from '@mui/x-date-pickers'; import CloudDownloadIcon from '@mui/icons-material/CloudDownload'; +import ValidationModal from '@/components/client/validationmodal'; +import { ValidationProceduresRDS } from '@/config/sqlrdsdefinitions/validations'; +import { MeasurementsSummaryViewGridColumns } from '@/components/client/datagridcolumns'; function debounce void>(fn: T, delay: number): T { let timeoutId: ReturnType; @@ -72,6 +73,12 @@ function debounce void>(fn: T, delay: number): T { type EditToolbarProps = EditToolbarCustomProps & GridToolbarProps & ToolbarPropsOverrides; +type VisibleFilter = 'valid' | 'errors' | 'pending'; + +interface ExtendedGridFilterModel extends GridFilterModel { + visible: VisibleFilter[]; +} + const EditToolbar = (props: EditToolbarProps) => { const { handleAddNewRow, @@ -265,34 +272,45 @@ export default function MeasurementsCommons(props: Readonly(null); // new state to track the new last page + const [newLastPage, setNewLastPage] = useState(null); const [isDialogOpen, setIsDialogOpen] = useState(false); const [pendingAction, setPendingAction] = useState({ actionType: '', actionId: null }); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [isValidationModalOpen, setIsValidationModalOpen] = useState(false); const [promiseArguments, setPromiseArguments] = useState<{ resolve: (value: GridRowModel) => void; reject: (reason?: any) => void; newRow: GridRowModel; oldRow: GridRowModel; } | null>(null); - const [filterModel, setFilterModel] = useState({ - items: [], - quickFilterValues: [] - }); const [usingQuery, setUsingQuery] = useState(''); const [isSaveHighlighted, setIsSaveHighlighted] = useState(false); - // custom states -- msvdatagrid const [validationErrors, setValidationErrors] = useState<{ [key: number]: CMError; }>({}); + const [validationProcedures, setValidationProcedures] = useState([]); const [showErrorRows, setShowErrorRows] = useState(true); const [showValidRows, setShowValidRows] = useState(true); + const [showPendingRows, setShowPendingRows] = useState(true); + const [filterModel, setFilterModel] = useState({ + items: [], + quickFilterValues: [], + visible: [ + ...(showErrorRows ? (['errors'] as VisibleFilter[]) : []), + ...(showValidRows ? (['valid'] as VisibleFilter[]) : []), + ...(showPendingRows ? (['pending'] as VisibleFilter[]) : []) + ] + }); + const [sortModel, setSortModel] = useState([{ field: 'measurementDate', sort: 'asc' }]); + const [clickedRow, setClickedRow] = useState(null); + const [errorCount, setErrorCount] = useState(null); + const [validCount, setValidCount] = useState(null); + const [pendingCount, setPendingCount] = useState(null); // context pulls and definitions const currentSite = useSiteContext(); @@ -300,12 +318,26 @@ export default function MeasurementsCommons(props: Readonly { + setFilterModel(prevModel => ({ + ...prevModel, + visible: [ + ...(showErrorRows ? (['errors'] as VisibleFilter[]) : []), + ...(showValidRows ? (['valid'] as VisibleFilter[]) : []), + ...(showPendingRows ? (['pending'] as VisibleFilter[]) : []) + ] + })); + }, [showErrorRows, showValidRows, showPendingRows]); + + const handleRowClick = (rowId: GridRowId) => { + setClickedRow(prev => (prev === rowId ? null : rowId)); + }; + const exportAllCSV = useCallback(async () => { const response = await fetch( `/api/formdownload/measurements/${currentSite?.schemaName ?? ''}/${currentPlot?.plotID ?? 0}/${currentCensus?.dateRanges[0].censusID ?? 0}`, @@ -364,11 +396,17 @@ export default function MeasurementsCommons(props: Readonly { const row = rows.find(row => rowId === row.id); - const error = validationErrors[row?.coreMeasurementID]; - if (!error) return false; - const errorFields = error.validationErrorIDs.flatMap(id => errorMapping[id.toString()] || []); + const error = validationErrors[Number(row?.coreMeasurementID)]; + if (error === undefined) return false; + const errorFields = error.validationErrorIDs.flatMap( + id => + validationProcedures + .find(vp => vp.validationID === id) // Compare id with validationID + ?.criteria?.split(';') || [] + ); return errorFields.includes(colField); }; @@ -391,10 +429,6 @@ export default function MeasurementsCommons(props: Readonly { - return rows.filter(row => rowHasError(row.id)).length; - }, [rows, gridColumns]); - const updateRow = async ( gridType: string, schemaName: string | undefined, @@ -435,6 +469,7 @@ export default function MeasurementsCommons(props: Readonly setTimeout(resolve, 1000)); + } catch (e: any) { + console.error(e); + } finally { + setLoading(false); + } + await new Promise(resolve => setTimeout(resolve, 1000)); // forced delay + await runFetchPaginated(); }; const performDeleteAction = async (id: GridRowId) => { @@ -548,6 +597,20 @@ export default function MeasurementsCommons(props: Readonly String(row.id) !== String(id))); + try { + setLoading(true, 'Refreshing Measurements Summary View...'); + const startTime = Date.now(); + const response = await fetch(`/api/refreshviews/measurementssummary/${currentSite?.schemaName ?? ''}`, { method: 'POST' }); + if (!response.ok) throw new Error('Measurements Summary View Refresh failure'); + const duration = (Date.now() - startTime) / 1000; + setLoading(true, `Completed in ${duration.toFixed(2)} seconds.`); + await new Promise(resolve => setTimeout(resolve, 1000)); + } catch (e: any) { + console.error(e); + } finally { + setLoading(false); + } + await new Promise(resolve => setTimeout(resolve, 1000)); // forced delay await fetchPaginatedData(paginationModel.page); } }; @@ -562,14 +625,6 @@ export default function MeasurementsCommons(props: Readonly) => { - setShowErrorRows(event.target.checked); - }; - - const handleShowValidRowsChange = (event: React.ChangeEvent) => { - setShowValidRows(event.target.checked); - }; - const handleAddNewRow = async () => { if (locked) { return; @@ -600,38 +655,27 @@ export default function MeasurementsCommons(props: Readonly 0) || (filterModel.quickFilterValues && filterModel.quickFilterValues.length > 0) - ? createQFFetchQuery( - currentSite?.schemaName ?? '', - gridType, - pageToFetch, - paginationModel.pageSize, - currentPlot?.plotID, - currentCensus?.plotCensusNumber, - currentQuadrat?.quadratID - ) - : createFetchQuery( - currentSite?.schemaName ?? '', - gridType, - pageToFetch, - paginationModel.pageSize, - currentPlot?.plotID, - currentCensus?.plotCensusNumber, - currentQuadrat?.quadratID - ); + let paginatedQuery = ''; + + paginatedQuery = createQFFetchQuery( + currentSite?.schemaName ?? '', + gridType, + pageToFetch, + paginationModel.pageSize, + currentPlot?.plotID, + currentCensus?.plotCensusNumber + ); + try { const response = await fetch(paginatedQuery, { - method: - (filterModel.items && filterModel.items.length > 0) || (filterModel.quickFilterValues && filterModel.quickFilterValues.length > 0) ? 'POST' : 'GET', + method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: - (filterModel.items && filterModel.items.length > 0) || (filterModel.quickFilterValues && filterModel.quickFilterValues.length > 0) - ? JSON.stringify({ filterModel }) - : undefined + body: JSON.stringify({ filterModel }) }); + const data = await response.json(); if (!response.ok) throw new Error(data.message || 'Error fetching data'); + setRows(data.output); setRowCount(data.totalCount); setUsingQuery(data.finishedQuery); @@ -646,40 +690,71 @@ export default function MeasurementsCommons(props: Readonly { if (currentPlot && currentCensus && paginationModel.page >= 0) { - fetchPaginatedData(paginationModel.page); + runFetchPaginated().catch(console.error); } - }, [currentPlot, currentCensus, paginationModel.page, sortModel, isNewRowAdded, filterModel]); + }, [currentPlot, currentCensus, paginationModel, sortModel, isNewRowAdded, filterModel]); useEffect(() => { - if (errorRowCount > 0) { + async function getCounts() { + const query = `SELECT + SUM(CASE WHEN vft.IsValidated = TRUE THEN 1 ELSE 0 END) AS CountValid, + SUM(CASE WHEN vft.IsValidated = FALSE THEN 1 ELSE 0 END) AS CountErrors, + SUM(CASE WHEN vft.IsValidated IS NULL THEN 1 ELSE 0 END) AS CountPending + FROM ${currentSite?.schemaName ?? ''}.${gridType} vft + JOIN ${currentSite?.schemaName ?? ''}.census c ON vft.PlotID = c.PlotID AND vft.CensusID = c.CensusID + WHERE vft.PlotID = ${currentPlot?.plotID ?? 0} + AND c.PlotID = ${currentPlot?.plotID ?? 0} + AND c.PlotCensusNumber = ${currentCensus?.plotCensusNumber ?? 0}`; + const response = await fetch(`/api/runquery`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(query) + }); + if (!response.ok) throw new Error('measurementscommons failure. runquery execution for errorRowCount failed.'); + const data = await response.json(); + return data[0]; + } + + getCounts().then(data => { + setValidCount(data.CountValid); + setErrorCount(data.CountErrors); + setPendingCount(data.CountPending); setSnackbar({ - children: `${errorRowCount} row(s) with validation errors detected.`, + children: `${data.CountErrors} row(s) with validation errors detected.`, severity: 'warning' }); + }); + }, [fetchPaginatedData]); + + async function getValidations() { + try { + const response = await fetch(`/api/validations/crud`, { method: 'GET' }); + const inbound = await response.json(); + if (JSON.stringify(validationProcedures) !== JSON.stringify(inbound)) setValidationProcedures(inbound); + } catch (e: any) { + console.error(`validation list pull failed with error: ${e}`); } - }, [errorRowCount]); + } + + useEffect(() => { + if (validationProcedures.length === 0) getValidations().catch(console.error); + }, []); const handleRefresh = useCallback(async () => { setRefresh(true); - await fetchPaginatedData(paginationModel.page); + await runFetchPaginated(); setRefresh(false); - }, [fetchPaginatedData, paginationModel.page, refresh]); + }, [paginationModel, refresh]); const processRowUpdate = useCallback( (newRow: GridRowModel, oldRow: GridRowModel) => @@ -693,7 +768,7 @@ export default function MeasurementsCommons(props: Readonly { @@ -807,7 +882,7 @@ export default function MeasurementsCommons(props: Readonly { setLoading(true); @@ -852,41 +927,40 @@ export default function MeasurementsCommons(props: Readonly { - const rowId = params.row.coreMeasurementID; - const validationError = validationErrors[Number(rowId)]; - const isPendingValidation = rows.find(row => row.coreMeasurementID === rowId)?.isValidated === null && !validationError; - const isValidated = params.row.isValidated; - - if (validationError) { - return ( - - - - ); - } else if (isPendingValidation) { - return ( - - - - ); - } else if (isValidated) { - return ( - - - - ); - } else { - return null; + const validationStatusColumn: GridColDef = useMemo( + () => ({ + field: 'isValidated', + headerName: '', + headerAlign: 'center', + align: 'center', + width: 50, + renderCell: (params: GridCellParams) => { + if (validationErrors[Number(params.row.coreMeasurementID)]) { + return ( + + + + ); + } else if (params.row.isValidated === null) { + return ( + + + + ); + } else if (params.row.isValidated) { + return ( + + + + ); + } else { + return null; + } } - } - }; + }), + [rows, validationErrors] + ); + const measurementDateColumn: GridColDef = { field: 'measurementDate', headerName: 'Date', @@ -910,57 +984,108 @@ export default function MeasurementsCommons(props: Readonly { + const error = validationErrors[Number(coreMeasurementID)]; + if (!error || !validationProcedures) return ''; + + return error.validationErrorIDs + .filter(id => + validationProcedures + .find(valID => valID.validationID === id) + ?.criteria?.split(';') + .includes(colField) + ) + .map(id => { + const index = error.validationErrorIDs.indexOf(id); + return error.descriptions[index]; + }) + .join('; '); + }; + const columns = useMemo(() => { const commonColumns = gridColumns.map(column => { - if (column.field === 'measuredDBH' || column.field === 'measuredHOM') { - return { ...column, filterOperators: [betweenOperator] } as GridColDef; - } else return column; - // return { - // ...column, - // renderCell: (params: GridCellParams) => { - // const cellValue = params.value !== undefined ? params.value?.toString() : ''; - // const cellError = cellHasError(column.field, params.id) ? getCellErrorMessages(column.field, params.id) : ''; - // return ( - // - // {cellError ? ( - // <> - // {cellValue} - // - // {cellError} - // - // - // ) : ( - // {cellValue} - // )} - // - // ); - // } - // }; + return { + ...column, + renderCell: (params: GridCellParams) => { + const value = typeof params.value === 'string' ? params.value : (params.value?.toString() ?? ''); + + const formattedValue = !isNaN(Number(value)) && value.includes('.') && value.split('.')[1].length > 2 ? Number(value).toFixed(2) : value; + const rowError = rowHasError(params.id); + const targetColumnName = validationProcedures + .find(vp => validationErrors[Number(params.row.coreMeasurementID)]?.validationErrorIDs.includes(vp.validationID ?? -1)) + ?.criteria?.split(';')[0]; + const cellError = cellHasError(column.field, params.id) ? getCellErrorMessages(column.field, Number(params.row.coreMeasurementID)) : ''; + if (rowError) console.log('targetColumn for row error: ', targetColumnName, ' cellError: ', cellError); + return ( + + {column.field === targetColumnName ? ( + <> + {column.field === 'measuredDBH' || column.field === 'measuredHOM' ? ( + + + {column.field === 'measuredDBH' && <>{params.row.measuredDBH ? Number(params.row.measuredDBH).toFixed(2) : 'null'}} + {column.field === 'measuredHOM' && <>{params.row.measuredHOM ? Number(params.row.measuredHOM).toFixed(2) : 'null'}} + + + {column.field === 'measuredDBH' && <>{params.row.dbhUnits ? (params.row.measuredDBH !== null ? params.row.dbhUnits : '') : ''}} + {column.field === 'measuredHOM' && <>{params.row.homUnits ? (params.row.measuredHOM !== null ? params.row.homUnits : '') : ''}} + + + ) : ( + {formattedValue} + )} + + {cellError} + + + ) : ( + <> + {column.field === 'measuredDBH' || column.field === 'measuredHOM' ? ( + + + {column.field === 'measuredDBH' && <>{params.row.measuredDBH ? Number(params.row.measuredDBH).toFixed(2) : 'null'}} + {column.field === 'measuredHOM' && <>{params.row.measuredHOM ? Number(params.row.measuredHOM).toFixed(2) : 'null'}} + + + {column.field === 'measuredDBH' && <>{params.row.dbhUnits ? (params.row.measuredDBH !== null ? params.row.dbhUnits : '') : ''}} + {column.field === 'measuredHOM' && <>{params.row.homUnits ? (params.row.measuredHOM !== null ? params.row.homUnits : '') : ''}} + + + ) : ( + {formattedValue} + )} + + )} + + ); + } + }; }); if (locked) { return [validationStatusColumn, measurementDateColumn, ...commonColumns]; } return [validationStatusColumn, measurementDateColumn, ...applyFilterToColumns(commonColumns), getGridActionsColumn()]; - }, [gridColumns, locked]); - - const filteredColumns = useMemo(() => filterColumns(rows, columns), [rows, columns]); + }, [MeasurementsSummaryViewGridColumns, locked, rows, validationProcedures, validationErrors]); const visibleRows = useMemo(() => { let filteredRows = rows; @@ -973,6 +1098,8 @@ export default function MeasurementsCommons(props: Readonly filterColumns(visibleRows, columns), [visibleRows, columns]); + const getRowClassName = (params: any) => { const rowId = params.id; if (rowHasError(rowId)) { @@ -1026,11 +1153,31 @@ export default function MeasurementsCommons(props: Readonly ({ - ...prevFilterModel, - items: [...(incomingValues.items || [])], - quickFilterValues: [...(incomingValues.quickFilterValues || [])] - })); + setFilterModel(prevFilterModel => { + return { + ...prevFilterModel, + items: [...(incomingValues.items || [])], + quickFilterValues: [...(incomingValues.quickFilterValues || [])] + }; + }); + } + + async function handleCloseValidationModal() { + setIsValidationModalOpen(false); + try { + setLoading(true, 'Refreshing Measurements Summary View...'); + const startTime = Date.now(); + const response = await fetch(`/api/refreshviews/measurementssummary/${currentSite?.schemaName ?? ''}`, { method: 'POST' }); + if (!response.ok) throw new Error('Measurements Summary View Refresh failure'); + const duration = (Date.now() - startTime) / 1000; + setLoading(true, `Completed in ${duration.toFixed(2)} seconds.`); + await new Promise(resolve => setTimeout(resolve, 1000)); + } catch (e: any) { + console.error(e); + } finally { + setLoading(false); + } + await runFetchPaginated(); } if (!currentSite || !currentPlot || !currentCensus) { @@ -1052,12 +1199,16 @@ export default function MeasurementsCommons(props: Readonly - - Show rows with errors: ({errorRowCount}) + setShowErrorRows(event.target.checked)} /> + Show rows failing validation: ({errorCount}) + + + setShowValidRows(event.target.checked)} /> + Show rows passing validation: ({validCount}) - - Show rows without errors: ({rows.length - errorRowCount}) + setShowPendingRows(event.target.checked)} /> + Show rows pending validation: ({pendingCount}) @@ -1067,13 +1218,16 @@ export default function MeasurementsCommons(props: Readonly handleRowClick(params.id)} disableColumnSelector onRowModesModelChange={handleRowModesModelChange} onRowEditStop={handleRowEditStop} processRowUpdate={processRowUpdate} loading={refresh} paginationMode="server" - onPaginationModelChange={setPaginationModel} + onPaginationModelChange={newPaginationModel => { + setPaginationModel(newPaginationModel); + }} onProcessRowUpdateError={error => { console.error('Row update error:', error); setSnackbar({ @@ -1088,11 +1242,16 @@ export default function MeasurementsCommons(props: Readonly setFilterModel(newFilterModel)} + onFilterModelChange={newFilterModel => { + setFilterModel(prevModel => ({ + ...prevModel, + ...newFilterModel + })); + }} ignoreDiacritics initialState={{ columns: { @@ -1112,7 +1271,7 @@ export default function MeasurementsCommons(props: Readonly setIsValidationModalOpen(true) }] } }} getRowHeight={() => 'auto'} @@ -1144,6 +1303,7 @@ export default function MeasurementsCommons(props: Readonly )} + {isValidationModalOpen && } ); } diff --git a/frontend/components/processors/processorhelperfunctions.tsx b/frontend/components/processors/processorhelperfunctions.tsx index 17293f27..9656aaeb 100644 --- a/frontend/components/processors/processorhelperfunctions.tsx +++ b/frontend/components/processors/processorhelperfunctions.tsx @@ -296,7 +296,7 @@ export async function runValidation( minHOM?: number | null; maxHOM?: number | null; } = {} -): Promise<{ TotalRows: number; Message: string }> { +): Promise { const connectionManager = new ConnectionManager(); try { @@ -310,8 +310,9 @@ export async function runValidation( .replace(/@maxDBH/g, params.maxDBH !== null && params.maxDBH !== undefined ? params.maxDBH.toString() : 'NULL') .replace(/@minHOM/g, params.minHOM !== null && params.minHOM !== undefined ? params.minHOM.toString() : 'NULL') .replace(/@maxHOM/g, params.maxHOM !== null && params.maxHOM !== undefined ? params.maxHOM.toString() : 'NULL') + .replace(/@validationProcedureID/g, validationProcedureID.toString()) .replace(/cmattributes/g, 'TEMP_CMATTRIBUTES_PLACEHOLDER') - .replace(/coremeasurements/g, `${schema}.coremeasurements`) // Fully qualify table names + .replace(/coremeasurements/g, `${schema}.coremeasurements`) .replace(/stems/g, `${schema}.stems`) .replace(/trees/g, `${schema}.trees`) .replace(/quadrats/g, `${schema}.quadrats`) @@ -371,34 +372,14 @@ export async function runValidation( .replace(/@maxHOM/g, params.maxHOM !== null && params.maxHOM !== undefined ? params.maxHOM.toString() : 'NULL'); // Execute the cursor query to get the rows that need validation - const cursorResults = await connectionManager.executeQuery(reformattedCursorQuery); - - if (cursorResults.length > 0) { - const insertErrorQuery = ` - INSERT INTO ${schema}.cmverrors (CoreMeasurementID, ValidationErrorID) - SELECT ?, ? - FROM DUAL - WHERE NOT EXISTS ( - SELECT 1 - FROM ${schema}.cmverrors - WHERE CoreMeasurementID = ? AND ValidationErrorID = ? - ); - `; - - // Insert errors for all rows that matched the validation condition - for (const row of cursorResults) { - await connectionManager.executeQuery(insertErrorQuery, [row.CoreMeasurementID, validationProcedureID, row.CoreMeasurementID, validationProcedureID]); - } - } - - return { - TotalRows: cursorResults.length, - Message: `Validation completed successfully. Total rows processed: ${cursorResults.length}` - }; + console.log('running validation: ', validationProcedureName); + console.log('running query: ', reformattedCursorQuery); + await connectionManager.executeQuery(reformattedCursorQuery); + return true; } catch (error: any) { await connectionManager.rollbackTransaction(); - console.error(`Error during ${validationProcedureName} validation:`, error.message); - throw new Error(`${validationProcedureName} validation failed. Please check the logs for more details.`); + console.error(`Error during ${validationProcedureName} or ${validationProcedureID} validation:`, error.message); + return false; } finally { await connectionManager.closeConnection(); } @@ -407,46 +388,69 @@ export async function runValidation( export async function updateValidatedRows(schema: string, params: { p_CensusID?: number | null; p_PlotID?: number | null }): Promise { const connectionManager = new ConnectionManager(); const tempTable = `CREATE TEMPORARY TABLE UpdatedRows (CoreMeasurementID INT);`; + const insertTemp = ` INSERT INTO UpdatedRows (CoreMeasurementID) SELECT cm.CoreMeasurementID FROM ${schema}.coremeasurements cm - LEFT JOIN ${schema}.cmverrors cme ON cm.CoreMeasurementID = cme.CoreMeasurementID JOIN ${schema}.census c ON cm.CensusID = c.CensusID WHERE cm.IsValidated IS NULL - AND (${params.p_CensusID} IS NULL OR c.CensusID = ${params.p_CensusID}) - AND (${params.p_PlotID} IS NULL OR c.PlotID = ${params.p_PlotID});`; - const query = ` + AND (${params.p_CensusID} IS NULL OR c.CensusID = ${params.p_CensusID}) + AND (${params.p_PlotID} IS NULL OR c.PlotID = ${params.p_PlotID}); + `; + + const updateValidation = ` UPDATE ${schema}.coremeasurements cm - LEFT JOIN ${schema}.cmverrors cme ON cm.CoreMeasurementID = cme.CoreMeasurementID - JOIN ${schema}.census c ON cm.CensusID = c.CensusID - SET cm.IsValidated = CASE - WHEN cme.CMVErrorID IS NULL THEN TRUE - WHEN cme.CMVErrorID IS NOT NULL THEN FALSE - ELSE cm.IsValidated - END + SET cm.IsValidated = ( + CASE + WHEN NOT EXISTS ( + SELECT 1 + FROM ${schema}.cmverrors cme + WHERE cme.CoreMeasurementID = cm.CoreMeasurementID + ) THEN TRUE -- No validation errors exist + ELSE FALSE -- Validation errors exist + END + ) WHERE cm.IsValidated IS NULL - AND cm.CoreMeasurementID IN (SELECT CoreMeasurementID FROM UpdatedRows) - AND c.CensusID = ${params.p_CensusID} AND c.PlotID = ${params.p_PlotID};`; + AND cm.CoreMeasurementID IN (SELECT CoreMeasurementID FROM UpdatedRows); + `; + const getUpdatedRows = ` SELECT cm.* FROM ${schema}.coremeasurements cm - JOIN UpdatedRows ur ON cm.CoreMeasurementID = ur.CoreMeasurementID;`; + WHERE cm.CoreMeasurementID IN (SELECT CoreMeasurementID FROM UpdatedRows); + `; + const dropTemp = `DROP TEMPORARY TABLE IF EXISTS UpdatedRows;`; + try { + // Begin transaction await connectionManager.beginTransaction(); - await connectionManager.executeQuery(dropTemp); // just in case + + // Ensure any leftover temporary table is cleared + await connectionManager.executeQuery(dropTemp); + + // Create temporary table and populate it await connectionManager.executeQuery(tempTable); await connectionManager.executeQuery(insertTemp); - await connectionManager.executeQuery(query); + + // Update validation states + await connectionManager.executeQuery(updateValidation); + + // Fetch and return the updated rows const results = await connectionManager.executeQuery(getUpdatedRows); + + // Clean up temporary table await connectionManager.executeQuery(dropTemp); + return MapperFactory.getMapper('coremeasurements').mapData(results); } catch (error: any) { + // Roll back on error await connectionManager.rollbackTransaction(); console.error(`Error during updateValidatedRows:`, error.message); - throw new Error(`updateValidatedRows failed. Please check the logs for more details.`); + throw new Error(`updateValidatedRows failed for validation: Please check the logs for more details.`); } finally { + // Close the connection await connectionManager.closeConnection(); } } diff --git a/frontend/components/uploadsystem/segments/uploadvalidation.tsx b/frontend/components/uploadsystem/segments/uploadvalidation.tsx index a896d657..0d7965de 100644 --- a/frontend/components/uploadsystem/segments/uploadvalidation.tsx +++ b/frontend/components/uploadsystem/segments/uploadvalidation.tsx @@ -3,9 +3,8 @@ import React, { useEffect, useState } from 'react'; import { Box, LinearProgress, Typography } from '@mui/material'; import { ReviewStates, UploadValidationProps } from '@/config/macros/uploadsystemmacros'; import CircularProgress from '@mui/joy/CircularProgress'; -import { useOrgCensusContext, usePlotContext } from '@/app/contexts/userselectionprovider'; +import { useOrgCensusContext, usePlotContext, useSiteContext } from '@/app/contexts/userselectionprovider'; import { CoreMeasurementsRDS } from '@/config/sqlrdsdefinitions/core'; -import { ValidationResponse } from '@/config/macros'; type ValidationMessages = { [key: string]: { id: number; description: string; definition: string }; @@ -13,29 +12,26 @@ type ValidationMessages = { const UploadValidation: React.FC = ({ setReviewState, schema }) => { const [validationMessages, setValidationMessages] = useState({}); - const [validationResults, setValidationResults] = useState>({}); const [isValidationComplete, setIsValidationComplete] = useState(false); const [errorsFound, setErrorsFound] = useState(false); const [apiErrors, setApiErrors] = useState([]); const [validationProgress, setValidationProgress] = useState>({}); - const [isUpdatingRows, setIsUpdatingRows] = useState(false); // New state for row update status + const [isUpdatingRows, setIsUpdatingRows] = useState(false); const [rowsPassed, setRowsPassed] = useState([]); + const currentSite = useSiteContext(); const currentPlot = usePlotContext(); const currentCensus = useOrgCensusContext(); const plotID = currentPlot?.plotID; useEffect(() => { - console.log('Loading core validation procedures...'); - fetch(`/api/validations/validationlist?schema=${schema}`, { method: 'GET' }) + fetch(`/api/validations/validationlist?schema=${currentSite?.schemaName}`, { method: 'GET' }) .then(response => response.json()) .then(data => { setValidationMessages(data.coreValidations); const initialProgress = Object.keys(data.coreValidations).reduce((acc, api) => ({ ...acc, [api]: 0 }), {}); setValidationProgress(initialProgress); - - // for now, site-specific validations are being ignored. Handling will be added later. }) .catch(error => { console.error('Error fetching validation messages:', error); @@ -44,88 +40,77 @@ const UploadValidation: React.FC = ({ setReviewState, sch useEffect(() => { if (Object.keys(validationMessages).length > 0) { - performNextValidation(0, false).catch(console.error); + performValidations().catch(console.error); } }, [validationMessages]); - const performNextValidation = async (index: number, foundError: boolean = false) => { - if (index >= Object.keys(validationMessages).length) { + const performValidations = async () => { + try { + const validationProcedureNames = Object.keys(validationMessages); + + const results = await Promise.all( + validationProcedureNames.map(async procedureName => { + const { id: validationProcedureID, definition: cursorQuery } = validationMessages[procedureName]; + + try { + const response = await fetch(`/api/validations/procedures/${procedureName}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + schema: currentSite?.schemaName, + validationProcedureID, + cursorQuery, + p_CensusID: currentCensus?.dateRanges[0].censusID, + p_PlotID: plotID, + minDBH: null, + maxDBH: null, + minHOM: null, + maxHOM: null + }) + }); + + if (!response.ok) { + throw new Error(`Error executing ${procedureName}`); + } + + const result: boolean = await response.json(); + setValidationProgress(prevProgress => ({ + ...prevProgress, + [procedureName]: 100 + })); + + return { procedureName, hasError: result }; + } catch (error: any) { + console.error(`Error performing validation for ${procedureName}:`, error); + setApiErrors(prev => [...prev, `Failed to execute ${procedureName}: ${error.message}`]); + setValidationProgress(prevProgress => ({ + ...prevProgress, + [procedureName]: -1 + })); + return { procedureName, hasError: true }; + } + }) + ); + + const errorsExist = results.some(({ hasError }) => hasError); + try { - setIsUpdatingRows(true); // Indicate that the update is starting + setIsUpdatingRows(true); const response = await fetch( - `/api/validations/updatepassedvalidations?schema=${schema}&plotID=${plotID}&censusID=${currentCensus?.dateRanges[0].censusID}`, - { - method: 'GET' - } + `/api/validations/updatepassedvalidations?schema=${currentSite?.schemaName}&plotID=${plotID}&censusID=${currentCensus?.dateRanges[0].censusID}`, + { method: 'GET' } ); setRowsPassed(await response.json()); - setIsUpdatingRows(false); // Indicate that the update is complete - setIsValidationComplete(true); - setErrorsFound(foundError); + setErrorsFound(errorsExist); } catch (error: any) { console.error('Error in updating validated rows:', error); setApiErrors(prev => [...prev, `Failed to update validated rows: ${error.message}`]); - setIsUpdatingRows(false); // Ensure the flag is reset even on error + } finally { + setIsUpdatingRows(false); + setIsValidationComplete(true); } - return; - } - - const validationProcedureName = Object.keys(validationMessages)[index]; - const validationProcedureID = validationMessages[validationProcedureName].id; // Retrieve the ID from the validationMessages object - const cursorQuery = validationMessages[validationProcedureName].definition; - - try { - const { response, hasError } = await performValidation(validationProcedureName, validationProcedureID, cursorQuery); - setValidationResults(prevResults => ({ - ...prevResults, - [validationProcedureName]: response - })); - setValidationProgress(prevProgress => ({ ...prevProgress, [validationProcedureName]: 100 })); - await performNextValidation(index + 1, foundError || hasError); } catch (error) { - console.error(`Error in performNextValidation for ${validationProcedureName}:`, error); - return; - } - }; - - const performValidation = async ( - validationProcedureName: string, - validationProcedureID: number, - cursorQuery: string - ): Promise<{ response: ValidationResponse; hasError: boolean }> => { - try { - const response = await fetch(`/api/validations/procedures/${validationProcedureName}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - schema, - validationProcedureID, - cursorQuery, - p_CensusID: currentCensus?.dateRanges[0].censusID, - p_PlotID: plotID, - minDBH: null, - maxDBH: null, - minHOM: null, - maxHOM: null - }) - }); - - if (!response.ok) { - throw new Error(`Error executing ${validationProcedureName}`); - } - - const result = await response.json(); - const hasError = result.failedRows > 0; - return { response: result, hasError }; - } catch (error: any) { - console.error(`Error performing validation for ${validationProcedureName}:`, error); - setApiErrors(prev => [...prev, `Failed to execute ${validationProcedureName}: ${error.message}`]); - setValidationProgress(prevProgress => ({ ...prevProgress, [validationProcedureName]: -1 })); - throw new Error(`Error performing validation for ${validationProcedureName}:`, error); - // return { - // response: { failedRows: 0, message: error.message, totalRows: 0 }, - // hasError: false - // }; + console.error('Error during validation process:', error); } }; @@ -140,7 +125,7 @@ const UploadValidation: React.FC = ({ setReviewState, sch }; useEffect(() => { - if (isValidationComplete) setReviewState(ReviewStates.UPDATE); + if (isValidationComplete) setReviewState(ReviewStates.UPLOAD_AZURE); }, [isValidationComplete]); return ( @@ -199,21 +184,6 @@ const UploadValidation: React.FC = ({ setReviewState, sch ))} )} - {Object.entries(validationResults).map(([validationProcedureName, result]) => ( - - {validationProcedureName}: - {result.failedRows > 0 ? ( - <> - - {result.message} - Failed Core Measurement IDs: {result.failedCoreMeasurementIDs?.join(', ') ?? 'None'} - - ) : ( - - Processed Rows: {result.totalRows}, Errors Detected: {result.failedRows} - - )} - - ))} {rowsPassed.length > 0 && rowsPassed.map(row => ( diff --git a/frontend/config/datagridhelpers.ts b/frontend/config/datagridhelpers.ts index 7a4e0f87..a80e247e 100644 --- a/frontend/config/datagridhelpers.ts +++ b/frontend/config/datagridhelpers.ts @@ -241,7 +241,7 @@ export const CellItemContainer = styled('div')({ * Function to determine if all entries in a column are null */ export function allValuesAreNull(rows: GridRowsProp, field: string): boolean { - return rows.length > 0 && rows.every(row => row[field] === undefined); + return rows.length > 0 && rows.every(row => row[field] === undefined || row[field] === null || row[field] === ''); } /** @@ -285,37 +285,20 @@ export interface MeasurementsCommonsProps { dynamicButtons: any[]; } -export interface IsolatedMeasurementsCommonsProps { - gridType: string; - gridColumns: GridColDef[]; - refresh: boolean; - setRefresh: Dispatch>; - initialRow?: GridRowModel; - fieldToFocus?: string; - locked?: boolean; - selectionOptions?: { value: string | number; label: string }[]; - onDataUpdate?: () => void; - clusters?: Record; - handleExportErrors?: () => Promise; -} - export const errorMapping: { [key: string]: string[] } = { '1': ['attributes'], '2': ['measuredDBH'], '3': ['measuredHOM'], '4': ['treeTag', 'stemTag'], '5': ['treeTag', 'stemTag', 'quadratName'], - '6': ['stemQuadX', 'stemQuadY'], - '7': ['speciesName'], + '6': ['stemLocalX', 'stemLocalY'], + '7': ['speciesCode'], '8': ['measurementDate'], '9': ['treeTag', 'stemTag', 'plotCensusNumber'], '10': ['treeTag', 'stemTag', 'plotCensusNumber'], '11': ['quadratName'], - '12': ['speciesName'], - '13': ['measuredDBH'], - '14': ['measuredDBH'], - '15': ['treeTag'], - '16': ['quadratName'] + '12': ['speciesCode'], + '13': ['measuredDBH', 'measuredHOM'] }; export const sortRowsByMeasurementDate = (rows: GridRowsProp, direction: GridSortDirection): GridRowsProp => { return rows.slice().sort((a, b) => { diff --git a/frontend/config/sqlrdsdefinitions/validations.ts b/frontend/config/sqlrdsdefinitions/validations.ts index 1dedf989..bb5e57a2 100644 --- a/frontend/config/sqlrdsdefinitions/validations.ts +++ b/frontend/config/sqlrdsdefinitions/validations.ts @@ -18,6 +18,7 @@ export type ValidationProceduresRDS = { validationID?: number; procedureName?: string; description?: string; + criteria?: string; definition?: string; isEnabled?: boolean; createdAt?: Date; diff --git a/frontend/config/sqlrdsdefinitions/views.ts b/frontend/config/sqlrdsdefinitions/views.ts index c532cc6d..d1f6e283 100644 --- a/frontend/config/sqlrdsdefinitions/views.ts +++ b/frontend/config/sqlrdsdefinitions/views.ts @@ -58,8 +58,8 @@ export type MeasurementsSummaryRDS = { speciesCode?: string; treeTag?: string; stemTag?: string; - localX?: number; - localY?: number; + stemLocalX?: number; + stemLocalY?: number; coordinateUnits?: string; measurementDate?: any; measuredDBH?: number; From b08b85296de43c08267f43aa6a03cb2acc3beb3a Mon Sep 17 00:00:00 2001 From: sambokar Date: Mon, 25 Nov 2024 14:30:55 -0500 Subject: [PATCH 13/15] snackbar reconfigured into dedicated array structure to allow multiple messages at once. --- .../[dataType]/[[...slugs]]/route.ts | 104 +++++++++++++-- .../datagrids/applications/msvdatagrid.tsx | 2 +- .../applications/viewfulltabledatagrid.tsx | 4 +- .../datagrids/measurementscommons.tsx | 124 ++++++++++++------ frontend/config/datagridhelpers.ts | 4 +- 5 files changed, 181 insertions(+), 57 deletions(-) diff --git a/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts index 9ff0325a..0403225b 100644 --- a/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts +++ b/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts @@ -150,18 +150,98 @@ export async function POST( let searchStub = ''; let filterStub = ''; switch (params.dataType) { + case 'validationprocedures': + if (filterModel.quickFilterValues) searchStub = buildSearchStub(columns, filterModel.quickFilterValues); + if (filterModel.items) filterStub = buildFilterModelStub(filterModel); + + paginatedQuery = ` + SELECT SQL_CALC_FOUND_ROWS * FROM catalog.${params.dataType} + ${searchStub || filterStub ? ` WHERE (${[searchStub, filterStub].filter(Boolean).join(' OR ')})` : ''}`; // validation procedures is special + queryParams.push(page * pageSize, pageSize); + break; + case 'attributes': + case 'species': + case 'stems': + case 'alltaxonomiesview': + case 'quadratpersonnel': + case 'sitespecificvalidations': + case 'roles': + if (filterModel.quickFilterValues) searchStub = buildSearchStub(columns, filterModel.quickFilterValues); + if (filterModel.items) filterStub = buildFilterModelStub(filterModel); + + paginatedQuery = `SELECT SQL_CALC_FOUND_ROWS * FROM ${schema}.${params.dataType} + ${searchStub || filterStub ? ` WHERE (${[searchStub, filterStub].filter(Boolean).join(' OR ')})` : ''}`; + queryParams.push(page * pageSize, pageSize); + break; + case 'personnel': + if (filterModel.quickFilterValues) searchStub = buildSearchStub(columns, filterModel.quickFilterValues, 'p'); + if (filterModel.items) filterStub = buildFilterModelStub(filterModel, 'p'); + + paginatedQuery = ` + SELECT SQL_CALC_FOUND_ROWS p.* + FROM ${schema}.${params.dataType} p + JOIN ${schema}.census c ON p.CensusID = c.CensusID + WHERE c.PlotID = ? + AND c.PlotCensusNumber = ? + ${searchStub || filterStub ? ` AND (${[searchStub, filterStub].filter(Boolean).join(' OR ')})` : ''}`; + queryParams.push(plotID, plotCensusNumber, page * pageSize, pageSize); + break; + case 'quadrats': + if (filterModel.quickFilterValues) searchStub = buildSearchStub(columns, filterModel.quickFilterValues, 'q'); + if (filterModel.items) filterStub = buildFilterModelStub(filterModel, 'q'); + + paginatedQuery = ` + SELECT SQL_CALC_FOUND_ROWS q.* + FROM ${schema}.quadrats q + JOIN ${schema}.censusquadrat cq ON q.QuadratID = cq.QuadratID + JOIN ${schema}.census c ON cq.CensusID = c.CensusID + WHERE q.PlotID = ? + AND c.PlotID = ? + AND c.PlotCensusNumber = ? + ${searchStub || filterStub ? ` AND (${[searchStub, filterStub].filter(Boolean).join(' OR ')})` : ''}`; + queryParams.push(plotID, plotID, plotCensusNumber, page * pageSize, pageSize); + break; + case 'personnelrole': + if (filterModel.quickFilterValues) searchStub = buildSearchStub(columns, filterModel.quickFilterValues, 'p'); + if (filterModel.items) filterStub = buildFilterModelStub(filterModel, 'p'); + + paginatedQuery = ` + SELECT SQL_CALC_FOUND_ROWS + p.PersonnelID, + p.CensusID, + p.FirstName, + p.LastName, + r.RoleName, + r.RoleDescription + FROM + personnel p + LEFT JOIN + roles r ON p.RoleID = r.RoleID + census c ON p.CensusID = c.CensusID + WHERE c.PlotID = ? + AND c.PlotCensusNumber = ? + ${searchStub || filterStub ? ` AND (${[searchStub, filterStub].filter(Boolean).join(' OR ')})` : ''}`; + queryParams.push(plotID, plotCensusNumber, page * pageSize, pageSize); + break; + case 'census': + if (filterModel.quickFilterValues) searchStub = buildSearchStub(columns, filterModel.quickFilterValues); + if (filterModel.items) filterStub = buildFilterModelStub(filterModel); + + paginatedQuery = ` + SELECT SQL_CALC_FOUND_ROWS * + FROM ${schema}.census + WHERE PlotID = ? + ${searchStub || filterStub ? ` AND (${[searchStub, filterStub].filter(Boolean).join(' OR ')})` : ''}`; + queryParams.push(plotID, page * pageSize, pageSize); + break; case 'measurementssummary': case 'measurementssummary_staging': case 'measurementssummaryview': case 'viewfulltable': case 'viewfulltableview': - if (filterModel.quickFilterValues) { - searchStub = buildSearchStub(columns, filterModel.quickFilterValues, 'vft'); - } + if (filterModel.quickFilterValues) searchStub = buildSearchStub(columns, filterModel.quickFilterValues, 'vft'); + if (filterModel.items) filterStub = buildFilterModelStub(filterModel, 'vft'); - if (filterModel.items) { - filterStub = buildFilterModelStub(filterModel, 'vft'); - } paginatedQuery = ` SELECT SQL_CALC_FOUND_ROWS vft.* FROM ${schema}.${params.dataType} vft @@ -193,13 +273,9 @@ export async function POST( queryParams.push(plotID, plotID, plotCensusNumber, page * pageSize, pageSize); break; case 'coremeasurements': - if (filterModel.quickFilterValues) { - searchStub = buildSearchStub(columns, filterModel.quickFilterValues, 'vft'); - } + if (filterModel.quickFilterValues) searchStub = buildSearchStub(columns, filterModel.quickFilterValues, 'pdt'); + if (filterModel.items) filterStub = buildFilterModelStub(filterModel, 'pdt'); - if (filterModel.items) { - filterStub = buildFilterModelStub(filterModel, 'vft'); - } const censusQuery = ` SELECT CensusID FROM ${schema}.census @@ -239,7 +315,9 @@ export async function POST( paginatedQuery += ` LIMIT ?, ?;`; if (paginatedQuery.match(/\?/g)?.length !== queryParams.length) { - throw new Error('Mismatch between query placeholders and parameters'); + throw new Error( + `Mismatch between query placeholders and parameters: paginated query length: ${paginatedQuery.match(/\?/g)?.length}, parameters length: ${queryParams.length}` + ); } console.log('completed query: ', format(paginatedQuery, queryParams)); const paginatedResults = await connectionManager.executeQuery(format(paginatedQuery, queryParams)); diff --git a/frontend/components/datagrids/applications/msvdatagrid.tsx b/frontend/components/datagrids/applications/msvdatagrid.tsx index ab158349..0be26258 100644 --- a/frontend/components/datagrids/applications/msvdatagrid.tsx +++ b/frontend/components/datagrids/applications/msvdatagrid.tsx @@ -53,7 +53,7 @@ export default function MeasurementsSummaryViewDataGrid() { const [rows, setRows] = React.useState([initialMeasurementsSummaryViewRDSRow] as GridRowsProp); const [rowCount, setRowCount] = useState(0); const [rowModesModel, setRowModesModel] = React.useState({}); - const [snackbar, setSnackbar] = React.useState | null>(null); + const [snackbar, setSnackbar] = React.useState>>([]); const [refresh, setRefresh] = useState(false); const [paginationModel, setPaginationModel] = useState({ page: 0, diff --git a/frontend/components/datagrids/applications/viewfulltabledatagrid.tsx b/frontend/components/datagrids/applications/viewfulltabledatagrid.tsx index ab525ef8..1e63d106 100644 --- a/frontend/components/datagrids/applications/viewfulltabledatagrid.tsx +++ b/frontend/components/datagrids/applications/viewfulltabledatagrid.tsx @@ -4,7 +4,7 @@ import { AlertProps } from '@mui/material'; import { GridRowsProp } from '@mui/x-data-grid'; import { randomId } from '@mui/x-data-grid-generator'; -import { useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { ViewFullTableGridColumns } from '@/components/client/datagridcolumns'; import MeasurementsCommons from '@/components/datagrids/measurementscommons'; import { ViewFullTableRDS } from '@/config/sqlrdsdefinitions/views'; @@ -110,7 +110,7 @@ export default function ViewFullTableDataGrid() { const [rows, setRows] = useState([initialViewFullTable] as GridRowsProp); const [rowCount, setRowCount] = useState(0); const [rowModesModel, setRowModesModel] = useState({}); - const [snackbar, setSnackbar] = useState | null>(null); + const [snackbar, setSnackbar] = React.useState>>([]); const [refresh, setRefresh] = useState(false); const [paginationModel, setPaginationModel] = useState({ page: 0, diff --git a/frontend/components/datagrids/measurementscommons.tsx b/frontend/components/datagrids/measurementscommons.tsx index e0d00b1b..9203ffed 100644 --- a/frontend/components/datagrids/measurementscommons.tsx +++ b/frontend/components/datagrids/measurementscommons.tsx @@ -1,6 +1,6 @@ // measurementcommons datagrid 'use client'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react'; import { GridActionsCellItem, GridCellParams, @@ -19,7 +19,7 @@ import { ToolbarPropsOverrides, useGridApiRef } from '@mui/x-data-grid'; -import { Alert, Button, Checkbox, IconButton, Snackbar } from '@mui/material'; +import { Alert, AlertProps, Button, Checkbox, IconButton, Snackbar } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/DeleteOutlined'; @@ -434,7 +434,7 @@ export default function MeasurementsCommons(props: Readonly void, + setSnackbar: Dispatch>>>, setIsNewRowAdded: (value: boolean) => void, setShouldAddRowAfterFetch: (value: boolean) => void, fetchPaginatedData: (page: number) => Promise, @@ -453,18 +453,28 @@ export default function MeasurementsCommons(props: Readonly [ + ...prev, + { + children: `Error: ${responseJSON.message}`, + severity: 'error' + }, + { + children: oldRow.isNew ? 'New row added!' : 'Row updated!', + severity: 'success' + } + ]); return Promise.reject(responseJSON.row); + } else { + setSnackbar(prev => [ + ...prev, + { + children: oldRow.isNew ? 'New row added!' : 'Row updated!', + severity: 'success' + } + ]); } - setSnackbar({ - children: oldRow.isNew ? 'New row added!' : 'Row updated!', - severity: 'success' - }); - if (oldRow.isNew) { setIsNewRowAdded(false); setShouldAddRowAfterFetch(false); @@ -474,7 +484,7 @@ export default function MeasurementsCommons(props: Readonly [...prev, { children: `Error: ${error.message}`, severity: 'error' }]); return Promise.reject(newRow); } }; @@ -580,22 +590,31 @@ export default function MeasurementsCommons(props: Readonly [ + ...prev, + { + children: `Error: Cannot delete row due to foreign key constraint in table ${error.referencingTable}`, + severity: 'error' + } + ]); } else { - setSnackbar({ - children: `Error: ${error.message || 'Deletion failed'}`, - severity: 'error' - }); + setSnackbar(prev => [ + ...prev, + { + children: `Error: ${error.message || 'Deletion failed'}`, + severity: 'error' + } + ]); } } else { if (handleSelectQuadrat) handleSelectQuadrat(null); - setSnackbar({ - children: 'Row successfully deleted', - severity: 'success' - }); + setSnackbar(prev => [ + ...prev, + { + children: 'Row successfully deleted', + severity: 'success' + } + ]); setRows(rows.filter(row => String(row.id) !== String(id))); try { setLoading(true, 'Refreshing Measurements Summary View...'); @@ -685,7 +704,7 @@ export default function MeasurementsCommons(props: Readonly [...prev, { children: 'Error fetching data', severity: 'error' }]); } finally { setLoading(false); } @@ -729,10 +748,22 @@ export default function MeasurementsCommons(props: Readonly 0) + setSnackbar(prev => [ + ...prev, + { + children: `${data.CountPending} row(s) pending validation.`, + severity: 'info' + }, + { + children: `${data.CountValid} row(s) passed validation.`, + severity: 'success' + }, + { + children: `${data.CountErrors} row(s) with validation errors detected.`, + severity: 'error' + } + ]); }); }, [fetchPaginatedData]); @@ -775,7 +806,7 @@ export default function MeasurementsCommons(props: Readonly setSnackbar(null); + const handleCloseSnackbar = () => setSnackbar([]); const handleRowEditStop: GridEventListener<'rowEditStop'> = (params, event) => { if (params.reason === GridRowEditStopReasons.rowFocusOut) { @@ -912,7 +943,7 @@ export default function MeasurementsCommons(props: Readonly [...prev, { children: 'Error fetching full data', severity: 'error' }]); } finally { setLoading(false); } @@ -1230,10 +1261,13 @@ export default function MeasurementsCommons(props: Readonly { console.error('Row update error:', error); - setSnackbar({ - children: 'Error updating row', - severity: 'error' - }); + setSnackbar(prev => [ + ...prev, + { + children: 'Error updating row', + severity: 'error' + } + ]); }} onCellKeyDown={(params, event) => { if (event.key === 'Enter') { @@ -1280,9 +1314,21 @@ export default function MeasurementsCommons(props: Readonly {!!snackbar && ( - - - + <> + {snackbar.map((snack, index) => ( + { + setSnackbar(prev => prev.filter((_, i) => i !== index)); // Remove the snackbar on close + }} + autoHideDuration={6000} + > + {snack.children} + + ))} + )} {isDialogOpen && promiseArguments && ( >; rowModesModel: GridRowModesModel; setRowModesModel: Dispatch>; - snackbar: Pick | null; - setSnackbar: Dispatch | null>>; + snackbar: Array>; + setSnackbar: Dispatch>>>; refresh: boolean; setRefresh: Dispatch>; paginationModel: { pageSize: number; page: number }; From 2130be81318e6bb9c7b1aa121432ffc5a273a2c1 Mon Sep 17 00:00:00 2001 From: sambokar Date: Tue, 26 Nov 2024 12:37:25 -0500 Subject: [PATCH 14/15] overhaul of validation error display completed. validation sequence tested and confirmed. error highlighting is rendering correctly. --- .../validationerrordisplay/route.ts | 10 +- .../datagrids/applications/msvdatagrid.tsx | 2 +- .../applications/viewfulltabledatagrid.tsx | 4 +- .../datagrids/measurementscommons.tsx | 420 ++++++++---------- frontend/config/datagridhelpers.ts | 4 +- frontend/config/macros/uploadsystemmacros.ts | 18 + frontend/package-lock.json | 39 -- 7 files changed, 215 insertions(+), 282 deletions(-) diff --git a/frontend/app/api/validations/validationerrordisplay/route.ts b/frontend/app/api/validations/validationerrordisplay/route.ts index 8804aa25..5b0ead14 100644 --- a/frontend/app/api/validations/validationerrordisplay/route.ts +++ b/frontend/app/api/validations/validationerrordisplay/route.ts @@ -6,6 +6,10 @@ import ConnectionManager from '@/config/connectionmanager'; export async function GET(request: NextRequest) { const conn = new ConnectionManager(); const schema = request.nextUrl.searchParams.get('schema'); + const plotIDParam = request.nextUrl.searchParams.get('plotIDParam'); + const censusPCNParam = request.nextUrl.searchParams.get('censusPCNParam'); + const plotID = plotIDParam ? parseInt(plotIDParam) : null; + const censusPCN = censusPCNParam ? parseInt(censusPCNParam) : null; if (!schema) throw new Error('No schema variable provided!'); try { @@ -15,7 +19,8 @@ export async function GET(request: NextRequest) { SELECT cm.CoreMeasurementID AS CoreMeasurementID, GROUP_CONCAT(ve.ValidationID) AS ValidationErrorIDs, - GROUP_CONCAT(ve.Description) AS Descriptions + GROUP_CONCAT(ve.Description) AS Descriptions, + GROUP_CONCAT(ve.Criteria) AS Criteria FROM ${schema}.cmverrors AS cve JOIN @@ -30,7 +35,8 @@ export async function GET(request: NextRequest) { const parsedValidationErrors: CMError[] = validationErrorsRows.map((row: any) => ({ coreMeasurementID: row.CoreMeasurementID, validationErrorIDs: row.ValidationErrorIDs.split(',').map(Number), - descriptions: row.Descriptions.split(',') + descriptions: row.Descriptions.split(','), + criteria: row.Criteria.split(',') })); console.log('parsedValidationErrors: ', parsedValidationErrors); return new NextResponse( diff --git a/frontend/components/datagrids/applications/msvdatagrid.tsx b/frontend/components/datagrids/applications/msvdatagrid.tsx index 0be26258..ab158349 100644 --- a/frontend/components/datagrids/applications/msvdatagrid.tsx +++ b/frontend/components/datagrids/applications/msvdatagrid.tsx @@ -53,7 +53,7 @@ export default function MeasurementsSummaryViewDataGrid() { const [rows, setRows] = React.useState([initialMeasurementsSummaryViewRDSRow] as GridRowsProp); const [rowCount, setRowCount] = useState(0); const [rowModesModel, setRowModesModel] = React.useState({}); - const [snackbar, setSnackbar] = React.useState>>([]); + const [snackbar, setSnackbar] = React.useState | null>(null); const [refresh, setRefresh] = useState(false); const [paginationModel, setPaginationModel] = useState({ page: 0, diff --git a/frontend/components/datagrids/applications/viewfulltabledatagrid.tsx b/frontend/components/datagrids/applications/viewfulltabledatagrid.tsx index 1e63d106..ab525ef8 100644 --- a/frontend/components/datagrids/applications/viewfulltabledatagrid.tsx +++ b/frontend/components/datagrids/applications/viewfulltabledatagrid.tsx @@ -4,7 +4,7 @@ import { AlertProps } from '@mui/material'; import { GridRowsProp } from '@mui/x-data-grid'; import { randomId } from '@mui/x-data-grid-generator'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { ViewFullTableGridColumns } from '@/components/client/datagridcolumns'; import MeasurementsCommons from '@/components/datagrids/measurementscommons'; import { ViewFullTableRDS } from '@/config/sqlrdsdefinitions/views'; @@ -110,7 +110,7 @@ export default function ViewFullTableDataGrid() { const [rows, setRows] = useState([initialViewFullTable] as GridRowsProp); const [rowCount, setRowCount] = useState(0); const [rowModesModel, setRowModesModel] = useState({}); - const [snackbar, setSnackbar] = React.useState>>([]); + const [snackbar, setSnackbar] = useState | null>(null); const [refresh, setRefresh] = useState(false); const [paginationModel, setPaginationModel] = useState({ page: 0, diff --git a/frontend/components/datagrids/measurementscommons.tsx b/frontend/components/datagrids/measurementscommons.tsx index 9203ffed..f5ce7938 100644 --- a/frontend/components/datagrids/measurementscommons.tsx +++ b/frontend/components/datagrids/measurementscommons.tsx @@ -19,7 +19,7 @@ import { ToolbarPropsOverrides, useGridApiRef } from '@mui/x-data-grid'; -import { Alert, AlertProps, Button, Checkbox, IconButton, Snackbar } from '@mui/material'; +import { Alert, AlertColor, AlertProps, AlertPropsColorOverrides, Button, Checkbox, IconButton, Snackbar } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/DeleteOutlined'; @@ -42,7 +42,7 @@ import { PendingAction, sortRowsByMeasurementDate } from '@/config/datagridhelpers'; -import { CMError } from '@/config/macros/uploadsystemmacros'; +import { CMError, CoreMeasurementError, ErrorMap, ValidationPair } from '@/config/macros/uploadsystemmacros'; import { useOrgCensusContext, usePlotContext, useQuadratContext, useSiteContext } from '@/app/contexts/userselectionprovider'; import { redirect } from 'next/navigation'; import moment from 'moment'; @@ -60,8 +60,8 @@ import { applyFilterToColumns } from '@/components/datagrids/filtrationsystem'; import { ClearIcon } from '@mui/x-date-pickers'; import CloudDownloadIcon from '@mui/icons-material/CloudDownload'; import ValidationModal from '@/components/client/validationmodal'; -import { ValidationProceduresRDS } from '@/config/sqlrdsdefinitions/validations'; import { MeasurementsSummaryViewGridColumns } from '@/components/client/datagridcolumns'; +import { OverridableStringUnion } from '@mui/types'; function debounce void>(fn: T, delay: number): T { let timeoutId: ReturnType; @@ -289,13 +289,11 @@ export default function MeasurementsCommons(props: Readonly({}); - const [validationProcedures, setValidationProcedures] = useState([]); + const [validationErrors, setValidationErrors] = useState({}); const [showErrorRows, setShowErrorRows] = useState(true); const [showValidRows, setShowValidRows] = useState(true); const [showPendingRows, setShowPendingRows] = useState(true); + const [hidingEmpty, setHidingEmpty] = useState(true); const [filterModel, setFilterModel] = useState({ items: [], quickFilterValues: [], @@ -307,7 +305,6 @@ export default function MeasurementsCommons(props: Readonly([{ field: 'measurementDate', sort: 'asc' }]); - const [clickedRow, setClickedRow] = useState(null); const [errorCount, setErrorCount] = useState(null); const [validCount, setValidCount] = useState(null); const [pendingCount, setPendingCount] = useState(null); @@ -334,10 +331,6 @@ export default function MeasurementsCommons(props: Readonly { - setClickedRow(prev => (prev === rowId ? null : rowId)); - }; - const exportAllCSV = useCallback(async () => { const response = await fetch( `/api/formdownload/measurements/${currentSite?.schemaName ?? ''}/${currentPlot?.plotID ?? 0}/${currentCensus?.dateRanges[0].censusID ?? 0}`, @@ -399,42 +392,31 @@ export default function MeasurementsCommons(props: Readonly { const row = rows.find(row => rowId === row.id); - const error = validationErrors[Number(row?.coreMeasurementID)]; - if (error === undefined) return false; - const errorFields = error.validationErrorIDs.flatMap( - id => - validationProcedures - .find(vp => vp.validationID === id) // Compare id with validationID - ?.criteria?.split(';') || [] - ); - return errorFields.includes(colField); + if (!row || !row.coreMeasurementID || !validationErrors[row.coreMeasurementID]) { + return false; + } + return validationErrors[Number(row.coreMeasurementID)].errors.find(error => error.validationPairs.find(vp => vp.criterion === colField)); }; const rowHasError = (rowId: GridRowId) => { - if (!rows || rows.length === 0) return false; + const row = rows.find(row => rowId === row.id); + if (!row || !row.coreMeasurementID || !validationErrors[row.coreMeasurementID]) { + return false; // No errors for this row + } return gridColumns.some(column => cellHasError(column.field, rowId)); }; const fetchErrorRows = async () => { - if (!rows || rows.length === 0) return []; + if (!rows || rows.length === 0 || !validationErrors) return []; return rows.filter(row => rowHasError(row.id)); }; - const getRowErrorDescriptions = (rowId: GridRowId): string[] => { - const row = rows.find(row => rowId === row.id); - const error = validationErrors[row?.coreMeasurementID]; - return error.validationErrorIDs.map(id => { - const index = error.validationErrorIDs.indexOf(id); - return error.descriptions[index]; // Assumes that descriptions are stored in the CMError object - }); - }; - const updateRow = async ( gridType: string, schemaName: string | undefined, newRow: GridRowModel, oldRow: GridRowModel, - setSnackbar: Dispatch>>>, + setSnackbar: Dispatch | null>>, setIsNewRowAdded: (value: boolean) => void, setShouldAddRowAfterFetch: (value: boolean) => void, fetchPaginatedData: (page: number) => Promise, @@ -453,28 +435,18 @@ export default function MeasurementsCommons(props: Readonly [ - ...prev, - { - children: `Error: ${responseJSON.message}`, - severity: 'error' - }, - { - children: oldRow.isNew ? 'New row added!' : 'Row updated!', - severity: 'success' - } - ]); + setSnackbar({ + children: `Error: ${responseJSON.message}`, + severity: 'error' + }); return Promise.reject(responseJSON.row); - } else { - setSnackbar(prev => [ - ...prev, - { - children: oldRow.isNew ? 'New row added!' : 'Row updated!', - severity: 'success' - } - ]); } + setSnackbar({ + children: oldRow.isNew ? 'New row added!' : 'Row updated!', + severity: 'success' + }); + if (oldRow.isNew) { setIsNewRowAdded(false); setShouldAddRowAfterFetch(false); @@ -484,7 +456,7 @@ export default function MeasurementsCommons(props: Readonly [...prev, { children: `Error: ${error.message}`, severity: 'error' }]); + setSnackbar({ children: `Error: ${error.message}`, severity: 'error' }); return Promise.reject(newRow); } }; @@ -590,31 +562,22 @@ export default function MeasurementsCommons(props: Readonly [ - ...prev, - { - children: `Error: Cannot delete row due to foreign key constraint in table ${error.referencingTable}`, - severity: 'error' - } - ]); + setSnackbar({ + children: `Error: Cannot delete row due to foreign key constraint in table ${error.referencingTable}`, + severity: 'error' + }); } else { - setSnackbar(prev => [ - ...prev, - { - children: `Error: ${error.message || 'Deletion failed'}`, - severity: 'error' - } - ]); + setSnackbar({ + children: `Error: ${error.message || 'Deletion failed'}`, + severity: 'error' + }); } } else { if (handleSelectQuadrat) handleSelectQuadrat(null); - setSnackbar(prev => [ - ...prev, - { - children: 'Row successfully deleted', - severity: 'success' - } - ]); + setSnackbar({ + children: 'Row successfully deleted', + severity: 'success' + }); setRows(rows.filter(row => String(row.id) !== String(id))); try { setLoading(true, 'Refreshing Measurements Summary View...'); @@ -695,6 +658,8 @@ export default function MeasurementsCommons(props: Readonly [...prev, { children: 'Error fetching data', severity: 'error' }]); + setSnackbar({ children: 'Error fetching data', severity: 'error' }); } finally { setLoading(false); } - }, 500), - [filterModel, currentSite, currentPlot, currentCensus, paginationModel.pageSize, isNewRowAdded, newLastPage] + }, 250), + [filterModel, currentSite, currentPlot, currentCensus, paginationModel, isNewRowAdded, newLastPage] ); async function runFetchPaginated() { @@ -723,6 +688,10 @@ export default function MeasurementsCommons(props: Readonly { + console.log('updated rows object: ', rows); + }, [rows]); + useEffect(() => { async function getCounts() { const query = `SELECT @@ -748,44 +717,18 @@ export default function MeasurementsCommons(props: Readonly 0) - setSnackbar(prev => [ - ...prev, - { - children: `${data.CountPending} row(s) pending validation.`, - severity: 'info' - }, - { - children: `${data.CountValid} row(s) passed validation.`, - severity: 'success' - }, - { - children: `${data.CountErrors} row(s) with validation errors detected.`, - severity: 'error' - } - ]); + const counts = [ + { count: data.CountErrors, message: `${data.CountErrors} row(s) with validation errors detected.`, severity: 'warning' }, + { count: data.CountPending, message: `${data.CountPending} row(s) pending validation.`, severity: 'info' }, + { count: data.CountValid, message: `${data.CountValid} row(s) passed validation.`, severity: 'success' } + ]; + const highestCount = counts.reduce((prev, current) => (current.count > prev.count ? current : prev)); + setSnackbar({ + children: highestCount.message, + severity: highestCount.severity as OverridableStringUnion | undefined + }); }); - }, [fetchPaginatedData]); - - async function getValidations() { - try { - const response = await fetch(`/api/validations/crud`, { method: 'GET' }); - const inbound = await response.json(); - if (JSON.stringify(validationProcedures) !== JSON.stringify(inbound)) setValidationProcedures(inbound); - } catch (e: any) { - console.error(`validation list pull failed with error: ${e}`); - } - } - - useEffect(() => { - if (validationProcedures.length === 0) getValidations().catch(console.error); - }, []); - - const handleRefresh = useCallback(async () => { - setRefresh(true); - await runFetchPaginated(); - setRefresh(false); - }, [paginationModel, refresh]); + }, [rows, paginationModel]); const processRowUpdate = useCallback( (newRow: GridRowModel, oldRow: GridRowModel) => @@ -806,7 +749,7 @@ export default function MeasurementsCommons(props: Readonly setSnackbar([]); + const handleCloseSnackbar = () => setSnackbar(null); const handleRowEditStop: GridEventListener<'rowEditStop'> = (params, event) => { if (params.reason === GridRowEditStopReasons.rowFocusOut) { @@ -890,26 +833,50 @@ export default function MeasurementsCommons(props: Readonly { try { - const response = await fetch(`/api/validations/validationerrordisplay?schema=${currentSite?.schemaName ?? ''}`); + const response = await fetch( + `/api/validations/validationerrordisplay?schema=${currentSite?.schemaName ?? ''}&plotIDParam=${currentPlot?.plotID ?? ''}&censusPCNParam=${currentCensus?.plotCensusNumber ?? ''}` + ); if (!response.ok) { throw new Error('Failed to fetch validation errors'); } const data = await response.json(); - const errors: CMError[] = data?.failed ?? []; - const errorMap = Array.isArray(errors) - ? errors.reduce>((acc, error) => { - acc[error?.coreMeasurementID] = error; + const errorMap: ErrorMap = Array.isArray(data?.failed as CMError[]) + ? (data.failed as CMError[]).reduce>((acc, error) => { + if (error.coreMeasurementID) { + const errorDetailsMap = new Map(); + + (error.validationErrorIDs || []).forEach((id, index) => { + const descriptions = error.descriptions?.[index]?.split(';') || []; + const criteria = error.criteria?.[index]?.split(';') || []; + + // Ensure descriptions and criteria are paired correctly + const validationPairs = descriptions.map((description, i) => ({ + description, + criterion: criteria[i] ?? '' // Default to empty if criteria is missing + })); + + if (!errorDetailsMap.has(id)) { + errorDetailsMap.set(id, []); + } + + // Append validation pairs to the corresponding ID + errorDetailsMap.get(id)!.push(...validationPairs); + }); + + acc[error.coreMeasurementID] = { + coreMeasurementID: error.coreMeasurementID, + errors: Array.from(errorDetailsMap.entries()).map(([id, validationPairs]) => ({ + id, + validationPairs + })) + }; + } return acc; }, {}) : {}; - - // Only update state if there is a difference - if (JSON.stringify(validationErrors) !== JSON.stringify(errorMap)) { - setValidationErrors(errorMap); - } - return errorMap; // Return the errorMap if you need to log it outside + setValidationErrors(errorMap); } catch (error) { console.error('Error fetching validation errors:', error); } @@ -943,7 +910,7 @@ export default function MeasurementsCommons(props: Readonly [...prev, { children: 'Error fetching full data', severity: 'error' }]); + setSnackbar({ children: 'Error fetching full data', severity: 'error' }); } finally { setLoading(false); } @@ -952,8 +919,7 @@ export default function MeasurementsCommons(props: Readonly { const errorRows = await fetchErrorRows(); return errorRows.map(row => { - const errors = getRowErrorDescriptions(row.id); - return { ...row, errors }; + return { ...row, errors: validationErrors[Number(row.coreMeasurementID)].errors ?? [] }; }); }; @@ -966,9 +932,24 @@ export default function MeasurementsCommons(props: Readonly { + console.log('validation errors full: ', validationErrors); + console.log('row: ', params.row); + console.log( + 'row in rows: ', + rows.find(row => row.coreMeasurementID === params.row.coreMeasurementID) + ); if (validationErrors[Number(params.row.coreMeasurementID)]) { + console.log('val error: ', validationErrors[Number(params.row.coreMeasurementID)]); + const validationStrings = + validationErrors[Number(params.row.coreMeasurementID)]?.errors.map(errorDetail => { + const pairsString = errorDetail.validationPairs + .map(pair => `(${pair.description} <--> ${pair.criterion})`) // Format each validation pair + .join(', '); // Combine all pairs for the errorDetail + + return `ID ${errorDetail.id}: ${pairsString}`; // Format the string for the ID + }) || []; return ( - + ); @@ -1007,31 +988,19 @@ export default function MeasurementsCommons(props: Readonly ), valueFormatter: value => { - // Check if the date is present and valid if (!value || !moment(value).utc().isValid()) { return ''; } - // Format the date as a dash-separated set of numbers return moment(value).utc().format('YYYY-MM-DD'); } }; const getCellErrorMessages = (colField: string, coreMeasurementID: number) => { - const error = validationErrors[Number(coreMeasurementID)]; - if (!error || !validationProcedures) return ''; - - return error.validationErrorIDs - .filter(id => - validationProcedures - .find(valID => valID.validationID === id) - ?.criteria?.split(';') - .includes(colField) - ) - .map(id => { - const index = error.validationErrorIDs.indexOf(id); - return error.descriptions[index]; - }) - .join('; '); + const error = validationErrors[coreMeasurementID].errors; + if (!error || !Array.isArray(error)) { + return ''; + } + return error.flatMap(errorDetail => errorDetail.validationPairs).find(vp => vp.criterion === colField)?.description || null; }; const columns = useMemo(() => { @@ -1040,14 +1009,30 @@ export default function MeasurementsCommons(props: Readonly { const value = typeof params.value === 'string' ? params.value : (params.value?.toString() ?? ''); - const formattedValue = !isNaN(Number(value)) && value.includes('.') && value.split('.')[1].length > 2 ? Number(value).toFixed(2) : value; const rowError = rowHasError(params.id); - const targetColumnName = validationProcedures - .find(vp => validationErrors[Number(params.row.coreMeasurementID)]?.validationErrorIDs.includes(vp.validationID ?? -1)) - ?.criteria?.split(';')[0]; const cellError = cellHasError(column.field, params.id) ? getCellErrorMessages(column.field, Number(params.row.coreMeasurementID)) : ''; - if (rowError) console.log('targetColumn for row error: ', targetColumnName, ' cellError: ', cellError); + + const isMeasurementField = column.field === 'measuredDBH' || column.field === 'measuredHOM'; + + const renderMeasurementDetails = () => ( + <> + + {column.field === 'measuredDBH' + ? params.row.measuredDBH + ? Number(params.row.measuredDBH).toFixed(2) + : 'null' + : params.row.measuredHOM + ? Number(params.row.measuredHOM).toFixed(2) + : 'null'} + + + {column.field === 'measuredDBH' && params.row.dbhUnits && params.row.measuredDBH !== null && params.row.dbhUnits} + {column.field === 'measuredHOM' && params.row.homUnits && params.row.measuredHOM !== null && params.row.homUnits} + + + ); + return ( - {column.field === targetColumnName ? ( - <> - {column.field === 'measuredDBH' || column.field === 'measuredHOM' ? ( - - - {column.field === 'measuredDBH' && <>{params.row.measuredDBH ? Number(params.row.measuredDBH).toFixed(2) : 'null'}} - {column.field === 'measuredHOM' && <>{params.row.measuredHOM ? Number(params.row.measuredHOM).toFixed(2) : 'null'}} - - - {column.field === 'measuredDBH' && <>{params.row.dbhUnits ? (params.row.measuredDBH !== null ? params.row.dbhUnits : '') : ''}} - {column.field === 'measuredHOM' && <>{params.row.homUnits ? (params.row.measuredHOM !== null ? params.row.homUnits : '') : ''}} - - - ) : ( - {formattedValue} - )} - - {cellError} - - + {isMeasurementField ? ( + {renderMeasurementDetails()} ) : ( - <> - {column.field === 'measuredDBH' || column.field === 'measuredHOM' ? ( - - - {column.field === 'measuredDBH' && <>{params.row.measuredDBH ? Number(params.row.measuredDBH).toFixed(2) : 'null'}} - {column.field === 'measuredHOM' && <>{params.row.measuredHOM ? Number(params.row.measuredHOM).toFixed(2) : 'null'}} - - - {column.field === 'measuredDBH' && <>{params.row.dbhUnits ? (params.row.measuredDBH !== null ? params.row.dbhUnits : '') : ''}} - {column.field === 'measuredHOM' && <>{params.row.homUnits ? (params.row.measuredHOM !== null ? params.row.homUnits : '') : ''}} - - - ) : ( - {formattedValue} - )} - + {formattedValue} + )} + {cellError !== '' && ( + + {cellError} + )} ); @@ -1116,20 +1073,12 @@ export default function MeasurementsCommons(props: Readonly { - let filteredRows = rows; - if (!showValidRows) { - filteredRows = filteredRows.filter(row => rowHasError(row.id)); - } - if (!showErrorRows) { - filteredRows = filteredRows.filter(row => !rowHasError(row.id)); - } - return filteredRows; - }, [rows, showErrorRows, showValidRows]); + }, [MeasurementsSummaryViewGridColumns, locked, rows, validationErrors]); - const filteredColumns = useMemo(() => filterColumns(visibleRows, columns), [visibleRows, columns]); + const filteredColumns = useMemo(() => { + if (hidingEmpty) return filterColumns(rows, columns); + return columns; + }, [rows, columns, hidingEmpty]); const getRowClassName = (params: any) => { const rowId = params.id; @@ -1211,6 +1160,17 @@ export default function MeasurementsCommons(props: Readonly setShowPendingRows(event.target.checked)} /> Show rows pending validation: ({pendingCount}) + + setHidingEmpty(event.target.checked)} /> + {hidingEmpty ? `Hiding Empty Columns` : `Hide Empty Columns`} + handleRowClick(params.id)} disableColumnSelector onRowModesModelChange={handleRowModesModelChange} onRowEditStop={handleRowEditStop} @@ -1261,13 +1224,10 @@ export default function MeasurementsCommons(props: Readonly { console.error('Row update error:', error); - setSnackbar(prev => [ - ...prev, - { - children: 'Error updating row', - severity: 'error' - } - ]); + setSnackbar({ + children: 'Error updating row', + severity: 'error' + }); }} onCellKeyDown={(params, event) => { if (event.key === 'Enter') { @@ -1299,7 +1259,7 @@ export default function MeasurementsCommons(props: Readonly {!!snackbar && ( - <> - {snackbar.map((snack, index) => ( - { - setSnackbar(prev => prev.filter((_, i) => i !== index)); // Remove the snackbar on close - }} - autoHideDuration={6000} - > - {snack.children} - - ))} - + + + )} {isDialogOpen && promiseArguments && ( >; rowModesModel: GridRowModesModel; setRowModesModel: Dispatch>; - snackbar: Array>; - setSnackbar: Dispatch>>>; + snackbar: Pick | null; + setSnackbar: Dispatch | null>>; refresh: boolean; setRefresh: Dispatch>; paginationModel: { pageSize: number; page: number }; diff --git a/frontend/config/macros/uploadsystemmacros.ts b/frontend/config/macros/uploadsystemmacros.ts index 6b9dcbec..819e3b3f 100644 --- a/frontend/config/macros/uploadsystemmacros.ts +++ b/frontend/config/macros/uploadsystemmacros.ts @@ -181,4 +181,22 @@ export interface CMError { coreMeasurementID: number; validationErrorIDs: number[]; descriptions: string[]; + criteria: string[]; } + +export type ValidationPair = { + description: string; + criterion: string; +}; + +export type ErrorDetail = { + id: number; + validationPairs: ValidationPair[]; +}; + +export type CoreMeasurementError = { + coreMeasurementID: number; + errors: ErrorDetail[]; +}; + +export type ErrorMap = Record; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1bb9e08f..c7217e0a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -3749,15 +3749,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/duplexer2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -5520,17 +5511,6 @@ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/space-separated-tokens": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", @@ -6082,15 +6062,6 @@ "stylis": "4.2.0" } }, - "node_modules/jszip/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/@mui/x-data-grid": { "version": "7.22.1", "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.22.1.tgz", @@ -6291,16 +6262,6 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/https-proxy-agent": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", From d234406a35a8fb43a23e9f28245197f0248f442d Mon Sep 17 00:00:00 2001 From: sambokar Date: Tue, 26 Nov 2024 12:37:48 -0500 Subject: [PATCH 15/15] cleanup --- frontend/app/api/validations/validationerrordisplay/route.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/app/api/validations/validationerrordisplay/route.ts b/frontend/app/api/validations/validationerrordisplay/route.ts index 5b0ead14..077ecc8b 100644 --- a/frontend/app/api/validations/validationerrordisplay/route.ts +++ b/frontend/app/api/validations/validationerrordisplay/route.ts @@ -8,8 +8,6 @@ export async function GET(request: NextRequest) { const schema = request.nextUrl.searchParams.get('schema'); const plotIDParam = request.nextUrl.searchParams.get('plotIDParam'); const censusPCNParam = request.nextUrl.searchParams.get('censusPCNParam'); - const plotID = plotIDParam ? parseInt(plotIDParam) : null; - const censusPCN = censusPCNParam ? parseInt(censusPCNParam) : null; if (!schema) throw new Error('No schema variable provided!'); try {