diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml
new file mode 100644
index 00000000..b2e1cc47
--- /dev/null
+++ b/.github/workflows/build-docs.yml
@@ -0,0 +1,101 @@
+name: Build, Test, and Deploy Writerside Documentation
+
+on:
+ push:
+ branches: # Trigger on push to any branch
+ - "*"
+ paths:
+ - "frontend/documentation/**" # Only run on changes in the documentation folder
+ workflow_dispatch:
+
+permissions:
+ id-token: write
+ pages: write
+
+env:
+ INSTANCE: 'documentation/fad'
+ ARTIFACT: 'webHelpFAD2-all.zip'
+ DOCKER_VERSION: '243.21565' # Writerside's recommended Docker version
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ # Step 1: Checkout repository
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ # Step 2: Build Writerside documentation
+ - name: Build docs using Writerside Docker builder
+ uses: JetBrains/writerside-github-action@v4
+ with:
+ instance: ${{ env.INSTANCE }}
+ artifact: ${{ env.ARTIFACT }}
+ docker-version: ${{ env.DOCKER_VERSION }}
+ args: --verbose
+
+ # 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:
+ name: docs
+ path: |
+ artifacts/${{ env.ARTIFACT }}
+ artifacts/report.json
+ retention-days: 7
+
+ test:
+ needs: build
+ runs-on: ubuntu-latest
+ steps:
+ # Step 1: Download artifacts
+ - name: Download artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: docs
+ path: artifacts
+
+ # Step 2: Test Writerside documentation
+ - name: Test documentation
+ uses: JetBrains/writerside-checker-action@v1
+ with:
+ instance: ${{ env.INSTANCE }}
+
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ needs: [build, test]
+ runs-on: ubuntu-latest
+ steps:
+ # Step 1: Download artifacts
+ - name: Download artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: docs
+
+ # Step 2: Unzip the artifact
+ - name: Unzip artifact
+ run: unzip -O UTF-8 -qq '${{ env.ARTIFACT }}' -d dir
+
+ # Step 3: Set up GitHub Pages
+ - name: Setup Pages
+ uses: actions/configure-pages@v4
+
+ # Step 4: Package and upload Pages artifact
+ - name: Package and upload Pages artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: dir
+
+ # Step 5: Deploy to GitHub Pages
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
+
diff --git a/frontend/README.md b/frontend/README.md
index bafe24a1..59ca98fa 100644
--- a/frontend/README.md
+++ b/frontend/README.md
@@ -1,6 +1,5 @@
-# The ForestGEO Data Entry App
+# ForestGEO Census Management Application
-liquibase generate-changelog --exclude-objects="\b\w*view\w*\b"
A cloud-native web application built to accelerate the pace of research for the Smithsonian
Institution's Forest Global Earth Observatory (ForestGEO). ForestGEO is a global forest research
network, unparalleled in size and scope, comprised of ecologists and research sites dedicated to
@@ -8,92 +7,56 @@ advancing long-term study of the world's forests. The ForestGEO app aims to empo
an efficient means of recording, validating, and publishing forest health data.
Learn more about ForestGEO [at their website](https://www.forestgeo.si.edu/).
-This application was built using Next.js 13 (app directory) and NextUI (v2).
-
-### Technical documentation:
-
-Please see the
-documentation [here](https://github.com/ForestGeoHack/ForestGEO/wiki/ForestGEO-App-Specification)
-
-## Project Structure
-
-- `prev_app/`: previous iteration of the ForestGEO app, which uses the
- Next.js v12 Pages router system. You can step into this directory to run the previous iteration of
- the application
-- `app/`: the primary routing structure and setup for the primary application
-- `components/`: requisite react components that are used within the application and icon
- information
-- `config/`: fonts information and general site information -- endpoint names, plot names, plot
- interface, etc.
-- `styles/`: tailwindcss formatting files and dropzone/validation table custom formatting files
-- `types/`: additional set up for SVG formatting
-
-### Running the project
-
-1. Before running the project, you must create an `.env.local` file in the overhead directory with
- the following values:
- - `AZURE_AD_CLIENT_ID`
- - `AZURE_AD_CLIENT_SECRET`
- - `AZURE_AD_TENANT_ID`
- - `NEXTAUTH_SECRET`
- - `NEXTAUTH_URL`
- - all `AZURE_` values must be created/populated from Azure's App Registration portal
-2. Once `.env.local` is made, run `npm install` from the overhead directory to install dependencies
-3. Run `npm run build` to compile/optimize the application for running
-4. Run `npm run dev` to create a dev instance of the application locally on your machine
-5. Navigate to `http://localhost:3000` to access the application
-
----
-
-### Understanding Next.JS Dynamic Routing
-
-Next.js's dynamic routing setup allows for built-in endpoint data processing. By using this, passing
-data from a component or root layout to a page/endpoint is simplified (rather than using useCallback
-or a React function). As a brief reminder, remember that when using Next.js 13, writing something
-like `app/example/filehandling.tsx` will generate a route pointing to `... /example` instead
-of `.../example/page`, and nesting successive folders will create a route with those
-folders: `app/example1/example2/example3/filehandling.tsx` has the
-route `... /example1/example2/example3/`.
-
-For a better explanation of how this works, please observe the browse
-endpoint: `app/(endpoints)/browse/[plotKey]/[plotNum]/filehandling.tsx`
-In order from left to right, please note the following points of interest:
-
-- `(endpoints)`: wrapping a folder in parentheses allows for better organization w/o using the
- wrapped folder name in the path. For example, accessing the Browse page locally does not require
- adding `/endpoints/` to the URL
-- `[plotKey]`: this is the first required variable when accessing this endpoint -- you will have to
- add some string `plotKey` to the end of the URL: `.../browse/[your plot key]` in order to
- successfully view the page.
- - wrapping a folder in `[]` will designate that folder as a **required** dynamic parameter
- - wrapping in `[...folderName]` designates `folderName` as a catch-all route. All following
- values after `folderName` (i.e., `.../a/b` will return `folderName = [a, b]` )
- - wrapping in `[[...folderName]]` designates `folderName` as an _optional_ catch-all route. As
- expected, all values for/after `folderName` will be returned as part of the dynamic route,
- but `undefined` will also be returned if no value is entered at all (instead of a 404 error)
-- `[plotNum]`: second required variable when accessing this endpoint - your resulting endpoint will
- look like (example) `http://localhost:3000/browse/plotKey/plotNum`.
-
----
-
-### Release Notes (v0.1.0):
-
-- endpoints have been added and routed to require a plot key/number combination for access
- - initial state has been converted to new `Plot {key: 'none', num: 0}` instead of `''`
-- MUI JoyUI has been partially implemented as a replacement for MaterialUI. However, due to time
- limitations, MaterialUI has still been incorporated into converted sections from ForestGeoHack
- - The current plan is to solely implement either NextUI or ChakraUI instead of either of these
- options, and future updates will include this information.
-- `SelectPlotProps` has been removed and replaced with NextJS dynamic routing (each endpoint will
- dynamically retrieve plot information). Endpoints have been updated to reflect dynamic param-based
- retrieval
- - The navigation bar has been updated to use useEffect to push live endpoint updates when the
- plot is changed (if you are at an endpoint and the plot is changed, the page will be reloaded
- to reflect that)
-- New components/moved-over information:
- - `Fileuploadcomponents` --> css code has been udpated to be dark theme-friendly
- - `FileList` --> moved over
- - `Loginlogout` --> created component, login/logout process has been relegated to avatar icon
- dropdown menu
- - `Plotselection` --> partially created from SelectPlot, changed to utilize dynamic
- routing/selection instead of requiring a new dropdown in each page
+## Setting up for Local Development
+
+This project uses NextJS v14(+), and server interactions and setup are handled through their interface. Please note
+that for local development, you will **not** be able to use the NextJS-provided `next start` command due to the way that
+the application is packaged for Azure deployment. Instead, please use the `next dev` command to start the local
+development server to use the application.
+
+> Note: the development server compiles and loads the application in real time as you interact with the website.
+> Accordingly, **load times for API endpoints and other components will be much longer than the actual site's.** Please
+> do not use these load times as an indicator of load times within the deployed application instance!
+
+### Production vs Development Branches
+
+The `main` branch of this repository is the production branch, and the `forestgeo-app-development` is the deployed
+development branch. When adding new features or making changes to the application, please branch off of the
+`forestgeo-app-development` branch instead of `main`. The production branch should not be used as a baseline and should
+only be modified via approved PRs.
+
+### Azure-side Setup Requirements
+
+The application maintains a live connection to an Azure Storage and a Azure MySQL server instance. Before you can use
+the application, please ensure that you work with a ForestGEO administrator to:
+
+1. add your email address to the managing database,
+2. provide you with a role and,
+3. assign the testing schemas to your account
+
+> It is critical that live sites actively being used by researchers are not mistakenly modified or damaged!
+
+### Setting up the Environment
+
+> **Note:** The following instructions assume that you have `NodeJS` and `npm` installed on your local machine.
+
+After cloning the repository, please run `npm install` to install required dependencies.
+
+The application requires a set of environmental variables stored in a `.env.local` file in order to run locally. Please
+contact a repository administrator to request access to the key-vault storage, named `forestgeo-app-key-vault`. Once you
+can access it, please retrieve all noted secrets in the repository and place them in your `.env.local` file. The name of
+the secret corresponds to the name of the environmental variable. Please use the following example as a template:
+
+Let's assume that the keyvault storage has a secret named `EXAMPLE-SECRET`, with a corresponding value of `1234`.
+In order to use this secret in your local environment, add it to your `.env.local` file like this:
+
+`EXAMPLE_SECRET=1234`
+
+Please note that the name of the secret in the keyvault uses **hyphens**, while the name of the environmental variable
+uses **underscores**. Please ensure you **replace all hyphens with underscores** when adding the secret to your
+`.env.local` file.
+
+Once you have successfully created your `.env.local` file, please run `npm run dev` to start the local development
+server.
+
+> **Ensure that you have port 3000 open and available on your local machine before starting the server!**
diff --git a/frontend/app/(hub)/dashboard/page.tsx b/frontend/app/(hub)/dashboard/page.tsx
index 1037c43f..630bf1ca 100644
--- a/frontend/app/(hub)/dashboard/page.tsx
+++ b/frontend/app/(hub)/dashboard/page.tsx
@@ -53,7 +53,6 @@ export default function DashboardPage() {
try {
setIsLoading(true);
- // Check if the required data is available, otherwise return a padded array
if (!currentSite || !currentPlot || !currentCensus) {
setChangelogHistory(Array(5).fill({}));
return;
@@ -161,14 +160,6 @@ export default function DashboardPage() {
-
-
-
- Plot-Species List - See existing taxonomy information for stems in your plot and census here.{' '}
- Requires a census.
-
-
-
diff --git a/frontend/app/(hub)/fixeddatainput/stemtaxonomies/error.tsx b/frontend/app/(hub)/fixeddatainput/stemtaxonomies/error.tsx
deleted file mode 100644
index eaaea954..00000000
--- a/frontend/app/(hub)/fixeddatainput/stemtaxonomies/error.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-'use client';
-
-import React from 'react';
-import { Box, Button, Typography } from '@mui/joy';
-
-const ErrorPage = ({ error, reset }: { error: Error; reset: () => void }) => {
- return (
-
- Something went wrong - Plot-Species List
- {error.message}
-
-
- );
-};
-
-export default ErrorPage;
diff --git a/frontend/app/(hub)/fixeddatainput/stemtaxonomies/page.tsx b/frontend/app/(hub)/fixeddatainput/stemtaxonomies/page.tsx
deleted file mode 100644
index 74cd3733..00000000
--- a/frontend/app/(hub)/fixeddatainput/stemtaxonomies/page.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-'use client';
-
-import IsolatedStemTaxonomiesViewDataGrid from '@/components/datagrids/applications/isolated/isolatedstemtaxonomiesviewdatagrid';
-
-export default function StemTaxonomiesPage() {
- return ;
-}
diff --git a/frontend/app/(hub)/layout.tsx b/frontend/app/(hub)/layout.tsx
index 5e917153..b299935e 100644
--- a/frontend/app/(hub)/layout.tsx
+++ b/frontend/app/(hub)/layout.tsx
@@ -7,7 +7,6 @@ import dynamic from 'next/dynamic';
import { Box, IconButton, Stack, Typography } from '@mui/joy';
import Divider from '@mui/joy/Divider';
import { useLoading } from '@/app/contexts/loadingprovider';
-import { getAllSchemas } from '@/components/processors/processorhelperfunctions';
import { useOrgCensusContext, usePlotContext, useSiteContext } from '@/app/contexts/userselectionprovider';
import { useOrgCensusListDispatch, usePlotListDispatch, useQuadratListDispatch, useSiteListDispatch } from '@/app/contexts/listselectionprovider';
import { getEndpointHeaderName, siteConfig } from '@/config/macros/siteconfigs';
@@ -96,7 +95,7 @@ export default function HubLayout({ children }: { children: React.ReactNode }) {
}
}
} catch (e: any) {
- const allsites = await getAllSchemas();
+ const allsites = await (await fetch(`/api/fetchall/sites?schema=${currentSite?.schemaName ?? ''}`)).json();
if (siteListDispatch) await siteListDispatch({ siteList: allsites });
} finally {
setLoading(false);
diff --git a/frontend/app/api/auth/[[...nextauth]]/route.ts b/frontend/app/api/auth/[[...nextauth]]/route.ts
index b19e1a03..519747a3 100644
--- a/frontend/app/api/auth/[[...nextauth]]/route.ts
+++ b/frontend/app/api/auth/[[...nextauth]]/route.ts
@@ -1,9 +1,9 @@
import NextAuth, { AzureADProfile } from 'next-auth';
import AzureADProvider from 'next-auth/providers/azure-ad';
-import { getAllowedSchemas, getAllSchemas } from '@/components/processors/processorhelperfunctions';
import { UserAuthRoles } from '@/config/macros';
-import { SitesRDS } from '@/config/sqlrdsdefinitions/zones';
-import { getConn, runQuery } from '@/components/processors/processormacros';
+import { SitesRDS, SitesResult } from '@/config/sqlrdsdefinitions/zones';
+import ConnectionManager from '@/config/connectionmanager';
+import MapperFactory from '@/config/datamapper';
const handler = NextAuth({
secret: process.env.NEXTAUTH_SECRET!,
@@ -28,11 +28,11 @@ const handler = NextAuth({
return false; // Email is not a valid string, abort sign-in
}
if (userEmail) {
- let conn, emailVerified, userStatus;
+ const connectionManager = new ConnectionManager();
+ let emailVerified, userStatus, userID;
try {
- conn = await getConn();
- const query = `SELECT UserStatus FROM catalog.users WHERE Email = '${userEmail}' LIMIT 1`;
- const results = await runQuery(conn, query);
+ const query = `SELECT UserID, UserStatus FROM catalog.users WHERE Email = '${userEmail}' LIMIT 1`;
+ const results = await connectionManager.executeQuery(query);
// emailVerified is true if there is at least one result
emailVerified = results.length > 0;
@@ -41,17 +41,21 @@ const handler = NextAuth({
return false;
}
userStatus = results[0].UserStatus;
+ userID = results[0].UserID;
} catch (e: any) {
console.error('Error fetching user status:', e);
throw new Error('Failed to fetch user status.');
- } finally {
- if (conn) conn.release();
}
user.userStatus = userStatus as UserAuthRoles;
user.email = userEmail;
// console.log('getting all sites: ');
- const allSites = await getAllSchemas();
- const allowedSites = await getAllowedSchemas(userEmail);
+ const allSites = MapperFactory.getMapper('sites').mapData(await connectionManager.executeQuery(`SELECT * FROM catalog.sites`));
+ const allowedSites = MapperFactory.getMapper('sites').mapData(
+ await connectionManager.executeQuery(
+ `SELECT s.* FROM catalog.sites AS s JOIN catalog.usersiterelations AS usr ON s.SiteID = usr.SiteID WHERE usr.UserID = ?`,
+ [userID]
+ )
+ );
if (!allowedSites || !allSites) {
console.error('User does not have any allowed sites.');
return false;
diff --git a/frontend/app/api/bulkcrud/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/bulkcrud/[dataType]/[[...slugs]]/route.ts
index 9651d2f5..f8936e95 100644
--- a/frontend/app/api/bulkcrud/[dataType]/[[...slugs]]/route.ts
+++ b/frontend/app/api/bulkcrud/[dataType]/[[...slugs]]/route.ts
@@ -1,10 +1,9 @@
// bulk data CRUD flow API endpoint -- intended to allow multiline interactions and bulk updates via datagrid
import { NextRequest, NextResponse } from 'next/server';
import { FileRowSet } from '@/config/macros/formdetails';
-import { PoolConnection } from 'mysql2/promise';
-import { getConn, InsertUpdateProcessingProps } from '@/components/processors/processormacros';
import { insertOrUpdate } from '@/components/processors/processorhelperfunctions';
-import { HTTPResponses } from '@/config/macros';
+import { HTTPResponses, InsertUpdateProcessingProps } from '@/config/macros';
+import ConnectionManager from '@/config/connectionmanager';
export async function POST(request: NextRequest, { params }: { params: { dataType: string; slugs?: string[] } }) {
const { dataType, slugs } = params;
@@ -20,15 +19,15 @@ export async function POST(request: NextRequest, { params }: { params: { dataTyp
return new NextResponse('No rows provided', { status: 400 });
}
console.log('rows produced: ', rows);
- let conn: PoolConnection | null = null;
+ const connectionManager = new ConnectionManager();
try {
- conn = await getConn();
for (const rowID in rows) {
+ await connectionManager.beginTransaction();
const rowData = rows[rowID];
console.log('rowData obtained: ', rowData);
const props: InsertUpdateProcessingProps = {
schema,
- connection: conn,
+ connectionManager: connectionManager,
formType: dataType,
rowData,
plotID,
@@ -38,8 +37,11 @@ export async function POST(request: NextRequest, { params }: { params: { dataTyp
};
console.log('assembled props: ', props);
await insertOrUpdate(props);
+ await connectionManager.commitTransaction();
}
+ return new NextResponse(JSON.stringify({ message: 'Insert to SQL successful' }), { status: HTTPResponses.OK });
} catch (e: any) {
+ await connectionManager.rollbackTransaction();
return new NextResponse(
JSON.stringify({
responseMessage: `Failure in connecting to SQL with ${e.message}`,
@@ -48,7 +50,6 @@ export async function POST(request: NextRequest, { params }: { params: { dataTyp
{ status: HTTPResponses.INTERNAL_SERVER_ERROR }
);
} finally {
- if (conn) conn.release();
+ await connectionManager.closeConnection();
}
- return new NextResponse(JSON.stringify({ message: 'Insert to SQL successful' }), { status: HTTPResponses.OK });
}
diff --git a/frontend/app/api/catalog/[firstName]/[lastName]/route.ts b/frontend/app/api/catalog/[firstName]/[lastName]/route.ts
index 220a2898..773769d1 100644
--- a/frontend/app/api/catalog/[firstName]/[lastName]/route.ts
+++ b/frontend/app/api/catalog/[firstName]/[lastName]/route.ts
@@ -1,18 +1,16 @@
import { NextRequest, NextResponse } from 'next/server';
-import { PoolConnection } from 'mysql2/promise';
-import { getConn, runQuery } from '@/components/processors/processormacros';
import { HTTPResponses } from '@/config/macros';
+import ConnectionManager from '@/config/connectionmanager';
export async function GET(_request: NextRequest, { params }: { params: { firstName: string; lastName: string } }) {
const { firstName, lastName } = params;
if (!firstName || !lastName) throw new Error('no first or last name provided!');
- let conn: PoolConnection | null = null;
+ const connectionManager = new ConnectionManager();
try {
- conn = await getConn();
const query = `SELECT UserID FROM catalog.users WHERE FirstName = ? AND LastName = ?;`;
- const results = await runQuery(conn, query, [firstName, lastName]);
+ const results = await connectionManager.executeQuery(query, [firstName, lastName]);
if (results.length === 0) {
throw new Error('User not found');
}
@@ -21,6 +19,6 @@ export async function GET(_request: NextRequest, { params }: { params: { firstNa
console.error('Error in GET request:', e.message);
return new NextResponse(JSON.stringify({ error: e.message }), { status: HTTPResponses.INTERNAL_SERVER_ERROR });
} finally {
- if (conn) conn.release();
+ await connectionManager.closeConnection();
}
}
diff --git a/frontend/app/api/changelog/overview/[changelogType]/[[...options]]/route.ts b/frontend/app/api/changelog/overview/[changelogType]/[[...options]]/route.ts
index 12b814eb..7eaba2fd 100644
--- a/frontend/app/api/changelog/overview/[changelogType]/[[...options]]/route.ts
+++ b/frontend/app/api/changelog/overview/[changelogType]/[[...options]]/route.ts
@@ -1,8 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
-import { PoolConnection } from 'mysql2/promise';
-import { getConn, runQuery } from '@/components/processors/processormacros';
import { HTTPResponses } from '@/config/macros';
import MapperFactory from '@/config/datamapper';
+import ConnectionManager from '@/config/connectionmanager';
export async function GET(request: NextRequest, { params }: { params: { changelogType: string; options?: string[] } }) {
const schema = request.nextUrl.searchParams.get('schema');
@@ -13,9 +12,8 @@ export async function GET(request: NextRequest, { params }: { params: { changelo
const [plotIDParam, pcnParam] = params.options;
const plotID = parseInt(plotIDParam);
const pcn = parseInt(pcnParam);
- let conn: PoolConnection | null = null;
+ const connectionManager = new ConnectionManager();
try {
- conn = await getConn();
let query = ``;
switch (params.changelogType) {
case 'unifiedchangelog':
@@ -34,13 +32,13 @@ export async function GET(request: NextRequest, { params }: { params: { changelo
break;
}
- const results = await runQuery(conn, query, [plotID, plotID, pcn]);
+ const results = await connectionManager.executeQuery(query, [plotID, plotID, pcn]);
return new NextResponse(results.length > 0 ? JSON.stringify(MapperFactory.getMapper(params.changelogType).mapData(results)) : null, {
status: HTTPResponses.OK
});
} catch (e: any) {
throw new Error('SQL query failed: ' + e.message);
} finally {
- if (conn) conn.release();
+ await connectionManager.closeConnection();
}
}
diff --git a/frontend/app/api/cmprevalidation/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/cmprevalidation/[dataType]/[[...slugs]]/route.ts
index 963accc9..94d49a3a 100644
--- a/frontend/app/api/cmprevalidation/[dataType]/[[...slugs]]/route.ts
+++ b/frontend/app/api/cmprevalidation/[dataType]/[[...slugs]]/route.ts
@@ -1,7 +1,6 @@
-import { getConn, runQuery } from '@/components/processors/processormacros';
-import { PoolConnection } from 'mysql2/promise';
import { NextRequest, NextResponse } from 'next/server';
import { HTTPResponses } from '@/config/macros';
+import ConnectionManager from '@/config/connectionmanager';
// datatype: table name
// expecting 1) schema 2) plotID 3) plotCensusNumber
@@ -20,16 +19,13 @@ export async function GET(_request: NextRequest, { params }: { params: { dataTyp
)
throw new Error('incorrect slugs provided');
- let connection: PoolConnection | null = null;
+ const connection = new ConnectionManager();
try {
- connection = await getConn();
-
switch (params.dataType) {
case 'attributes':
case 'species':
const baseQuery = `SELECT 1 FROM ${schema}.${params.dataType} LIMIT 1`; // Check if the table has any row
- const baseResults = await runQuery(connection, baseQuery);
- if (connection) connection.release();
+ const baseResults = await connection.executeQuery(baseQuery);
if (baseResults.length === 0)
return new NextResponse(null, {
status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE
@@ -37,8 +33,7 @@ export async function GET(_request: NextRequest, { params }: { params: { dataTyp
break;
case 'personnel':
const pQuery = `SELECT 1 FROM ${schema}.personnel WHERE CensusID IN (SELECT CensusID from ${schema}.census WHERE PlotID = ${plotID} AND PlotCensusNumber = ${plotCensusNumber})`; // Check if the table has any row
- const pResults = await runQuery(connection, pQuery);
- if (connection) connection.release();
+ const pResults = await connection.executeQuery(pQuery);
if (pResults.length === 0)
return new NextResponse(null, {
status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE
@@ -49,8 +44,7 @@ export async function GET(_request: NextRequest, { params }: { params: { dataTyp
JOIN ${schema}.censusquadrat cq ON cq.QuadratID = q.QuadratID
JOIN ${schema}.census c ON cq.CensusID = c.CensusID
WHERE q.PlotID = ${plotID} AND c.PlotCensusNumber = ${plotCensusNumber} LIMIT 1`;
- const results = await runQuery(connection, query);
- if (connection) connection.release();
+ const results = await connection.executeQuery(query);
if (results.length === 0)
return new NextResponse(null, {
status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE
@@ -61,26 +55,12 @@ export async function GET(_request: NextRequest, { params }: { params: { dataTyp
JOIN ${schema}.census c ON C.CensusID = cm.CensusID
JOIN ${schema}.plots p ON p.PlotID = c.PlotID
WHERE p.PlotID = ${plotID} AND c.PlotCensusNumber = ${plotCensusNumber} LIMIT 1`;
- const pvResults = await runQuery(connection, pvQuery);
- if (connection) connection.release();
+ const pvResults = await connection.executeQuery(pvQuery);
if (pvResults.length === 0)
return new NextResponse(null, {
status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE
});
break;
- // case 'subquadrats':
- // const subquadratsQuery = `SELECT 1
- // FROM ${schema}.${params.dataType} s
- // JOIN ${schema}.quadrats q ON s.QuadratID = q.QuadratID
- // WHERE q.PlotID = ${plotID}
- // AND q.CensusID IN (SELECT CensusID from ${schema}.census WHERE PlotID = ${plotID} AND PlotCensusNumber = ${plotCensusNumber}) LIMIT 1`;
- // const subquadratsResults = await runQuery(connection, subquadratsQuery);
- // if (connection) connection.release();
- // if (subquadratsResults.length === 0)
- // return new NextResponse(null, {
- // status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE
- // });
- // break;
case 'quadratpersonnel':
// Validation for quadrats table
const quadratsQuery = `SELECT 1
@@ -90,8 +70,7 @@ export async function GET(_request: NextRequest, { params }: { params: { dataTyp
JOIN ${schema}.personnel p ON p.CensusID = c.CensusID
WHERE q.PlotID = ${plotID}
AND c.PlotCensusNumber = ${plotCensusNumber} LIMIT 1`;
- const quadratsResults = await runQuery(connection, quadratsQuery);
- if (connection) connection.release();
+ const quadratsResults = await connection.executeQuery(quadratsQuery);
if (quadratsResults.length === 0)
return new NextResponse(null, {
status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE
@@ -99,8 +78,7 @@ export async function GET(_request: NextRequest, { params }: { params: { dataTyp
// Validation for personnel table
const personnelQuery = `SELECT 1 FROM ${schema}.personnel LIMIT 1`;
- const personnelResults = await runQuery(connection, personnelQuery);
- if (connection) connection.release();
+ const personnelResults = await connection.executeQuery(personnelQuery);
if (personnelResults.length === 0)
return new NextResponse(null, {
status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE
@@ -113,7 +91,6 @@ export async function GET(_request: NextRequest, { params }: { params: { dataTyp
});
}
// If all conditions are satisfied
- connection.release();
return new NextResponse(null, { status: HTTPResponses.OK });
} catch (e: any) {
console.error(e);
@@ -121,6 +98,6 @@ export async function GET(_request: NextRequest, { params }: { params: { dataTyp
status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE
});
} finally {
- if (connection) connection.release();
+ await connection.closeConnection();
}
}
diff --git a/frontend/app/api/details/cmid/route.ts b/frontend/app/api/details/cmid/route.ts
index c9f169f2..6454d4cb 100644
--- a/frontend/app/api/details/cmid/route.ts
+++ b/frontend/app/api/details/cmid/route.ts
@@ -1,15 +1,13 @@
import { NextRequest, NextResponse } from 'next/server';
-import { getConn, runQuery } from '@/components/processors/processormacros';
-import { PoolConnection } from 'mysql2/promise';
import { HTTPResponses } from '@/config/macros';
+import ConnectionManager from '@/config/connectionmanager';
export async function GET(request: NextRequest) {
const cmID = parseInt(request.nextUrl.searchParams.get('cmid')!);
const schema = request.nextUrl.searchParams.get('schema');
if (!schema) throw new Error('no schema variable provided!');
- let conn: PoolConnection | null = null;
+ const connectionManager = new ConnectionManager();
try {
- conn = await getConn();
const query = `
SELECT
cm.CoreMeasurementID,
@@ -35,7 +33,7 @@ export async function GET(request: NextRequest) {
${schema}.census c ON cm.CensusID = c.CensusID
WHERE
cm.CoreMeasurementID = ?;`;
- const results = await runQuery(conn, query, [cmID]);
+ const results = await connectionManager.executeQuery(query, [cmID]);
return new NextResponse(
JSON.stringify(
results.map((row: any) => ({
@@ -51,6 +49,6 @@ export async function GET(request: NextRequest) {
} catch (error: any) {
throw new Error('SQL query failed: ' + error.message);
} finally {
- if (conn) conn.release();
+ await connectionManager.closeConnection();
}
}
diff --git a/frontend/app/api/fetchall/[[...slugs]]/route.ts b/frontend/app/api/fetchall/[[...slugs]]/route.ts
index bbaf8c6f..61dea390 100644
--- a/frontend/app/api/fetchall/[[...slugs]]/route.ts
+++ b/frontend/app/api/fetchall/[[...slugs]]/route.ts
@@ -1,8 +1,7 @@
-import { PoolConnection } from 'mysql2/promise';
import { NextRequest, NextResponse } from 'next/server';
-import { getConn, runQuery } from '@/components/processors/processormacros';
import MapperFactory from '@/config/datamapper';
import { HTTPResponses } from '@/config/macros';
+import ConnectionManager from '@/config/connectionmanager';
const buildQuery = (schema: string, fetchType: string, plotID?: string, plotCensusNumber?: string, quadratID?: string): string => {
if (fetchType === 'plots') {
@@ -59,16 +58,15 @@ export async function GET(request: NextRequest, { params }: { params: { slugs?:
console.log('fetchall --> slugs provided: fetchType: ', dataType, 'plotID: ', plotID, 'plotcensusnumber: ', plotCensusNumber, 'quadratID: ', quadratID);
const query = buildQuery(schema, dataType, plotID, plotCensusNumber, quadratID);
- let conn: PoolConnection | null = null;
+ const connectionManager = new ConnectionManager();
try {
- conn = await getConn();
- const results = await runQuery(conn, query);
+ const results = await connectionManager.executeQuery(query);
return new NextResponse(JSON.stringify(MapperFactory.getMapper(dataType).mapData(results)), { status: HTTPResponses.OK });
} catch (error) {
console.error('Error:', error);
throw new Error('Call failed');
} finally {
- if (conn) conn.release();
+ await connectionManager.closeConnection();
}
}
diff --git a/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts
index 0e37856f..1aadb314 100644
--- a/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts
+++ b/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts
@@ -1,15 +1,10 @@
-import { getConn, runQuery } from '@/components/processors/processormacros';
import MapperFactory from '@/config/datamapper';
import { handleError } from '@/utils/errorhandler';
-import { format, PoolConnection } from 'mysql2/promise';
+import { format } from 'mysql2/promise';
import { NextRequest, NextResponse } from 'next/server';
-import {
- AllTaxonomiesViewQueryConfig,
- handleDeleteForSlices,
- handleUpsertForSlices,
- StemTaxonomiesViewQueryConfig
-} from '@/components/processors/processorhelperfunctions';
-import { HTTPResponses } from '@/config/macros'; // slugs SHOULD CONTAIN AT MINIMUM: schema, page, pageSize, plotID, plotCensusNumber, (optional) quadratID, (optional) speciesID
+import { AllTaxonomiesViewQueryConfig, handleDeleteForSlices, handleUpsertForSlices } from '@/components/processors/processorhelperfunctions';
+import { HTTPResponses } from '@/config/macros';
+import ConnectionManager from '@/config/connectionmanager'; // slugs SHOULD CONTAIN AT MINIMUM: schema, page, pageSize, plotID, plotCensusNumber, (optional) quadratID, (optional) speciesID
// slugs SHOULD CONTAIN AT MINIMUM: schema, page, pageSize, plotID, plotCensusNumber, (optional) quadratID, (optional) speciesID
export async function GET(
@@ -19,24 +14,20 @@ export async function GET(
}: {
params: { dataType: string; slugs?: string[] };
}
-): Promise> {
+): 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;
- let conn: PoolConnection | null = null;
- let updatedMeasurementsExist = false;
- let censusIDs;
- let pastCensusIDs: string | any[];
+
+ const connectionManager = new ConnectionManager();
try {
- conn = await getConn();
let paginatedQuery = ``;
const queryParams: any[] = [];
@@ -55,7 +46,6 @@ export async function GET(
case 'species':
case 'stems':
case 'alltaxonomiesview':
- case 'stemtaxonomiesview':
case 'quadratpersonnel':
case 'sitespecificvalidations':
case 'roles':
@@ -99,36 +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 'subquadrats':
- // if (!quadratID || quadratID === 0) {
- // throw new Error('QuadratID must be provided as part of slug fetch query, referenced fixeddata slug route');
- // }
- // paginatedQuery = `
- // SELECT SQL_CALC_FOUND_ROWS s.*
- // FROM ${schema}.subquadrats s
- // JOIN ${schema}.quadrats q ON s.QuadratID = q.QuadratID
- // JOIN ${schema}.census c ON q.CensusID = c.CensusID
- // WHERE q.QuadratID = ?
- // AND q.PlotID = ?
- // AND c.PlotID = ?
- // AND c.PlotCensusNumber = ? LIMIT ?, ?;`;
- // queryParams.push(quadratID, plotID, plotID, plotCensusNumber, page * pageSize, pageSize);
- // break;
case 'census':
paginatedQuery = `
SELECT SQL_CALC_FOUND_ROWS *
@@ -136,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 runQuery(conn, 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}`);
}
@@ -179,44 +104,25 @@ export async function GET(
if (paginatedQuery.match(/\?/g)?.length !== queryParams.length) {
throw new Error('Mismatch between query placeholders and parameters');
}
-
- const paginatedResults = await runQuery(conn, format(paginatedQuery, queryParams));
+ const paginatedResults = await connectionManager.executeQuery(format(paginatedQuery, queryParams));
const totalRowsQuery = 'SELECT FOUND_ROWS() as totalRows';
- const totalRowsResult = await runQuery(conn, totalRowsQuery);
+ 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
- }),
- { status: HTTPResponses.OK }
- );
- } else {
- return new NextResponse(
- JSON.stringify({
- output: MapperFactory.getMapper(params.dataType).mapData(paginatedResults),
- deprecated: undefined,
- totalCount: totalRows
- }),
- { 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) {
- if (conn) await conn.rollback();
throw new Error(error);
} finally {
- if (conn) conn.release();
+ await connectionManager.closeConnection();
}
}
@@ -229,13 +135,12 @@ export async function POST(request: NextRequest, { params }: { params: { dataTyp
const plotID = plotIDParam ? parseInt(plotIDParam) : undefined;
const censusID = censusIDParam ? parseInt(censusIDParam) : undefined;
- let conn: PoolConnection | null = null;
+ const connectionManager = new ConnectionManager();
const { newRow } = await request.json();
let insertIDs: { [key: string]: number } = {};
try {
- conn = await getConn();
- await conn.beginTransaction();
+ await connectionManager.beginTransaction();
if (Object.keys(newRow).includes('isNew')) delete newRow.isNew;
@@ -249,20 +154,17 @@ export async function POST(request: NextRequest, { params }: { params: { dataTyp
case 'alltaxonomiesview':
queryConfig = AllTaxonomiesViewQueryConfig;
break;
- case 'stemtaxonomiesview':
- queryConfig = StemTaxonomiesViewQueryConfig;
- break;
default:
throw new Error('Incorrect view call');
}
// Use handleUpsertForSlices and retrieve the insert IDs
- insertIDs = await handleUpsertForSlices(conn, schema, newRowData, queryConfig);
+ 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 runQuery(conn, insertQuery);
+ const results = await connectionManager.executeQuery(insertQuery);
insertIDs = { attributes: results.insertId }; // Standardize output with table name as key
}
// Handle all other cases
@@ -270,24 +172,22 @@ export async function POST(request: NextRequest, { params }: { params: { dataTyp
delete newRowData[demappedGridID];
if (params.dataType === 'plots') delete newRowData.NumQuadrats;
const insertQuery = format('INSERT INTO ?? SET ?', [`${schema}.${params.dataType}`, newRowData]);
- const results = await runQuery(conn, insertQuery);
+ 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 runQuery(conn, cqQuery);
+ const results = await connectionManager.executeQuery(cqQuery);
if (results.length === 0) throw new Error('Error inserting to censusquadrats');
}
}
- // Commit the transaction and return the standardized response
- await conn.commit();
return NextResponse.json({ message: 'Insert successful', createdIDs: insertIDs }, { status: HTTPResponses.OK });
} catch (error: any) {
- return handleError(error, conn, newRow);
+ return handleError(error, connectionManager, newRow);
} finally {
- if (conn) conn.release();
+ await connectionManager.closeConnection();
}
}
@@ -297,31 +197,27 @@ export async function PATCH(request: NextRequest, { params }: { params: { dataTy
const [schema, gridID] = params.slugs;
if (!schema || !gridID) throw new Error('no schema or gridID provided');
- let conn: PoolConnection | null = null;
+ 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 {
- conn = await getConn();
- await conn.beginTransaction();
+ await connectionManager.beginTransaction();
// Handle views with handleUpsertForSlices (applies to both insert and update logic)
- if (['alltaxonomiesview', 'stemtaxonomiesview'].includes(params.dataType)) {
+ if (params.dataType === 'alltaxonomiesview') {
let queryConfig;
switch (params.dataType) {
case 'alltaxonomiesview':
queryConfig = AllTaxonomiesViewQueryConfig;
break;
- case 'stemtaxonomiesview':
- queryConfig = StemTaxonomiesViewQueryConfig;
- break;
default:
throw new Error('Incorrect view call');
}
// Use handleUpsertForSlices for update operations as well (updates where needed)
- updateIDs = await handleUpsertForSlices(conn, schema, newRow, queryConfig);
+ updateIDs = await handleUpsertForSlices(connectionManager, schema, newRow, queryConfig);
}
// Handle non-view table updates
@@ -338,21 +234,17 @@ export async function PATCH(request: NextRequest, { params }: { params: { dataTy
);
// Execute the UPDATE query
- await runQuery(conn, updateQuery);
+ await connectionManager.executeQuery(updateQuery);
// For non-view tables, standardize the response format
updateIDs = { [params.dataType]: gridIDKey };
}
- // Commit the transaction
- await conn.commit();
-
- // Return a standardized response with updated IDs
return NextResponse.json({ message: 'Update successful', updatedIDs: updateIDs }, { status: HTTPResponses.OK });
} catch (error: any) {
- return handleError(error, conn, newRow);
+ return handleError(error, connectionManager, newRow);
} finally {
- if (conn) conn.release();
+ await connectionManager.closeConnection();
}
}
@@ -362,15 +254,14 @@ export async function DELETE(request: NextRequest, { params }: { params: { dataT
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');
- let conn: PoolConnection | null = null;
+ const connectionManager = new ConnectionManager();
const demappedGridID = gridID.charAt(0).toUpperCase() + gridID.substring(1);
const { newRow } = await request.json();
try {
- conn = await getConn();
- await conn.beginTransaction();
+ await connectionManager.beginTransaction();
// Handle deletion for views
- if (['alltaxonomiesview', 'stemtaxonomiesview', 'measurementssummaryview'].includes(params.dataType)) {
+ if (['alltaxonomiesview', 'measurementssummaryview'].includes(params.dataType)) {
const deleteRowData = MapperFactory.getMapper(params.dataType).demapData([newRow])[0];
// Prepare query configuration based on view
@@ -379,17 +270,12 @@ export async function DELETE(request: NextRequest, { params }: { params: { dataT
case 'alltaxonomiesview':
queryConfig = AllTaxonomiesViewQueryConfig;
break;
- case 'stemtaxonomiesview':
- queryConfig = StemTaxonomiesViewQueryConfig;
- break;
default:
throw new Error('Incorrect view call');
}
// Use handleDeleteForSlices for handling deletion, taking foreign key constraints into account
- await handleDeleteForSlices(conn, schema, deleteRowData, queryConfig);
-
- await conn.commit();
+ await handleDeleteForSlices(connectionManager, schema, deleteRowData, queryConfig);
return NextResponse.json({ message: 'Delete successful' }, { status: HTTPResponses.OK });
}
@@ -399,14 +285,14 @@ export async function DELETE(request: NextRequest, { params }: { params: { dataT
// 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 runQuery(conn, qDeleteQuery);
+ await connectionManager.executeQuery(qDeleteQuery);
}
const deleteQuery = format(`DELETE FROM ?? WHERE ?? = ?`, [`${schema}.${params.dataType}`, demappedGridID, gridIDKey]);
- await runQuery(conn, deleteQuery);
- await conn.commit();
+ 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(
@@ -416,9 +302,8 @@ export async function DELETE(request: NextRequest, { params }: { params: { dataT
},
{ status: HTTPResponses.FOREIGN_KEY_CONFLICT }
);
- }
- return handleError(error, conn, newRow);
+ } else return handleError(error, connectionManager, newRow);
} finally {
- if (conn) conn.release();
+ await connectionManager.closeConnection();
}
}
diff --git a/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts
new file mode 100644
index 00000000..0403225b
--- /dev/null
+++ b/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts
@@ -0,0 +1,478 @@
+import { NextRequest, NextResponse } from 'next/server';
+import ConnectionManager from '@/config/connectionmanager';
+import { escape } from 'mysql2';
+import { format } from 'mysql2/promise';
+import MapperFactory from '@/config/datamapper';
+import { HTTPResponses } from '@/config/macros';
+import { GridFilterItem, GridFilterModel } from '@mui/x-data-grid';
+import { handleError } from '@/utils/errorhandler';
+import { AllTaxonomiesViewQueryConfig, handleDeleteForSlices, handleUpsertForSlices } from '@/components/processors/processorhelperfunctions';
+
+type VisibleFilter = 'valid' | 'errors' | 'pending';
+
+interface ExtendedGridFilterModel extends GridFilterModel {
+ visible: VisibleFilter[];
+}
+
+export async function POST(
+ request: NextRequest,
+ {
+ params
+ }: {
+ params: { dataType: string; slugs?: string[] };
+ }
+) {
+ // trying to ensure that system correctly retains edit/add functionality -- not necessarily needed currently but better safe than sorry
+ const body = await request.json();
+ 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 {
+ 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();
+ }
+ } 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 '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.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 = ?
+ ${
+ 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 'coremeasurements':
+ if (filterModel.quickFilterValues) searchStub = buildSearchStub(columns, filterModel.quickFilterValues, 'pdt');
+ if (filterModel.items) filterStub = buildFilterModelStub(filterModel, 'pdt');
+
+ 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 = ? 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 = `
+ 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(', ')})
+ ${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}`);
+ }
+ paginatedQuery += ` LIMIT ?, ?;`;
+
+ if (paginatedQuery.match(/\?/g)?.length !== queryParams.length) {
+ 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));
+
+ 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();
+ }
+ }
+}
+
+// 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();
+
+ // 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);
+ }
+
+ // 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) {
+ 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/formdownload/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/formdownload/[dataType]/[[...slugs]]/route.ts
index 0f3b83d6..0a238aca 100644
--- a/frontend/app/api/formdownload/[dataType]/[[...slugs]]/route.ts
+++ b/frontend/app/api/formdownload/[dataType]/[[...slugs]]/route.ts
@@ -1,9 +1,8 @@
import { NextRequest, NextResponse } from 'next/server';
-import { PoolConnection } from 'mysql2/promise';
-import { getConn, runQuery } from '@/components/processors/processormacros';
import MapperFactory from '@/config/datamapper';
import { AttributesRDS } from '@/config/sqlrdsdefinitions/core';
import { HTTPResponses } from '@/config/macros';
+import ConnectionManager from '@/config/connectionmanager';
export async function GET(_request: NextRequest, { params }: { params: { dataType: string; slugs?: string[] } }) {
const { dataType, slugs } = params;
@@ -14,17 +13,16 @@ export async function GET(_request: NextRequest, { params }: { params: { dataTyp
const plotID = plotIDParam ? parseInt(plotIDParam) : undefined;
const censusID = censusIDParam ? parseInt(censusIDParam) : undefined;
- let conn: PoolConnection | null = null;
+ const connectionManager = new ConnectionManager();
let query: string = '';
let results: any[] = [];
let mappedResults: any[] = [];
let formMappedResults: any[] = [];
try {
- conn = await getConn();
switch (dataType) {
case 'attributes':
query = `SELECT * FROM ${schema}.attributes`;
- results = await runQuery(conn, query);
+ results = await connectionManager.executeQuery(query);
mappedResults = MapperFactory.getMapper('attributes').mapData(results);
formMappedResults = mappedResults.map((row: AttributesRDS) => ({
code: row.code,
@@ -38,7 +36,7 @@ export async function GET(_request: NextRequest, { params }: { params: { dataTyp
LEFT JOIN ${schema}.roles r ON p.RoleID = r.RoleID
LEFT JOIN ${schema}.census c ON c.CensusID = p.CensusID
WHERE c.PlotID = ? AND p.CensusID = ?`;
- results = await runQuery(conn, query, [plotID, censusID]);
+ results = await connectionManager.executeQuery(query, [plotID, censusID]);
formMappedResults = results.map((row: any) => ({
firstname: row.FirstName,
lastname: row.LastName,
@@ -58,7 +56,7 @@ export async function GET(_request: NextRequest, { params }: { params: { dataTyp
JOIN ${schema}.quadrats q ON q.QuadratID = st.QuadratID
JOIN ${schema}.censusquadrat cq ON cq.QuadratID = q.QuadratID
WHERE q.PlotID = ? AND cq.CensusID = ?`;
- results = await runQuery(conn, query, [plotID, censusID]);
+ results = await connectionManager.executeQuery(query, [plotID, censusID]);
formMappedResults = results.map((row: any) => ({
spcode: row.SpeciesCode,
family: row.Family,
@@ -74,7 +72,7 @@ export async function GET(_request: NextRequest, { params }: { params: { dataTyp
query = `SELECT * FROM ${schema}.quadrats q
JOIN ${schema}.censusquadrat cq ON cq.QuadratID = q.QuadratID
WHERE q.PlotID = ? AND cq.CensusID = ?`;
- results = await runQuery(conn, query, [plotID, censusID]);
+ results = await connectionManager.executeQuery(query, [plotID, censusID]);
formMappedResults = results.map((row: any) => ({
quadrat: row.QuadratName,
startx: row.StartX,
@@ -102,7 +100,7 @@ export async function GET(_request: NextRequest, { params }: { params: { dataTyp
JOIN ${schema}.censusquadrat cq ON cq.QuadratID = q.QuadratID
JOIN ${schema}.species s ON s.SpeciesID = t.SpeciesID
WHERE q.PlotID = ? AND cq.CensusID = ?`;
- results = await runQuery(conn, query, [plotID, censusID]);
+ results = await connectionManager.executeQuery(query, [plotID, censusID]);
formMappedResults = results.map((row: any) => ({
tag: row.TreeTag,
stemtag: row.StemTag,
@@ -125,6 +123,6 @@ export async function GET(_request: NextRequest, { params }: { params: { dataTyp
} catch (e: any) {
throw new Error(e);
} finally {
- if (conn) conn.release();
+ await connectionManager.closeConnection();
}
}
diff --git a/frontend/app/api/formsearch/attributes/route.ts b/frontend/app/api/formsearch/attributes/route.ts
deleted file mode 100644
index e4801463..00000000
--- a/frontend/app/api/formsearch/attributes/route.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import { getConn, runQuery } from '@/components/processors/processormacros';
-import { FORMSEARCH_LIMIT } from '@/config/macros/azurestorage';
-import { HTTPResponses } from '@/config/macros';
-
-export async function GET(request: NextRequest): Promise> {
- const schema = request.nextUrl.searchParams.get('schema');
- if (!schema || schema === 'undefined') throw new Error('no schema provided!');
- const partialCode = request.nextUrl.searchParams.get('searchfor')!;
- const conn = await getConn();
- try {
- const query =
- partialCode === ''
- ? `SELECT DISTINCT Code FROM ${schema}.attributes ORDER BY Code LIMIT ${FORMSEARCH_LIMIT}`
- : `SELECT DISTINCT Code FROM ${schema}.attributes WHERE Code LIKE ? ORDER BY Code LIMIT ${FORMSEARCH_LIMIT}`;
- const queryParams = partialCode === '' ? [] : [`%${partialCode}%`];
- const results = await runQuery(conn, query, queryParams);
-
- return new NextResponse(JSON.stringify(results.map((row: any) => row.Code)), { status: HTTPResponses.OK });
- } catch (error: any) {
- console.error('Error in GET Attributes:', error.message || error);
- throw new Error('Failed to fetch attribute data');
- } finally {
- if (conn) conn.release();
- }
-}
diff --git a/frontend/app/api/formsearch/personnel/route.ts b/frontend/app/api/formsearch/personnel/route.ts
deleted file mode 100644
index fe91e7aa..00000000
--- a/frontend/app/api/formsearch/personnel/route.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import { getConn, runQuery } from '@/components/processors/processormacros';
-import { FORMSEARCH_LIMIT } from '@/config/macros/azurestorage';
-import { HTTPResponses } from '@/config/macros';
-
-export async function GET(request: NextRequest): Promise> {
- const schema = request.nextUrl.searchParams.get('schema');
- if (!schema || schema === 'undefined') throw new Error('no schema provided!');
- const partialLastName = request.nextUrl.searchParams.get('searchfor')!;
- const conn = await getConn();
- try {
- const query =
- partialLastName === ''
- ? `SELECT FirstName, LastName
- FROM ${schema}.personnel
- ORDER BY LastName
- LIMIT ${FORMSEARCH_LIMIT}`
- : `SELECT FirstName, LastName
- FROM ${schema}.personnel
- WHERE LastName LIKE ?
- ORDER BY LastName
- LIMIT ${FORMSEARCH_LIMIT}`;
- const queryParams = partialLastName === '' ? [] : [`%${partialLastName}%`];
- const results = await runQuery(conn, query, queryParams);
-
- // Properly mapping results to return an array of { label, code }
- return new NextResponse(JSON.stringify(results.map((row: any) => `${row.FirstName} ${row.LastName}`)), { status: HTTPResponses.OK });
- } catch (error: any) {
- console.error('Error in GET Personnel:', error.message || error);
- throw new Error('Failed to fetch personnel data');
- } finally {
- if (conn) conn.release();
- }
-}
diff --git a/frontend/app/api/formsearch/personnelblock/route.ts b/frontend/app/api/formsearch/personnelblock/route.ts
deleted file mode 100644
index 192ef806..00000000
--- a/frontend/app/api/formsearch/personnelblock/route.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import { PoolConnection } from 'mysql2/promise';
-import { getConn, runQuery } from '@/components/processors/processormacros';
-import { FORMSEARCH_LIMIT } from '@/config/macros/azurestorage';
-import { HTTPResponses } from '@/config/macros';
-import { PersonnelRDS, PersonnelResult } from '@/config/sqlrdsdefinitions/personnel';
-
-export async function GET(request: NextRequest): Promise> {
- const schema = request.nextUrl.searchParams.get('schema');
- if (!schema) throw new Error('no schema provided!');
- const partialLastName = request.nextUrl.searchParams.get('searchfor')!;
- const conn = await getConn();
- try {
- const query =
- partialLastName === ''
- ? `SELECT DISTINCT PersonnelID, FirstName, LastName, Role
- FROM ${schema}.personnel
- ORDER BY LastName
- LIMIT ${FORMSEARCH_LIMIT}`
- : `SELECT DISTINCT PersonnelID, FirstName, LastName, Role
- FROM ${schema}.personnel
- WHERE LastName LIKE ?
- ORDER BY LastName
- LIMIT ${FORMSEARCH_LIMIT}`;
- const queryParams = partialLastName === '' ? [] : [`%${partialLastName}%`];
- const results = await runQuery(conn, query, queryParams);
-
- const personnelRows: PersonnelRDS[] = results.map((row: PersonnelResult, index: number) => ({
- id: index + 1,
- personnelID: row.PersonnelID,
- firstName: row.FirstName,
- lastName: row.LastName,
- roleID: row.RoleID
- }));
-
- // Properly mapping results to return an array of { label, code }
- return new NextResponse(JSON.stringify(personnelRows), {
- status: HTTPResponses.OK
- });
- } catch (error: any) {
- console.error('Error in GET Personnel:', error.message || error);
- throw new Error('Failed to fetch personnel data');
- } finally {
- if (conn) conn.release();
- }
-}
-
-export async function PUT(request: NextRequest): Promise {
- let conn: PoolConnection | null = null;
- const schema = request.nextUrl.searchParams.get('schema');
- const quadratID = parseInt(request.nextUrl.searchParams.get('quadratID')!, 10);
- if (!schema || schema === 'undefined' || isNaN(quadratID)) throw new Error('Missing required parameters');
-
- try {
- const updatedPersonnelIDs: number[] = await request.json();
-
- conn = await getConn();
- await conn.beginTransaction();
-
- // Fetch current personnel IDs
- const currentPersonnelQuery = `SELECT PersonnelID FROM ${schema}.quadratpersonnel WHERE QuadratID = ?`;
- const currentPersonnelResult: { PersonnelID: number }[] = await runQuery(conn, currentPersonnelQuery, [quadratID]);
- const currentPersonnelIds = currentPersonnelResult.map(p => p.PersonnelID);
-
- // Determine personnel to add or remove
- const personnelToAdd = updatedPersonnelIDs.filter(id => !currentPersonnelIds.includes(id));
- const personnelToRemove = currentPersonnelIds.filter(id => !updatedPersonnelIDs.includes(id));
-
- // Remove personnel
- for (const personnelId of personnelToRemove) {
- await runQuery(conn, `DELETE FROM ${schema}.quadratpersonnel WHERE QuadratID = ? AND PersonnelID = ?`, [quadratID, personnelId]);
- }
-
- // Add new personnel associations
- for (const personnelId of personnelToAdd) {
- await runQuery(conn, `INSERT INTO ${schema}.quadratpersonnel (QuadratID, PersonnelID) VALUES (?, ?)`, [quadratID, personnelId]);
- }
-
- // Commit the transaction
- await conn.commit();
-
- return NextResponse.json({ message: 'Personnel updated successfully' }, { status: HTTPResponses.OK });
- } catch (error) {
- await conn?.rollback();
- console.error('Error:', error);
- throw new Error('Personnel update failed');
- } finally {
- if (conn) conn.release();
- }
-}
diff --git a/frontend/app/api/formsearch/quadrats/route.ts b/frontend/app/api/formsearch/quadrats/route.ts
deleted file mode 100644
index fc1fbc91..00000000
--- a/frontend/app/api/formsearch/quadrats/route.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import { getConn, runQuery } from '@/components/processors/processormacros';
-import { FORMSEARCH_LIMIT } from '@/config/macros/azurestorage';
-import { HTTPResponses } from '@/config/macros';
-
-export async function GET(request: NextRequest): Promise> {
- const schema = request.nextUrl.searchParams.get('schema');
- if (!schema || schema === 'undefined') throw new Error('no schema provided!');
- const partialQuadratName = request.nextUrl.searchParams.get('searchfor')!;
- const conn = await getConn();
- try {
- const query =
- partialQuadratName === ''
- ? `SELECT QuadratName
- FROM ${schema}.quadrats
- ORDER BY QuadratName
- LIMIT ${FORMSEARCH_LIMIT}`
- : `SELECT QuadratName
- FROM ${schema}.quadrats
- WHERE QuadratName LIKE ?
- ORDER BY QuadratName
- LIMIT ${FORMSEARCH_LIMIT}`;
- const queryParams = partialQuadratName === '' ? [] : [`%${partialQuadratName}%`];
- const results = await runQuery(conn, query, queryParams);
- return new NextResponse(JSON.stringify(results.map((row: any) => row.QuadratName)), { status: HTTPResponses.OK });
- } catch (error: any) {
- console.error('Error in GET Quadrats:', error.message || error);
- throw new Error('Failed to fetch quadrat data');
- } finally {
- if (conn) conn.release();
- }
-}
diff --git a/frontend/app/api/formsearch/species/route.ts b/frontend/app/api/formsearch/species/route.ts
deleted file mode 100644
index 55189288..00000000
--- a/frontend/app/api/formsearch/species/route.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import { getConn, runQuery } from '@/components/processors/processormacros';
-import { FORMSEARCH_LIMIT } from '@/config/macros/azurestorage';
-import { HTTPResponses } from '@/config/macros';
-
-export async function GET(request: NextRequest): Promise> {
- const schema = request.nextUrl.searchParams.get('schema');
- if (!schema || schema === 'undefined') throw new Error('no schema provided!');
- const partialSpeciesCode = request.nextUrl.searchParams.get('searchfor')!;
- const conn = await getConn();
- try {
- const query =
- partialSpeciesCode === ''
- ? `SELECT SpeciesCode
- FROM ${schema}.species
- ORDER BY SpeciesCode
- LIMIT ${FORMSEARCH_LIMIT}`
- : `SELECT SpeciesCode
- FROM ${schema}.species
- WHERE SpeciesCode LIKE ?
- ORDER BY SpeciesCode
- LIMIT ${FORMSEARCH_LIMIT}`;
- const queryParams = partialSpeciesCode === '' ? [] : [`%${partialSpeciesCode}%`];
- const results = await runQuery(conn, query, queryParams);
- return new NextResponse(JSON.stringify(results.map((row: any) => row.SpeciesCode)), { status: HTTPResponses.OK });
- } catch (error: any) {
- console.error('Error in GET Quadrats:', error.message || error);
- throw new Error('Failed to fetch quadrat data');
- } finally {
- if (conn) conn.release();
- }
-}
diff --git a/frontend/app/api/formsearch/stems/route.ts b/frontend/app/api/formsearch/stems/route.ts
deleted file mode 100644
index 4e81f4cf..00000000
--- a/frontend/app/api/formsearch/stems/route.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import { getConn, runQuery } from '@/components/processors/processormacros';
-import { FORMSEARCH_LIMIT } from '@/config/macros/azurestorage';
-import { HTTPResponses } from '@/config/macros';
-
-export async function GET(request: NextRequest): Promise> {
- const schema = request.nextUrl.searchParams.get('schema');
- if (!schema || schema === 'undefined') throw new Error('no schema provided!');
- const partialStemTag = request.nextUrl.searchParams.get('searchfor')!;
- const conn = await getConn();
- try {
- const query =
- partialStemTag === ''
- ? `SELECT StemTag
- FROM ${schema}.stems
- ORDER BY StemTag
- LIMIT ${FORMSEARCH_LIMIT}`
- : `SELECT StemTag
- FROM ${schema}.stems
- WHERE StemTag LIKE ?
- ORDER BY StemTag
- LIMIT ${FORMSEARCH_LIMIT}`;
- const queryParams = partialStemTag === '' ? [] : [`%${partialStemTag}%`];
- const results = await runQuery(conn, query, queryParams);
- return new NextResponse(JSON.stringify(results.map((row: any) => (row.StemTag ? row.StemTag : ''))), { status: HTTPResponses.OK });
- } catch (error: any) {
- console.error('Error in GET Quadrats:', error.message || error);
- throw new Error('Failed to fetch quadrat data');
- } finally {
- if (conn) conn.release();
- }
-}
diff --git a/frontend/app/api/formsearch/trees/route.ts b/frontend/app/api/formsearch/trees/route.ts
deleted file mode 100644
index 95eea4f4..00000000
--- a/frontend/app/api/formsearch/trees/route.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import { getConn, runQuery } from '@/components/processors/processormacros';
-import { FORMSEARCH_LIMIT } from '@/config/macros/azurestorage';
-import { HTTPResponses } from '@/config/macros';
-
-export async function GET(request: NextRequest): Promise> {
- const schema = request.nextUrl.searchParams.get('schema');
- if (!schema || schema === 'undefined') throw new Error('no schema provided!');
- const partialTreeTag = request.nextUrl.searchParams.get('searchfor')!;
- const conn = await getConn();
- try {
- const query =
- partialTreeTag === ''
- ? `SELECT TreeTag
- FROM ${schema}.trees
- ORDER BY TreeTag
- LIMIT ${FORMSEARCH_LIMIT}`
- : `SELECT TreeTag
- FROM ${schema}.trees
- WHERE TreeTag LIKE ?
- ORDER BY TreeTag
- LIMIT ${FORMSEARCH_LIMIT}`;
- const queryParams = partialTreeTag === '' ? [] : [`%${partialTreeTag}%`];
- const results = await runQuery(conn, query, queryParams);
- return new NextResponse(JSON.stringify(results.map((row: any) => row.TreeTag)), { status: HTTPResponses.OK });
- } catch (error: any) {
- console.error('Error in GET Quadrats:', error.message || error);
- throw new Error('Failed to fetch quadrat data');
- } finally {
- if (conn) conn.release();
- }
-}
diff --git a/frontend/app/api/formvalidation/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/formvalidation/[dataType]/[[...slugs]]/route.ts
index f9122cd0..4b01f793 100644
--- a/frontend/app/api/formvalidation/[dataType]/[[...slugs]]/route.ts
+++ b/frontend/app/api/formvalidation/[dataType]/[[...slugs]]/route.ts
@@ -1,7 +1,7 @@
-import { getConn, runQuery } from '@/components/processors/processormacros';
import { HTTPResponses } from '@/config/macros';
-import { format, PoolConnection } from 'mysql2/promise';
+import { format } from 'mysql2/promise';
import { NextRequest, NextResponse } from 'next/server';
+import ConnectionManager from '@/config/connectionmanager';
// dataType
// slugs: schema, columnName, value ONLY
@@ -16,18 +16,17 @@ export async function GET(request: NextRequest, { params }: { params: { dataType
if (!schema || !columnName || !value) return new NextResponse(null, { status: 404 });
- let conn: PoolConnection | null = null;
+ const connectionManager = new ConnectionManager();
try {
- conn = await getConn();
const query = `SELECT 1 FROM ?? WHERE ?? = ? LIMIT 1`;
const formatted = format(query, [`${schema}.${params.dataType}`, columnName, value]);
- const results = await runQuery(conn, formatted);
+ const results = await connectionManager.executeQuery(formatted);
if (results.length === 0) return new NextResponse(null, { status: 404 });
return new NextResponse(null, { status: HTTPResponses.OK });
} catch (error: any) {
console.error(error);
throw error;
} finally {
- if (conn) conn.release();
+ await connectionManager.closeConnection();
}
}
diff --git a/frontend/app/api/postvalidation/route.ts b/frontend/app/api/postvalidation/route.ts
index e1ee5424..5493e7d6 100644
--- a/frontend/app/api/postvalidation/route.ts
+++ b/frontend/app/api/postvalidation/route.ts
@@ -1,27 +1,33 @@
import { NextRequest, NextResponse } from 'next/server';
-import { getConn, runQuery } from '@/components/processors/processormacros';
import { HTTPResponses } from '@/config/macros';
+import ConnectionManager from '@/config/connectionmanager';
export async function GET(request: NextRequest) {
const schema = request.nextUrl.searchParams.get('schema');
if (!schema) throw new Error('no schema variable provided!');
- const conn = await getConn();
- const query = `SELECT QueryID, QueryName, Description FROM ${schema}.postvalidationqueries WHERE IsEnabled IS TRUE;`;
- const results = await runQuery(conn, query);
- if (results.length === 0) {
- return new NextResponse(JSON.stringify({ message: 'No queries found' }), {
- status: HTTPResponses.NOT_FOUND
+ const connectionManager = new ConnectionManager();
+ try {
+ const query = `SELECT QueryID, QueryName, Description FROM ${schema}.postvalidationqueries WHERE IsEnabled IS TRUE;`;
+ const results = await connectionManager.executeQuery(query);
+ if (results.length === 0) {
+ return new NextResponse(JSON.stringify({ message: 'No queries found' }), {
+ status: HTTPResponses.NOT_FOUND
+ });
+ }
+ const postValidations = results.map((row: any) => ({
+ queryID: row.QueryID,
+ queryName: row.QueryName,
+ queryDescription: row.Description
+ }));
+ return new NextResponse(JSON.stringify(postValidations), {
+ status: HTTPResponses.OK
});
+ } catch (e: any) {
+ throw e;
+ } finally {
+ await connectionManager.closeConnection();
}
- const postValidations = results.map((row: any) => ({
- queryID: row.QueryID,
- queryName: row.QueryName,
- queryDescription: row.Description
- }));
- return new NextResponse(JSON.stringify(postValidations), {
- status: HTTPResponses.OK
- });
}
// searchParams: schema, plot, census
diff --git a/frontend/app/api/postvalidationbyquery/[schema]/[plotID]/[censusID]/[queryID]/route.ts b/frontend/app/api/postvalidationbyquery/[schema]/[plotID]/[censusID]/[queryID]/route.ts
index e40acc17..05a1052c 100644
--- a/frontend/app/api/postvalidationbyquery/[schema]/[plotID]/[censusID]/[queryID]/route.ts
+++ b/frontend/app/api/postvalidationbyquery/[schema]/[plotID]/[censusID]/[queryID]/route.ts
@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { HTTPResponses } from '@/config/macros';
-import { getConn, runQuery } from '@/components/processors/processormacros';
import moment from 'moment';
+import ConnectionManager from '@/config/connectionmanager';
export async function GET(_request: NextRequest, { params }: { params: { schema: string; plotID: string; censusID: string; queryID: string } }) {
const { schema } = params;
@@ -13,12 +13,14 @@ export async function GET(_request: NextRequest, { params }: { params: { schema:
return new NextResponse('Missing parameters', { status: HTTPResponses.INVALID_REQUEST });
}
- const conn = await getConn();
+ const connectionManager = new ConnectionManager();
try {
const query = `SELECT QueryDefinition FROM ${schema}.postvalidationqueries WHERE QueryID = ${queryID}`;
- const results = await runQuery(conn, query);
+ const results = await connectionManager.executeQuery(query);
- if (results.length === 0) return new NextResponse('Query not found', { status: HTTPResponses.NOT_FOUND });
+ if (results.length === 0) {
+ return new NextResponse('Query not found', { status: HTTPResponses.NOT_FOUND });
+ }
const replacements = {
schema: schema,
@@ -26,8 +28,8 @@ export async function GET(_request: NextRequest, { params }: { params: { schema:
currentCensusID: censusID
};
const formattedQuery = results[0].QueryDefinition.replace(/\${(.*?)}/g, (_match: any, p1: string) => replacements[p1 as keyof typeof replacements]);
-
- const queryResults = await runQuery(conn, formattedQuery);
+ await connectionManager.beginTransaction();
+ const queryResults = await connectionManager.executeQuery(formattedQuery);
if (queryResults.length === 0) throw new Error('failure');
@@ -36,20 +38,20 @@ export async function GET(_request: NextRequest, { params }: { params: { schema:
const successUpdate = `UPDATE ${schema}.postvalidationqueries
SET LastRunAt = ?, LastRunResult = ?, LastRunStatus = 'success'
WHERE QueryID = ${queryID}`;
- await runQuery(conn, successUpdate, [currentTime, successResults]);
-
+ await connectionManager.executeQuery(successUpdate, [currentTime, successResults]);
return new NextResponse(null, { status: HTTPResponses.OK });
} catch (e: any) {
+ await connectionManager.rollbackTransaction();
if (e.message === 'failure') {
const currentTime = moment().format('YYYY-MM-DD HH:mm:ss');
const failureUpdate = `UPDATE ${schema}.postvalidationqueries
SET LastRunAt = ?, LastRunStatus = 'failure'
WHERE QueryID = ${queryID}`;
- await runQuery(conn, failureUpdate, [currentTime]);
+ await connectionManager.executeQuery(failureUpdate, [currentTime]);
return new NextResponse(null, { status: HTTPResponses.OK }); // if the query itself fails, that isn't a good enough reason to return a crash. It should just be logged.
}
return new NextResponse('Internal Server Error', { status: HTTPResponses.INTERNAL_SERVER_ERROR });
} finally {
- if (conn) conn.release();
+ await connectionManager.closeConnection();
}
}
diff --git a/frontend/app/api/refreshviews/[view]/[schema]/route.ts b/frontend/app/api/refreshviews/[view]/[schema]/route.ts
index 24879ff2..a01e4e2d 100644
--- a/frontend/app/api/refreshviews/[view]/[schema]/route.ts
+++ b/frontend/app/api/refreshviews/[view]/[schema]/route.ts
@@ -1,21 +1,21 @@
-import { getConn, runQuery } from '@/components/processors/processormacros';
import { HTTPResponses } from '@/config/macros';
-import { PoolConnection } from 'mysql2/promise';
import { NextRequest, NextResponse } from 'next/server';
+import ConnectionManager from '@/config/connectionmanager';
export async function POST(_request: NextRequest, { params }: { params: { view: string; schema: string } }) {
if (!params.schema || params.schema === 'undefined' || !params.view || params.view === 'undefined' || !params) throw new Error('schema not provided');
const { view, schema } = params;
- let connection: PoolConnection | null = null;
+ const connectionManager = new ConnectionManager();
try {
- connection = await getConn();
+ await connectionManager.beginTransaction();
const query = `CALL ${schema}.Refresh${view === 'viewfulltable' ? 'ViewFullTable' : view === 'measurementssummary' ? 'MeasurementsSummary' : ''}();`;
- await runQuery(connection, query);
+ await connectionManager.executeQuery(query);
return new NextResponse(null, { status: HTTPResponses.OK });
} catch (e: any) {
+ await connectionManager.rollbackTransaction();
console.error('Error:', e);
throw new Error('Call failed: ', e);
} finally {
- if (connection) connection.release();
+ await connectionManager.closeConnection();
}
}
diff --git a/frontend/app/api/rollover/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/rollover/[dataType]/[[...slugs]]/route.ts
index 4374cb07..f97cb332 100644
--- a/frontend/app/api/rollover/[dataType]/[[...slugs]]/route.ts
+++ b/frontend/app/api/rollover/[dataType]/[[...slugs]]/route.ts
@@ -1,7 +1,6 @@
-import { getConn, runQuery } from '@/components/processors/processormacros';
import { HTTPResponses } from '@/config/macros';
-import { PoolConnection } from 'mysql2/promise';
import { NextRequest, NextResponse } from 'next/server';
+import ConnectionManager from '@/config/connectionmanager';
/**
* Handles the POST request for the rollover API endpoint, which allows users to roll over quadrat or personnel data from one census to another within a specified schema.
@@ -15,29 +14,28 @@ export async function POST(request: NextRequest, { params }: { params: { dataTyp
const [schema, plotID, sourceCensusID, newCensusID] = params.slugs;
if (!schema || !plotID || !sourceCensusID || !newCensusID) throw new Error('no schema or plotID or censusID provided');
- let conn: PoolConnection | null = null;
+ const connectionManager = new ConnectionManager();
try {
const { incoming } = await request.json();
if (!Array.isArray(incoming) || incoming.length === 0) throw new Error('No quadrat or personnel IDs provided');
- conn = await getConn();
- if (conn) console.log('connection created.');
+ if (connectionManager) console.log('connection created.');
let query = ``;
let queryParams = [];
- await conn.beginTransaction();
+ await connectionManager.beginTransaction();
console.log('transaction started.');
switch (params.dataType) {
case 'quadrats':
query = `
- INSERT INTO censusquadrat (CensusID, QuadratID)
+ INSERT INTO ${schema}.censusquadrat (CensusID, QuadratID)
SELECT ?, q.QuadratID
- FROM quadrats q
+ FROM ${schema}.quadrats q
WHERE q.QuadratID IN (${incoming.map(() => '?').join(', ')});`;
queryParams = [Number(newCensusID), ...incoming];
- await runQuery(conn, query, queryParams);
+ await connectionManager.executeQuery(query, queryParams);
break;
case 'personnel':
query = `
@@ -50,30 +48,19 @@ export async function POST(request: NextRequest, { params }: { params: { dataTyp
FROM ${schema}.personnel
WHERE CensusID = ? AND PersonnelID IN (${incoming.map(() => '?').join(', ')});`;
queryParams = [Number(newCensusID), Number(sourceCensusID), ...incoming];
- await runQuery(conn, query, queryParams);
+ await connectionManager.executeQuery(query, queryParams);
break;
default:
throw new Error('Invalid data type');
}
- await conn.commit(); // testing
return new NextResponse(JSON.stringify({ message: 'Rollover successful' }), { status: HTTPResponses.OK });
} catch (error: any) {
- await conn?.rollback();
+ await connectionManager.rollbackTransaction();
console.error('Error in rollover API:', error.message);
return new NextResponse(JSON.stringify({ error: error.message }), {
status: 500
});
} finally {
- if (conn) conn.release();
+ await connectionManager.closeConnection();
}
}
-
-/**
- * Handles the POST request for the rollover API endpoint, which allows users to rollover quadrat or personnel data from one census to another within a given schema.
- *
- * The slugs provided in the URL MUST include (in order): a schema, plotID, source censusID, and new censusID to target.
- *
- * @param request - The NextRequest object containing the request data.
- * @param params - The URL parameters, including the dataType, schema, plotID, source censusID, and new censusID.
- * @returns A NextResponse with a success message or an error message.
- */
diff --git a/frontend/app/api/runquery/route.ts b/frontend/app/api/runquery/route.ts
new file mode 100644
index 00000000..794bb940
--- /dev/null
+++ b/frontend/app/api/runquery/route.ts
@@ -0,0 +1,14 @@
+import { NextRequest, NextResponse } from 'next/server';
+import ConnectionManager from '@/config/connectionmanager';
+
+// this is intended as a dedicated server-side execution pipeline for a given query. Results will be returned as-is to caller.
+export async function POST(request: NextRequest) {
+ const query = await request.json(); // receiving query already formatted and prepped for execution
+
+ const connectionManager = new ConnectionManager();
+ const results = await connectionManager.executeQuery(query);
+ return new NextResponse(JSON.stringify(results), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ });
+}
diff --git a/frontend/app/api/specieslimits/[speciesID]/route.ts b/frontend/app/api/specieslimits/[speciesID]/route.ts
index 561acb64..7dac21cb 100644
--- a/frontend/app/api/specieslimits/[speciesID]/route.ts
+++ b/frontend/app/api/specieslimits/[speciesID]/route.ts
@@ -1,24 +1,22 @@
import { NextRequest, NextResponse } from 'next/server';
-import { PoolConnection } from 'mysql2/promise';
-import { getConn, runQuery } from '@/components/processors/processormacros';
import MapperFactory from '@/config/datamapper';
import { HTTPResponses } from '@/config/macros';
+import ConnectionManager from '@/config/connectionmanager';
export async function GET(request: NextRequest, { params }: { params: { speciesID: string } }) {
const schema = request.nextUrl.searchParams.get('schema');
if (!schema) throw new Error('Schema not provided');
if (params.speciesID === 'undefined') throw new Error('SpeciesID not provided');
- let conn: PoolConnection | null = null;
+ const connectionManager = new ConnectionManager();
try {
- conn = await getConn();
const query = `SELECT * FROM ${schema}.specieslimits WHERE SpeciesID = ?`;
- const results = await runQuery(conn, query, [params.speciesID]);
+ const results = await connectionManager.executeQuery(query, [params.speciesID]);
return new NextResponse(JSON.stringify(MapperFactory.getMapper('specieslimits').mapData(results)), { status: HTTPResponses.OK });
} catch (error: any) {
throw new Error(error);
} finally {
- if (conn) conn.release();
+ await connectionManager.closeConnection();
}
}
@@ -27,18 +25,18 @@ export async function PATCH(request: NextRequest, { params }: { params: { specie
if (!schema) throw new Error('Schema not provided');
if (params.speciesID === 'undefined') throw new Error('SpeciesID not provided');
const { newRow } = await request.json();
- let conn: PoolConnection | null = null;
+ const connectionManager = new ConnectionManager();
try {
- conn = await getConn();
- await conn.beginTransaction();
+ await connectionManager.beginTransaction();
const newRowData = MapperFactory.getMapper('specieslimits').demapData([newRow])[0];
const { ['SpeciesLimitID']: gridIDKey, ...remainingProperties } = newRowData;
const query = `UPDATE ${schema}.specieslimits SET ? WHERE ?? = ?`;
- const results = await runQuery(conn, query, [remainingProperties, 'SpeciesLimitID', gridIDKey]);
+ await connectionManager.executeQuery(query, [remainingProperties, 'SpeciesLimitID', gridIDKey]);
+ return new NextResponse(null, { status: HTTPResponses.OK });
} catch (e: any) {
- await conn?.rollback();
+ await connectionManager.rollbackTransaction();
throw new Error(e);
} finally {
- if (conn) conn.release();
+ await connectionManager.closeConnection();
}
}
diff --git a/frontend/app/api/sqlload/route.ts b/frontend/app/api/sqlload/route.ts
index b448ae36..c89efd52 100644
--- a/frontend/app/api/sqlload/route.ts
+++ b/frontend/app/api/sqlload/route.ts
@@ -1,9 +1,8 @@
import { NextRequest, NextResponse } from 'next/server';
-import { getConn, InsertUpdateProcessingProps } from '@/components/processors/processormacros';
-import { PoolConnection } from 'mysql2/promise';
-import { HTTPResponses } from '@/config/macros';
+import { HTTPResponses, InsertUpdateProcessingProps } from '@/config/macros';
import { FileRow, FileRowSet } from '@/config/macros/formdetails';
import { insertOrUpdate } from '@/components/processors/processorhelperfunctions';
+import ConnectionManager from '@/config/connectionmanager';
export async function POST(request: NextRequest) {
const fileRowSet: FileRowSet = await request.json();
@@ -36,50 +35,18 @@ export async function POST(request: NextRequest) {
// full name
const fullName = request.nextUrl.searchParams.get('user') ?? undefined;
- let connection: PoolConnection | null = null; // Use PoolConnection type
-
- try {
- connection = await getConn();
- } catch (error) {
- if (error instanceof Error) {
- console.error('Error processing files:', error.message);
- return new NextResponse(
- JSON.stringify({
- responseMessage: `Failure in connecting to SQL with ${error.message}`,
- error: error.message
- }),
- { status: HTTPResponses.SQL_CONNECTION_FAILURE }
- );
- } else {
- console.error('Unknown error in connecting to SQL:', error);
- return new NextResponse(
- JSON.stringify({
- responseMessage: `Unknown SQL connection error with error: ${error}`
- }),
- { status: HTTPResponses.SQL_CONNECTION_FAILURE }
- );
- }
- }
-
- if (!connection) {
- console.error('Container client or SQL connection is undefined.');
- return new NextResponse(
- JSON.stringify({
- responseMessage: 'Container client or SQL connection is undefined'
- }),
- { status: HTTPResponses.SERVICE_UNAVAILABLE }
- );
- }
+ const connectionManager = new ConnectionManager();
const idToRows: { coreMeasurementID: number; fileRow: FileRow }[] = [];
for (const rowId in fileRowSet) {
+ await connectionManager.beginTransaction();
console.log(`rowID: ${rowId}`);
const row = fileRowSet[rowId];
console.log('row for row ID: ', row);
try {
const props: InsertUpdateProcessingProps = {
schema,
- connection,
+ connectionManager: connectionManager,
formType,
rowData: row,
plotID,
@@ -93,7 +60,9 @@ export async function POST(request: NextRequest) {
} else if (formType === 'measurements' && coreMeasurementID === undefined) {
throw new Error('CoreMeasurement insertion failure at row: ' + row);
}
+ await connectionManager.commitTransaction();
} catch (error) {
+ await connectionManager.rollbackTransaction();
if (error instanceof Error) {
console.error(`Error processing row for file ${fileName}:`, error.message);
return new NextResponse(
@@ -112,9 +81,8 @@ export async function POST(request: NextRequest) {
{ status: HTTPResponses.SERVICE_UNAVAILABLE }
);
}
- } finally {
- if (connection) connection.release();
}
}
+ await connectionManager.closeConnection();
return new NextResponse(JSON.stringify({ message: 'Insert to SQL successful', idToRows: idToRows }), { status: HTTPResponses.OK });
}
diff --git a/frontend/app/api/sqlmonitor/route.ts b/frontend/app/api/sqlmonitor/route.ts
deleted file mode 100644
index 01c308a7..00000000
--- a/frontend/app/api/sqlmonitor/route.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { poolMonitor } from '@/components/processors/processormacros';
-import { HTTPResponses } from '@/config/macros';
-import { NextResponse } from 'next/server';
-
-export async function GET() {
- try {
- const status = poolMonitor.getPoolStatus();
- return NextResponse.json({ message: 'Monitoring check successful ', status }, { status: HTTPResponses.OK });
- } catch (error: any) {
- // If there's an error in getting the pool status
- console.error('Error in pool monitoring:', error);
- return NextResponse.json({ message: 'Monitoring check failed', error: error.message }, { status: 500 });
- }
-}
diff --git a/frontend/app/api/structure/[schema]/route.ts b/frontend/app/api/structure/[schema]/route.ts
index 87bd3b5d..9b06e2d3 100644
--- a/frontend/app/api/structure/[schema]/route.ts
+++ b/frontend/app/api/structure/[schema]/route.ts
@@ -1,6 +1,5 @@
import { NextRequest } from 'next/server';
-import { getConn, runQuery } from '@/components/processors/processormacros';
-import { PoolConnection } from 'mysql2/promise';
+import ConnectionManager from '@/config/connectionmanager';
export async function GET(_request: NextRequest, { params }: { params: { schema: string } }) {
const schema = params.schema;
@@ -8,14 +7,14 @@ export async function GET(_request: NextRequest, { params }: { params: { schema:
const query = `SELECT table_name, column_name
FROM information_schema.columns
WHERE table_schema = ?`;
- let conn: PoolConnection | null = null;
+ const connectionManager = new ConnectionManager();
try {
- conn = await getConn();
- return new Response(JSON.stringify(await runQuery(conn, query, [schema])), { status: 200 });
+ const results = await connectionManager.executeQuery(query, [schema]);
+ return new Response(JSON.stringify(results), { status: 200 });
} catch (e: any) {
console.error('Error:', e);
throw new Error('Call failed: ', e);
} finally {
- if (conn) conn.release();
+ await connectionManager.closeConnection();
}
}
diff --git a/frontend/app/api/validations/crud/route.ts b/frontend/app/api/validations/crud/route.ts
index 508eceef..952d7e4b 100644
--- a/frontend/app/api/validations/crud/route.ts
+++ b/frontend/app/api/validations/crud/route.ts
@@ -1,76 +1,75 @@
import { NextRequest, NextResponse } from 'next/server';
import { ValidationProceduresRDS } from '@/config/sqlrdsdefinitions/validations';
-import { format, PoolConnection } from 'mysql2/promise';
-import { getConn, runQuery } from '@/components/processors/processormacros';
+import { format } from 'mysql2/promise';
import { HTTPResponses } from '@/config/macros';
import MapperFactory from '@/config/datamapper';
+import ConnectionManager from '@/config/connectionmanager';
export async function GET(_request: NextRequest) {
- let conn: PoolConnection | null = null;
+ const connectionManager = new ConnectionManager();
try {
- conn = await getConn();
const query = `SELECT * FROM catalog.validationprocedures;`;
- const results = await runQuery(conn, query);
+ const results = await connectionManager.executeQuery(query);
return new NextResponse(JSON.stringify(MapperFactory.getMapper('validationprocedures').mapData(results)), { status: HTTPResponses.OK });
} catch (error: any) {
console.error('Error:', error);
return NextResponse.json({}, { status: HTTPResponses.CONFLICT });
} finally {
- if (conn) conn.release();
+ await connectionManager.closeConnection();
}
}
export async function POST(request: NextRequest) {
const { validationProcedure }: { validationProcedure: ValidationProceduresRDS } = await request.json();
- let conn: PoolConnection | null = null;
+ const connectionManager = new ConnectionManager();
try {
- conn = await getConn();
delete validationProcedure['validationID'];
const insertQuery = format('INSERT INTO ?? SET ?', [`catalog.validationprocedures`, validationProcedure]);
- const results = await runQuery(conn, insertQuery);
+ const results = await connectionManager.executeQuery(insertQuery);
const insertID = results.insertId;
return NextResponse.json({ insertID }, { status: HTTPResponses.OK });
} catch (error: any) {
console.error('Error:', error);
+ await connectionManager.rollbackTransaction();
return NextResponse.json({}, { status: HTTPResponses.CONFLICT });
} finally {
- if (conn) conn.release();
+ await connectionManager.closeConnection();
}
}
export async function PATCH(request: NextRequest) {
const { validationProcedure }: { validationProcedure: ValidationProceduresRDS } = await request.json();
- let conn: PoolConnection | null = null;
+ const connectionManager = new ConnectionManager();
try {
- conn = await getConn();
const updatedValidationProcedure = delete validationProcedure['validationID'];
const updateQuery = format('UPDATE ?? SET ? WHERE ValidationID = ?', [
`catalog.validationprocedures`,
updatedValidationProcedure,
validationProcedure.validationID
]);
- await runQuery(conn, updateQuery);
+ await connectionManager.executeQuery(updateQuery);
return NextResponse.json({}, { status: HTTPResponses.OK });
} catch (error: any) {
console.error('Error:', error);
+ await connectionManager.rollbackTransaction();
return NextResponse.json({}, { status: HTTPResponses.CONFLICT });
} finally {
- if (conn) conn.release();
+ await connectionManager.closeConnection();
}
}
export async function DELETE(request: NextRequest) {
const { validationProcedure }: { validationProcedure: ValidationProceduresRDS } = await request.json();
- let conn: PoolConnection | null = null;
+ const connectionManager = new ConnectionManager();
try {
- conn = await getConn();
const deleteQuery = format('DELETE FROM ?? WHERE ValidationID = ?', [`catalog.validationprocedures`, validationProcedure.validationID]);
- await runQuery(conn, deleteQuery);
+ await connectionManager.executeQuery(deleteQuery);
return NextResponse.json({}, { status: HTTPResponses.OK });
} catch (error: any) {
console.error('Error:', error);
+ await connectionManager.rollbackTransaction();
return NextResponse.json({}, { status: HTTPResponses.CONFLICT });
} finally {
- if (conn) conn.release();
+ 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 a68def84..077ecc8b 100644
--- a/frontend/app/api/validations/validationerrordisplay/route.ts
+++ b/frontend/app/api/validations/validationerrordisplay/route.ts
@@ -1,23 +1,24 @@
import { NextRequest, NextResponse } from 'next/server';
-import { getConn, runQuery } from '@/components/processors/processormacros';
-import { PoolConnection } from 'mysql2/promise';
import { CMError } from '@/config/macros/uploadsystemmacros';
import { HTTPResponses } from '@/config/macros';
+import ConnectionManager from '@/config/connectionmanager';
export async function GET(request: NextRequest) {
- let conn: PoolConnection | null = null;
+ 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');
if (!schema) throw new Error('No schema variable provided!');
try {
- conn = await getConn();
-
+ await conn.beginTransaction();
// Query to fetch existing validation errors
const validationErrorsQuery = `
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
@@ -27,13 +28,15 @@ export async function GET(request: NextRequest) {
GROUP BY
cm.CoreMeasurementID;
`;
- const validationErrorsRows = await runQuery(conn, validationErrorsQuery);
+ const validationErrorsRows = await conn.executeQuery(validationErrorsQuery);
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(
JSON.stringify({
failed: parsedValidationErrors
@@ -46,10 +49,11 @@ export async function GET(request: NextRequest) {
}
);
} catch (error: any) {
+ await conn.rollbackTransaction();
return new NextResponse(JSON.stringify({ error: error.message }), {
status: 500
});
} finally {
- if (conn) conn.release();
+ await conn.closeConnection();
}
}
diff --git a/frontend/app/api/validations/validationlist/route.ts b/frontend/app/api/validations/validationlist/route.ts
index 0e3b80bb..d77b426a 100644
--- a/frontend/app/api/validations/validationlist/route.ts
+++ b/frontend/app/api/validations/validationlist/route.ts
@@ -1,7 +1,6 @@
-import { getConn, runQuery } from '@/components/processors/processormacros';
import { HTTPResponses } from '@/config/macros';
-import { PoolConnection } from 'mysql2/promise';
import { NextRequest, NextResponse } from 'next/server';
+import ConnectionManager from '@/config/connectionmanager';
type ValidationProcedure = {
ValidationID: number;
@@ -22,16 +21,15 @@ type ValidationMessages = {
};
export async function GET(request: NextRequest): Promise> {
- let conn: PoolConnection | null = null;
+ const conn = new ConnectionManager();
const schema = request.nextUrl.searchParams.get('schema');
if (!schema) throw new Error('No schema variable provided!');
try {
- conn = await getConn();
const query = `SELECT ValidationID, ProcedureName, Description, Definition FROM catalog.validationprocedures WHERE IsEnabled IS TRUE;`;
- const results: ValidationProcedure[] = await runQuery(conn, query);
+ const results: ValidationProcedure[] = await conn.executeQuery(query);
- const customQuery = `SELECT ValidationProcedureID, Name, Description, Definition FROM ${schema}.sitespecificvalidations;`;
- const customResults: SiteSpecificValidations[] = await runQuery(conn, customQuery);
+ const customQuery = `SELECT ValidationProcedureID, Name, Description, Definition FROM ${schema}.sitespecificvalidations WHERE IsEnabled IS TRUE;`;
+ const customResults: SiteSpecificValidations[] = await conn.executeQuery(customQuery);
const validationMessages: ValidationMessages = results.reduce((acc, { ValidationID, ProcedureName, Description, Definition }) => {
acc[ProcedureName] = { id: ValidationID, description: Description, definition: Definition };
@@ -42,17 +40,17 @@ export async function GET(request: NextRequest): Promise 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}}
);
};
@@ -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 48e70303..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 { ValidationResponse } from '@/components/processors/processormacros';
-import { updateValidatedRows } from '@/components/processors/processorhelperfunctions';
+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/isolatedalltaxonomiesdatagrid.tsx b/frontend/components/datagrids/applications/isolated/isolatedalltaxonomiesdatagrid.tsx
index ee57873a..609025a1 100644
--- a/frontend/components/datagrids/applications/isolated/isolatedalltaxonomiesdatagrid.tsx
+++ b/frontend/components/datagrids/applications/isolated/isolatedalltaxonomiesdatagrid.tsx
@@ -2,7 +2,7 @@
'use client';
import { GridColDef, GridRenderEditCellParams } from '@mui/x-data-grid';
import React, { useEffect, useState } from 'react';
-import { Box, Button, DialogContent, DialogTitle, Modal, ModalClose, ModalDialog, Stack, Typography } from '@mui/joy';
+import { Box, Button, DialogContent, DialogTitle, Modal, ModalClose, ModalDialog } from '@mui/joy';
import { useSession } from 'next-auth/react';
import UploadParentModal from '@/components/uploadsystemhelpers/uploadparentmodal';
import { FormType } from '@/config/macros/formdetails';
@@ -273,37 +273,6 @@ export default function IsolatedAllTaxonomiesViewDataGrid() {
return (
<>
-
-
-
- {session?.user.userStatus !== 'field crew' && (
-
- Note: ADMINISTRATOR VIEW
-
- )}
-
-
-
-
-
-
-
-
-
{
@@ -335,6 +304,10 @@ export default function IsolatedAllTaxonomiesViewDataGrid() {
Species: ['speciesCode', 'speciesName', 'speciesIDLevel', 'speciesAuthority', 'fieldFamily', 'validCode', 'speciesDescription'],
Subspecies: ['subspeciesName', 'subspeciesAuthority']
}}
+ dynamicButtons={[
+ { label: 'Manual Entry Form', onClick: () => setIsManualEntryFormOpen(true) },
+ { label: 'Upload', onClick: () => setIsUploadModalOpen(true) }
+ ]}
/>
{}} sx={{ display: 'flex', flex: 1, alignItems: 'center', justifyContent: 'center' }}>
diff --git a/frontend/components/datagrids/applications/isolated/isolatedattributesdatagrid.tsx b/frontend/components/datagrids/applications/isolated/isolatedattributesdatagrid.tsx
index 3a3dfed9..2c011d4c 100644
--- a/frontend/components/datagrids/applications/isolated/isolatedattributesdatagrid.tsx
+++ b/frontend/components/datagrids/applications/isolated/isolatedattributesdatagrid.tsx
@@ -2,14 +2,11 @@
// isolated attributes datagrid
import React, { useState } from 'react';
-import { Box, Button, Stack, Typography } from '@mui/joy';
-import { useSession } from 'next-auth/react';
import UploadParentModal from '@/components/uploadsystemhelpers/uploadparentmodal';
import { AttributeGridColumns } from '@/components/client/datagridcolumns';
import { FormType } from '@/config/macros/formdetails';
import IsolatedDataGridCommons from '@/components/datagrids/isolateddatagridcommons';
import MultilineModal from '@/components/datagrids/applications/multiline/multilinemodal';
-import { useSiteContext } from '@/app/contexts/userselectionprovider';
export default function IsolatedAttributesDataGrid() {
const initialAttributesRDSRow = {
@@ -21,42 +18,9 @@ export default function IsolatedAttributesDataGrid() {
const [refresh, setRefresh] = useState(false);
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
const [isManualEntryFormOpen, setIsManualEntryFormOpen] = useState(false);
- const { data: session } = useSession();
- const currentSite = useSiteContext();
return (
<>
-
-
-
- {session?.user.userStatus !== 'field crew' && (
-
- Note: ADMINISTRATOR VIEW
-
- )}
-
-
-
-
-
-
-
-
-
{
@@ -87,6 +51,10 @@ export default function IsolatedAttributesDataGrid() {
Description: ['description'],
Status: ['status']
}}
+ dynamicButtons={[
+ { label: 'Manual Entry Form', onClick: () => setIsManualEntryFormOpen(true) },
+ { label: 'Upload', onClick: () => setIsUploadModalOpen(true) }
+ ]}
/>
>
);
diff --git a/frontend/components/datagrids/applications/isolated/isolatedmsvstagingdatagrid.tsx b/frontend/components/datagrids/applications/isolated/isolatedmsvstagingdatagrid.tsx
index c6adc00b..6ed934b6 100644
--- a/frontend/components/datagrids/applications/isolated/isolatedmsvstagingdatagrid.tsx
+++ b/frontend/components/datagrids/applications/isolated/isolatedmsvstagingdatagrid.tsx
@@ -3,8 +3,6 @@
import { MeasurementsSummaryStagingRDS } from '@/config/sqlrdsdefinitions/views';
import { useOrgCensusContext, usePlotContext } from '@/app/contexts/userselectionprovider';
import React, { useState } from 'react';
-import { useSession } from 'next-auth/react';
-import { Box, Button, Typography } from '@mui/joy';
import UploadParentModal from '@/components/uploadsystemhelpers/uploadparentmodal';
import { FormType } from '@/config/macros/formdetails';
import IsolatedDataGridCommons from '@/components/datagrids/isolateddatagridcommons';
@@ -29,8 +27,8 @@ export default function IsolatedMeasurementsSummaryDraftDataGrid() {
speciesCode: '',
treeTag: '',
stemTag: '',
- localX: 0,
- localY: 0,
+ stemLocalX: 0,
+ stemLocalY: 0,
coordinateUnits: '',
measurementDate: null,
measuredDBH: 0,
@@ -42,48 +40,10 @@ export default function IsolatedMeasurementsSummaryDraftDataGrid() {
attributes: ''
};
const [refresh, setRefresh] = useState(false);
- const { data: session } = useSession();
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
return (
<>
-
-
-
- {session?.user.userStatus !== 'field crew' && (
-
- Note: ADMINISTRATOR VIEW
-
- )}
-
- Note: This is a locked view and will not allow modification.
-
-
- Please use this view as a way to confirm changes made to measurements.
-
-
-
- {/* Upload Button */}
-
-
-
{
@@ -99,6 +59,7 @@ export default function IsolatedMeasurementsSummaryDraftDataGrid() {
setRefresh={setRefresh}
initialRow={initialMeasurementsSummaryStagingRDSRow}
fieldToFocus={'quadratName'}
+ dynamicButtons={[{ label: 'Upload', onClick: () => setIsUploadModalOpen(true) }]}
/>
>
);
diff --git a/frontend/components/datagrids/applications/isolated/isolatedpersonneldatagrid.tsx b/frontend/components/datagrids/applications/isolated/isolatedpersonneldatagrid.tsx
index 1bf30e80..a482e716 100644
--- a/frontend/components/datagrids/applications/isolated/isolatedpersonneldatagrid.tsx
+++ b/frontend/components/datagrids/applications/isolated/isolatedpersonneldatagrid.tsx
@@ -5,7 +5,6 @@ import React, { useEffect, useState } from 'react';
import { useSession } from 'next-auth/react';
import { Box, Button, Chip, IconButton, Modal, ModalDialog, Stack, Typography } from '@mui/joy';
import UploadParentModal from '@/components/uploadsystemhelpers/uploadparentmodal';
-import Link from 'next/link';
import { FormType } from '@/config/macros/formdetails';
import { PersonnelGridColumns } from '@/components/client/datagridcolumns';
import { useOrgCensusContext, useSiteContext } from '@/app/contexts/userselectionprovider';
@@ -89,17 +88,11 @@ export default function IsolatedPersonnelDataGrid() {
}}
>
- {session?.user.userStatus !== 'field crew' && (
+ {session && (
- Note: ADMINISTRATOR VIEW
+ Role: {session.user.userStatus}
)}
-
- Note: This is a locked view and will not allow modification.
-
-
- Please use this view as a way to confirm changes made to measurements.
-
@@ -111,11 +104,6 @@ export default function IsolatedPersonnelDataGrid() {
Upload
-
-
-
@@ -174,6 +162,12 @@ export default function IsolatedPersonnelDataGrid() {
Name: ['firstName', 'lastName'],
Role: ['roleID']
}}
+ dynamicButtons={[
+ { label: 'Manual Entry Form', onClick: () => setIsManualEntryFormOpen(true) },
+ { label: 'Upload', onClick: () => setIsUploadModalOpen(true) },
+ { label: 'View Quadrat Personnel', onClick: () => console.log('View Quadrat Personnel clicked') },
+ { label: 'Edit Roles', onClick: () => setIsRolesModalOpen(true) }
+ ]}
/>
>
);
diff --git a/frontend/components/datagrids/applications/isolated/isolatedquadratpersonneldatagrid.tsx b/frontend/components/datagrids/applications/isolated/isolatedquadratpersonneldatagrid.tsx
index e821002b..d256e49f 100644
--- a/frontend/components/datagrids/applications/isolated/isolatedquadratpersonneldatagrid.tsx
+++ b/frontend/components/datagrids/applications/isolated/isolatedquadratpersonneldatagrid.tsx
@@ -121,6 +121,7 @@ export default function IsolatedQuadratPersonnelDataGrid() {
setRefresh={setRefresh}
initialRow={initialQuadratPersonnelRDSRow}
fieldToFocus={'quadratID'}
+ dynamicButtons={[]}
/>
>
);
diff --git a/frontend/components/datagrids/applications/isolated/isolatedquadratsdatagrid.tsx b/frontend/components/datagrids/applications/isolated/isolatedquadratsdatagrid.tsx
index 95552b79..63ea57ae 100644
--- a/frontend/components/datagrids/applications/isolated/isolatedquadratsdatagrid.tsx
+++ b/frontend/components/datagrids/applications/isolated/isolatedquadratsdatagrid.tsx
@@ -1,10 +1,8 @@
// quadrats datagrid
'use client';
import React, { useState } from 'react';
-import { Box, Button, Stack, Typography } from '@mui/joy';
import { useSession } from 'next-auth/react';
import UploadParentModal from '@/components/uploadsystemhelpers/uploadparentmodal';
-import Link from 'next/link';
import { quadratGridColumns } from '@/components/client/datagridcolumns';
import { FormType } from '@/config/macros/formdetails';
import { QuadratRDS } from '@/config/sqlrdsdefinitions/zones';
@@ -37,47 +35,6 @@ export default function IsolatedQuadratsDataGrid() {
return (
<>
-
-
-
- {session?.user.userStatus !== 'field crew' && (
-
- Note: ADMINISTRATOR VIEW
-
- )}
-
- Note: This is a locked view and will not allow modification.
-
-
- Please use this view as a way to confirm changes made to measurements.
-
-
-
-
-
-
-
-
-
-
-
-
{
@@ -108,6 +65,10 @@ export default function IsolatedQuadratsDataGrid() {
Area: ['area', 'areaUnits'],
Misc: ['quadratShape']
}}
+ dynamicButtons={[
+ { label: 'Manual Entry Form', onClick: () => setIsManualEntryFormOpen(true) },
+ { label: 'Upload', onClick: () => setIsUploadModalOpen(true) }
+ ]}
/>
>
);
diff --git a/frontend/components/datagrids/applications/isolated/isolatedrolesdatagrid.tsx b/frontend/components/datagrids/applications/isolated/isolatedrolesdatagrid.tsx
index 380acc36..d330a7d2 100644
--- a/frontend/components/datagrids/applications/isolated/isolatedrolesdatagrid.tsx
+++ b/frontend/components/datagrids/applications/isolated/isolatedrolesdatagrid.tsx
@@ -1,14 +1,12 @@
// roles datagrid
'use client';
import React, { useState } from 'react';
-import { Box, Typography } from '@mui/joy';
-import { useSession } from 'next-auth/react';
import { RolesGridColumns } from '@/components/client/datagridcolumns';
import { RoleRDS } from '@/config/sqlrdsdefinitions/personnel';
import IsolatedDataGridCommons from '@/components/datagrids/isolateddatagridcommons';
type IsolatedRolesDataGridProps = {
- onRolesUpdated: () => void; // Accept the onRolesUpdated prop
+ onRolesUpdated: () => void;
};
export default function IsolatedRolesDataGrid(props: IsolatedRolesDataGridProps) {
@@ -20,32 +18,9 @@ export default function IsolatedRolesDataGrid(props: IsolatedRolesDataGridProps)
roleDescription: ''
};
const [refresh, setRefresh] = useState(false);
- const { data: session } = useSession();
return (
<>
-
-
-
- {session?.user.userStatus !== 'field crew' && (
-
- Note: ADMINISTRATOR VIEW
-
- )}
-
-
-
-
>
);
diff --git a/frontend/components/datagrids/applications/isolated/isolatedstemtaxonomiesviewdatagrid.tsx b/frontend/components/datagrids/applications/isolated/isolatedstemtaxonomiesviewdatagrid.tsx
index 272bf0f9..e7e3a1a0 100644
--- a/frontend/components/datagrids/applications/isolated/isolatedstemtaxonomiesviewdatagrid.tsx
+++ b/frontend/components/datagrids/applications/isolated/isolatedstemtaxonomiesviewdatagrid.tsx
@@ -1,7 +1,6 @@
// stemtaxonomiesview datagrid
'use client';
import React, { useState } from 'react';
-import { Box, Button, Typography } from '@mui/joy';
import { useSession } from 'next-auth/react';
import UploadParentModal from '@/components/uploadsystemhelpers/uploadparentmodal';
import { StemTaxonomiesViewGridColumns } from '@/components/client/datagridcolumns';
@@ -38,33 +37,6 @@ export default function IsolatedStemTaxonomiesViewDataGrid() {
return (
<>
-
-
-
- {session?.user.userStatus !== 'field crew' && (
-
- Note: ADMINISTRATOR VIEW
-
- )}
-
-
- {/* Upload Button */}
-
-
-
-
{
@@ -90,6 +62,7 @@ export default function IsolatedStemTaxonomiesViewDataGrid() {
Species: ['speciesCode', 'speciesName', 'validCode', 'speciesAuthority', 'speciesIDLevel', 'speciesFieldFamily'],
Subspecies: ['subspeciesName', 'subspeciesAuthority']
}}
+ dynamicButtons={[{ label: 'Upload', onClick: () => setIsUploadModalOpen(true) }]}
/>
>
);
diff --git a/frontend/components/datagrids/applications/msvdatagrid.tsx b/frontend/components/datagrids/applications/msvdatagrid.tsx
index 92296e4c..c1c37879 100644
--- a/frontend/components/datagrids/applications/msvdatagrid.tsx
+++ b/frontend/components/datagrids/applications/msvdatagrid.tsx
@@ -4,14 +4,13 @@ import { useOrgCensusContext, usePlotContext, useSiteContext } from '@/app/conte
import React, { useEffect, useState } from 'react';
import { GridRowModes, GridRowModesModel, GridRowsProp } from '@mui/x-data-grid';
import { randomId } from '@mui/x-data-grid-generator';
-import { Box, Button, Snackbar, Stack, Typography } from '@mui/joy';
+import { Snackbar } from '@mui/joy';
import UploadParentModal from '@/components/uploadsystemhelpers/uploadparentmodal';
import MeasurementsCommons from '@/components/datagrids/measurementscommons';
import { MeasurementsSummaryViewGridColumns } from '@/components/client/datagridcolumns';
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,
@@ -80,7 +79,6 @@ export default function MeasurementsSummaryViewDataGrid() {
const addNewRowToGrid = () => {
const id = randomId();
- // Define new row structure based on MeasurementsSummaryRDS type
const newRow = {
...initialMeasurementsSummaryViewRDSRow,
id: id,
@@ -113,46 +111,6 @@ export default function MeasurementsSummaryViewDataGrid() {
)}
-
-
-
-
-
-
- Note: This plot does not accept subquadrats.
- Please ensure that you use quadrat names when submitting new measurements instead of subquadrat names
-
-
-
-
-
-
-
-
-
-
{
@@ -190,6 +148,10 @@ export default function MeasurementsSummaryViewDataGrid() {
shouldAddRowAfterFetch={shouldAddRowAfterFetch}
setShouldAddRowAfterFetch={setShouldAddRowAfterFetch}
addNewRowToGrid={addNewRowToGrid}
+ dynamicButtons={[
+ { label: 'Manual Entry Form', onClick: () => setIsManualEntryFormOpen(true) },
+ { label: 'Upload', onClick: () => setIsUploadModalOpen(true) }
+ ]}
/>
{
const handleExportClick = async () => {
if (!handleExportAll) return;
- const fullData = await handleExportAll(filterModel);
+ const fullData = await handleExportAll();
const blob = new Blob([JSON.stringify(fullData, null, 2)], {
type: 'application/json'
});
diff --git a/frontend/components/datagrids/applications/viewfulltabledatagrid.tsx b/frontend/components/datagrids/applications/viewfulltabledatagrid.tsx
index d8707c68..ab525ef8 100644
--- a/frontend/components/datagrids/applications/viewfulltabledatagrid.tsx
+++ b/frontend/components/datagrids/applications/viewfulltabledatagrid.tsx
@@ -1,11 +1,9 @@
// viewfulltable view datagrid
'use client';
-import { Box, Typography } from '@mui/joy';
import { AlertProps } from '@mui/material';
import { GridRowsProp } from '@mui/x-data-grid';
import { randomId } from '@mui/x-data-grid-generator';
-import { useSession } from 'next-auth/react';
import { useEffect, useState } from 'react';
import { ViewFullTableGridColumns } from '@/components/client/datagridcolumns';
import MeasurementsCommons from '@/components/datagrids/measurementscommons';
@@ -120,7 +118,6 @@ export default function ViewFullTableDataGrid() {
});
const [isNewRowAdded, setIsNewRowAdded] = useState(false);
const [shouldAddRowAfterFetch, setShouldAddRowAfterFetch] = useState(false);
- const { data: session } = useSession();
const { setLoading } = useLoading();
const currentSite = useSiteContext();
@@ -140,71 +137,57 @@ export default function ViewFullTableDataGrid() {
};
async function reloadVFT() {
- setLoading(true, 'Refreshing Historical View...');
- const response = await fetch(`/api/refreshviews/viewfulltable/${currentSite?.schemaName ?? ''}`, { method: 'POST' });
- if (!response.ok) throw new Error('Historical View Refresh failure');
- await new Promise(resolve => setTimeout(resolve, 1000));
+ try {
+ setLoading(true, 'Refreshing Historical View...');
+ const startTime = Date.now();
+ const response = await fetch(`/api/refreshviews/viewfulltable/${currentSite?.schemaName ?? ''}`, { method: 'POST' });
+ if (!response.ok) throw new Error('Historical View Refresh failure');
+ setLoading(true, 'Processing data...');
+ await response.json();
+ 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(() => {
- reloadVFT()
- .catch(console.error)
- .then(() => setLoading(false));
- }, []);
+ let isMounted = true;
+ if (isMounted) {
+ reloadVFT().catch(console.error);
+ }
+ return () => {
+ isMounted = false;
+ };
+ }, [refresh]);
return (
<>
-
-
-
- {session?.user.userStatus !== 'field crew' && (
-
- Note: ADMINISTRATOR VIEW
-
- )}
-
-
-
-
-
+
>
);
}
diff --git a/frontend/components/datagrids/datagridcommons.tsx b/frontend/components/datagrids/datagridcommons.tsx
index 0bd6f4b5..192e4bce 100644
--- a/frontend/components/datagrids/datagridcommons.tsx
+++ b/frontend/components/datagrids/datagridcommons.tsx
@@ -54,7 +54,7 @@ type EditToolbarProps = EditToolbarCustomProps & GridToolbarProps & ToolbarProps
const EditToolbar = ({ handleAddNewRow, handleRefresh, handleExportAll, locked, filterModel }: EditToolbarProps) => {
const handleExportClick = async () => {
if (!handleExportAll) return;
- const fullData = await handleExportAll(filterModel);
+ const fullData = await handleExportAll();
const blob = new Blob([JSON.stringify(fullData, null, 2)], {
type: 'application/json'
});
diff --git a/frontend/components/datagrids/filtrationsystem.tsx b/frontend/components/datagrids/filtrationsystem.tsx
new file mode 100644
index 00000000..18eb2425
--- /dev/null
+++ b/frontend/components/datagrids/filtrationsystem.tsx
@@ -0,0 +1,133 @@
+'use client';
+
+import { GetApplyQuickFilterFn, GridColDef, GridFilterInputValueProps, GridFilterOperator, useGridRootProps } from '@mui/x-data-grid';
+import { TextField, TextFieldProps } from '@mui/material';
+import { useEffect, useRef, useState } from 'react';
+import { Box } from '@mui/joy';
+import SyncIcon from '@mui/icons-material/Sync';
+
+// starting with quick filter
+export const getApplyQuickFilterFnSameYear: GetApplyQuickFilterFn = value => {
+ if (!value || value.length !== 4 || !/\d{4}/.test(value)) {
+ return null;
+ }
+ return cellValue => {
+ if (cellValue instanceof Date) {
+ return cellValue.getFullYear() === Number(value);
+ }
+ return false;
+ };
+};
+
+export function applyFilterToColumns(columns: GridColDef[]) {
+ return columns.map(column => {
+ if (column.field === 'dateCreated') {
+ return {
+ ...column,
+ getApplyQuickFilterFn: getApplyQuickFilterFnSameYear
+ };
+ }
+ if (column.field === 'name') {
+ return {
+ ...column,
+ getApplyQuickFilterFn: undefined
+ };
+ }
+ return column;
+ });
+}
+
+// customizing full filtration system
+// multiple values operator:
+function InputNumberInterval(props: GridFilterInputValueProps) {
+ const rootProps = useGridRootProps();
+ const { item, applyValue, focusElementRef = null } = props;
+
+ const filterTimeout = useRef();
+ const [filterValueState, setFilterValueState] = useState<[string, string]>(item.value ?? '');
+ const [applying, setIsApplying] = useState(false);
+
+ useEffect(() => {
+ return () => {
+ clearTimeout(filterTimeout.current);
+ };
+ }, []);
+
+ useEffect(() => {
+ const itemValue = item.value ?? [undefined, undefined];
+ setFilterValueState(itemValue);
+ }, [item.value]);
+
+ const updateFilterValue = (lowerBound: string, upperBound: string) => {
+ clearTimeout(filterTimeout.current);
+ setFilterValueState([lowerBound, upperBound]);
+
+ setIsApplying(true);
+ filterTimeout.current = setTimeout(() => {
+ setIsApplying(false);
+ applyValue({ ...item, value: [lowerBound, upperBound] });
+ }, rootProps.filterDebounceMs);
+ };
+
+ const handleUpperFilterChange: TextFieldProps['onChange'] = event => {
+ const newUpperBound = event.target.value;
+ updateFilterValue(filterValueState[0], newUpperBound);
+ };
+ const handleLowerFilterChange: TextFieldProps['onChange'] = event => {
+ const newLowerBound = event.target.value;
+ updateFilterValue(newLowerBound, filterValueState[1]);
+ };
+
+ return (
+
+
+ } : undefined
+ }}
+ />
+
+ );
+}
+
+export const betweenOperator: GridFilterOperator = {
+ label: 'Between',
+ value: 'between',
+ getApplyFilterFn: filterItem => {
+ if (!Array.isArray(filterItem.value) || filterItem.value.length !== 2) {
+ return null;
+ }
+ if (filterItem.value[0] == null || filterItem.value[1] == null) {
+ return null;
+ }
+ return (value: number) => {
+ return value != null && filterItem.value[0] <= value && value <= filterItem.value[1];
+ };
+ },
+ InputComponent: InputNumberInterval
+};
diff --git a/frontend/components/datagrids/isolateddatagridcommons.tsx b/frontend/components/datagrids/isolateddatagridcommons.tsx
index b4ec5f18..50e9a1ab 100644
--- a/frontend/components/datagrids/isolateddatagridcommons.tsx
+++ b/frontend/components/datagrids/isolateddatagridcommons.tsx
@@ -5,6 +5,7 @@ import {
createDeleteQuery,
createFetchQuery,
createPostPatchQuery,
+ createQFFetchQuery,
EditToolbarCustomProps,
filterColumns,
getColumnVisibilityModel,
@@ -23,23 +24,21 @@ import {
GridRowModes,
GridRowModesModel,
GridRowsProp,
- GridToolbar,
GridToolbarContainer,
GridToolbarProps,
+ GridToolbarQuickFilter,
ToolbarPropsOverrides,
useGridApiRef
} from '@mui/x-data-grid';
-import { Alert, AlertProps, Button, Snackbar } from '@mui/material';
+import { Alert, AlertProps, Button, IconButton, Snackbar } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import RefreshIcon from '@mui/icons-material/Refresh';
-import FileDownloadIcon from '@mui/icons-material/FileDownload';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useOrgCensusContext, usePlotContext, useQuadratContext, useSiteContext } from '@/app/contexts/userselectionprovider';
-import { useLoading } from '@/app/contexts/loadingprovider';
import { useDataValidityContext } from '@/app/contexts/datavalidityprovider';
import { useSession } from 'next-auth/react';
import { HTTPResponses, UnifiedValidityFlags } from '@/config/macros';
-import { Tooltip, Typography } from '@mui/joy';
+import { Dropdown, Menu, MenuButton, MenuItem, Stack, Tooltip } from '@mui/joy';
import SaveIcon from '@mui/icons-material/Save';
import CancelIcon from '@mui/icons-material/Close';
import EditIcon from '@mui/icons-material/Edit';
@@ -50,49 +49,155 @@ import { StyledDataGrid } from '@/config/styleddatagrid';
import ConfirmationDialog from '@/components/datagrids/confirmationdialog';
import { randomId } from '@mui/x-data-grid-generator';
import SkipReEnterDataModal from '@/components/datagrids/skipreentrydatamodal';
-import { FileDownloadTwoTone } from '@mui/icons-material';
import { FormType, getTableHeaders } from '@/config/macros/formdetails';
+import { applyFilterToColumns } from '@/components/datagrids/filtrationsystem';
+import { ClearIcon } from '@mui/x-date-pickers';
+import CloudDownloadIcon from '@mui/icons-material/CloudDownload';
type EditToolbarProps = EditToolbarCustomProps & GridToolbarProps & ToolbarPropsOverrides;
-const EditToolbar = ({ handleAddNewRow, handleRefresh, handleExportAll, handleExportCSV, locked, filterModel }: EditToolbarProps) => {
- if (!handleAddNewRow || !handleRefresh) return <>>;
- const handleExportClick = async () => {
- if (!handleExportAll) return;
- const fullData = await handleExportAll(filterModel);
- const blob = new Blob([JSON.stringify(fullData, null, 2)], {
- type: 'application/json'
+const EditToolbar = (props: EditToolbarProps) => {
+ const { handleAddNewRow, handleRefresh, handleExportAll, handleExportCSV, handleQuickFilterChange, locked, filterModel, dynamicButtons = [] } = props;
+ if (!handleAddNewRow || !handleRefresh || !handleQuickFilterChange || !handleExportAll) return <>>;
+ const [inputValue, setInputValue] = useState('');
+ const [isTyping, setIsTyping] = useState(false);
+
+ const handleInputChange = (event: React.ChangeEvent) => {
+ setInputValue(event.target.value);
+ setIsTyping(true);
+ };
+
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (event.key === 'Enter') {
+ handleQuickFilterChange({
+ ...filterModel,
+ items: filterModel?.items || [],
+ quickFilterValues: inputValue.split(' ') || []
+ });
+ setIsTyping(false);
+ }
+ };
+
+ const handleClearInput = () => {
+ setInputValue('');
+ handleQuickFilterChange({
+ ...filterModel,
+ items: filterModel?.items || [],
+ quickFilterValues: []
});
+ setIsTyping(false);
+ };
+
+ useEffect(() => {
+ if (isTyping) {
+ const timeout = setTimeout(() => setIsTyping(false), 2000);
+ return () => clearTimeout(timeout);
+ }
+ }, [isTyping, inputValue]);
+
+ function exportFilterModel() {
+ const jsonData = JSON.stringify(filterModel, null, 2);
+ const blob = new Blob([jsonData], { type: 'application/json' });
const url = URL.createObjectURL(blob);
+
const link = document.createElement('a');
link.href = url;
- link.download = 'data.json';
- document.body.appendChild(link);
+ link.download = 'results.json';
link.click();
- document.body.removeChild(link);
- };
+
+ URL.revokeObjectURL(url);
+ }
return (
-
- } onClick={async () => await handleAddNewRow()} disabled={locked}>
- Add Row
-
- } onClick={async () => await handleRefresh()}>
- Refresh
-
- } onClick={handleExportClick}>
- Export Full Data
-
- } onClick={handleExportCSV}>
- Export as Form CSV
-
+
+
+
+
+
+
+
+
+
+
+
+
+ } onClick={async () => await handleAddNewRow()} disabled={locked}>
+ Add Row
+
+ } onClick={async () => await handleRefresh()}>
+ Refresh
+
+
+
+ }
+ >
+ Export...
+
+
+
+
+
+
+ {dynamicButtons.map((button: any, index: number) => (
+
+ ))}
+
);
};
export default function IsolatedDataGridCommons(props: Readonly) {
- const { gridColumns, gridType, refresh, setRefresh, locked = false, selectionOptions, initialRow, fieldToFocus, clusters } = props;
+ const { gridColumns, gridType, refresh, setRefresh, locked = false, selectionOptions, initialRow, fieldToFocus, clusters, dynamicButtons = [] } = props;
const [rows, setRows] = useState([initialRow] as GridRowsProp);
const [rowCount, setRowCount] = useState(0);
@@ -102,11 +207,13 @@ export default function IsolatedDataGridCommons(props: Readonly(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
+ const [usingQuery, setUsingQuery] = useState('');
const [pendingAction, setPendingAction] = useState({
actionType: '',
actionId: null
@@ -118,81 +225,45 @@ export default function IsolatedDataGridCommons(props: Readonly(null);
const [filterModel, setFilterModel] = useState({
- items: []
+ items: [],
+ quickFilterValues: []
});
- const [rowsUpdated, setRowsUpdated] = useState(false);
- const [rowModesModelUpdated, setRowModesModelUpdated] = useState(false);
const currentPlot = usePlotContext();
const currentCensus = useOrgCensusContext();
const currentQuadrat = useQuadratContext();
const currentSite = useSiteContext();
- const { setLoading } = useLoading();
const { triggerRefresh } = useDataValidityContext();
useSession();
const apiRef = useGridApiRef();
- // Track when rows and rowModesModel are updated using useEffect
- useEffect(() => {
- if (rows.length > 0) {
- setRowsUpdated(true);
- }
- }, [rows]);
-
- useEffect(() => {
- if (Object.keys(rowModesModel).length > 0) {
- setRowModesModelUpdated(true);
- }
- }, [rowModesModel]);
-
- // Function to wait for rows and rowModesModel to update
- const waitForStateUpdates = async () => {
- return new Promise(resolve => {
- const checkUpdates = () => {
- if (rowsUpdated && rowModesModelUpdated) {
- resolve();
- } else {
- setTimeout(checkUpdates, 50); // Check every 50ms until both are updated
- }
- };
- checkUpdates();
- });
- };
-
- useEffect(() => {
- if (!isNewRowAdded) {
- fetchPaginatedData(paginationModel.page).catch(console.error);
- }
- }, [paginationModel.page]);
-
useEffect(() => {
- if (currentPlot?.plotID || currentCensus?.plotCensusNumber) {
+ if (currentPlot?.plotID || currentCensus?.plotCensusNumber || !isNewRowAdded) {
fetchPaginatedData(paginationModel.page).catch(console.error);
}
- }, [currentPlot, currentCensus, paginationModel.page]);
+ }, [currentPlot, currentCensus, paginationModel.page, filterModel]);
useEffect(() => {
if (refresh && currentSite) {
handleRefresh().then(() => {
if (refresh) {
- setRefresh(false); // Only update state if it hasn't already been reset
+ setRefresh(false);
}
});
}
- }, [refresh, currentSite]); // No need for setRefresh in dependencies
+ }, [refresh, currentSite]);
useEffect(() => {
const updatedRowModesModel = rows.reduce((acc, row) => {
if (row.id) {
- acc[row.id] = rowModesModel[row.id] || { mode: GridRowModes.View }; // Ensure valid row ID is used
+ acc[row.id] = rowModesModel[row.id] || { mode: GridRowModes.View };
}
return acc;
}, {} as GridRowModesModel);
- // Clean invalid rowModesModel entries like '0'
const cleanedRowModesModel = Object.fromEntries(Object.entries(updatedRowModesModel).filter(([key]) => key !== '0'));
if (JSON.stringify(cleanedRowModesModel) !== JSON.stringify(rowModesModel)) {
@@ -201,30 +272,226 @@ export default function IsolatedDataGridCommons(props: Readonly {
- setLoading(true, 'Fetching full dataset...');
- let partialQuery = ``;
- if (currentPlot?.plotID) partialQuery += `/${currentPlot.plotID}`;
- if (currentCensus?.plotCensusNumber) partialQuery += `/${currentCensus.plotCensusNumber}`;
- if (currentQuadrat?.quadratID) partialQuery += `/${currentQuadrat.quadratID}`;
- const fullDataQuery = `/api/fetchall/${gridType}` + partialQuery + `?schema=${currentSite?.schemaName}`;
-
+ setLoading(true);
try {
- const response = await fetch(fullDataQuery, {
- method: 'GET',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(filterModel)
- });
- const data = await response.json();
- if (!response.ok) throw new Error(data.message || 'Error fetching full data');
- return data.output;
+ const reworkedQuery = usingQuery
+ .replace(/\bSQL_CALC_FOUND_ROWS\b\s*/i, '')
+ .replace(/\bLIMIT\s+\d+\s*,\s*\d+/i, '')
+ .trim();
+
+ const results = await (
+ await fetch(`/api/runquery`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(reworkedQuery)
+ })
+ ).json();
+
+ const jsonData = JSON.stringify(results, null, 2);
+ const blob = new Blob([jsonData], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = 'results.json';
+ link.click();
+
+ URL.revokeObjectURL(url);
} catch (error) {
console.error('Error fetching full data:', error);
setSnackbar({ children: 'Error fetching full data', severity: 'error' });
- return [];
} finally {
setLoading(false);
}
- }, [filterModel, currentPlot, currentCensus, currentQuadrat, currentSite, gridType, setLoading]);
+ }, [usingQuery, filterModel, currentPlot, currentCensus, currentQuadrat, currentSite, gridType, setLoading]);
+
+ const exportAllCSV = useCallback(async () => {
+ setLoading(true);
+ switch (gridType) {
+ case 'attributes':
+ const aResponse = await fetch(
+ `/api/formdownload/attributes/${currentSite?.schemaName ?? ''}/${currentPlot?.plotID ?? 0}/${currentCensus?.dateRanges[0].censusID ?? 0}`,
+ { method: 'GET' }
+ );
+ const aData = await aResponse.json();
+ let aCSVRows =
+ getTableHeaders(FormType.attributes)
+ .map(row => row.label)
+ .join(',') + '\n';
+ aData.forEach((row: any) => {
+ const values = getTableHeaders(FormType.attributes)
+ .map(rowHeader => rowHeader.label)
+ .map(header => row[header])
+ .map(value => {
+ if (value === undefined || value === null || value === '') {
+ return null;
+ }
+ if (typeof value === 'number') {
+ return value;
+ }
+ const parsedValue = parseFloat(value);
+ if (!isNaN(parsedValue)) {
+ return parsedValue;
+ }
+ if (typeof value === 'string') {
+ value = value.replace(/"/g, '""');
+ value = `"${value}"`;
+ }
+
+ return value;
+ });
+ aCSVRows += values.join(',') + '\n';
+ });
+ const aBlob = new Blob([aCSVRows], {
+ type: 'text/csv;charset=utf-8;'
+ });
+ const aURL = URL.createObjectURL(aBlob);
+ const aLink = document.createElement('a');
+ aLink.href = aURL;
+ aLink.download = `attributesform_${currentSite?.schemaName ?? ''}_${currentPlot?.plotName ?? ''}_${currentCensus?.plotCensusNumber ?? 0}.csv`;
+ document.body.appendChild(aLink);
+ aLink.click();
+ document.body.removeChild(aLink);
+ break;
+ case 'quadrats':
+ const qResponse = await fetch(
+ `/api/formdownload/quadrats/${currentSite?.schemaName ?? ''}/${currentPlot?.plotID ?? 0}/${currentCensus?.dateRanges[0].censusID ?? 0}`,
+ { method: 'GET' }
+ );
+ const qData = await qResponse.json();
+ let qCSVRows =
+ getTableHeaders(FormType.quadrats)
+ .map(row => row.label)
+ .join(',') + '\n';
+ qData.forEach((row: any) => {
+ const values = getTableHeaders(FormType.quadrats)
+ .map(rowHeader => rowHeader.label)
+ .map(header => row[header])
+ .map(value => {
+ if (value === undefined || value === null || value === '') {
+ return null;
+ }
+ if (typeof value === 'number') {
+ return value;
+ }
+ const parsedValue = parseFloat(value);
+ if (!isNaN(parsedValue)) {
+ return parsedValue;
+ }
+ if (typeof value === 'string') {
+ value = value.replace(/"/g, '""');
+ value = `"${value}"`;
+ }
+
+ return value;
+ });
+ qCSVRows += values.join(',') + '\n';
+ });
+ const qBlob = new Blob([qCSVRows], {
+ type: 'text/csv;charset=utf-8;'
+ });
+ const qURL = URL.createObjectURL(qBlob);
+ const qLink = document.createElement('a');
+ qLink.href = qURL;
+ qLink.download = `quadratsform_${currentSite?.schemaName ?? ''}_${currentPlot?.plotName ?? ''}_${currentCensus?.plotCensusNumber ?? 0}.csv`;
+ document.body.appendChild(qLink);
+ qLink.click();
+ document.body.removeChild(qLink);
+ break;
+ case 'personnel':
+ const pResponse = await fetch(
+ `/api/formdownload/personnel/${currentSite?.schemaName ?? ''}/${currentPlot?.plotID ?? 0}/${currentCensus?.dateRanges[0].censusID ?? 0}`,
+ { method: 'GET' }
+ );
+ const pData = await pResponse.json();
+ let pCSVRows =
+ getTableHeaders(FormType.personnel)
+ .map(row => row.label)
+ .join(',') + '\n';
+ pData.forEach((row: any) => {
+ const values = getTableHeaders(FormType.personnel)
+ .map(rowHeader => rowHeader.label)
+ .map(header => row[header])
+ .map(value => {
+ if (value === undefined || value === null || value === '') {
+ return null;
+ }
+ if (typeof value === 'number') {
+ return value;
+ }
+ const parsedValue = parseFloat(value);
+ if (!isNaN(parsedValue)) {
+ return parsedValue;
+ }
+ if (typeof value === 'string') {
+ value = value.replace(/"/g, '""');
+ value = `"${value}"`;
+ }
+
+ return value;
+ });
+ pCSVRows += values.join(',') + '\n';
+ });
+ const pBlob = new Blob([pCSVRows], {
+ type: 'text/csv;charset=utf-8;'
+ });
+ const pURL = URL.createObjectURL(pBlob);
+ const pLink = document.createElement('a');
+ pLink.href = pURL;
+ pLink.download = `personnelform_${currentSite?.schemaName ?? ''}_${currentPlot?.plotName ?? ''}_${currentCensus?.plotCensusNumber ?? 0}.csv`;
+ document.body.appendChild(pLink);
+ pLink.click();
+ document.body.removeChild(pLink);
+ break;
+ case 'species':
+ case 'alltaxonomiesview':
+ const sResponse = await fetch(
+ `/api/formdownload/species/${currentSite?.schemaName ?? ''}/${currentPlot?.plotID ?? 0}/${currentCensus?.dateRanges[0].censusID ?? 0}`,
+ { method: 'GET' }
+ );
+ const sData = await sResponse.json();
+ let sCSVRows =
+ getTableHeaders(FormType.species)
+ .map(row => row.label)
+ .join(',') + '\n';
+ sData.forEach((row: any) => {
+ const values = getTableHeaders(FormType.species)
+ .map(rowHeader => rowHeader.label)
+ .map(header => row[header])
+ .map(value => {
+ if (value === undefined || value === null || value === '') {
+ return null;
+ }
+ if (typeof value === 'number') {
+ return value;
+ }
+ const parsedValue = parseFloat(value);
+ if (!isNaN(parsedValue)) {
+ return parsedValue;
+ }
+ if (typeof value === 'string') {
+ value = value.replace(/"/g, '""');
+ value = `"${value}"`;
+ }
+
+ return value;
+ });
+ sCSVRows += values.join(',') + '\n';
+ });
+ const sBlob = new Blob([sCSVRows], {
+ type: 'text/csv;charset=utf-8;'
+ });
+ const sURL = URL.createObjectURL(sBlob);
+ const sLink = document.createElement('a');
+ sLink.href = sURL;
+ sLink.download = `speciesform_${currentSite?.schemaName ?? ''}_${currentPlot?.plotName ?? ''}_${currentCensus?.plotCensusNumber ?? 0}.csv`;
+ document.body.appendChild(sLink);
+ sLink.click();
+ document.body.removeChild(sLink);
+ break;
+ }
+ setLoading(false);
+ }, [currentPlot, currentCensus, currentSite, gridType]);
const exportAllCSV = useCallback(async () => {
switch (gridType) {
@@ -421,7 +688,6 @@ export default function IsolatedDataGridCommons(props: Readonly {
if (locked || !promiseArguments) return;
- setLoading(true, 'Saving changes...');
+ setLoading(true);
try {
- // Set the row to view mode after confirmation
setRowModesModel(prevModel => ({
...prevModel,
[id]: { mode: GridRowModes.View }
@@ -493,12 +753,13 @@ export default function IsolatedDataGridCommons(props: Readonly {
if (locked) return;
- setLoading(true, 'Deleting...');
+ setLoading(true);
const rowToDelete = rows.find(row => String(row.id) === String(id));
- if (!rowToDelete) return; // Ensure row exists
+ if (!rowToDelete) return;
const deleteQuery = createDeleteQuery(currentSite?.schemaName ?? '', gridType, getGridID(gridType), rowToDelete.id);
@@ -554,7 +815,7 @@ export default function IsolatedDataGridCommons(props: Readonly prevRows.filter(row => row.id !== id)); // Update rows by removing the deleted row
+ setRows(prevRows => prevRows.filter(row => row.id !== id));
setSnackbar({
children: 'Row successfully deleted',
severity: 'success'
@@ -575,33 +836,28 @@ export default function IsolatedDataGridCommons(props: Readonly async () => {
+ (id: GridRowId) => () => {
if (locked) return;
const updatedRowModesModel = { ...rowModesModel };
if (!updatedRowModesModel[id] || updatedRowModesModel[id].mode === undefined) {
- updatedRowModesModel[id] = { mode: GridRowModes.View }; // Set default mode if it doesn't exist
+ updatedRowModesModel[id] = { mode: GridRowModes.View };
}
- // Stop edit mode and apply changes locally without committing to the server yet
apiRef.current.stopRowEditMode({ id, ignoreModifications: true });
- // Get the original row data (before edits)
const oldRow = rows.find(row => String(row.id) === String(id));
- // Use getRowWithUpdatedValues to fetch all updated field values (the field is ignored in row editing mode)
- const updatedRow = apiRef.current.getRowWithUpdatedValues(id, 'anyField'); // 'anyField' is a dummy value, ignored in row editing
+ const updatedRow = apiRef.current.getRowWithUpdatedValues(id, 'anyField');
if (oldRow && updatedRow) {
- // Set promise arguments before opening the modal
setPromiseArguments({
- resolve: (value: GridRowModel) => {}, // Define resolve
- reject: (reason?: any) => {}, // Define reject
- oldRow, // Pass the old (original) row
- newRow: updatedRow // Pass the updated (edited) row
+ resolve: (value: GridRowModel) => {},
+ reject: (reason?: any) => {},
+ oldRow,
+ newRow: updatedRow
});
- // Open the confirmation dialog for reentry data
openConfirmationDialog('save', id);
}
},
@@ -623,9 +879,9 @@ export default function IsolatedDataGridCommons(props: Readonly {
+ const handleAddNewRow = useCallback(() => {
if (locked) return;
- if (isNewRowAdded) return; // Debounce double adds
+ if (isNewRowAdded) return;
const newRowCount = rowCount + 1;
const calculatedNewLastPage = Math.ceil(newRowCount / paginationModel.pageSize) - 1;
const existingLastPage = Math.ceil(rowCount / paginationModel.pageSize) - 1;
@@ -635,7 +891,7 @@ export default function IsolatedDataGridCommons(props: Readonly {
return [...prevRows, newRow];
@@ -643,7 +899,7 @@ export default function IsolatedDataGridCommons(props: Readonly {
return {
...prevModel,
- [id]: { mode: GridRowModes.Edit, fieldToFocus } // Add the new row with 'Edit' mode
+ [id]: { mode: GridRowModes.Edit, fieldToFocus }
};
});
@@ -657,26 +913,45 @@ export default function IsolatedDataGridCommons(props: Readonly {
- setLoading(true, 'Loading data...');
- const paginatedQuery = createFetchQuery(
- currentSite?.schemaName ?? '',
- gridType,
- pageToFetch,
- paginationModel.pageSize,
- currentPlot?.plotID,
- currentCensus?.plotCensusNumber,
- currentQuadrat?.quadratID
- );
+ setLoading(true);
+ const paginatedQuery =
+ (filterModel.items && filterModel.items.length > 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
+ );
try {
- const response = await fetch(paginatedQuery, { method: 'GET' });
+ const response = await fetch(paginatedQuery, {
+ method:
+ (filterModel.items && filterModel.items.length > 0) || (filterModel.quickFilterValues && filterModel.quickFilterValues.length > 0) ? 'POST' : 'GET',
+ headers: { 'Content-Type': 'application/json' },
+ body:
+ (filterModel.items && filterModel.items.length > 0) || (filterModel.quickFilterValues && filterModel.quickFilterValues.length > 0)
+ ? JSON.stringify({ filterModel })
+ : undefined
+ });
const data = await response.json();
if (!response.ok) throw new Error(data.message || 'Error fetching data');
- console.log('rows: ', data.output);
setRows(data.output);
setRowCount(data.totalCount);
+ setUsingQuery(data.finishedQuery);
if (isNewRowAdded && pageToFetch === newLastPage) {
- await handleAddNewRow();
+ handleAddNewRow();
}
} catch (error) {
console.error('Error fetching data:', error);
@@ -704,8 +979,9 @@ export default function IsolatedDataGridCommons(props: Readonly {
- // If the row is newly added and is being canceled, skip the update
if (newRow?.isNew && !newRow?.id) {
- return oldRow; // Return the old row without making changes
+ return oldRow;
}
- setLoading(true, 'Processing changes...');
+ setLoading(true);
- // Handle new rows by confirming the save action with the user
if (newRow.isNew || !newRow.id) {
setPromiseArguments({
resolve: async (confirmedRow: GridRowModel) => {
@@ -781,7 +1055,6 @@ export default function IsolatedDataGridCommons(props: Readonly {
const firstEditableColumn = filteredColumns.find(col => col.editable);
if (firstEditableColumn) {
@@ -858,19 +1129,16 @@ export default function IsolatedDataGridCommons(props: Readonly String(row.id) === String(id));
if (row?.isNew) {
- // Remove the new row from the rows state
setRows(oldRows => oldRows.filter(row => row.id !== id));
- // Safely remove the row from rowModesModel
setRowModesModel(prevModel => {
const updatedModel = { ...prevModel };
- delete updatedModel[id]; // Remove the newly added row from rowModesModel
+ delete updatedModel[id];
return updatedModel;
});
- setIsNewRowAdded(false); // Reset the flag indicating a new row was added
+ setIsNewRowAdded(false);
} else {
- // Revert the row to view mode if it's an existing row
setRowModesModel(prevModel => ({
...prevModel,
[id]: { mode: GridRowModes.View, ignoreModifications: true }
@@ -880,6 +1148,14 @@ export default function IsolatedDataGridCommons(props: Readonly ({
+ ...prevFilterModel,
+ items: [...(incomingValues.items || [])],
+ quickFilterValues: [...(incomingValues.quickFilterValues || [])]
+ }));
+ }
+
const getEnhancedCellAction = useCallback(
(type: string, icon: any, onClick: any) => (
@@ -931,16 +1207,10 @@ export default function IsolatedDataGridCommons(props: Readonly {
- return [...gridColumns, getGridActionsColumn()];
+ return [...applyFilterToColumns(gridColumns), getGridActionsColumn()];
}, [gridColumns, rowModesModel, getGridActionsColumn]);
- const filteredColumns = useMemo(() => {
- console.log('columns unfiltered: ', columns);
- console.log('rows: ', rows);
- console.log('filtered: ', filterColumns(rows, columns));
- if (gridType !== 'quadratpersonnel') return filterColumns(rows, columns);
- else return columns;
- }, [rows, columns]);
+ const filteredColumns = useMemo(() => (gridType !== 'quadratpersonnel' ? filterColumns(rows, columns) : columns), [rows, columns]);
const handleCellDoubleClick: GridEventListener<'cellDoubleClick'> = params => {
if (locked) return;
@@ -950,23 +1220,12 @@ export default function IsolatedDataGridCommons(props: Readonly = (params, event) => {
+ const handleCellKeyDown: GridEventListener<'cellKeyDown'> = (_params, event) => {
if (event.key === 'Enter' && !locked) {
event.defaultMuiPrevented = true;
- // console.log('params: ', params);
- // setRowModesModel(prevModel => ({
- // ...prevModel,
- // [params.id]: { mode: GridRowModes.Edit }
- // }));
}
if (event.key === 'Escape') {
event.defaultMuiPrevented = true;
- // console.log('params: ', params);
- // setRowModesModel(prevModel => ({
- // ...prevModel,
- // [params.id]: { mode: GridRowModes.View, ignoreModifications: true }
- // }));
- // handleCancelClick(params.id, event);
}
};
@@ -986,12 +1245,11 @@ export default function IsolatedDataGridCommons(props: Readonly
- Note: The Grid is filtered by your selected Plot and Plot ID
{
- // Create a promise to wait for state updates
const waitForStateUpdates = async () => {
return new Promise(resolve => {
const checkUpdates = () => {
if (rows.length > 0 && Object.keys(rowModesModel).length > 0) {
- resolve(); // Resolve when states are updated
+ resolve();
} else {
- setTimeout(checkUpdates, 50); // Retry every 50ms
+ setTimeout(checkUpdates, 50);
}
};
checkUpdates();
});
};
-
- // Wait for rows and rowModesModel to update
await waitForStateUpdates();
-
- // Now call the actual processRowUpdate logic after the state has settled
try {
return await processRowUpdate(newRow, oldRow);
} catch (error) {
console.error('Error processing row update:', error);
setSnackbar({ children: 'Error updating row', severity: 'error' });
- return Promise.reject(error); // Handle error if needed
+ return Promise.reject(error);
}
}}
onProcessRowUpdateError={error => {
@@ -1033,7 +1286,7 @@ export default function IsolatedDataGridCommons(props: Readonly setFilterModel(newFilterModel)}
+ ignoreDiacritics
initialState={{
columns: {
columnVisibilityModel: getColumnVisibilityModel(gridType)
@@ -1056,7 +1310,9 @@ export default function IsolatedDataGridCommons(props: Readonly 'auto'}
@@ -1068,12 +1324,7 @@ export default function IsolatedDataGridCommons(props: Readonly
)}
{isDialogOpen && promiseArguments && (
-
+
)}
{isDeleteDialogOpen && (
void>(fn: T, delay: number): T {
let timeoutId: ReturnType;
@@ -71,30 +73,73 @@ function debounce void>(fn: T, delay: number): T {
type EditToolbarProps = EditToolbarCustomProps & GridToolbarProps & ToolbarPropsOverrides;
-const EditToolbar = ({
- handleAddNewRow,
- handleRefresh,
- handleExportAll,
- handleExportErrors,
- handleExportCSV,
- handleRunValidations,
- locked,
- filterModel
-}: EditToolbarProps) => {
- const handleExportClick = async () => {
- if (!handleExportAll) return;
- const fullData = await handleExportAll(filterModel);
- const blob = new Blob([JSON.stringify(fullData, null, 2)], {
- type: 'application/json'
+type VisibleFilter = 'valid' | 'errors' | 'pending';
+
+interface ExtendedGridFilterModel extends GridFilterModel {
+ visible: VisibleFilter[];
+}
+
+const EditToolbar = (props: EditToolbarProps) => {
+ const {
+ handleAddNewRow,
+ handleExportErrors,
+ handleRefresh,
+ handleExportAll,
+ handleExportCSV,
+ handleQuickFilterChange,
+ locked,
+ filterModel,
+ dynamicButtons = []
+ } = props;
+ if (!handleAddNewRow || !handleExportErrors || !handleRefresh || !handleQuickFilterChange || !handleExportAll) return <>>;
+ const [inputValue, setInputValue] = useState('');
+ const [isTyping, setIsTyping] = useState(false);
+
+ const handleInputChange = (event: React.ChangeEvent) => {
+ setInputValue(event.target.value);
+ setIsTyping(true);
+ };
+
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (event.key === 'Enter') {
+ handleQuickFilterChange({
+ ...filterModel,
+ items: filterModel?.items || [],
+ quickFilterValues: inputValue.split(' ') || []
+ });
+ setIsTyping(false);
+ }
+ };
+
+ const handleClearInput = () => {
+ setInputValue('');
+ handleQuickFilterChange({
+ ...filterModel,
+ items: filterModel?.items || [],
+ quickFilterValues: []
});
+ setIsTyping(false);
+ };
+
+ useEffect(() => {
+ if (isTyping) {
+ const timeout = setTimeout(() => setIsTyping(false), 2000);
+ return () => clearTimeout(timeout);
+ }
+ }, [isTyping, inputValue]);
+
+ function exportFilterModel() {
+ const jsonData = JSON.stringify(filterModel, null, 2);
+ const blob = new Blob([jsonData], { type: 'application/json' });
const url = URL.createObjectURL(blob);
+
const link = document.createElement('a');
link.href = url;
- link.download = 'data.json';
- document.body.appendChild(link);
+ link.download = 'results.json';
link.click();
- document.body.removeChild(link);
- };
+
+ URL.revokeObjectURL(url);
+ }
const handleExportErrorsClick = async () => {
if (!handleExportErrors) return;
@@ -113,35 +158,95 @@ const EditToolbar = ({
return (
-
- } onClick={handleAddNewRow} disabled={locked}>
- Add Row
-
- } onClick={handleRefresh}>
- Refresh
-
- } onClick={handleExportClick}>
- Export Full Data
-
- } onClick={handleExportErrorsClick}>
- Export Errors
-
- } onClick={handleExportCSV}>
- Export Form CSV
-
- {/*} onClick={handleRunValidations}>*/}
- {/* Run Validations*/}
- {/**/}
+
+
+
+
+
+
+
+
+
+
+
+
+ } onClick={async () => await handleAddNewRow()} disabled={locked}>
+ Add Row
+
+ } onClick={async () => await handleRefresh()}>
+ Refresh
+
+
+
+ }
+ >
+ Export...
+
+
+
+
+
+
+ {dynamicButtons.map((button: any, index: number) => (
+
+ ))}
+
);
};
-/**
- * Renders custom UI components for measurement summary view.
- *
- * Handles state and logic for editing, saving, deleting rows, pagination,
- * validation errors, printing, exporting, and more.
- */
export default function MeasurementsCommons(props: Readonly) {
const {
addNewRowToGrid,
@@ -161,39 +266,48 @@ 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: []
- });
+ const [usingQuery, setUsingQuery] = useState('');
const [isSaveHighlighted, setIsSaveHighlighted] = useState(false);
- // custom states -- msvdatagrid
- const [validationErrors, setValidationErrors] = useState<{
- [key: number]: CMError;
- }>({});
+ const [validationErrors, setValidationErrors] = useState({});
const [showErrorRows, setShowErrorRows] = useState(true);
const [showValidRows, setShowValidRows] = useState(true);
- const [errorRowsForExport, setErrorRowsForExport] = useState([]);
+ const [showPendingRows, setShowPendingRows] = useState(true);
+ const [hidingEmpty, setHidingEmpty] = 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 [errorCount, setErrorCount] = useState(null);
+ const [validCount, setValidCount] = useState(null);
+ const [pendingCount, setPendingCount] = useState(null);
// context pulls and definitions
const currentSite = useSiteContext();
@@ -201,13 +315,22 @@ 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 exportAllCSV = useCallback(async () => {
const response = await fetch(
`/api/formdownload/measurements/${currentSite?.schemaName ?? ''}/${currentPlot?.plotID ?? 0}/${currentCensus?.dateRanges[0].censusID ?? 0}`,
@@ -266,43 +389,34 @@ 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()] || []);
- 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 errorRowCount = useMemo(() => {
- return rows.filter(row => rowHasError(row.id)).length;
- }, [rows, gridColumns]);
-
const updateRow = async (
gridType: string,
schemaName: string | undefined,
newRow: GridRowModel,
oldRow: GridRowModel,
- setSnackbar: (value: { children: string; severity: 'error' | 'success' }) => void,
+ setSnackbar: Dispatch | null>>,
setIsNewRowAdded: (value: boolean) => void,
setShouldAddRowAfterFetch: (value: boolean) => void,
fetchPaginatedData: (page: number) => Promise,
@@ -337,6 +451,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) => {
@@ -450,6 +579,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);
}
};
@@ -464,14 +607,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;
@@ -494,11 +629,6 @@ export default function MeasurementsCommons(props: Readonly {
- console.error(message, error);
- setSnackbar({ children: `Error: ${message}`, severity: 'error' });
- };
-
const fetchPaginatedData = useCallback(
debounce(async (pageToFetch: number) => {
if (!currentSite || !currentPlot || !currentCensus) {
@@ -506,77 +636,99 @@ 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, fetchPaginatedData]);
+ }, [currentPlot, currentCensus, paginationModel, sortModel, isNewRowAdded, filterModel]);
useEffect(() => {
- if (errorRowCount > 0) {
- setSnackbar({
- children: `${errorRowCount} row(s) with validation errors detected.`,
- severity: 'warning'
+ console.log('updated rows object: ', rows);
+ }, [rows]);
+
+ useEffect(() => {
+ 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];
}
- }, [errorRowCount]);
- const handleRefresh = useCallback(async () => {
- setRefresh(true);
- await fetch(`/api/refreshviews/measurementssummary/${currentSite?.schemaName}`, { method: 'POST' });
- setTimeout(async () => {
- await fetchPaginatedData(paginationModel.page);
- }, 2000);
- setRefresh(false);
- }, [fetchPaginatedData, paginationModel.page, refresh]);
+ getCounts().then(data => {
+ setValidCount(data.CountValid);
+ setErrorCount(data.CountErrors);
+ setPendingCount(data.CountPending);
+ 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
+ });
+ });
+ }, [rows, paginationModel]);
const processRowUpdate = useCallback(
(newRow: GridRowModel, oldRow: GridRowModel) =>
@@ -590,7 +742,7 @@ export default function MeasurementsCommons(props: Readonly {
@@ -681,106 +833,146 @@ 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);
}
- }, [currentSite?.schemaName, fetchPaginatedData]);
-
- const fetchFullData = async () => {
- setLoading(true, 'Fetching full dataset...');
- let partialQuery = ``;
- if (currentPlot?.plotID) partialQuery += `/${currentPlot.plotID}`;
- if (currentCensus?.plotCensusNumber) partialQuery += `/${currentCensus.plotCensusNumber}`;
- if (currentQuadrat?.quadratID) partialQuery += `/${currentQuadrat.quadratID}`;
- const fullDataQuery = `/api/fetchall/${gridType}` + partialQuery + `?schema=${currentSite?.schemaName}`;
+ }, [currentSite?.schemaName]);
+ const fetchFullData = useCallback(async () => {
+ setLoading(true);
try {
- const response = await fetch(fullDataQuery, {
- method: 'GET',
- headers: { 'Content-Type': 'application/json' }
- // body: JSON.stringify(filterModel)
- });
- if (!response.ok) throw new Error(response.statusText || 'Error fetching full data');
- return await response.json();
+ const reworkedQuery = usingQuery
+ .replace(/\bSQL_CALC_FOUND_ROWS\b\s*/i, '')
+ .replace(/\bLIMIT\s+\d+\s*,\s*\d+/i, '')
+ .trim();
+
+ const results = await (
+ await fetch(`/api/runquery`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(reworkedQuery)
+ })
+ ).json();
+
+ const jsonData = JSON.stringify(results, null, 2);
+ const blob = new Blob([jsonData], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = 'results.json';
+ link.click();
+
+ URL.revokeObjectURL(url);
} catch (error) {
console.error('Error fetching full data:', error);
setSnackbar({ children: 'Error fetching full data', severity: 'error' });
- return [];
} finally {
setLoading(false);
}
- };
+ }, [usingQuery, filterModel, currentPlot, currentCensus, currentQuadrat, currentSite, gridType, setLoading]);
const handleExportErrors = async () => {
const errorRows = await fetchErrorRows();
return errorRows.map(row => {
- const errors = getRowErrorDescriptions(row.id);
- return { ...row, errors };
+ return { ...row, errors: validationErrors[Number(row.coreMeasurementID)].errors ?? [] };
});
};
// custom column formatting:
- const validationStatusColumn: GridColDef = {
- field: 'isValidated',
- headerName: '',
- headerAlign: 'center',
- align: 'center',
- width: 50,
- renderCell: (params: GridCellParams) => {
- console.log('val stat rendercell');
- console.log('val stat params: ', params);
- const rowId = params.row.coreMeasurementID;
- console.log('rowId located: ', rowId);
- const validationError = validationErrors[Number(rowId)];
- console.log('searched for val error: ', validationError);
- const isPendingValidation = rows.find(row => row.coreMeasurementID === rowId)?.isValidated && !validationError;
- console.log('pending validation? ', isPendingValidation);
- const isValidated = params.row.isValidated;
- console.log('is validated?', isValidated);
-
- if (validationError) {
- return (
-
-
-
- );
- } else if (isPendingValidation) {
- return (
-
-
-
+ const validationStatusColumn: GridColDef = useMemo(
+ () => ({
+ field: 'isValidated',
+ headerName: '',
+ headerAlign: 'center',
+ align: 'center',
+ width: 50,
+ renderCell: (params: GridCellParams) => {
+ console.log('validation errors full: ', validationErrors);
+ console.log('row: ', params.row);
+ console.log(
+ 'row in rows: ',
+ rows.find(row => row.coreMeasurementID === params.row.coreMeasurementID)
);
- } else if (isValidated) {
- return (
-
-
-
- );
- } else {
- return null;
+ 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 (
+
+
+
+ );
+ } 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',
@@ -796,80 +988,97 @@ 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[coreMeasurementID].errors;
+ if (!error || !Array.isArray(error)) {
+ return '';
+ }
+ return error.flatMap(errorDetail => errorDetail.validationPairs).find(vp => vp.criterion === colField)?.description || null;
+ };
+
const columns = useMemo(() => {
- console.log('test: ');
const commonColumns = gridColumns.map(column => {
- Object.keys(validationErrors).forEach(validationError => {
- console.log('validationerror: ', validationError);
- });
- return column;
- // return {
- // ...column,
- // renderCell: (params: GridCellParams) => {
- // const cellValue = params.value !== undefined ? params.value?.toString() : '';
- // console.log('cellValue', cellValue);
- // const cellError = cellHasError(column.field, params.id) ? getCellErrorMessages(column.field, params.id) : '';
- // console.log('cellERror', cellError);
- // 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 cellError = cellHasError(column.field, params.id) ? getCellErrorMessages(column.field, Number(params.row.coreMeasurementID)) : '';
+
+ 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 (
+
+ {isMeasurementField ? (
+ {renderMeasurementDetails()}
+ ) : (
+ {formattedValue}
+ )}
+ {cellError !== '' && (
+
+ {cellError}
+
+ )}
+
+ );
+ }
+ };
});
if (locked) {
return [validationStatusColumn, measurementDateColumn, ...commonColumns];
}
- return [validationStatusColumn, measurementDateColumn, ...commonColumns, getGridActionsColumn()];
- }, [gridColumns, locked]);
+ return [validationStatusColumn, measurementDateColumn, ...applyFilterToColumns(commonColumns), getGridActionsColumn()];
+ }, [MeasurementsSummaryViewGridColumns, locked, rows, validationErrors]);
- const filteredColumns = useMemo(() => filterColumns(rows, columns), [rows, columns]);
-
- const visibleRows = useMemo(() => {
- 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]);
+ const filteredColumns = useMemo(() => {
+ if (hidingEmpty) return filterColumns(rows, columns);
+ return columns;
+ }, [rows, columns, hidingEmpty]);
const getRowClassName = (params: any) => {
const rowId = params.id;
@@ -923,6 +1132,45 @@ export default function MeasurementsCommons(props: Readonly {
+ 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();
+ }
+
+ const testRows = [
+ {
+ id: 1,
+ coreMeasurementID: 2770850,
+ measurementDate: '2016-02-25T05:00:00.000Z',
+ measuredDBH: null,
+ isValidated: false,
+ measuredHOM: '1.300000'
+ }
+ ];
+
if (!currentSite || !currentPlot || !currentCensus) {
redirect('/dashboard');
} else {
@@ -942,18 +1190,26 @@ 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})
+
+
+ setHidingEmpty(event.target.checked)} />
+ {hidingEmpty ? `Hiding Empty Columns` : `Hide Empty Columns`}
{
+ setPaginationModel(newPaginationModel);
+ }}
onProcessRowUpdateError={error => {
console.error('Row update error:', error);
setSnackbar({
@@ -973,16 +1231,22 @@ export default function MeasurementsCommons(props: Readonly {
if (event.key === 'Enter') {
- handleEnterKeyNavigation(params, event).then(r => {
- console.log(r);
- });
+ handleEnterKeyNavigation(params, event).then(r => {});
}
}}
paginationModel={paginationModel}
rowCount={rowCount}
- pageSizeOptions={[paginationModel.pageSize]}
+ pageSizeOptions={[10, 25, 50, 100]}
sortModel={sortModel}
onSortModelChange={handleSortModelChange}
+ filterModel={filterModel}
+ onFilterModelChange={newFilterModel => {
+ setFilterModel(prevModel => ({
+ ...prevModel,
+ ...newFilterModel
+ }));
+ }}
+ ignoreDiacritics
initialState={{
columns: {
columnVisibilityModel: getColumnVisibilityModel(gridType)
@@ -995,10 +1259,13 @@ export default function MeasurementsCommons(props: Readonly setIsValidationModalOpen(true) }]
}
}}
getRowHeight={() => 'auto'}
@@ -1030,6 +1297,7 @@ export default function MeasurementsCommons(props: Readonly
)}
+ {isValidationModalOpen && }
);
}
diff --git a/frontend/components/forms/autocompletemultiselect.tsx b/frontend/components/forms/autocompletemultiselect.tsx
deleted file mode 100644
index f31f5a44..00000000
--- a/frontend/components/forms/autocompletemultiselect.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-'use client';
-import React, { useEffect, useState } from 'react';
-import Autocomplete from '@mui/material/Autocomplete';
-import TextField from '@mui/material/TextField';
-import CircularProgress from '@mui/material/CircularProgress';
-import { useSiteContext } from '@/app/contexts/userselectionprovider';
-
-export interface AutocompleteMultiSelectProps {
- initialValue: string[];
- onChange: (selected: string[]) => void;
-}
-
-export const AutocompleteMultiSelect: React.FC = props => {
- const { initialValue, onChange } = props;
- const [open, setOpen] = useState(false);
- const [options, setOptions] = useState([]);
- const [inputValue, _setInputValue] = useState('');
- const loading = open && options.length === 0;
- const currentSite = useSiteContext();
- if (!currentSite) throw new Error('Site must be selected!');
-
- useEffect(() => {
- let active = true;
-
- if (!loading) {
- return undefined;
- }
-
- (async () => {
- const response = await fetch(`/api/formsearch/attributes?schema=${currentSite.schemaName}&searchfor=${encodeURIComponent(inputValue)}`);
- const items = await response.json();
-
- if (active) {
- setOptions(items);
- }
- })();
-
- return () => {
- active = false;
- };
- }, [loading, inputValue]);
-
- useEffect(() => {
- if (!open) {
- setOptions([]);
- }
- }, [open]);
-
- return (
- setOpen(true)}
- onClose={() => setOpen(false)}
- options={options}
- getOptionLabel={option => option}
- isOptionEqualToValue={(option, value) => option === value}
- loading={loading}
- value={initialValue}
- onChange={(_event, newValue) => onChange(newValue)}
- filterSelectedOptions
- renderInput={params => (
-
- {loading ? : null}
- {params.InputProps.endAdornment}
- >
- )
- }}
- />
- )}
- />
- );
-};
diff --git a/frontend/components/forms/censusacinputform.tsx b/frontend/components/forms/censusacinputform.tsx
deleted file mode 100644
index 1c0fc53a..00000000
--- a/frontend/components/forms/censusacinputform.tsx
+++ /dev/null
@@ -1,457 +0,0 @@
-'use client';
-import React, { useState } from 'react';
-import {
- DataGrid,
- GridActionsCellItem,
- GridColDef,
- GridEventListener,
- GridRenderCellParams,
- GridRowEditStopReasons,
- GridRowId,
- GridRowModel,
- GridRowModes,
- GridRowModesModel,
- GridRowsProp,
- GridToolbarContainer,
- GridToolbarProps,
- ToolbarPropsOverrides
-} from '@mui/x-data-grid';
-import { randomId } from '@mui/x-data-grid-generator';
-import AddIcon from '@mui/icons-material/Add';
-import SaveIcon from '@mui/icons-material/Save';
-import CancelIcon from '@mui/icons-material/Close';
-import EditIcon from '@mui/icons-material/Edit';
-import { DeleteIcon } from '@/components/icons';
-import { Box, Button } from '@mui/material';
-import AutocompleteFixedData from '@/components/forms/autocompletefixeddata';
-import { AutocompleteMultiSelect } from '@/components/forms/autocompletemultiselect';
-import Divider from '@mui/joy/Divider';
-import Typography from '@mui/joy/Typography';
-import { useOrgCensusContext, usePlotContext, useSiteContext } from '@/app/contexts/userselectionprovider';
-import { useSession } from 'next-auth/react';
-import UploadValidation from '@/components/uploadsystem/segments/uploadvalidation';
-import UploadUpdateValidations from '@/components/uploadsystem/segments/uploadupdatevalidations';
-import { ReviewStates } from '@/config/macros/uploadsystemmacros';
-import { DialogContent, DialogTitle, Modal, ModalDialog } from '@mui/joy';
-import { unitSelectionOptions } from '@/config/macros';
-
-interface EditToolbarCustomProps {
- handleAddNewRow?: () => void;
- handleRefresh?: () => Promise;
- locked?: boolean;
-}
-
-type EditToolbarProps = EditToolbarCustomProps & GridToolbarProps & ToolbarPropsOverrides;
-
-function EditToolbar(props: Readonly) {
- const { setRows, setRowModesModel } = props;
- const handleClick = () => {
- const id = randomId();
- setRows((oldRows: any) => [
- ...oldRows,
- {
- id,
- date: new Date(),
- personnel: '',
- quadratName: '',
- treeTag: '',
- stemTag: '',
- stemX: 0,
- stemY: 0,
- speciesCode: '',
- dbh: 0,
- hom: 0,
- codes: [], // Initialize codes as an empty array
- comments: '',
- isNew: true
- }
- ]);
- setRowModesModel((oldModel: any) => ({
- ...oldModel,
- [id]: { mode: GridRowModes.Edit, fieldToFocus: 'quadratName' }
- }));
- };
-
- return (
-
- } onClick={handleClick}>
- Add record
-
-
- );
-}
-
-const CensusAutocompleteInputForm = () => {
- /**
- * "tag": "Trees.TreeTag",
- * "stemtag": "Stems.StemTag",
- * "spcode": "Species.SpeciesCode",
- * "subquadrat": "Subquadrats.SubquadratName",
- * "lx": "Stems.StemQuadX",
- * "ly": "Stems.StemQuadY",
- * "dbh": "CoreMeasurements.MeasuredDBH",
- * "dbhunit": "CoreMeasurements.DBHUnit"
- * "codes": "Attributes.Code",
- * "hom": "CoreMeasurement.MeasuredHOM",
- * "homunit": "CoreMeasurements.HOMUnit",
- * "date": "CoreMeasurement.MeasurementDate",
- */
- const initialRows: GridRowsProp = [
- {
- id: 0,
- stemTag: '',
- treeTag: '',
- speciesCode: '',
- subquadratName: '',
- stemX: 0,
- stemY: 0,
- date: new Date(),
- dbh: 0,
- dbhUnit: '',
- hom: 0,
- homUnit: '',
- codes: [], // Initialize codes as an empty array
- personnel: '',
- comments: ''
- }
- ];
- const columns: GridColDef[] = [
- {
- field: 'date',
- headerName: 'Date',
- type: 'date',
- headerClassName: 'header',
- maxWidth: 100,
- flex: 1,
- align: 'left',
- editable: true,
- valueGetter: (params: any) => {
- if (!params.value) return null;
- return new Date(params.value);
- }
- },
- {
- field: 'personnel',
- headerName: 'Personnel',
- flex: 1,
- align: 'right',
- renderCell: (params: GridRenderCellParams) => (
- handlePersonnelChange(params.id, newValue)} />
- )
- },
- {
- field: 'quadrat',
- headerName: 'Quadrat',
- flex: 1,
- align: 'right',
- renderCell: (params: GridRenderCellParams) => (
- handleQuadratChange(params.id, newValue)} />
- )
- },
- {
- field: 'treeTag',
- headerName: 'Tree Tag',
- flex: 1,
- align: 'right',
- renderCell: (params: GridRenderCellParams) => (
- handleTreeTagChange(params.id, newValue)} />
- )
- },
- {
- field: 'stemTag',
- headerName: 'Stem Tag',
- flex: 1,
- align: 'right',
- renderCell: (params: GridRenderCellParams) => (
- handleStemTagChange(params.id, newValue)} />
- )
- },
- {
- field: 'speciesCode',
- headerName: 'Species Code',
- flex: 1,
- align: 'right',
- renderCell: (params: GridRenderCellParams) => (
- handleSpeciesCodeChange(params.id, newValue)} />
- )
- },
- {
- field: 'dbh',
- headerName: 'DBH',
- headerClassName: 'header',
- type: 'number',
- editable: true,
- maxWidth: 75,
- flex: 1,
- align: 'right'
- },
- {
- field: 'dbhUnit',
- headerName: '<- Unit',
- headerClassName: 'header',
- flex: 1,
- align: 'left',
- editable: true,
- type: 'singleSelect',
- valueOptions: unitSelectionOptions
- },
- {
- field: 'hom',
- headerName: 'HOM',
- headerClassName: 'header',
- type: 'number',
- editable: true,
- maxWidth: 75,
- flex: 1,
- align: 'right'
- },
- {
- field: 'homUnit',
- headerName: '<- Unit',
- headerClassName: 'header',
- flex: 1,
- align: 'left',
- editable: true,
- type: 'singleSelect',
- valueOptions: unitSelectionOptions
- },
- {
- field: 'codes',
- headerName: 'Codes',
- width: 200,
- flex: 1,
- align: 'left',
- renderCell: (params: GridRenderCellParams) => (
- handleCodesChange(params.id, newCodes)} />
- )
- },
- {
- field: 'actions',
- type: 'actions',
- headerName: 'Actions',
- maxWidth: 100,
- cellClassName: 'actions',
- flex: 1,
- align: 'center',
- getActions: ({ id }) => {
- const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;
-
- if (isInEditMode) {
- return [
- }
- label="Save"
- key="Save"
- sx={{
- color: 'primary.main'
- }}
- onClick={handleSaveClick(id)}
- />,
- } label="Cancel" key="Cancel" className="textPrimary" onClick={handleCancelClick(id)} color="inherit" />
- ];
- }
-
- return [
- } label="Edit" key="Edit" className="textPrimary" onClick={handleEditClick(id)} color="inherit" />,
- } label="Delete" key="Delete" onClick={handleDeleteClick(id)} color="inherit" />
- ];
- }
- }
- ];
- const [rows, setRows] = React.useState(initialRows);
- const [rowModesModel, setRowModesModel] = React.useState({});
- // New state to track if the form is ready for submission
- const [isFormComplete, setIsFormComplete] = useState(false);
-
- const [activeStep, setActiveStep] = useState('validation'); // 'validation', 'update', or 'summary'
- const [validationResults, setValidationResults] = useState(null);
- const [updateResults, setUpdateResults] = useState(null);
-
- const [reviewState, setReviewState] = useState(ReviewStates.UPLOAD_SQL); // placeholder
-
- const currentPlot = usePlotContext();
- const currentCensus = useOrgCensusContext();
- const currentSite = useSiteContext();
-
- const { data: session } = useSession();
- const handleQuadratChange = (id: number | string, newValue: string) => {
- setRows(rows.map(row => (row.id === id ? { ...row, quadratName: newValue } : row)));
- };
- const handlePersonnelChange = (id: number | string, newValue: string) => {
- setRows(rows.map(row => (row.id === id ? { ...row, personnel: newValue } : row)));
- };
- const handleTreeTagChange = (id: number | string, newValue: string) => {
- setRows(rows.map(row => (row.id === id ? { ...row, treeTag: newValue } : row)));
- };
- const handleStemTagChange = (id: number | string, newValue: string) => {
- setRows(rows.map(row => (row.id === id ? { ...row, stemTag: newValue } : row)));
- };
- const handleSpeciesCodeChange = (id: number | string, newValue: string) => {
- setRows(rows.map(row => (row.id === id ? { ...row, speciesCode: newValue } : row)));
- };
- const handleCodesChange = (id: GridRowId, newCodes: string[]) => {
- setRows(rows.map(row => (row.id === id ? { ...row, codes: newCodes } : row)));
- };
- const handleRowEditStop: GridEventListener<'rowEditStop'> = (params, event) => {
- if (params.reason === GridRowEditStopReasons.rowFocusOut) {
- event.defaultMuiPrevented = true;
- }
- };
-
- const handleEditClick = (id: GridRowId) => () => {
- setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } });
- };
-
- const handleSaveClick = (id: GridRowId) => () => {
- setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } });
- };
-
- const handleDeleteClick = (id: GridRowId) => () => {
- setRows(rows.filter(row => row.id !== id));
- };
-
- const handleCancelClick = (id: GridRowId) => () => {
- setRowModesModel({
- ...rowModesModel,
- [id]: { mode: GridRowModes.View, ignoreModifications: true }
- });
-
- const editedRow = rows.find(row => row.id === id);
- if (editedRow!.isNew) {
- setRows(rows.filter(row => row.id !== id));
- }
- };
-
- const processRowUpdate = (newRow: GridRowModel) => {
- const updatedRow = { ...newRow, isNew: false };
- setRows(rows.map(row => (row.id === newRow.id ? updatedRow : row)));
- return updatedRow;
- };
-
- const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => {
- setRowModesModel(newRowModesModel);
- };
-
- // Function to check if all required fields in a row are filled
- const checkFormCompletion = () => {
- const allRowsComplete = rows.every(row => {
- // Add checks for all required fields here
- return row.date && row.personnel && row.quadratName && row.treeTag && row.stemTag && row.speciesCode && row.dbh && row.hom;
- });
- setIsFormComplete(allRowsComplete);
- };
-
- // Update the form completion status whenever rows change
- React.useEffect(() => {
- checkFormCompletion();
- }, [rows]);
-
- // Function to format and submit data
- const handleSubmit = async () => {
- const formattedData = rows.reduce((acc, row) => {
- // Map the row data to the FileRow structure
- acc[row.id] = {
- date: row.date.toISOString(),
- personnel: row.personnel,
- quadratName: row.quadratName,
- treeTag: row.treeTag,
- stemTag: row.stemTag,
- speciesCode: row.speciesCode,
- dbh: row.dbh.toString(),
- hom: row.hom.toString(),
- codes: row.codes.join(';')
- };
- return acc;
- }, {});
-
- const fileRowSet = { censusData: formattedData }; // Assuming 'censusData' as the file name
-
- try {
- // Add code to retrieve additional required parameters like schema, fileName, etc.
- const response = await fetch(
- `/api/sqlload?schema=${currentSite?.schemaName ?? ''}&fileName=censusData&plot=${currentPlot?.plotID}&census=${currentCensus?.dateRanges[0].censusID}&user=${session?.user?.name}&formType=measurements&uom=metric`,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(fileRowSet)
- }
- );
-
- const responseData = await response.json(); // not gonna do anything with this, this output is intended for upload system.
- // Handle the response
- if (response.ok) {
- setReviewState(ReviewStates.VALIDATE);
- } else {
- console.error('Error submitting form:', responseData);
- // Handle submission error
- }
- } catch (error) {
- console.error('Error submitting form:', error);
- }
- };
- // Render different components based on activeStep
- let content;
- switch (activeStep) {
- case 'validation':
- content = ;
- break;
- case 'update':
- content = ;
- break;
- default:
- content = null;
- }
- return (
-
-
- Plot Name: {currentPlot?.plotName ?? 'None'}, Census ID: {currentCensus?.dateRanges[0].censusID ?? '0'}
-
-
-
-
-
- 'dataGridCell'}
- rowHeight={75}
- rows={rows}
- columns={columns}
- autoHeight
- checkboxSelection
- disableRowSelectionOnClick
- editMode="row"
- rowModesModel={rowModesModel}
- onRowModesModelChange={handleRowModesModelChange}
- onRowEditStop={handleRowEditStop}
- processRowUpdate={processRowUpdate}
- slots={{
- toolbar: EditToolbar
- }}
- slotProps={{
- toolbar: { setRows, setRowModesModel }
- }}
- />
-
-
- Validation and Update Stages
-
- {reviewState === ReviewStates.VALIDATE && }
- {reviewState === ReviewStates.UPDATE && }
-
-
-
-
- );
-};
-
-export default CensusAutocompleteInputForm;
diff --git a/frontend/components/forms/censusinlinevalidationform.tsx b/frontend/components/forms/censusinlinevalidationform.tsx
deleted file mode 100644
index 08cb5744..00000000
--- a/frontend/components/forms/censusinlinevalidationform.tsx
+++ /dev/null
@@ -1,446 +0,0 @@
-'use client';
-import React, { useEffect, useState } from 'react';
-import {
- DataGrid,
- GridActionsCellItem,
- GridColDef,
- GridRenderCellParams,
- GridRowId,
- GridRowModel,
- GridRowModes,
- GridRowModesModel,
- GridRowParams,
- GridRowsProp,
- GridToolbarContainer,
- GridToolbarProps,
- GridValidRowModel
-} from '@mui/x-data-grid';
-import { randomId } from '@mui/x-data-grid-generator';
-import AddIcon from '@mui/icons-material/Add';
-import SaveIcon from '@mui/icons-material/Save';
-import CancelIcon from '@mui/icons-material/Close';
-import EditIcon from '@mui/icons-material/Edit';
-import DeleteIcon from '@mui/icons-material/Delete';
-import { Box, Button } from '@mui/material';
-import Divider from '@mui/joy/Divider';
-import Typography from '@mui/joy/Typography';
-import { useOrgCensusContext, usePlotContext, useSiteContext } from '@/app/contexts/userselectionprovider';
-import { useSession } from 'next-auth/react';
-import { unitSelectionOptions } from '@/config/macros';
-
-type EditToolbarProps = GridToolbarProps;
-
-function EditToolbar(props: EditToolbarProps) {
- const { setRows, setRowModesModel } = props;
- const handleClick = () => {
- const id = randomId();
- setRows((oldRows: GridValidRowModel[]) => [
- ...oldRows,
- {
- id,
- stemTag: '',
- treeTag: '',
- speciesCode: '',
- subquadratName: '',
- personnel: '',
- stemX: 0,
- stemY: 0,
- date: new Date(),
- dbh: 0,
- dbhUnit: '',
- hom: 0,
- homUnit: '',
- codes: [], // Initialize codes as an empty array
- comments: '',
- isNew: true
- }
- ]);
- setRowModesModel((oldModel: GridRowModesModel) => ({
- ...oldModel,
- [id]: { mode: GridRowModes.Edit, fieldToFocus: 'quadratName' }
- }));
- };
-
- return (
-
- } onClick={handleClick}>
- Add record
-
-
- );
-}
-
-const CensusAutocompleteInputForm = () => {
- const initialRows: GridRowsProp = [
- {
- id: 0,
- stemTag: '',
- treeTag: '',
- speciesCode: '',
- subquadratName: '',
- stemX: 0,
- stemY: 0,
- date: new Date(),
- dbh: 0,
- dbhUnit: '',
- hom: 0,
- homUnit: '',
- codes: '', // Initialize codes as an empty array
- personnel: '',
- comments: ''
- }
- ];
-
- // Custom render function to show errors
- const renderValidationCell = (params: GridRenderCellParams, fieldName: string) => {
- const cellValue = params.value !== undefined ? params.value.toString() : '';
- const cellError = cellHasError(fieldName, params.id) ? getCellErrorMessages(fieldName, params.id) : '';
- console.log(`Rendering cell - Field: ${fieldName}, Error: ${cellError}`);
- return (
-
- {cellError ? (
- <>
- {cellValue}
-
- {cellError}
-
- >
- ) : (
- {cellValue}
- )}
-
- );
- };
-
- const columns: GridColDef[] = [
- {
- field: 'date',
- headerName: 'Date',
- type: 'date',
- headerClassName: 'header',
- maxWidth: 100,
- flex: 1,
- align: 'left',
- editable: true,
- valueGetter: (params: any) => {
- return new Date(params.value) ?? new Date();
- }
- },
- {
- field: 'personnel',
- headerName: 'Personnel',
- flex: 1,
- align: 'right',
- editable: true,
- renderCell: (params: GridRenderCellParams) => renderValidationCell(params, 'personnel')
- },
- {
- field: 'subquadratName',
- headerName: 'Subquadrat',
- flex: 1,
- align: 'right',
- editable: true,
- renderCell: (params: GridRenderCellParams) => renderValidationCell(params, 'subquadratName')
- },
- {
- field: 'treeTag',
- headerName: 'Tree Tag',
- flex: 1,
- align: 'right',
- editable: true,
- renderCell: (params: GridRenderCellParams) => renderValidationCell(params, 'treeTag')
- },
- {
- field: 'stemTag',
- headerName: 'Stem Tag',
- flex: 1,
- align: 'right',
- editable: true,
- renderCell: (params: GridRenderCellParams) => renderValidationCell(params, 'stemTag')
- },
- {
- field: 'speciesCode',
- headerName: 'SP Code',
- flex: 1,
- align: 'right',
- editable: true,
- renderCell: (params: GridRenderCellParams) => renderValidationCell(params, 'speciesCode')
- },
- {
- field: 'dbh',
- headerName: 'DBH',
- headerClassName: 'header',
- type: 'number',
- editable: true,
- maxWidth: 75,
- flex: 1,
- align: 'right'
- },
- {
- field: 'dbhUnit',
- headerName: '<- Unit',
- headerClassName: 'header',
- flex: 1,
- align: 'left',
- editable: true,
- type: 'singleSelect',
- valueOptions: unitSelectionOptions
- },
- {
- field: 'hom',
- headerName: 'HOM',
- headerClassName: 'header',
- type: 'number',
- editable: true,
- maxWidth: 75,
- flex: 1,
- align: 'right'
- },
- {
- field: 'homUnit',
- headerName: '<- Unit',
- headerClassName: 'header',
- flex: 1,
- align: 'left',
- editable: true,
- type: 'singleSelect',
- valueOptions: unitSelectionOptions
- },
- {
- field: 'codes',
- headerName: 'Codes',
- width: 200,
- flex: 1,
- align: 'left',
- editable: true
- },
- {
- field: 'actions',
- type: 'actions',
- headerName: 'Actions',
- maxWidth: 100,
- cellClassName: 'actions',
- flex: 1,
- align: 'center',
- getActions: ({ id }) => {
- const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;
-
- if (isInEditMode) {
- return [
- }
- label="Save"
- key="Save"
- sx={{
- color: 'primary.main'
- }}
- onClick={handleSaveClick(id)}
- />,
- } label="Cancel" key="Cancel" className="textPrimary" onClick={handleCancelClick(id)} color="inherit" />
- ];
- }
-
- return [
- } label="Edit" key="Edit" className="textPrimary" onClick={handleEditClick(id)} color="inherit" />,
- } label="Delete" key="Delete" onClick={handleDeleteClick(id)} color="inherit" />
- ];
- }
- }
- ];
-
- const [rows, setRows] = useState(initialRows);
- const [rowModesModel, setRowModesModel] = useState({});
- const [validationErrors, setValidationErrors] = useState<{
- [key: string]: string | null;
- }>({});
- const [isFormComplete, setIsFormComplete] = useState(false);
-
- const currentPlot = usePlotContext();
- const currentCensus = useOrgCensusContext();
- const currentSite = useSiteContext();
-
- const { data: session } = useSession();
-
- const validateField = async (tableName: string, fieldName: string, value: string, rowId: GridRowId) => {
- try {
- const response = await fetch(`/api/formvalidation/${currentSite?.schemaName}/${tableName}/${fieldName}/${value}`, {
- method: 'GET'
- });
- if (!response.ok) {
- const errorText = `${value}: Invalid ${fieldName}, not found in ${tableName}.`;
- setValidationErrors(prev => ({
- ...prev,
- [`${rowId}-${fieldName}`]: errorText
- }));
- return false;
- }
- setValidationErrors(prev => ({
- ...prev,
- [`${rowId}-${fieldName}`]: null
- }));
- return true;
- } catch (error) {
- console.error(`Error validating ${fieldName}:`, error);
- setValidationErrors(prev => ({
- ...prev,
- [`${rowId}-${fieldName}`]: `Validation error for ${fieldName}.`
- }));
- return false;
- }
- };
-
- const validateAllFields = async (row: GridValidRowModel) => {
- const [firstName = '', lastName = ''] = row.personnel.split(' ');
- const validations = [
- validateField('stems', 'StemTag', row.stemTag, row.id),
- validateField('trees', 'TreeTag', row.treeTag, row.id),
- validateField('species', 'SpeciesCode', row.speciesCode, row.id),
- validateField('subquadrats', 'SubquadratName', row.quadratName, row.id),
- validateField('personnel', 'FirstName', firstName, row.id),
- validateField('personnel', 'LastName', lastName, row.id)
- ];
-
- const results = await Promise.all(validations);
- const allValid = results.every(result => result);
- if (!allValid) {
- console.error('One or more fields are invalid.');
- }
- return allValid;
- };
-
- const handleEditClick = (id: GridRowId) => () => {
- setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } });
- };
-
- const handleSaveClick = (id: GridRowId) => async () => {
- const row = rows.find(row => row.id === id);
- if (row) {
- const isValid = await validateAllFields(row);
- if (isValid) {
- setRowModesModel({
- ...rowModesModel,
- [id]: { mode: GridRowModes.View }
- });
- } else {
- console.error('Validation failed for row:', id);
- }
- }
- };
-
- const handleDeleteClick = (id: GridRowId) => () => {
- setRows(rows.filter(row => row.id !== id));
- };
-
- const handleCancelClick = (id: GridRowId) => () => {
- setRowModesModel({
- ...rowModesModel,
- [id]: { mode: GridRowModes.View, ignoreModifications: true }
- });
-
- const editedRow = rows.find(row => row.id === id);
- if (editedRow!.isNew) {
- setRows(rows.filter(row => row.id !== id));
- }
- };
-
- const processRowUpdate = (newRow: GridRowModel) => {
- const updatedRow = { ...newRow, isNew: false };
- setRows(rows.map(row => (row.id === newRow.id ? updatedRow : row)));
- return updatedRow;
- };
-
- const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => {
- setRowModesModel(newRowModesModel);
- };
-
- const checkFormCompletion = () => {
- const allRowsComplete = rows.every(row => {
- return row.date && row.personnel && row.quadratName && row.treeTag && row.stemTag && row.speciesCode && row.dbh && row.hom;
- });
- setIsFormComplete(allRowsComplete);
- };
-
- useEffect(() => {
- checkFormCompletion();
- }, [rows]);
-
- // Prevent saving row on Enter key press or any other shortcut
- const handleCellKeyDown = (params: any, event: { key: string; preventDefault: () => void }) => {
- if (event.key === 'Enter') {
- event.preventDefault();
- }
- };
-
- const rowHasError = (rowId: GridRowId) => {
- return Object.keys(validationErrors).some(key => key.startsWith(rowId + '-') && validationErrors[key] != null);
- };
-
- const getRowClassName = (params: GridRowParams) => {
- if (rowHasError(params.id)) return 'error-row';
- return '';
- };
-
- const cellHasError = (fieldName: string, rowId: GridRowId) => {
- return validationErrors[`${rowId}-${fieldName}`] != null;
- };
-
- const getCellErrorMessages = (fieldName: string, rowId: GridRowId) => {
- return validationErrors[`${rowId}-${fieldName}`];
- };
-
- return (
-
-
- Plot Name: {currentPlot?.plotName ?? 'None'}, Census ID: {currentCensus?.dateRanges[0].censusID ?? '0'}
-
-
- {/* */}
-
-
- 'dataGridCell'}
- rows={rows}
- columns={columns}
- getRowHeight={() => 'auto'}
- autoHeight
- checkboxSelection
- disableRowSelectionOnClick
- editMode="row"
- rowModesModel={rowModesModel}
- onRowModesModelChange={handleRowModesModelChange}
- processRowUpdate={processRowUpdate}
- onCellKeyDown={handleCellKeyDown}
- slots={{
- toolbar: EditToolbar
- }}
- slotProps={{
- toolbar: { setRows, setRowModesModel } // Ensure these methods are passed correctly
- }}
- />
-
- );
-};
-
-export default CensusAutocompleteInputForm;
diff --git a/frontend/components/forms/personnelautocompletemultiselect.tsx b/frontend/components/forms/personnelautocompletemultiselect.tsx
deleted file mode 100644
index f1d32728..00000000
--- a/frontend/components/forms/personnelautocompletemultiselect.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-'use client';
-import React, { useEffect, useState } from 'react';
-import Autocomplete from '@mui/material/Autocomplete';
-import TextField from '@mui/material/TextField';
-import CircularProgress from '@mui/material/CircularProgress';
-import { useSiteContext } from '@/app/contexts/userselectionprovider';
-
-import { PersonnelRDS } from '@/config/sqlrdsdefinitions/personnel';
-
-export interface PersonnelAutocompleteMultiSelectProps {
- initialValue: PersonnelRDS[];
- onChange: (selected: PersonnelRDS[]) => void;
- locked: boolean;
-}
-
-export const PersonnelAutocompleteMultiSelect: React.FC = props => {
- const { locked, onChange } = props;
- const [open, setOpen] = useState(false);
- const [options, setOptions] = useState([]);
- const [inputValue, setInputValue] = useState('');
- const [tempSelectedPersonnel, setTempSelectedPersonnel] = useState([]);
- const [timer, setTimer] = useState(null);
-
- const loading = open && options.length === 0;
- const currentSite = useSiteContext();
- if (!currentSite) throw new Error('Site must be selected!');
-
- // Function to refresh data
- const refreshData = () => {
- fetch(`/api/formsearch/personnelblock?schema=${currentSite.schemaName}&searchfor=${encodeURIComponent(inputValue)}`)
- .then(response => response.json())
- .then(data => {
- setOptions(data);
- })
- .catch(error => {
- console.error('Error fetching data:', error);
- });
- };
- const handleConfirm = () => {
- onChange(tempSelectedPersonnel);
- };
-
- useEffect(() => {
- if (timer) clearTimeout(timer);
- const newTimer = setTimeout(refreshData, 5000); // Refresh after 5 seconds of inactivity
- setTimer(newTimer);
-
- return () => {
- clearTimeout(newTimer);
- };
- }, [inputValue]);
-
- useEffect(() => {
- let active = true;
-
- if (!loading) {
- return undefined;
- }
-
- (async () => {
- const response = await fetch(`/api/formsearch/personnelblock?schema=${currentSite.schemaName}&searchfor=${encodeURIComponent(inputValue)}`, {
- method: 'GET'
- });
- const items = await response.json();
- console.log(items);
- if (active) {
- setOptions(items);
- }
- })();
-
- return () => {
- active = false;
- };
- }, [loading, inputValue]);
-
- useEffect(() => {
- if (!open) {
- setOptions([]);
- }
- }, [open]);
-
- useEffect(() => {
- // Pre-load options with empty input value
- refreshData();
- }, []); // This will run only once when the component mounts
-
- return (
- <>
- setOpen(true)}
- onClose={() => setOpen(false)}
- options={options}
- getOptionLabel={option => `${option.lastName}, ${option.firstName} | ${option.roleID}`}
- isOptionEqualToValue={(option, value) => JSON.stringify(option) === JSON.stringify(value)}
- loading={loading}
- value={undefined}
- disabled={locked}
- onChange={(_event, newValue) => {
- setTempSelectedPersonnel(newValue);
- handleConfirm();
- setInputValue('');
- }}
- inputValue={inputValue}
- onInputChange={(_event, newInputValue) => setInputValue(newInputValue)}
- filterSelectedOptions
- renderInput={params => (
-
- {loading ? : null}
- {params.InputProps.endAdornment}
- >
- )
- }}
- />
- )}
- />
- >
- );
-};
diff --git a/frontend/components/loginlogout.tsx b/frontend/components/loginlogout.tsx
index 1199c727..3e0c627d 100644
--- a/frontend/components/loginlogout.tsx
+++ b/frontend/components/loginlogout.tsx
@@ -8,11 +8,9 @@ import LogoutRoundedIcon from '@mui/icons-material/LogoutRounded';
import LoginRoundedIcon from '@mui/icons-material/LoginRounded';
import CircularProgress from '@mui/joy/CircularProgress';
import { Skeleton } from '@mui/joy';
-import { useRouter } from 'next/navigation';
export const LoginLogout = () => {
const { data: session, status } = useSession();
- const router = useRouter();
const handleRetryLogin = () => {
signIn('azure-ad', { callbackUrl: '/dashboard' }, { prompt: 'login' }).catch((error: any) => {
diff --git a/frontend/components/processors/processcensus.tsx b/frontend/components/processors/processcensus.tsx
index ce6a8bb4..adb3b9d2 100644
--- a/frontend/components/processors/processcensus.tsx
+++ b/frontend/components/processors/processcensus.tsx
@@ -1,12 +1,12 @@
-import { runQuery, SpecialProcessingProps } from '@/components/processors/processormacros';
import moment from 'moment';
import { createError, fetchPrimaryKey, handleUpsert } from '@/config/utils';
import { SpeciesResult, StemResult, TreeResult } from '@/config/sqlrdsdefinitions/taxonomies';
import { QuadratResult } from '@/config/sqlrdsdefinitions/zones';
import { CMAttributesResult, CoreMeasurementsResult } from '@/config/sqlrdsdefinitions/core';
+import { SpecialProcessingProps } from '@/config/macros';
export async function processCensus(props: Readonly): Promise {
- const { connection, rowData, schema, plotID, censusID } = props;
+ const { connectionManager, rowData, schema, plotID, censusID } = props;
if (!plotID || !censusID) {
console.error('Missing required parameters: plotID or censusID');
throw new Error('Process Census: Missing plotID or censusID');
@@ -14,29 +14,52 @@ export async function processCensus(props: Readonly): Pr
const { tag, stemtag, spcode, quadrat, lx, ly, coordinateunit, dbh, dbhunit, hom, homunit, date, codes } = rowData;
try {
- await connection.beginTransaction();
// Fetch species
- const speciesID = await fetchPrimaryKey(schema, 'species', { SpeciesCode: spcode }, connection, 'SpeciesID');
+ const speciesID = await fetchPrimaryKey(schema, 'species', { SpeciesCode: spcode }, connectionManager, 'SpeciesID');
// Fetch quadrat
- const quadratID = await fetchPrimaryKey(schema, 'quadrats', { QuadratName: quadrat, PlotID: plotID }, connection, 'QuadratID');
+ const quadratID = await fetchPrimaryKey(schema, 'quadrats', { QuadratName: quadrat, PlotID: plotID }, connectionManager, 'QuadratID');
if (tag) {
// Handle Tree Upsert
- const treeID = await handleUpsert(connection, schema, 'trees', { TreeTag: tag, SpeciesID: speciesID }, 'TreeID');
+ const { id: treeID, operation: treeOperation } = await handleUpsert(
+ connectionManager,
+ schema,
+ 'trees',
+ {
+ TreeTag: tag,
+ SpeciesID: speciesID
+ },
+ 'TreeID'
+ );
+ console.log('tree tag: ', tag, ' was ', treeOperation, ' on ID # ', treeID);
if (stemtag || lx || ly) {
+ let stemStatus: 'new recruit' | 'multistem' | 'old tree';
// Handle Stem Upsert
- const stemID = await handleUpsert(
- connection,
+ const { id: stemID, operation: stemOperation } = await handleUpsert(
+ connectionManager,
schema,
'stems',
{ StemTag: stemtag, TreeID: treeID, QuadratID: quadratID, LocalX: lx, LocalY: ly, CoordinateUnits: coordinateunit },
'StemID'
);
+ console.log('stem tag: ', stemtag, ' was ', stemOperation, ' on ID # ', stemID);
+
+ if (stemOperation === 'inserted') {
+ stemStatus = treeOperation === 'inserted' ? 'new recruit' : 'multistem';
+ } else {
+ stemStatus = 'old tree';
+ }
+ console.log('stem status: ', stemStatus);
+
+ // Prepare additional fields for core measurements
+ const userDefinedFields = JSON.stringify({
+ treestemstate: { stem: stemOperation, tree: treeOperation, status: stemStatus }
+ });
// Handle Core Measurement Upsert
- const coreMeasurementID = await handleUpsert(
- connection,
+ const { id: coreMeasurementID } = await handleUpsert(
+ connectionManager,
schema,
'coremeasurements',
{
@@ -49,7 +72,7 @@ export async function processCensus(props: Readonly): Pr
MeasuredHOM: hom ? parseFloat(hom) : null,
HOMUnit: homunit,
Description: null,
- UserDefinedFields: null
+ UserDefinedFields: userDefinedFields // using this to track the operation on the tree and stem
},
'CoreMeasurementID'
);
@@ -64,11 +87,11 @@ export async function processCensus(props: Readonly): Pr
console.error('No valid attribute codes found:', codes);
} else {
for (const code of parsedCodes) {
- const attributeRows = await runQuery(connection, `SELECT COUNT(*) as count FROM ${schema}.attributes WHERE Code = ?`, [code]);
+ const attributeRows = await connectionManager.executeQuery(`SELECT COUNT(*) as count FROM ${schema}.attributes WHERE Code = ?`, [code]);
if (!attributeRows || attributeRows.length === 0 || !attributeRows[0].count) {
throw createError(`Attribute code ${code} not found or query failed.`, { code });
}
- await handleUpsert(connection, schema, 'cmattributes', { CoreMeasurementID: coreMeasurementID, Code: code }, 'CMAID');
+ await handleUpsert(connectionManager, schema, 'cmattributes', { CoreMeasurementID: coreMeasurementID, Code: code }, 'CMAID');
}
}
}
@@ -85,14 +108,12 @@ export async function processCensus(props: Readonly): Pr
SET c.StartDate = m.FirstMeasurementDate, c.EndDate = m.LastMeasurementDate
WHERE c.CensusID = ${censusID};`;
- await runQuery(connection, combinedQuery);
- await connection.commit();
+ await connectionManager.executeQuery(combinedQuery);
console.log('Upsert successful. CoreMeasurement ID generated:', coreMeasurementID);
return coreMeasurementID;
}
}
} catch (error: any) {
- await connection.rollback();
console.error('Upsert failed:', error.message);
throw error;
}
diff --git a/frontend/components/processors/processorhelperfunctions.tsx b/frontend/components/processors/processorhelperfunctions.tsx
index 665432b8..9656aaeb 100644
--- a/frontend/components/processors/processorhelperfunctions.tsx
+++ b/frontend/components/processors/processorhelperfunctions.tsx
@@ -1,16 +1,15 @@
-import { PoolConnection } from 'mysql2/promise';
-import { fileMappings, getConn, InsertUpdateProcessingProps, runQuery } from '@/components/processors/processormacros';
import { processCensus } from '@/components/processors/processcensus';
import MapperFactory from '@/config/datamapper';
-import { SitesRDS, SitesResult } from '@/config/sqlrdsdefinitions/zones';
import { handleUpsert } from '@/config/utils';
import { AllTaxonomiesViewRDS, AllTaxonomiesViewResult } from '@/config/sqlrdsdefinitions/views';
+import ConnectionManager from '@/config/connectionmanager';
+import { fileMappings, InsertUpdateProcessingProps } from '@/config/macros';
// need to try integrating this into validation system:
export async function insertOrUpdate(props: InsertUpdateProcessingProps): Promise {
const { formType, schema, ...subProps } = props;
- const { connection, rowData } = subProps;
+ const { connectionManager, rowData } = subProps;
const mapping = fileMappings[formType];
if (!mapping) {
throw new Error(`Mapping not found for file type: ${formType}`);
@@ -43,13 +42,10 @@ export async function insertOrUpdate(props: InsertUpdateProcessingProps): Promis
try {
// Execute the query using the provided connection
- await connection.beginTransaction();
- await connection.query(query, values);
- await connection.commit();
+ await connectionManager.executeQuery(query, values);
} catch (error) {
// Rollback the transaction in case of an error
console.log(`INSERT OR UPDATE: error in query execution: ${error}. Rollback commencing and error rethrow: `);
- await connection.rollback();
throw error; // Re-throw the error after rollback
}
}
@@ -57,73 +53,6 @@ export async function insertOrUpdate(props: InsertUpdateProcessingProps): Promis
}
}
-/**
- * Retrieves all available sites from the database.
- *
- * @returns {Promise} An array of `SitesRDS` objects representing the available sites.
- * @throws {Error} If there is an error connecting to the database or executing the query.
- */
-export async function getAllSchemas(): Promise {
- const connection: PoolConnection | null = await getConn();
- try {
- // Query to get sites
- const sitesQuery = `
- SELECT *
- FROM catalog.sites
- `;
- const sitesParams: any[] | undefined = [];
- const sitesResults = await runQuery(connection, sitesQuery, sitesParams);
-
- return MapperFactory.getMapper('sites').mapData(sitesResults);
- } catch (error: any) {
- throw new Error(error);
- } finally {
- if (connection) connection.release();
- }
-}
-
-/**
- * Retrieves the list of sites that the user with the given email address is allowed to access.
- *
- * @param email - The email address of the user.
- * @returns {Promise} An array of `SitesRDS` objects representing the sites the user is allowed to access.
- * @throws {Error} If there is an error connecting to the database or executing the query, or if the user is not found.
- */
-export async function getAllowedSchemas(email: string): Promise {
- const connection: PoolConnection | null = await getConn();
- try {
- // Query to get user ID
- const userQuery = `
- SELECT UserID
- FROM catalog.users
- WHERE Email = ?
- `;
- const userParams = [email];
- const userResults = await runQuery(connection, userQuery, userParams);
-
- if (userResults.length === 0) {
- throw new Error('User not found');
- }
- const userID = userResults[0].UserID;
-
- // Query to get sites
- const sitesQuery = `
- SELECT s.*
- FROM catalog.sites AS s
- JOIN catalog.usersiterelations AS usr ON s.SiteID = usr.SiteID
- WHERE usr.UserID = ?
- `;
- const sitesParams = [userID];
- const sitesResults = await runQuery(connection, sitesQuery, sitesParams);
-
- return MapperFactory.getMapper('sites').mapData(sitesResults);
- } catch (error: any) {
- throw new Error(error);
- } finally {
- if (connection) connection.release();
- }
-}
-
type FieldList = string[];
interface UpdateQueryConfig {
@@ -137,7 +66,7 @@ interface UpdateQueryConfig {
}
export async function handleUpsertForSlices(
- connection: PoolConnection,
+ connectionManager: ConnectionManager,
schema: string,
newRow: Partial,
config: UpdateQueryConfig
@@ -180,7 +109,7 @@ export async function handleUpsertForSlices(
}
// Perform the upsert and store the resulting ID
- insertedIds[sliceKey] = await handleUpsert(connection, schema, sliceKey, rowData, primaryKey as keyof Result);
+ insertedIds[sliceKey] = (await handleUpsert(connectionManager, schema, sliceKey, rowData, primaryKey as keyof Result)).id;
}
return insertedIds;
@@ -200,7 +129,7 @@ function getPreviousSlice(currentSlice: string, slices: { [key: string]: any }):
// Helper function to get the immediate previous slice based on dependencies
export async function handleDeleteForSlices(
- connection: PoolConnection,
+ connectionManager: ConnectionManager,
schema: string,
rowData: Partial,
config: UpdateQueryConfig
@@ -238,7 +167,7 @@ export async function handleDeleteForSlices(
`;
try {
console.log('Deleting related rows from trees for SpeciesID:', primaryKeyValue);
- await runQuery(connection, deleteFromRelatedTableQuery, [primaryKeyValue]);
+ await connectionManager.executeQuery(deleteFromRelatedTableQuery, [primaryKeyValue]);
} catch (error) {
console.error(`Error deleting related rows from trees for SpeciesID ${primaryKeyValue}:`, error);
throw new Error(`Failed to delete related rows from trees for SpeciesID ${primaryKeyValue}.`);
@@ -256,7 +185,7 @@ export async function handleDeleteForSlices(
console.log('Delete query:', deleteQuery);
// Use runQuery helper for executing the delete query
- await runQuery(connection, deleteQuery, [primaryKeyValue]);
+ await connectionManager.executeQuery(deleteQuery, [primaryKeyValue]);
} catch (error) {
console.error(`Error during deletion in ${sliceKey}:`, error);
throw new Error(`Failed to delete from ${sliceKey}. Please check the logs for details.`);
@@ -367,11 +296,11 @@ export async function runValidation(
minHOM?: number | null;
maxHOM?: number | null;
} = {}
-): Promise<{ TotalRows: number; Message: string }> {
- const conn = await getConn();
+): Promise {
+ const connectionManager = new ConnectionManager();
try {
- await conn.beginTransaction();
+ await connectionManager.beginTransaction();
// Dynamically replace SQL variables with actual TypeScript input values
const formattedCursorQuery = cursorQuery
@@ -381,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`)
@@ -423,7 +353,7 @@ export async function runValidation(
AND (${params.p_PlotID !== null ? `q.PlotID = ${params.p_PlotID}` : 'TRUE'})
LIMIT 1;
`;
- const speciesLimits = await runQuery(conn, speciesLimitsQuery);
+ const speciesLimits = await connectionManager.executeQuery(speciesLimitsQuery);
if (speciesLimits.length > 0) {
// If any species-specific limits were fetched, update the variables
@@ -442,85 +372,85 @@ 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 runQuery(conn, 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 runQuery(conn, insertErrorQuery, [row.CoreMeasurementID, validationProcedureID, row.CoreMeasurementID, validationProcedureID]);
- }
- }
-
- await conn.commit();
- 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 conn.rollback();
- console.error(`Error during ${validationProcedureName} validation:`, error.message);
- throw new Error(`${validationProcedureName} validation failed. Please check the logs for more details.`);
+ await connectionManager.rollbackTransaction();
+ console.error(`Error during ${validationProcedureName} or ${validationProcedureID} validation:`, error.message);
+ return false;
} finally {
- if (conn) conn.release();
+ await connectionManager.closeConnection();
}
}
export async function updateValidatedRows(schema: string, params: { p_CensusID?: number | null; p_PlotID?: number | null }): Promise {
- const conn = await getConn();
- const setVariables = `SET @p_CensusID = ?, @p_PlotID = ?;`;
+ 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 (@p_CensusID IS NULL OR c.CensusID = @p_CensusID)
- AND (@p_PlotID IS NULL OR c.PlotID = @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 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 {
- await conn.beginTransaction();
- await runQuery(conn, dropTemp); // just in case
- await runQuery(conn, setVariables, [params.p_CensusID || null, params.p_PlotID || null]);
- await runQuery(conn, tempTable);
- await runQuery(conn, insertTemp);
- await runQuery(conn, query);
- const results = await runQuery(conn, getUpdatedRows);
- await runQuery(conn, dropTemp);
- await conn.commit();
+ // Begin transaction
+ await connectionManager.beginTransaction();
+
+ // 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);
+
+ // 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) {
- await conn.rollback();
+ // 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 {
- if (conn) conn.release();
+ // Close the connection
+ await connectionManager.closeConnection();
}
}
diff --git a/frontend/components/processors/processormacros.tsx b/frontend/components/processors/processormacros.tsx
index d1d3b5d7..701c474b 100644
--- a/frontend/components/processors/processormacros.tsx
+++ b/frontend/components/processors/processormacros.tsx
@@ -1,102 +1,6 @@
import { PoolConnection, PoolOptions } from 'mysql2/promise';
-import { FileRow } from '@/config/macros/formdetails';
-import { processSpecies } from '@/components/processors/processspecies';
-import { processCensus } from '@/components/processors/processcensus';
import { PoolMonitor } from '@/config/poolmonitor';
-import { processPersonnel } from '@/components/processors/processpersonnel';
-import { processQuadrats } from '@/components/processors/processquadrats';
-export interface SpecialProcessingProps {
- connection: PoolConnection;
- rowData: FileRow;
- schema: string;
- plotID?: number;
- censusID?: number;
- quadratID?: number;
- fullName?: string;
-}
-
-export interface InsertUpdateProcessingProps extends SpecialProcessingProps {
- formType: string;
-}
-
-export type FileMapping = {
- tableName: string;
- columnMappings: { [fileColumn: string]: string };
- specialProcessing?: (props: Readonly) => Promise;
-};
-
-// Define the mappings for each file type
-export const fileMappings: Record = {
- attributes: {
- tableName: 'Attributes',
- columnMappings: {
- code: 'Code',
- description: 'Description',
- status: 'Status'
- }
- },
- personnel: {
- tableName: 'Personnel',
- columnMappings: {
- firstname: 'FirstName',
- lastname: 'LastName',
- role: 'Role'
- },
- specialProcessing: processPersonnel
- },
- species: {
- tableName: '',
- columnMappings: {
- spcode: 'Species.SpeciesCode',
- family: 'Family.Family',
- genus: 'Genus.GenusName',
- species: 'Species.SpeciesName',
- subspecies: 'Species.SubspeciesName', // optional
- IDLevel: 'Species.IDLevel',
- authority: 'Species.Authority',
- subauthority: 'Species.SubspeciesAuthority' // optional
- },
- specialProcessing: processSpecies
- },
- quadrats: {
- tableName: 'quadrats',
- // "quadrats": [{label: "quadrat"}, {label: "startx"}, {label: "starty"}, {label: "dimx"}, {label: "dimy"}, {label: "unit"}, {label: "quadratshape"}],
- columnMappings: {
- quadrat: 'QuadratName',
- plotID: 'PlotID',
- startx: 'StartX',
- starty: 'StartY',
- coordinateunit: 'CoordinateUnits',
- dimx: 'DimensionX',
- dimy: 'DimensionY',
- dimensionunit: 'DimensionUnits',
- quadratshape: 'QuadratShape'
- },
- specialProcessing: processQuadrats
- },
- // "subquadrats": "subquadrat, quadrat, dimx, dimy, xindex, yindex, unit, orderindex",
- subquadrats: {
- tableName: 'subquadrats',
- columnMappings: {
- subquadrat: 'SubquadratName',
- quadrat: 'QuadratID',
- plotID: 'PlotID',
- censusID: 'CensusID',
- dimx: 'DimensionX',
- dimy: 'DimensionY',
- xindex: 'X',
- yindex: 'Y',
- unit: 'Unit',
- orderindex: 'Ordering'
- }
- },
- measurements: {
- tableName: '', // Multiple tables involved
- columnMappings: {},
- specialProcessing: processCensus
- }
-};
const sqlConfig: PoolOptions = {
user: process.env.AZURE_SQL_USER,
password: process.env.AZURE_SQL_PASSWORD,
@@ -119,7 +23,7 @@ export async function getSqlConnection(tries: number): Promise {
// Check if the pool is closed and reinitialize if necessary
if (poolMonitor.isPoolClosed()) {
console.log('Connection pool is closed. Reinitializing...');
- poolMonitor.reinitializePool();
+ await poolMonitor.reinitializePool();
}
const connection = await poolMonitor.getConnection();
@@ -181,98 +85,3 @@ export async function runQuery(connection: PoolConnection, query: string, params
throw error;
}
}
-
-export function getCatalogSchema() {
- const catalogSchema = process.env.AZURE_SQL_CATALOG_SCHEMA;
- if (!catalogSchema) throw new Error('Environmental variable extraction for catalog schema failed');
- return catalogSchema;
-}
-
-export type ValidationResponse = {
- totalRows: number;
- failedRows: number;
- message: string;
- failedCoreMeasurementIDs?: number[];
-};
-export type UpdateValidationResponse = {
- rowsValidated: any;
-};
-
-export interface QueryConfig {
- schema: string;
- table: string;
- joins?: {
- table: string;
- alias: string;
- on: string;
- }[];
- conditionals?: string;
- pagination: {
- page: number;
- pageSize: number;
- };
- extraParams?: any[];
-}
-
-export function buildPaginatedQuery(config: QueryConfig): {
- query: string;
- params: any[];
-} {
- const { schema, table, joins, conditionals, pagination, extraParams } = config;
- const { page, pageSize } = pagination;
- const startRow = page * pageSize;
- const queryParams = extraParams || [];
-
- // Establish an alias for the primary table for consistency in joins and selections
- const tableAlias = table[0].toLowerCase(); // Simple default alias based on first letter of table name
-
- // Build the base query with possible joins
- let query = `SELECT SQL_CALC_FOUND_ROWS ${tableAlias}.* FROM ${schema}.${table} AS ${tableAlias}`;
- if (joins) {
- joins.forEach(join => {
- query += ` LEFT JOIN ${schema}.${join.table} AS ${join.alias} ON ${join.on}`;
- });
- }
-
- if (conditionals) {
- query += ` WHERE ${conditionals}`;
- }
-
- // Add LIMIT clause
- query += ` LIMIT ?, ?`;
- queryParams.push(startRow, pageSize); // Ensure these are the last parameters added
-
- return { query, params: queryParams };
-}
-
-// Function to close all active connections
-async function closeConnections() {
- console.log('Closing all active connections...');
- await poolMonitor.closeAllConnections();
- console.log('All connections closed.');
-}
-
-// Function to handle graceful shutdown
-async function gracefulShutdown() {
- console.log('Initiating graceful shutdown...');
- try {
- await closeConnections();
- console.log('Graceful shutdown complete.');
- process.exit(0);
- } catch (error) {
- console.error('Error during graceful shutdown:', error);
- process.exit(1);
- }
-}
-
-// Capture SIGINT signal (triggered by ctrl+c)
-process.on('SIGINT', async () => {
- console.log('SIGINT signal received.');
- await gracefulShutdown();
-});
-
-// Capture SIGTERM signal (triggered by process kill)
-process.on('SIGTERM', async () => {
- console.log('SIGTERM signal received.');
- await gracefulShutdown();
-});
diff --git a/frontend/components/processors/processpersonnel.tsx b/frontend/components/processors/processpersonnel.tsx
index 9d97dac1..3bed3c99 100644
--- a/frontend/components/processors/processpersonnel.tsx
+++ b/frontend/components/processors/processpersonnel.tsx
@@ -1,17 +1,15 @@
import { createError, createInsertOrUpdateQuery, createSelectQuery } from '@/config/utils';
-import { runQuery, SpecialProcessingProps } from '@/components/processors/processormacros';
import { PersonnelResult, RoleResult } from '@/config/sqlrdsdefinitions/personnel';
+import { SpecialProcessingProps } from '@/config/macros';
export async function processPersonnel(props: Readonly) {
- const { connection, rowData, schema, censusID } = props;
+ const { connectionManager, rowData, schema, censusID } = props;
if (!censusID) throw createError('CensusID missing', { censusID });
if (!rowData.role) throw createError('Row data does not contain a role property', { rowData });
const { firstname, lastname, role, roledescription } = rowData;
try {
- await connection.beginTransaction();
-
// Normalize the role name
const normalizedRole = role
.toLowerCase()
@@ -22,7 +20,7 @@ export async function processPersonnel(props: Readonly)
// Handle Role insertion/updation
const roleQuery = createSelectQuery(schema, 'roles', { RoleName: normalizedRole });
console.log('role query: ', roleQuery);
- const existingRoles = await runQuery(connection, roleQuery, [normalizedRole]);
+ const existingRoles = await connectionManager.executeQuery(roleQuery, [normalizedRole]);
console.log('existing roles: ', existingRoles);
let roleID;
@@ -33,7 +31,7 @@ export async function processPersonnel(props: Readonly)
console.log('existing role id: ', roleID);
const updateRoleQuery = `UPDATE \`${schema}\`.\`roles\` SET RoleDescription = ? WHERE RoleID = ?`;
console.log('update role query: ', updateRoleQuery);
- await runQuery(connection, updateRoleQuery, [roledescription, roleID]);
+ await connectionManager.executeQuery(updateRoleQuery, [roledescription, roleID]);
console.log('Role updated with description:', roledescription);
} else {
// If the role does not exist, insert a new role
@@ -41,7 +39,7 @@ export async function processPersonnel(props: Readonly)
RoleName: normalizedRole,
RoleDescription: roledescription
});
- const insertResult = await runQuery(connection, insertRoleQuery, [normalizedRole, roledescription]);
+ const insertResult = await connectionManager.executeQuery(insertRoleQuery, [normalizedRole, roledescription]);
roleID = insertResult.insertId;
console.log('New role inserted with RoleID:', roleID);
}
@@ -55,28 +53,25 @@ export async function processPersonnel(props: Readonly)
};
const personnelQuery = createSelectQuery(schema, 'personnel', personnelData);
- const existingPersonnel = await runQuery(connection, personnelQuery, Object.values(personnelData));
+ const existingPersonnel = await connectionManager.executeQuery(personnelQuery, Object.values(personnelData));
let personnelID;
if (existingPersonnel.length > 0) {
// If personnel exists, update the row
personnelID = existingPersonnel[0].PersonnelID;
const updatePersonnelQuery = createInsertOrUpdateQuery(schema, 'personnel', personnelData);
- await runQuery(connection, updatePersonnelQuery, Object.values(personnelData));
+ await connectionManager.executeQuery(updatePersonnelQuery, Object.values(personnelData));
console.log('Personnel updated:', personnelID);
} else {
// Insert new personnel record
const insertPersonnelQuery = createInsertOrUpdateQuery(schema, 'personnel', personnelData);
- const insertResult = await runQuery(connection, insertPersonnelQuery, Object.values(personnelData));
+ const insertResult = await connectionManager.executeQuery(insertPersonnelQuery, Object.values(personnelData));
personnelID = insertResult.insertId;
console.log('New personnel inserted with PersonnelID:', personnelID);
}
-
- await connection.commit();
console.log('Upsert successful. Personnel ID:', personnelID);
return personnelID;
} catch (error: any) {
- await connection.rollback();
console.error('Upsert failed:', error.message);
throw createError('Upsert failed', { error });
}
diff --git a/frontend/components/processors/processquadrats.tsx b/frontend/components/processors/processquadrats.tsx
index ef2bd7ae..a43780b8 100644
--- a/frontend/components/processors/processquadrats.tsx
+++ b/frontend/components/processors/processquadrats.tsx
@@ -1,15 +1,14 @@
-import { SpecialProcessingProps } from '@/components/processors/processormacros';
import { createError, handleUpsert } from '@/config/utils';
import { CensusQuadratResult, QuadratResult } from '@/config/sqlrdsdefinitions/zones';
+import { SpecialProcessingProps } from '@/config/macros';
export async function processQuadrats(props: Readonly) {
- const { connection, rowData, schema, plotID, censusID } = props;
+ const { connectionManager, rowData, schema, plotID, censusID } = props;
if (!censusID || !plotID) throw createError('CensusID missing', { censusID });
const { quadrat, startx, starty, coordinateunit, dimx, dimy, dimensionunit, area, areaunit, quadratshape } = rowData;
try {
- await connection.beginTransaction();
const quadratsData: Partial = {
QuadratName: quadrat,
PlotID: plotID,
@@ -24,7 +23,7 @@ export async function processQuadrats(props: Readonly) {
QuadratShape: quadratshape
};
- const quadratID = await handleUpsert(connection, schema, 'quadrats', quadratsData, 'QuadratID');
+ const { id: quadratID } = await handleUpsert(connectionManager, schema, 'quadrats', quadratsData, 'QuadratID');
if (!quadratID) throw createError('upsert failure for row: ', { quadratsData });
// need to update censusquadrat
@@ -33,13 +32,11 @@ export async function processQuadrats(props: Readonly) {
CensusID: censusID,
QuadratID: quadratID
};
- const cqID = await handleUpsert(connection, schema, 'censusquadrat', cqData, 'CQID');
+ const { id: cqID } = await handleUpsert(connectionManager, schema, 'censusquadrat', cqData, 'CQID');
if (!cqID) throw createError('upsert failure on censusquadrat for row: ', { cqData });
- await connection.commit();
return quadratID;
} catch (error: any) {
- await connection.rollback();
console.error('Upsert failed:', error.message);
throw createError('Upsert failed', { error });
}
diff --git a/frontend/components/processors/processspecies.tsx b/frontend/components/processors/processspecies.tsx
index a83824a3..ba1177b7 100644
--- a/frontend/components/processors/processspecies.tsx
+++ b/frontend/components/processors/processspecies.tsx
@@ -1,6 +1,6 @@
-import { SpecialProcessingProps } from '@/components/processors/processormacros';
import { FamilyResult, GenusResult, SpeciesResult } from '@/config/sqlrdsdefinitions/taxonomies';
import { createError, handleUpsert } from '@/config/utils';
+import { SpecialProcessingProps } from '@/config/macros';
function cleanInputData(data: any) {
const cleanedData: any = {};
@@ -13,35 +13,20 @@ function cleanInputData(data: any) {
}
export async function processSpecies(props: Readonly): Promise {
- const { connection, rowData, schema } = props;
+ const { connectionManager, rowData, schema } = props;
console.log('rowData: ', rowData);
try {
- await connection.beginTransaction();
-
- // Handle Family insertion/updation
let familyID: number | undefined;
if (rowData.family) {
- try {
- familyID = await handleUpsert(connection, schema, 'family', { Family: rowData.family }, 'FamilyID');
- } catch (error: any) {
- console.error('Family upsert failed:', error.message);
- throw createError('Family upsert failed', { error });
- }
+ familyID = (await handleUpsert(connectionManager, schema, 'family', { Family: rowData.family }, 'FamilyID')).id;
}
- // Handle Genus insertion/updation
let genusID: number | undefined;
if (rowData.genus) {
- try {
- genusID = await handleUpsert(connection, schema, 'genus', { Genus: rowData.genus, FamilyID: familyID }, 'GenusID');
- } catch (error: any) {
- console.error('Genus upsert failed:', error.message);
- throw createError('Genus upsert failed', { error });
- }
+ genusID = (await handleUpsert(connectionManager, schema, 'genus', { Genus: rowData.genus, FamilyID: familyID }, 'GenusID')).id;
}
- // Handle Species insertion/updation
let speciesID: number | undefined;
if (rowData.spcode) {
const speciesData = {
@@ -57,19 +42,12 @@ export async function processSpecies(props: Readonly): P
const cleanedSpeciesData = cleanInputData(speciesData);
console.log('Cleaned species data: ', cleanedSpeciesData);
- try {
- speciesID = await handleUpsert(connection, schema, 'species', cleanedSpeciesData, 'SpeciesID');
- } catch (error: any) {
- console.error('Species upsert failed:', error.message);
- throw createError('Species upsert failed', { error });
- }
+ speciesID = (await handleUpsert(connectionManager, schema, 'species', cleanedSpeciesData, 'SpeciesID')).id;
}
- await connection.commit();
console.log('Upsert successful');
return speciesID;
} catch (error: any) {
- await connection.rollback();
console.error('Upsert failed:', error.message);
throw createError('Upsert failed', { error });
}
diff --git a/frontend/components/sidebar.tsx b/frontend/components/sidebar.tsx
index 99413faa..ce186e7f 100644
--- a/frontend/components/sidebar.tsx
+++ b/frontend/components/sidebar.tsx
@@ -508,12 +508,12 @@ export default function Sidebar(props: SidebarProps) {
size={'md'}
data-testid={'plot-select-component'}
renderValue={renderPlotValue}
- onChange={async (_event: React.SyntheticEvent | null, newValue: string | null) => {
+ onChange={async (event: React.SyntheticEvent | null, newValue: string | null) => {
+ event?.preventDefault();
const selectedPlot = plotListContext?.find(plot => plot?.plotName === newValue) || undefined;
await handlePlotSelection(selectedPlot);
}}
>
-
{plotListContext?.map(item => (