diff --git a/.github/workflows/dev-forestgeo-livesite.yml b/.github/workflows/dev-forestgeo-livesite.yml index 3bfbc59b..a6b701b2 100644 --- a/.github/workflows/dev-forestgeo-livesite.yml +++ b/.github/workflows/dev-forestgeo-livesite.yml @@ -19,9 +19,9 @@ jobs: - uses: actions/checkout@v4 - name: Set up Node.js version - uses: actions/setup-node@v3 + uses: actions/setup-node@v4.0.4 with: - node-version: '18.x' + node-version: '20.x' - name: create env file (in frontend/ directory) -- development id: create-env-file-dev @@ -48,22 +48,22 @@ jobs: echo OWNER=${{ secrets.OWNER }} >> frontend/.env echo REPO=${{ secrets.REPO }} >> frontend/.env - - name: Cache node modules - uses: actions/cache@v2 - with: - path: frontend/node_modules - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- +# - name: Cache node modules +# uses: actions/cache@v2 +# with: +# path: frontend/node_modules +# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} +# restore-keys: | +# ${{ runner.os }}-node- - - name: Cache Next.js build - uses: actions/cache@v2 - with: - path: frontend/build/cache - key: ${{ runner.os }}-next-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/.next/cache') }} - restore-keys: | - ${{ runner.os }}-next- - ${{ runner.os }}-next-${{ hashFiles('**/package-lock.json') }} +# - name: Cache Next.js build +# uses: actions/cache@v2 +# with: +# path: frontend/build/cache +# key: ${{ runner.os }}-next-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/.next/cache') }} +# restore-keys: | +# ${{ runner.os }}-next- +# ${{ runner.os }}-next-${{ hashFiles('**/package-lock.json') }} - name: move into frontend --> npm install, build, and test run: | diff --git a/.github/workflows/main-forestgeo-livesite.yml b/.github/workflows/main-forestgeo-livesite.yml index 3da01339..86c5e5e2 100644 --- a/.github/workflows/main-forestgeo-livesite.yml +++ b/.github/workflows/main-forestgeo-livesite.yml @@ -13,7 +13,7 @@ jobs: build-app-production: if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest - environment: development + environment: production steps: - uses: actions/checkout@v4 @@ -30,7 +30,6 @@ jobs: echo AZURE_AD_CLIENT_SECRET=${{ secrets.AZURE_AD_CLIENT_SECRET_PRODUCTION }} >> frontend/.env echo AZURE_AD_CLIENT_ID=${{ secrets.AZURE_AD_CLIENT_ID_PRODUCTION }} >> frontend/.env echo AZURE_AD_TENANT_ID=${{ secrets.AZURE_AD_TENANT_ID_PRODUCTION }} >> frontend/.env - echo NEXTAUTH_URL=${{ secrets.NEXTAUTH_URL_DEV }} >> frontend/.env echo NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }} >> frontend/.env echo AZURE_SQL_USER=${{ secrets.AZURE_SQL_USER }} >> frontend/.env echo AZURE_SQL_PASSWORD=${{ secrets.AZURE_SQL_PASSWORD }} >> frontend/.env @@ -86,7 +85,7 @@ jobs: deploy-app-production: needs: build-app-production runs-on: ubuntu-latest - environment: development + environment: production steps: - name: Download build artifact @@ -103,4 +102,4 @@ jobs: app-name: 'forestgeo-livesite' slot-name: 'Production' publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_PRODUCTION }} - package: frontend/build/standalone + package: frontend/build/standalone \ No newline at end of file diff --git a/frontend/CHANGELOG.md b/frontend/CHANGELOG.md index 63f4382e..c04d7c05 100644 --- a/frontend/CHANGELOG.md +++ b/frontend/CHANGELOG.md @@ -118,11 +118,11 @@ 1. FixedData cases' queries updated to correctly work with updated schemas 2. New tables/cases added: - 1. `personnelrole` - 2. `sitespecificvalidations` - 3. `roles` - 4. `measurementssummary` - 5. `viewfulltable` + 1. `personnelrole` + 2. `sitespecificvalidations` + 3. `roles` + 4. `measurementssummary` + 5. `viewfulltable` ###### POST @@ -142,18 +142,18 @@ 1. Postvalidation summary statistics calculation endpoint 2. Statistics queries: - 1. `number of records by quadrat` - 2. `all stem records by quadrat (count only)` - 3. `live stem records by quadrat (count only)` - 4. `tree records by quadrat (count only)` - 5. `number of dead or missing stems by census` - 6. `trees outside of plot limits` - 7. `stems with largest DBH/HOM measurements by species` - 8. `all trees that were recorded in last census that are NOT in current census` - 9. `number of new stems per quadrat per census` - 10. `quadrats with most and least new stems per census` - 11. `number of dead stems per quadrat per census` - 12. `number of dead stems per species per census` + 1. `number of records by quadrat` + 2. `all stem records by quadrat (count only)` + 3. `live stem records by quadrat (count only)` + 4. `tree records by quadrat (count only)` + 5. `number of dead or missing stems by census` + 6. `trees outside of plot limits` + 7. `stems with largest DBH/HOM measurements by species` + 8. `all trees that were recorded in last census that are NOT in current census` + 9. `number of new stems per quadrat per census` + 10. `quadrats with most and least new stems per census` + 11. `number of dead stems per quadrat per census` + 12. `number of dead stems per species per census` #### frontend/app/api/refreshviews/[view]/[schema]/route.ts @@ -220,7 +220,7 @@ 3. customized cell and edit cell rendering added 4. some exceptions exist -- for instances where specific additional handling is needed, column states are directly defined in the datagrid components themselves. - 1. `alltaxonomiesview` -- specieslimits column customized addition + 1. `alltaxonomiesview` -- specieslimits column customized addition #### GitHub Feedback Modal @@ -250,18 +250,18 @@ 1. The DataGridCommons generic datagrid instance has been replaced by the IsolatedDataGridCommons instance, which isolates as much information as possible to the generic instance rather than the existing DataGridCommons, which requires parameter drilling of all MUI X DataGrid parameters. Current datagrids using this new implementation are: - - `alltaxonomiesview` - - `attributes` - - `personnel` - - `quadratpersonnel` - - `quadrats` - - `roles` - - `stemtaxonomiesview` + - `alltaxonomiesview` + - `attributes` + - `personnel` + - `quadratpersonnel` + - `quadrats` + - `roles` + - `stemtaxonomiesview` 2. found that attempting to use typescript runtime utilities to create "default" initial states for each RDS type was causing cascading failures. Due to the way that runtime utility functions work, no data was actually reaching the datagrids importing those initial states - 1. replaced with manual definition of initial states -- planning on centralizing this to another place, similar to - the `datagridcolumns.tsx` file + 1. replaced with manual definition of initial states -- planning on centralizing this to another place, similar to + the `datagridcolumns.tsx` file 3. `measurementssummaryview` datagrid instance added as a replacement to the previously defined summary page #### Re-Entry Data Modal @@ -307,20 +307,19 @@ 7. materialized view reload has been adjusted to be optional. user should be able to continue the process even if one or more of the views fails. ---- +--- ### SQL Updates 1. Schema has been been updated -- new tables added: - 1. `roles` - outlines user roles - 2. `specieslimits` - allows setting min/max bounds on measurements - 3. `specimens` - recording specimen data (added on request by ForestGEO) - 4. `unifiedchangelog` - partitioned table that tracks all changes to all tables in schema. All tables have triggers - that automatically update the `unifiedchangelog` on every change - 5. `sitespecificvalidations` - for specific validations applicable only to the host site + 1. `roles` - outlines user roles + 2. `specieslimits` - allows setting min/max bounds on measurements + 3. `specimens` - recording specimen data (added on request by ForestGEO) + 4. `unifiedchangelog` - partitioned table that tracks all changes to all tables in schema. All tables have triggers + that automatically update the `unifiedchangelog` on every change + 5. `sitespecificvalidations` - for specific validations applicable only to the host site 2. validation stored procedures have been deprecated and removed, replaced with `validationprocedures` and `sitespecificvalidations` tables 3. migration script set has been completed and tested 4. trigger definitions have been recorded 5. view implementations have been updated - diff --git a/frontend/__tests__/alltaxonomiesview_uqc.test.tsx b/frontend/__tests__/alltaxonomiesview_uqc.test.tsx new file mode 100644 index 00000000..86c8dc70 --- /dev/null +++ b/frontend/__tests__/alltaxonomiesview_uqc.test.tsx @@ -0,0 +1,104 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { getConn } from '@/components/processors/processormacros'; +import { PoolConnection } from 'mysql2/promise'; +import { AllTaxonomiesViewQueryConfig, handleUpsertForSlices } from '@/components/processors/processorhelperfunctions'; +import * as utils from '@/config/utils'; +import { AllTaxonomiesViewResult } from '@/config/sqlrdsdefinitions/views'; // Import utils module + +// Mock getConn and handleUpsert using vi.mock +vi.mock('@/components/processors/processormacros', () => ({ + getConn: vi.fn(), + runQuery: vi.fn() +})); + +vi.mock('@/config/utils', async () => { + // Import the actual utils module to access the original functions + const actualUtils = await vi.importActual('@/config/utils'); + + return { + ...actualUtils, // Keep all original utilities + handleUpsert: vi.fn() // Mock only handleUpsert + }; +}); + +describe('handleUpsertForSlices with AllTaxonomiesViewQueryConfig', () => { + let connection: PoolConnection; + + beforeEach(() => { + connection = { + beginTransaction: vi.fn(), + commit: vi.fn(), + rollback: vi.fn(), + release: vi.fn(), + query: vi.fn(), + execute: vi.fn() + } as unknown as PoolConnection; + + vi.mocked(getConn).mockResolvedValue(connection); + vi.mocked(utils.handleUpsert).mockResolvedValue(1); // Mock handleUpsert to return a consistent ID + + vi.clearAllMocks(); + }); + + it('should correctly propagate foreign keys across slices for AllTaxonomiesViewQueryConfig', async () => { + const newRow = { + Family: 'Fabaceae', + Genus: 'Acacia', + GenusAuthority: 'Willd.', + SpeciesCode: 'AC001', + SpeciesName: 'Acacia nilotica', + ValidCode: 'Y', + SubspeciesName: 'nilotica', + SpeciesAuthority: 'L.', + IDLevel: 2, + SubspeciesAuthority: 'DC.', + FieldFamily: 'Leguminosae', + Description: 'Thorny shrub' + }; + + const insertedIds = await handleUpsertForSlices(connection, 'schema_name', newRow, AllTaxonomiesViewQueryConfig); + + // Verify that handleUpsert was called 3 times (family, genus, species) + expect(utils.handleUpsert).toHaveBeenCalledTimes(3); + + // Verify that FamilyID, GenusID, and SpeciesID are propagated correctly + expect(utils.handleUpsert).toHaveBeenNthCalledWith(1, connection, 'schema_name', 'family', { Family: 'Fabaceae' }, 'FamilyID'); + expect(utils.handleUpsert).toHaveBeenNthCalledWith( + 2, + connection, + 'schema_name', + 'genus', + expect.objectContaining({ + FamilyID: 1, + Genus: 'Acacia', + GenusAuthority: 'Willd.' + }), + 'GenusID' + ); + expect(utils.handleUpsert).toHaveBeenNthCalledWith( + 3, + connection, + 'schema_name', + 'species', + expect.objectContaining({ + GenusID: 1, + SpeciesCode: 'AC001', + SpeciesName: 'Acacia nilotica', + ValidCode: 'Y', + SubspeciesName: 'nilotica', + SpeciesAuthority: 'L.', + IDLevel: 2, + SubspeciesAuthority: 'DC.', + FieldFamily: 'Leguminosae', + Description: 'Thorny shrub' + }), + 'SpeciesID' + ); + + expect(insertedIds).toEqual({ + family: 1, + genus: 1, + species: 1 + }); + }); +}); diff --git a/frontend/__tests__/stemtaxonomiesview_uqc.test.tsx b/frontend/__tests__/stemtaxonomiesview_uqc.test.tsx new file mode 100644 index 00000000..598d3706 --- /dev/null +++ b/frontend/__tests__/stemtaxonomiesview_uqc.test.tsx @@ -0,0 +1,70 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { getConn } from '@/components/processors/processormacros'; +import { PoolConnection } from 'mysql2/promise'; +import { handleUpsertForSlices, StemTaxonomiesViewQueryConfig } from '@/components/processors/processorhelperfunctions'; +import * as utils from '@/config/utils'; // Import utils module + +// Mock getConn and handleUpsert using vi.mock +vi.mock('@/components/processors/processormacros', () => ({ + getConn: vi.fn(), + runQuery: vi.fn() +})); + +vi.mock('@/config/utils', async () => { + // Import the actual utils module to access the original functions + const actualUtils = await vi.importActual('@/config/utils'); + + return { + ...actualUtils, // Keep all original utilities + handleUpsert: vi.fn() // Mock only handleUpsert + }; +}); + +describe('handleUpsertForSlices', () => { + let connection: PoolConnection; + + beforeEach(() => { + connection = { + beginTransaction: vi.fn(), + commit: vi.fn(), + rollback: vi.fn(), + release: vi.fn(), + query: vi.fn(), + execute: vi.fn() + } as unknown as PoolConnection; + + vi.mocked(getConn).mockResolvedValue(connection); + vi.mocked(utils.handleUpsert).mockResolvedValue(1); // Mock handleUpsert to return a consistent ID + + vi.clearAllMocks(); + }); + + it('should correctly propagate foreign keys across slices', async () => { + const newRow = { + TreeTag: 'T001', + StemTag: 'S001', + SpeciesCode: 'SP001', + SpeciesName: 'Test Species' + }; + + const insertedIds = await handleUpsertForSlices(connection, 'schema_name', newRow, StemTaxonomiesViewQueryConfig); + + // Verify that handleUpsert was called 5 times (family, genus, species, trees, stems) + expect(utils.handleUpsert).toHaveBeenCalledTimes(5); + + // Verify that the FamilyID, GenusID, and SpeciesID are propagated correctly + expect(utils.handleUpsert).toHaveBeenNthCalledWith(1, connection, 'schema_name', 'family', { SpeciesCode: 'SP001' }, 'FamilyID'); + expect(utils.handleUpsert).toHaveBeenNthCalledWith(2, connection, 'schema_name', 'genus', { FamilyID: 1, Family: undefined, Genus: undefined }, 'GenusID'); + expect(utils.handleUpsert).toHaveBeenNthCalledWith(3, connection, 'schema_name', 'species', { GenusID: 1, SpeciesName: 'Test Species' }, 'SpeciesID'); + expect(utils.handleUpsert).toHaveBeenNthCalledWith(4, connection, 'schema_name', 'trees', { SpeciesID: 1, StemTag: 'S001' }, 'TreeID'); + expect(utils.handleUpsert).toHaveBeenNthCalledWith(5, connection, 'schema_name', 'stems', { TreeTag: 'T001', TreeID: 1 }, 'StemID'); + + expect(insertedIds).toEqual({ + family: 1, + genus: 1, + species: 1, + trees: 1, + stems: 1 + }); + }); +}); diff --git a/frontend/app/(hub)/dashboard/page.tsx b/frontend/app/(hub)/dashboard/page.tsx index 5343d85f..1037c43f 100644 --- a/frontend/app/(hub)/dashboard/page.tsx +++ b/frontend/app/(hub)/dashboard/page.tsx @@ -31,6 +31,7 @@ import { useEffect, useState } from 'react'; import { UnifiedChangelogRDS } from '@/config/sqlrdsdefinitions/core'; import moment from 'moment'; import Avatar from '@mui/joy/Avatar'; +import { useLoading } from '@/app/contexts/loadingprovider'; export default function DashboardPage() { const { triggerPulse, isPulsing } = useLockAnimation(); @@ -43,6 +44,8 @@ export default function DashboardPage() { const userRole = session?.user?.userStatus; const allowedSites = session?.user?.sites; + const { setLoading } = useLoading(); + const [changelogHistory, setChangelogHistory] = useState(Array(5)); const [isLoading, setIsLoading] = useState(false); diff --git a/frontend/app/(hub)/measurementshub/postvalidation/page.tsx b/frontend/app/(hub)/measurementshub/postvalidation/page.tsx index 86ecfb66..2e28040a 100644 --- a/frontend/app/(hub)/measurementshub/postvalidation/page.tsx +++ b/frontend/app/(hub)/measurementshub/postvalidation/page.tsx @@ -1,92 +1,246 @@ 'use client'; import { useOrgCensusContext, usePlotContext, useSiteContext } from '@/app/contexts/userselectionprovider'; -import { useEffect, useState } from 'react'; -import { Box, LinearProgress } from '@mui/joy'; - -interface PostValidations { - queryID: number; - queryName: string; - queryDescription: string; -} - -interface PostValidationResults { - count: number; - data: any; -} +import React, { useEffect, useState } from 'react'; +import { Box, Button, Checkbox, Table, Typography, useTheme } from '@mui/joy'; +import { PostValidationQueriesRDS } from '@/config/sqlrdsdefinitions/validations'; +import PostValidationRow from '@/components/client/postvalidationrow'; +import { Paper, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material'; +import { Done } from '@mui/icons-material'; +import { useLoading } from '@/app/contexts/loadingprovider'; export default function PostValidationPage() { const currentSite = useSiteContext(); const currentPlot = usePlotContext(); const currentCensus = useOrgCensusContext(); - const [postValidations, setPostValidations] = useState([]); - const [validationResults, setValidationResults] = useState>({}); - const [loadingQueries, setLoadingQueries] = useState(false); + const [postValidations, setPostValidations] = useState([]); + const [expandedQuery, setExpandedQuery] = useState(null); + const [expandedResults, setExpandedResults] = useState(null); + const [selectedResults, setSelectedResults] = useState([]); + const replacements = { + schema: currentSite?.schemaName, + currentPlotID: currentPlot?.plotID, + currentCensusID: currentCensus?.dateRanges[0].censusID + }; + const { setLoading } = useLoading(); - // Fetch post-validation queries on first render - useEffect(() => { - async function loadQueries() { - try { - setLoadingQueries(true); - const response = await fetch(`/api/postvalidation?schema=${currentSite?.schemaName}`, { method: 'GET' }); - const data = await response.json(); - setPostValidations(data); - } catch (error) { - console.error('Error loading queries:', error); - } finally { - setLoadingQueries(false); - } + const enabledPostValidations = postValidations.filter(query => query.isEnabled); + const disabledPostValidations = postValidations.filter(query => !query.isEnabled); + + const theme = useTheme(); + const isDarkMode = theme.palette.mode === 'dark'; + + async function fetchValidationResults(postValidation: PostValidationQueriesRDS) { + if (!postValidation.queryID) return; + try { + await fetch( + `/api/postvalidationbyquery/${currentSite?.schemaName}/${currentPlot?.plotID}/${currentCensus?.dateRanges[0].censusID}/${postValidation.queryID}`, + { method: 'GET' } + ); + } catch (error: any) { + console.error(`Error fetching validation results for query ${postValidation.queryID}:`, error); + throw new Error(error); } + } - if (currentSite?.schemaName) { - loadQueries(); + async function loadPostValidations() { + try { + const response = await fetch(`/api/fetchall/postvalidationqueries?schema=${currentSite?.schemaName}`, { method: 'GET' }); + const data = await response.json(); + setPostValidations(data); + } catch (error) { + console.error('Error loading queries:', error); } - }, [currentSite?.schemaName]); + } - // Fetch validation results for each query - useEffect(() => { - async function fetchValidationResults(postValidation: PostValidations) { - try { - const response = await fetch( - `/api/postvalidationbyquery/${currentSite?.schemaName}/${currentPlot?.plotID}/${currentCensus?.dateRanges[0].censusID}/${postValidation.queryID}`, - { method: 'GET' } - ); - const data = await response.json(); - setValidationResults(prev => ({ - ...prev, - [postValidation.queryID]: data - })); - } catch (error) { - console.error(`Error fetching validation results for query ${postValidation.queryID}:`, error); - setValidationResults(prev => ({ - ...prev, - [postValidation.queryID]: null // Mark as failed if there was an error - })); - } + function saveResultsToFile() { + if (selectedResults.length === 0) { + alert('Please select at least one result to save.'); + return; + } + const blob = new Blob([JSON.stringify(selectedResults, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'results.json'; + a.click(); + URL.revokeObjectURL(url); + } + + function printResults() { + if (selectedResults.length === 0) { + alert('Please select at least one result to print.'); + return; } + const printContent = selectedResults.map(result => JSON.stringify(result, null, 2)).join('\n\n'); + const printWindow = window.open('', '', 'width=600,height=400'); + printWindow?.document.write(`
${printContent}
`); + printWindow?.document.close(); + printWindow?.print(); + } - if (postValidations.length > 0 && currentPlot?.plotID && currentCensus?.dateRanges) { - postValidations.forEach(postValidation => { - fetchValidationResults(postValidation).then(r => console.log(r)); - }); + useEffect(() => { + setLoading(true); + loadPostValidations() + .catch(console.error) + .then(() => setLoading(false)); + }, []); + + const handleExpandClick = (queryID: number) => { + setExpandedQuery(expandedQuery === queryID ? null : queryID); + }; + + const handleExpandResultsClick = (queryID: number) => { + setExpandedResults(expandedResults === queryID ? null : queryID); + }; + + const handleSelectResult = (postVal: PostValidationQueriesRDS) => { + setSelectedResults(prev => (prev.includes(postVal) ? prev.filter(id => id !== postVal) : [...prev, postVal])); + }; + + const handleSelectAllChange = (event: React.ChangeEvent) => { + if (event.target.checked) { + // Select all: add all validations to selectedResults + setSelectedResults([...enabledPostValidations, ...disabledPostValidations]); + } else { + // Deselect all: clear selectedResults + setSelectedResults([]); } - }, [postValidations, currentPlot?.plotID, currentCensus?.dateRanges, currentSite?.schemaName]); + }; + + // Check if all items are selected + const isAllSelected = selectedResults.length === postValidations.length && postValidations.length > 0; return ( - - {loadingQueries ? ( - - ) : postValidations.length > 0 ? ( - - {postValidations.map(postValidation => ( - -
{postValidation.queryName}
- {validationResults[postValidation.queryID] ? : } -
- ))} + + + These statistics can be used to analyze entered data. Please select and run, download, or print statistics as needed. + + + + + + + + {postValidations.length > 0 ? ( + + + + + + + + } + label={isAllSelected ? 'Deselect All' : 'Select All'} + checked={isAllSelected} + slotProps={{ + root: ({ checked, focusVisible }) => ({ + sx: !checked + ? { + '& svg': { opacity: focusVisible ? 1 : 0 }, + '&:hover svg': { + opacity: 1 + } + } + : undefined + }) + }} + onChange={e => handleSelectAllChange(e)} + /> + + Query Name + + Query Definition + + Description + Last Run At + Last Run Result + + + + + {enabledPostValidations.map(postValidation => ( + + ))} + + {disabledPostValidations.map(postValidation => ( + + ))} + +
+
) : ( -
No validations available.
+ No validations available. )}
); diff --git a/frontend/app/(hub)/measurementshub/summary/page.tsx b/frontend/app/(hub)/measurementshub/summary/page.tsx index da8b5f8b..7b894282 100644 --- a/frontend/app/(hub)/measurementshub/summary/page.tsx +++ b/frontend/app/(hub)/measurementshub/summary/page.tsx @@ -1,4 +1,4 @@ -import MeasurementsSummaryViewDataGrid from '@/components/datagrids/applications/measurementssummaryviewdatagrid'; +import MeasurementsSummaryViewDataGrid from '@/components/datagrids/applications/msvdatagrid'; export default function SummaryPage() { return ; diff --git a/frontend/app/(hub)/measurementshub/validations/page.tsx b/frontend/app/(hub)/measurementshub/validations/page.tsx index 99afe371..8fd2c617 100644 --- a/frontend/app/(hub)/measurementshub/validations/page.tsx +++ b/frontend/app/(hub)/measurementshub/validations/page.tsx @@ -62,7 +62,6 @@ export default function ValidationsPage() { try { const response = await fetch('/api/validations/crud', { method: 'GET' }); const data = await response.json(); - console.log('data: ', data); setGlobalValidations(data); } catch (err) { console.error('Error fetching validations:', err); diff --git a/frontend/app/api/auth/[[...nextauth]]/route.ts b/frontend/app/api/auth/[[...nextauth]]/route.ts index 64a1f752..b19e1a03 100644 --- a/frontend/app/api/auth/[[...nextauth]]/route.ts +++ b/frontend/app/api/auth/[[...nextauth]]/route.ts @@ -21,33 +21,26 @@ const handler = NextAuth({ }, callbacks: { async signIn({ user, profile, email: signInEmail }) { - console.log('callback -- signin'); const azureProfile = profile as AzureADProfile; const userEmail = user.email || signInEmail || azureProfile.preferred_username; - console.log('user email: ', userEmail); if (typeof userEmail !== 'string') { console.error('User email is not a string:', userEmail); return false; // Email is not a valid string, abort sign-in } if (userEmail) { - console.log('getting connection'); let conn, emailVerified, userStatus; try { conn = await getConn(); - console.log('obtained'); const query = `SELECT UserStatus FROM catalog.users WHERE Email = '${userEmail}' LIMIT 1`; const results = await runQuery(conn, query); - console.log('results: ', results); // emailVerified is true if there is at least one result emailVerified = results.length > 0; - console.log('emailVerified: ', emailVerified); if (!emailVerified) { console.error('User email not found.'); return false; } userStatus = results[0].UserStatus; - console.log('userStatus: ', userStatus); } catch (e: any) { console.error('Error fetching user status:', e); throw new Error('Failed to fetch user status.'); @@ -66,7 +59,6 @@ const handler = NextAuth({ user.sites = allowedSites; user.allsites = allSites; - // console.log('all sites: ', user.allsites); } return true; }, diff --git a/frontend/app/api/bulkcrud/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/bulkcrud/[dataType]/[[...slugs]]/route.ts new file mode 100644 index 00000000..9651d2f5 --- /dev/null +++ b/frontend/app/api/bulkcrud/[dataType]/[[...slugs]]/route.ts @@ -0,0 +1,54 @@ +// 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'; + +export async function POST(request: NextRequest, { params }: { params: { dataType: string; slugs?: string[] } }) { + const { dataType, slugs } = params; + if (!dataType || !slugs) { + return new NextResponse('No dataType or SLUGS provided', { status: HTTPResponses.INVALID_REQUEST }); + } + const [schema, plotIDParam, censusIDParam] = slugs; + const plotID = parseInt(plotIDParam); + const censusID = parseInt(censusIDParam); + console.log('params: schema: ', schema, ', plotID: ', plotID, ', censusID: ', censusID); + const rows: FileRowSet = await request.json(); + if (!rows) { + return new NextResponse('No rows provided', { status: 400 }); + } + console.log('rows produced: ', rows); + let conn: PoolConnection | null = null; + try { + conn = await getConn(); + for (const rowID in rows) { + const rowData = rows[rowID]; + console.log('rowData obtained: ', rowData); + const props: InsertUpdateProcessingProps = { + schema, + connection: conn, + formType: dataType, + rowData, + plotID, + censusID, + quadratID: undefined, + fullName: undefined + }; + console.log('assembled props: ', props); + await insertOrUpdate(props); + } + } catch (e: any) { + return new NextResponse( + JSON.stringify({ + responseMessage: `Failure in connecting to SQL with ${e.message}`, + error: e.message + }), + { status: HTTPResponses.INTERNAL_SERVER_ERROR } + ); + } finally { + if (conn) conn.release(); + } + 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 new file mode 100644 index 00000000..220a2898 --- /dev/null +++ b/frontend/app/api/catalog/[firstName]/[lastName]/route.ts @@ -0,0 +1,26 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { PoolConnection } from 'mysql2/promise'; +import { getConn, runQuery } from '@/components/processors/processormacros'; +import { HTTPResponses } from '@/config/macros'; + +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; + + try { + conn = await getConn(); + const query = `SELECT UserID FROM catalog.users WHERE FirstName = ? AND LastName = ?;`; + const results = await runQuery(conn, query, [firstName, lastName]); + if (results.length === 0) { + throw new Error('User not found'); + } + return new NextResponse(JSON.stringify(results[0].UserID), { status: HTTPResponses.OK }); + } catch (e: any) { + 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(); + } +} diff --git a/frontend/app/api/cmprevalidation/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/cmprevalidation/[dataType]/[[...slugs]]/route.ts index 23a2f19f..963accc9 100644 --- a/frontend/app/api/cmprevalidation/[dataType]/[[...slugs]]/route.ts +++ b/frontend/app/api/cmprevalidation/[dataType]/[[...slugs]]/route.ts @@ -56,6 +56,18 @@ export async function GET(_request: NextRequest, { params }: { params: { dataTyp status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE }); break; + case 'postvalidation': + const pvQuery = `SELECT 1 FROM ${schema}.coremeasurements cm + 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(); + 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 diff --git a/frontend/app/api/fetchall/[[...slugs]]/route.ts b/frontend/app/api/fetchall/[[...slugs]]/route.ts index 42daf7c6..bbaf8c6f 100644 --- a/frontend/app/api/fetchall/[[...slugs]]/route.ts +++ b/frontend/app/api/fetchall/[[...slugs]]/route.ts @@ -14,7 +14,7 @@ const buildQuery = (schema: string, fetchType: string, plotID?: string, plotCens ${schema}.quadrats q ON p.PlotID = q.PlotID GROUP BY p.PlotID ${plotID && plotID !== 'undefined' && !isNaN(parseInt(plotID)) ? `HAVING p.PlotID = ${plotID}` : ''}`; - } else if (fetchType === 'roles') { + } else if (fetchType === 'roles' || fetchType === 'attributes') { return `SELECT * FROM ${schema}.${fetchType}`; } else if (fetchType === 'quadrats') { diff --git a/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts index f8cd1329..0e37856f 100644 --- a/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts +++ b/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts @@ -64,9 +64,9 @@ export async function GET( break; case 'personnel': paginatedQuery = ` - SELECT SQL_CALC_FOUND_ROWS q.* - FROM ${schema}.${params.dataType} q - JOIN ${schema}.census c ON q.CensusID = c.CensusID + 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 = ? LIMIT ?, ?;`; queryParams.push(plotID, plotCensusNumber, page * pageSize, pageSize); @@ -100,17 +100,18 @@ export async function GET( 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 q.* - FROM ${schema}.${params.dataType} q - JOIN ${schema}.census c ON q.PlotID = c.PlotID AND q.CensusID = c.CensusID - WHERE q.PlotID = ? + 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 q.MeasurementDate ASC LIMIT ?, ?;`; + ORDER BY vft.MeasurementDate ASC LIMIT ?, ?;`; queryParams.push(plotID, plotID, plotCensusNumber, page * pageSize, pageSize); break; // case 'subquadrats': @@ -364,7 +365,6 @@ export async function DELETE(request: NextRequest, { params }: { params: { dataT let conn: PoolConnection | null = null; const demappedGridID = gridID.charAt(0).toUpperCase() + gridID.substring(1); const { newRow } = await request.json(); - console.log('newrow: ', newRow); try { conn = await getConn(); await conn.beginTransaction(); diff --git a/frontend/app/api/formdownload/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/formdownload/[dataType]/[[...slugs]]/route.ts new file mode 100644 index 00000000..0f3b83d6 --- /dev/null +++ b/frontend/app/api/formdownload/[dataType]/[[...slugs]]/route.ts @@ -0,0 +1,130 @@ +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'; + +export async function GET(_request: NextRequest, { params }: { params: { dataType: string; slugs?: string[] } }) { + const { dataType, slugs } = params; + if (!dataType || !slugs) throw new Error('data type or slugs not provided'); + const [schema, plotIDParam, censusIDParam] = slugs; + if (!schema) throw new Error('no schema provided'); + + const plotID = plotIDParam ? parseInt(plotIDParam) : undefined; + const censusID = censusIDParam ? parseInt(censusIDParam) : undefined; + + let conn: PoolConnection | null = null; + 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); + mappedResults = MapperFactory.getMapper('attributes').mapData(results); + formMappedResults = mappedResults.map((row: AttributesRDS) => ({ + code: row.code, + description: row.description, + status: row.status + })); + return new NextResponse(JSON.stringify(formMappedResults), { status: HTTPResponses.OK }); + case 'personnel': + query = `SELECT p.FirstName AS FirstName, p.LastName AS LastName, r.RoleName AS RoleName, r.RoleDescription AS RoleDescription + FROM ${schema}.personnel p + 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]); + formMappedResults = results.map((row: any) => ({ + firstname: row.FirstName, + lastname: row.LastName, + role: row.RoleName, + roledescription: row.RoleDescription + })); + return new NextResponse(JSON.stringify(formMappedResults), { status: HTTPResponses.OK }); + case 'species': + query = `SELECT DISTINCT s.SpeciesCode AS SpeciesCode, f.Family AS Family, + g.Genus AS Genus, s.SpeciesName AS SpeciesName, s.SubspeciesName AS SubspeciesName, + s.IDLevel AS IDLevel, s.SpeciesAuthority AS SpeciesAuthority, s.SubspeciesAuthority AS SubspeciesAuthority + FROM ${schema}.species s + JOIN ${schema}.genus g ON g.GenusID = s.GenusID + JOIN ${schema}.family f ON f.FamilyID = g.FamilyID + JOIN ${schema}.trees t ON t.SpeciesID = s.SpeciesID + JOIN ${schema}.stems st ON st.TreeID = t.TreeID + 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]); + formMappedResults = results.map((row: any) => ({ + spcode: row.SpeciesCode, + family: row.Family, + genus: row.Genus, + species: row.SpeciesName, + subspecies: row.SubspeciesName, + idlevel: row.IDLevel, + authority: row.SpeciesAuthority, + subspeciesauthority: row.SubspeciesAuthority + })); + return new NextResponse(JSON.stringify(formMappedResults), { status: HTTPResponses.OK }); + case 'quadrats': + 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]); + formMappedResults = results.map((row: any) => ({ + quadrat: row.QuadratName, + startx: row.StartX, + starty: row.StartY, + coordinateunit: row.CoordinateUnits, + dimx: row.DimensionX, + dimy: row.DimensionY, + dimensionunit: row.DimensionUnits, + area: row.Area, + areaunit: row.AreaUnits, + quadratshape: row.QuadratShape + })); + return new NextResponse(JSON.stringify(formMappedResults), { status: HTTPResponses.OK }); + case 'measurements': + query = `SELECT st.StemTag AS StemTag, t.TreeTag AS TreeTag, s.SpeciesCode AS SpeciesCode, q.QuadratName AS QuadratName, + q.StartX AS StartX, q.StartY AS StartY, q.CoordinateUnits AS CoordinateUnits, cm.MeasuredDBH AS MeasuredDBH, cm.DBHUnit AS DBHUnit, + cm.MeasuredHOM AS MeasuredHOM, cm.HOMUnit AS HOMUnit, cm.MeasurementDate AS MeasurementDate, + (SELECT GROUP_CONCAT(ca.Code SEPARATOR '; ') + FROM ${schema}.cmattributes ca + WHERE ca.CoreMeasurementID = cm.CoreMeasurementID) AS Codes + FROM ${schema}.coremeasurements cm + JOIN ${schema}.stems st ON st.StemID = cm.StemID + JOIN ${schema}.trees t ON t.TreeID = st.TreeID + JOIN ${schema}.quadrats q ON q.QuadratID = st.QuadratID + 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]); + formMappedResults = results.map((row: any) => ({ + tag: row.TreeTag, + stemtag: row.StemTag, + spcode: row.SpeciesCode, + quadrat: row.QuadratName, + lx: row.StartX, + ly: row.StartY, + coordinateunit: row.CoordinateUnits, + dbh: row.MeasuredDBH, + dbhunit: row.DBHUnit, + hom: row.MeasuredHOM, + homunit: row.HOMUnit, + date: row.MeasurementDate, + codes: row.Codes + })); + return new NextResponse(JSON.stringify(formMappedResults), { status: HTTPResponses.OK }); + default: + throw new Error('incorrect data type passed in'); + } + } catch (e: any) { + throw new Error(e); + } finally { + if (conn) conn.release(); + } +} diff --git a/frontend/app/api/postvalidation/route.ts b/frontend/app/api/postvalidation/route.ts index 0df42b31..e1ee5424 100644 --- a/frontend/app/api/postvalidation/route.ts +++ b/frontend/app/api/postvalidation/route.ts @@ -58,7 +58,7 @@ export async function GET(request: NextRequest) { // JOIN ${schema}.stems s ON s.TreeID = t.TreeID // JOIN ${schema}.quadrats q ON q.QuadratID = s.QuadratID // WHERE q.CensusID = ${currentCensusID} AND q.PlotID = ${currentPlotID};`, -// countNumDeadMissingByCensus: `SELECT cm.CensusID, COUNT(s.StemID) AS DeadOrMissingStems +// countNumDeadMissingByCensus: `SELECT s.StemID, COUNT(s.StemID) AS DeadOrMissingStems // FROM ${schema}.stems s // JOIN ${schema}.cmattributes cma ON s.StemID = cma.CoreMeasurementID // JOIN ${schema}.attributes a ON cma.Code = a.Code 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 90a100dc..e40acc17 100644 --- a/frontend/app/api/postvalidationbyquery/[schema]/[plotID]/[censusID]/[queryID]/route.ts +++ b/frontend/app/api/postvalidationbyquery/[schema]/[plotID]/[censusID]/[queryID]/route.ts @@ -1,36 +1,55 @@ import { NextRequest, NextResponse } from 'next/server'; import { HTTPResponses } from '@/config/macros'; import { getConn, runQuery } from '@/components/processors/processormacros'; +import moment from 'moment'; export async function GET(_request: NextRequest, { params }: { params: { schema: string; plotID: string; censusID: string; queryID: string } }) { const { schema } = params; const plotID = parseInt(params.plotID); const censusID = parseInt(params.censusID); const queryID = parseInt(params.queryID); + if (!schema || !plotID || !censusID || !queryID) { return new NextResponse('Missing parameters', { status: HTTPResponses.INVALID_REQUEST }); } + const conn = await getConn(); - const query = `SELECT QueryDefinition FROM ${schema}.postvalidationqueries WHERE QueryID = ${queryID}`; - const results = await runQuery(conn, query); - if (results.length === 0) { - return new NextResponse('Query not found', { status: HTTPResponses.NOT_FOUND }); - } - const replacements = { - schema: schema, - currentPlotID: plotID, - 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); - if (queryResults.length === 0) { - return new NextResponse('Query returned no results', { status: HTTPResponses.NOT_FOUND }); + try { + const query = `SELECT QueryDefinition FROM ${schema}.postvalidationqueries WHERE QueryID = ${queryID}`; + const results = await runQuery(conn, query); + + if (results.length === 0) return new NextResponse('Query not found', { status: HTTPResponses.NOT_FOUND }); + + const replacements = { + schema: schema, + currentPlotID: plotID, + 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); + + if (queryResults.length === 0) throw new Error('failure'); + + const currentTime = moment().format('YYYY-MM-DD HH:mm:ss'); + const successResults = JSON.stringify(queryResults); + const successUpdate = `UPDATE ${schema}.postvalidationqueries + SET LastRunAt = ?, LastRunResult = ?, LastRunStatus = 'success' + WHERE QueryID = ${queryID}`; + await runQuery(conn, successUpdate, [currentTime, successResults]); + + return new NextResponse(null, { status: HTTPResponses.OK }); + } catch (e: any) { + 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]); + 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(); } - return new NextResponse( - JSON.stringify({ - count: queryResults.length, - data: queryResults - }), - { status: HTTPResponses.OK } - ); } diff --git a/frontend/app/api/sqlload/route.ts b/frontend/app/api/sqlload/route.ts index 3e82e070..b448ae36 100644 --- a/frontend/app/api/sqlload/route.ts +++ b/frontend/app/api/sqlload/route.ts @@ -39,7 +39,6 @@ export async function POST(request: NextRequest) { let connection: PoolConnection | null = null; // Use PoolConnection type try { - const i = 0; connection = await getConn(); } catch (error) { if (error instanceof Error) { diff --git a/frontend/app/api/validations/procedures/[validationType]/route.ts b/frontend/app/api/validations/procedures/[validationType]/route.ts index 7606a619..43f23c52 100644 --- a/frontend/app/api/validations/procedures/[validationType]/route.ts +++ b/frontend/app/api/validations/procedures/[validationType]/route.ts @@ -5,7 +5,6 @@ import { HTTPResponses } from '@/config/macros'; export async function POST(request: NextRequest, { params }: { params: { validationProcedureName: string } }) { try { const { schema, validationProcedureID, cursorQuery, p_CensusID, p_PlotID, minDBH, maxDBH, minHOM, maxHOM } = await request.json(); - console.log('data: ', schema, validationProcedureID, cursorQuery, p_CensusID, p_PlotID, minDBH, maxDBH, minHOM, maxHOM); // Execute the validation procedure using the provided inputs const validationResponse = await runValidation(validationProcedureID, params.validationProcedureName, schema, cursorQuery, { diff --git a/frontend/components/client/datagridcolumns.tsx b/frontend/components/client/datagridcolumns.tsx index e5cc4cd3..39a724d7 100644 --- a/frontend/components/client/datagridcolumns.tsx +++ b/frontend/components/client/datagridcolumns.tsx @@ -1,16 +1,11 @@ import { areaSelectionOptions, unitSelectionOptions } from '@/config/macros'; -import { Accordion, AccordionDetails, AccordionGroup, AccordionSummary, Box, FormHelperText, Input, Option, Select, Stack, Typography } from '@mui/joy'; +import { Box, FormHelperText, Input, Option, Select, Stack, Typography } from '@mui/joy'; import { GridColDef, GridRenderEditCellParams, useGridApiRef } from '@mui/x-data-grid'; import React, { useEffect, useState } from 'react'; -import Avatar from '@mui/joy/Avatar'; -import { ExpandMore } from '@mui/icons-material'; -import { useSession } from 'next-auth/react'; -import CodeMirror from '@uiw/react-codemirror'; -import { sql } from '@codemirror/lang-sql'; import { AttributeStatusOptions } from '@/config/sqlrdsdefinitions/core'; export const formatHeader = (word1: string, word2: string) => ( - + {word1} @@ -79,7 +74,6 @@ export const quadratGridColumns: GridColDef[] = [ headerName: 'Coordinate Units', headerClassName: 'header', flex: 1, - // renderHeader: () => formatHeader('Coordinate', 'Units'), align: 'right', headerAlign: 'right', editable: true, @@ -104,7 +98,6 @@ export const quadratGridColumns: GridColDef[] = [ headerName: 'Area Unit', headerClassName: 'header', flex: 1, - // renderHeader: () => formatHeader('Area', 'Unit'), align: 'right', headerAlign: 'right', editable: true, @@ -550,7 +543,7 @@ export const MeasurementsSummaryViewGridColumns: GridColDef[] = [ editable: true }, { - field: 'stemLocalX', + field: 'localX', headerName: 'X', headerAlign: 'left', headerClassName: 'header', @@ -564,7 +557,7 @@ export const MeasurementsSummaryViewGridColumns: GridColDef[] = [ editable: true }, { - field: 'stemLocalY', + field: 'localY', headerName: 'Y', headerAlign: 'left', headerClassName: 'header', @@ -578,7 +571,7 @@ export const MeasurementsSummaryViewGridColumns: GridColDef[] = [ editable: true }, { - field: 'stemUnits', + field: 'coordinateUnits', headerName: 'Stem Units', headerClassName: 'header', flex: 0.4, @@ -596,30 +589,9 @@ export const MeasurementsSummaryViewGridColumns: GridColDef[] = [ flex: 0.8, align: 'right', editable: true, - // type: 'number', - // valueFormatter: (value: any) => { - // return Number(value).toFixed(2); - // } renderCell: renderDBHCell, renderEditCell: renderEditDBHCell - // valueFormatter: (params: any) => { - // const value = params.row.measuredDBH ? Number(params.row.measuredDBH).toFixed(2) : 'null'; - // const units = params.row.dbhUnits || ''; - // return `${value} ${units}`; - // } }, - // { - // field: 'dbhUnits', - // headerName: 'DBH Units', - // headerClassName: 'header', - // flex: 0.4, - // maxWidth: 65, - // renderHeader: () => formatHeader('DBH', 'Units'), - // align: 'center', - // editable: true, - // type: 'singleSelect', - // valueOptions: unitSelectionOptions - // }, { field: 'measuredHOM', headerName: 'HOM', @@ -628,30 +600,9 @@ export const MeasurementsSummaryViewGridColumns: GridColDef[] = [ align: 'right', headerAlign: 'left', editable: true, - // type: 'number', - // valueFormatter: (value: any) => { - // return Number(value).toFixed(2); - // } renderCell: renderHOMCell, renderEditCell: renderEditHOMCell - // valueFormatter: (params: any) => { - // const value = params.row.measuredDBH ? Number(params.row.measuredDBH).toFixed(2) : 'null'; - // const units = params.row.dbhUnits || ''; - // return `${value} ${units}`; - // } }, - // { - // field: 'homUnits', - // headerName: 'HOM Units', - // headerClassName: 'header', - // flex: 0.4, - // maxWidth: 65, - // renderHeader: () => formatHeader('HOM', 'Units'), - // align: 'center', - // editable: true, - // type: 'singleSelect', - // valueOptions: unitSelectionOptions - // }, { field: 'description', headerName: 'Description', @@ -663,7 +614,7 @@ export const MeasurementsSummaryViewGridColumns: GridColDef[] = [ { field: 'attributes', headerName: 'Attributes', headerClassName: 'header', flex: 1, align: 'left', editable: true } ]; -export const CensusGridColumns: GridColDef[] = [ +export const StemGridColumns: GridColDef[] = [ { field: 'id', headerName: '#', @@ -674,164 +625,69 @@ export const CensusGridColumns: GridColDef[] = [ editable: false }, { - field: 'censusID', - headerName: 'ID', - type: 'number', + field: 'stemTag', + headerName: 'Stem Tag', headerClassName: 'header', flex: 1, align: 'left', - editable: false + type: 'string', + editable: true }, { - field: 'plotCensusNumber', - headerName: 'PlotCensusNumber', - type: 'number', + field: 'localX', + headerName: 'Plot X', headerClassName: 'header', flex: 1, align: 'left', - editable: false + type: 'number', + valueFormatter: (value: any) => { + return Number(value).toFixed(2); + }, + editable: true }, { - field: 'startDate', - headerName: 'Starting', + field: 'localY', + headerName: 'Plot Y', headerClassName: 'header', flex: 1, align: 'left', - type: 'date', - editable: true, - valueFormatter: (params: any) => { - if (params) { - return new Date(params).toDateString(); - } else return 'null'; - } + type: 'number', + valueFormatter: (value: any) => { + return Number(value).toFixed(2); + }, + editable: true }, { - field: 'endDate', - headerName: 'Ending', + field: 'coordinateUnits', + headerName: 'Unit', headerClassName: 'header', - type: 'date', flex: 1, align: 'left', - editable: true, - valueFormatter: (params: any) => { - if (params) { - return new Date(params).toDateString(); - } else return 'null'; - } - }, - { - field: 'description', - headerName: 'Description', - headerClassName: 'header', - flex: 1, - type: 'string', + type: 'singleSelect', + valueOptions: unitSelectionOptions, editable: true - } -]; - -export const ValidationErrorGridColumns: GridColDef[] = [ - { - field: 'id', - headerName: '#', - headerClassName: 'header', - flex: 0.3, - align: 'right', - headerAlign: 'right', - editable: false - }, - { field: 'validationErrorID', headerName: 'ValidationErrorID', headerClassName: 'header', flex: 1, align: 'left' }, - { - field: 'validationErrorDescription', - headerName: 'ValidationErrorDescription', - headerClassName: 'header', - flex: 1, - align: 'left' - } -]; - -export const CoreMeasurementsGridColumns: GridColDef[] = [ - { - field: 'id', - headerName: 'ID', - headerClassName: 'header', - flex: 0.3, - align: 'right', - headerAlign: 'right', - editable: false - }, - { - field: 'coreMeasurementID', - headerName: '#', - headerAlign: 'left', - headerClassName: 'header', - flex: 0.25, - align: 'left' }, { - field: 'censusID', - headerName: 'Census ID', - headerAlign: 'left', + field: 'moved', + headerName: 'Moved', headerClassName: 'header', flex: 1, align: 'left', + type: 'boolean', editable: true }, { - field: 'stemID', - headerName: 'Stem ID', - headerAlign: 'left', + field: 'stemDescription', + headerName: 'StemDescription', headerClassName: 'header', flex: 1, align: 'left', + type: 'string', editable: true - }, - { - field: 'measuredDBH', - headerName: 'DBH', - headerClassName: 'header', - flex: 0.8, - align: 'right', - editable: true, - renderCell: renderDBHCell, - renderEditCell: renderEditDBHCell - }, - { - field: 'dbhUnits', - headerName: 'DBH Units', - headerClassName: 'header', - flex: 0.4, - maxWidth: 65, - renderHeader: () => formatHeader('DBH', 'Units'), - align: 'center', - editable: true, - type: 'singleSelect', - valueOptions: unitSelectionOptions - }, - { - field: 'measuredHOM', - headerName: 'HOM', - headerClassName: 'header', - flex: 0.5, - align: 'right', - headerAlign: 'left', - editable: true, - renderCell: renderHOMCell, - renderEditCell: renderEditHOMCell - }, - { - field: 'homUnits', - headerName: 'HOM Units', - headerClassName: 'header', - maxWidth: 65, - renderHeader: () => formatHeader('HOM', 'Units'), - align: 'center', - editable: true, - type: 'singleSelect', - valueOptions: unitSelectionOptions } ]; -export const SubquadratGridColumns: GridColDef[] = [ +export const SpeciesLimitsGridColumns: GridColDef[] = [ { field: 'id', headerName: '#', @@ -841,75 +697,67 @@ export const SubquadratGridColumns: GridColDef[] = [ headerAlign: 'right', editable: false }, - { field: 'ordering', headerName: 'Order', headerClassName: 'header', flex: 1, align: 'left', editable: false }, { - field: 'subquadratName', - headerName: 'Name', + field: 'speciesLimitID', + headerName: '#', headerClassName: 'header', - flex: 1, + flex: 0.3, align: 'left', - type: 'string', - editable: true + headerAlign: 'left', + editable: false }, - { field: 'quadratID', headerName: 'Quadrat', headerClassName: 'header', flex: 1, align: 'left', editable: false }, { - field: 'dimensionX', - headerName: 'X-Dimension', + field: 'speciesID', + headerName: 'SpeciesID', headerClassName: 'header', - flex: 1, + flex: 0.3, align: 'left', - type: 'number', - editable: true + headerAlign: 'left', + editable: false }, { - field: 'dimensionY', - headerName: 'Y-Dimension', - headerClassName: 'header', - flex: 1, + field: 'limitType', + headerName: 'LimitType', + renderHeader: () => formatHeader('Limit', 'Type'), + flex: 0.5, align: 'left', - type: 'number', - valueFormatter: (value: any) => { - return Number(value).toFixed(2); - }, + headerAlign: 'left', + type: 'singleSelect', + valueOptions: ['DBH', 'HOM'], editable: true }, { - field: 'qX', - headerName: 'X', - headerClassName: 'header', - flex: 1, + field: 'lowerBound', + headerName: 'LowerBound', + renderHeader: () => formatHeader('Lower', 'Limit'), + flex: 0.5, align: 'left', + headerAlign: 'left', type: 'number', - valueFormatter: (value: any) => { - return Number(value).toFixed(2); - }, editable: true }, { - field: 'qY', - headerName: 'Y', - headerClassName: 'header', - flex: 1, + field: 'upperBound', + headerName: 'UpperBound', + renderHeader: () => formatHeader('Upper', 'Limit'), + flex: 0.5, align: 'left', + headerAlign: 'left', type: 'number', - valueFormatter: (value: any) => { - return Number(value).toFixed(2); - }, editable: true }, { field: 'unit', headerName: 'Units', headerClassName: 'header', - flex: 1, + flex: 0.3, align: 'left', type: 'singleSelect', - valueOptions: unitSelectionOptions, - editable: true + valueOptions: unitSelectionOptions } ]; -export const StemGridColumns: GridColDef[] = [ +export const RolesGridColumns: GridColDef[] = [ { field: 'id', headerName: '#', @@ -920,523 +768,27 @@ export const StemGridColumns: GridColDef[] = [ editable: false }, { - field: 'stemTag', - headerName: 'Stem Tag', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'string', - editable: true - }, - { - field: 'localX', - headerName: 'Plot X', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'number', - valueFormatter: (value: any) => { - return Number(value).toFixed(2); - }, - editable: true - }, - { - field: 'localY', - headerName: 'Plot Y', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'number', - valueFormatter: (value: any) => { - return Number(value).toFixed(2); - }, - editable: true - }, - { - field: 'coordinateUnits', - headerName: 'Unit', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'singleSelect', - valueOptions: unitSelectionOptions, - editable: true - }, - { - field: 'moved', - headerName: 'Moved', + field: 'roleID', + headerName: '#', headerClassName: 'header', - flex: 1, - align: 'left', - type: 'boolean', - editable: true + flex: 0.2, + align: 'right', + headerAlign: 'right', + editable: false }, + { field: 'roleName', headerName: 'Role', headerClassName: 'header', flex: 1, align: 'left', editable: true }, { - field: 'stemDescription', - headerName: 'StemDescription', + field: 'roleDescription', + headerName: 'Description', headerClassName: 'header', flex: 1, align: 'left', - type: 'string', editable: true } ]; - -export const SpeciesInventoryGridColumns: GridColDef[] = [ - { - field: 'id', - headerName: '#', - headerClassName: 'header', - flex: 0.3, - align: 'right', - headerAlign: 'right', - editable: false - }, - { field: 'speciesInventoryID', headerName: 'SpeciesInventoryID', headerClassName: 'header', flex: 1, align: 'left' }, - { field: 'censusID', headerName: 'CensusID', headerClassName: 'header', flex: 1, align: 'left' }, - { field: 'plotID', headerName: 'PlotID', headerClassName: 'header', flex: 1, align: 'left' }, - { field: 'speciesID', headerName: 'SpeciesID', headerClassName: 'header', flex: 1, align: 'left' }, - { field: 'subSpeciesID', headerName: 'SubSpeciesID', headerClassName: 'header', flex: 1, align: 'left' } -]; - -export const SpeciesGridColumns: GridColDef[] = [ - { - field: 'id', - headerName: '#', - headerClassName: 'header', - flex: 0.3, - align: 'right', - headerAlign: 'right', - editable: false - }, - { - field: 'speciesCode', - headerName: 'SpCode', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'string', - editable: true, - maxWidth: 125 - }, - { - field: 'speciesName', - headerName: 'Species', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'string', - editable: true - }, - { - field: 'subspeciesName', - headerName: 'Subspecies', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'string', - editable: true - }, - { - field: 'idLevel', - headerName: 'IDLevel', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'string', - editable: true - }, - { - field: 'speciesAuthority', - headerName: 'SpeciesAuth', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'string', - editable: true - }, - { - field: 'subspeciesAuthority', - headerName: 'SubspeciesAuth', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'string', - editable: true - }, - { - field: 'fieldFamily', - headerName: 'FieldFamily', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'string', - editable: true - }, - { - field: 'description', - headerName: 'Description', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'string', - editable: true - }, - { - field: 'validCode', - headerName: 'Valid Code', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'string', - editable: true - } -]; - -export const SpeciesLimitsGridColumns: GridColDef[] = [ - { - field: 'id', - headerName: '#', - headerClassName: 'header', - flex: 0.3, - align: 'right', - headerAlign: 'right', - editable: false - }, - { - field: 'speciesLimitID', - headerName: '#', - headerClassName: 'header', - flex: 0.3, - align: 'left', - headerAlign: 'left', - editable: false - }, - { - field: 'speciesID', - headerName: 'SpeciesID', - headerClassName: 'header', - flex: 0.3, - align: 'left', - headerAlign: 'left', - editable: false - }, - { - field: 'limitType', - headerName: 'LimitType', - renderHeader: () => formatHeader('Limit', 'Type'), - flex: 0.5, - align: 'left', - headerAlign: 'left', - type: 'singleSelect', - valueOptions: ['DBH', 'HOM'], - editable: true - }, - { - field: 'lowerBound', - headerName: 'LowerBound', - renderHeader: () => formatHeader('Lower', 'Limit'), - flex: 0.5, - align: 'left', - headerAlign: 'left', - type: 'number', - editable: true - }, - { - field: 'upperBound', - headerName: 'UpperBound', - renderHeader: () => formatHeader('Upper', 'Limit'), - flex: 0.5, - align: 'left', - headerAlign: 'left', - type: 'number', - editable: true - }, - { - field: 'unit', - headerName: 'Units', - headerClassName: 'header', - flex: 0.3, - align: 'left', - type: 'singleSelect', - valueOptions: unitSelectionOptions - } -]; - -export const RolesGridColumns: GridColDef[] = [ - { - field: 'id', - headerName: '#', - headerClassName: 'header', - flex: 0.3, - align: 'right', - headerAlign: 'right', - editable: false - }, - { - field: 'roleID', - headerName: '#', - headerClassName: 'header', - flex: 0.2, - align: 'right', - headerAlign: 'right', - editable: false - }, - // { field: 'roleID', headerName: 'RoleID', headerClassName: 'header', flex: 1, align: 'left', editable: false }, - { field: 'roleName', headerName: 'Role', headerClassName: 'header', flex: 1, align: 'left', editable: true }, - { - field: 'roleDescription', - headerName: 'Description', - headerClassName: 'header', - flex: 1, - align: 'left', - editable: true - } -]; - -export const ReferenceGridColumns: GridColDef[] = [ - { - field: 'id', - headerName: '#', - headerClassName: 'header', - flex: 0.3, - align: 'right', - headerAlign: 'right', - editable: false - }, - { field: 'referenceID', headerName: 'ReferenceID', headerClassName: 'header', flex: 1, align: 'left' }, - { field: 'publicationTitle', headerName: 'PublicationTitle', headerClassName: 'header', flex: 1, align: 'left' }, - { field: 'fullReference', headerName: 'FullReference', headerClassName: 'header', flex: 1, align: 'left' }, - { - field: 'dateOfPublication', - headerName: 'DateOfPublication', - type: 'date', - headerClassName: 'header', - flex: 1, - align: 'left', - valueGetter: (params: any) => { - if (!params.value) return null; - return new Date(params.value); - } - } -]; - -export const PlotGridColumns: GridColDef[] = [ - { - field: 'id', - headerName: '#', - headerClassName: 'header', - flex: 0.3, - align: 'right', - headerAlign: 'right', - editable: false - }, - { field: 'plotID', headerName: 'PlotID', headerClassName: 'header', flex: 1, align: 'left', editable: false }, - { field: 'plotName', headerName: 'PlotName', headerClassName: 'header', flex: 1, align: 'left', editable: true }, - { - field: 'locationName', - headerName: 'LocationName', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'string', - editable: true - }, - { - field: 'countryName', - headerName: 'CountryName', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'string', - editable: true - }, - { - field: 'dimensionX', - headerName: 'DimX', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'number', - valueFormatter: (value: any) => { - return Number(value).toFixed(2); - }, - editable: true - }, - { - field: 'dimensionY', - headerName: 'DimY', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'number', - valueFormatter: (value: any) => { - return Number(value).toFixed(2); - }, - editable: true - }, - { - field: 'area', - headerName: 'Area', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'number', - valueFormatter: (value: any) => { - return Number(value).toFixed(2); - }, - editable: true - }, - { - field: 'globalX', - headerName: 'GlobalX', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'number', - valueFormatter: (value: any) => { - return Number(value).toFixed(2); - }, - editable: true - }, - { - field: 'globalY', - headerName: 'GlobalY', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'number', - valueFormatter: (value: any) => { - return Number(value).toFixed(2); - }, - editable: true - }, - { - field: 'globalZ', - headerName: 'GlobalZ', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'number', - valueFormatter: (value: any) => { - return Number(value).toFixed(2); - }, - editable: true - }, - { - field: 'unit', - headerName: 'Units', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'singleSelect', - valueOptions: unitSelectionOptions - }, - { - field: 'plotShape', - headerName: 'PlotShape', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'string', - editable: true - }, - { - field: 'plotDescription', - headerName: 'PlotDescription', - headerClassName: 'header', - flex: 1, - align: 'left', - type: 'string', - editable: true - } -]; - -export const GenusGridColumns: GridColDef[] = [ - { - field: 'id', - headerName: '#', - headerClassName: 'header', - flex: 0.3, - align: 'right', - headerAlign: 'right', - editable: false - }, - { field: 'genusID', headerName: 'GenusID', headerClassName: 'header', flex: 1, align: 'left', editable: false }, - { field: 'familyID', headerName: 'FamilyID', headerClassName: 'header', flex: 1, align: 'left', editable: false }, - { field: 'genus', headerName: 'GenusName', headerClassName: 'header', flex: 1, align: 'left', editable: true }, - { - field: 'referenceID', - headerName: 'ReferenceID', - headerClassName: 'header', - flex: 1, - align: 'left', - editable: false - }, - { - field: 'genusAuthority', - headerName: 'Authority', - headerClassName: 'header', - flex: 1, - align: 'left', - editable: true - } -]; - -export const FamilyGridColumns: GridColDef[] = [ - { - field: 'id', - headerName: '#', - headerClassName: 'header', - flex: 0.3, - align: 'right', - headerAlign: 'right', - editable: false - }, - { field: 'familyID', headerName: 'FamilyID', headerClassName: 'header', flex: 1, align: 'left', editable: false }, - { field: 'family', headerName: 'Family', headerClassName: 'header', flex: 1, align: 'left', editable: false }, - { - field: 'referenceID', - headerName: 'ReferenceID', - headerClassName: 'header', - flex: 1, - align: 'left', - editable: false - } -]; -export const CMVErrorGridColumns: GridColDef[] = [ - { - field: 'id', - headerName: '#', - headerClassName: 'header', - flex: 0.3, - align: 'right', - headerAlign: 'right', - editable: false - }, - { field: 'cmvErrorID', headerName: 'CMVErrorID', headerClassName: 'header', flex: 1, align: 'left' }, - { field: 'coreMeasurementID', headerName: 'CoreMeasurementID', headerClassName: 'header', flex: 1, align: 'left' }, - { field: 'validationErrorID', headerName: 'ValidationErrorID', headerClassName: 'header', flex: 1, align: 'left' } -]; - -export const CMAttributeGridColumns: GridColDef[] = [ - { - field: 'id', - headerName: '#', - headerClassName: 'header', - flex: 0.3, - align: 'right', - headerAlign: 'right', - editable: false - }, - { field: 'cmaID', headerName: 'CMAID', headerClassName: 'header', flex: 1, align: 'left' }, - { field: 'coreMeasurementID', headerName: 'CoreMeasurementID', headerClassName: 'header', flex: 1, align: 'left' }, - { field: 'code', headerName: 'Code', headerClassName: 'header', flex: 1, align: 'left' } -]; - -// Combine the column definitions -const combineColumns = (primary: GridColDef[], secondary: GridColDef[]): GridColDef[] => { - const combined = [...primary]; +// Combine the column definitions +const combineColumns = (primary: GridColDef[], secondary: GridColDef[]): GridColDef[] => { + const combined = [...primary]; secondary.forEach(secondaryColumn => { const primaryColumnIndex = primary.findIndex(primaryColumn => primaryColumn.field === secondaryColumn.field); @@ -1476,177 +828,4 @@ export const ViewFullTableGridColumns = rawColumns.map(column => { return column; }); -export const ValidationProceduresGridColumns: GridColDef[] = [ - { field: 'id', headerName: 'ID', headerClassName: 'header' }, - { field: 'validationID', headerName: '#', headerClassName: 'header' }, - { - field: 'procedureName', - headerName: 'Procedure', - headerClassName: 'header', - type: 'string', - editable: true, - flex: 1, - renderCell: (params: GridRenderEditCellParams) => { - const value = params.row.procedureName.replace(/(DBH|HOM)([A-Z])/g, '$1 $2').replace(/([a-z])([A-Z])/g, '$1 $2'); - return {value}; - } - }, - { - field: 'description', - headerName: 'Description', - headerClassName: 'header', - type: 'string', - editable: true, - flex: 1, - renderCell: (params: GridRenderEditCellParams) => { - return {params.row.description}; - } - }, - { - field: 'definition', - headerName: 'SQL Implementation', - headerClassName: 'header', - type: 'string', - editable: true, - flex: 1, - renderCell: (params: GridRenderEditCellParams) => { - const { data: session } = useSession(); - let isEditing = false; - if (typeof params.id === 'string') { - isEditing = params.rowModesModel[parseInt(params.id)]?.mode === 'edit'; - } - const isAdmin = session?.user?.userStatus === 'db admin' || session?.user?.userStatus === 'global'; - - if (isEditing && isAdmin) { - return ( - { - // Update the grid row with the new value from CodeMirror - params.api.updateRows([{ ...params.row, definition: value }]); - }} - /> - ); - } - - return ( - - - - - - - {params.row.description} - - - {params.row.definition} - - - - ); - } - }, - { - field: 'createdAt', - headerName: 'Created At', - renderHeader: () => formatHeader('Created', 'At'), - type: 'date', - headerClassName: 'header', - headerAlign: 'center', - valueGetter: (params: any) => { - if (!params || !params.value) return null; - return new Date(params.value); - }, - editable: true, - flex: 0.4 - }, - { - field: 'updatedAt', - headerName: 'Updated At', - renderHeader: () => formatHeader('Updated', 'At'), - type: 'date', - headerClassName: 'header', - headerAlign: 'center', - valueGetter: (params: any) => { - if (!params || !params.value) return null; - return new Date(params.value); - }, - editable: true, - flex: 0.4 - }, - { field: 'isEnabled', headerName: 'Active?', headerClassName: 'header', type: 'boolean', editable: true, flex: 0.2 } -]; - -export const SiteSpecificValidationsGridColumns: GridColDef[] = [ - { field: 'id', headerName: 'ID', headerClassName: 'header' }, - { field: 'validationProcedureID', headerName: '#', headerClassName: 'header' }, - { - field: 'name', - headerName: 'Procedure', - headerClassName: 'header', - type: 'string', - editable: true, - flex: 1, - renderCell: (params: GridRenderEditCellParams) => { - const value = params.row.procedureName.replace(/(DBH|HOM)([A-Z])/g, '$1 $2').replace(/([a-z])([A-Z])/g, '$1 $2'); - return {value}; - } - }, - { - field: 'description', - headerName: 'Description', - headerClassName: 'header', - type: 'string', - editable: true, - flex: 1, - renderCell: (params: GridRenderEditCellParams) => { - return {params.row.description}; - } - }, - { - field: 'definition', - headerName: 'SQL Implementation', - headerClassName: 'header', - type: 'string', - editable: true, - flex: 1, - renderCell: (params: GridRenderEditCellParams) => { - return ( - - - - - - - {params.row.description} - - - {params.row.description} - - - - ); - } - }, - { field: 'isEnabled', headerName: 'Active?', headerClassName: 'header', type: 'boolean', editable: true, flex: 0.2 } -]; +// FORM GRID COLUMNS diff --git a/frontend/components/client/formcolumns.tsx b/frontend/components/client/formcolumns.tsx new file mode 100644 index 00000000..254beb53 --- /dev/null +++ b/frontend/components/client/formcolumns.tsx @@ -0,0 +1,734 @@ +'use client'; + +import { GridColDef, GridRenderEditCellParams, useGridApiContext } from '@mui/x-data-grid'; +import { areaSelectionOptions, unitSelectionOptions } from '@/config/macros'; +import { formatHeader } from '@/components/client/datagridcolumns'; +import moment from 'moment/moment'; +import { Box, Input, Tooltip } from '@mui/joy'; +import { DatePicker } from '@mui/x-date-pickers'; +import React, { useEffect, useRef, useState } from 'react'; +import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; +import { AttributeStatusOptions } from '@/config/sqlrdsdefinitions/core'; +import { styled } from '@mui/joy/styles'; +import { CheckCircleOutlined } from '@mui/icons-material'; + +export const renderDatePicker = (params: GridRenderEditCellParams) => { + const convertedValue = params.row.date ? moment(params.row.date, 'YYYY-MM-DD') : null; + if (!convertedValue) return <>; + + return ( + + + + ); +}; + +export const renderEditDatePicker = (params: GridRenderEditCellParams) => { + const apiRef = useGridApiContext(); + const { id, row } = params; + + return ( + + { + apiRef.current.setEditCellValue({ id, field: 'date', value: newValue ? newValue.format('YYYY-MM-DD') : null }); + }} + /> + + ); +}; + +const getClosestAreaUnit = (input: string): string | null => { + const normalizedInput = input.trim().toLowerCase(); + + // Define threshold for acceptable "closeness" (tune this value) + const threshold = 2; + + let closestUnit: string | null = null; + let minDistance = Infinity; + + for (const option of areaSelectionOptions) { + const distance = levenshteinDistance(normalizedInput, option); + if (distance < minDistance && distance <= threshold) { + minDistance = distance; + closestUnit = option; + } + } + + // Return the closest match if within the acceptable threshold, otherwise return null + return closestUnit; +}; + +export const EditUnitsCell = (params: GridRenderEditCellParams & { fieldName: string; isArea: boolean }) => { + const apiRef = useGridApiContext(); + const { id, fieldName, hasFocus, isArea } = params; + const [value, setValue] = useState(params.row[fieldName]); + const [error, setError] = useState(false); + const ref = useRef(null); + + useEnhancedEffect(() => { + if (hasFocus && ref.current) { + const input = ref.current.querySelector(`input[value="${value}"]`); + input?.focus(); + } + }, [hasFocus, value]); + + useEffect(() => { + if (!(apiRef.current.getCellMode(id, fieldName) === 'edit')) { + apiRef.current.startCellEditMode({ id, field: fieldName }); + } + }, [apiRef, id, fieldName]); + + useEffect(() => { + setError(!(isArea ? getClosestAreaUnit(value) : getClosestUnit(value))); + }, [value]); + + const handleCommit = () => { + const isValid = isArea ? getClosestAreaUnit(value) : getClosestUnit(value); + + if (!isValid) { + apiRef.current.setEditCellValue({ + id, + field: fieldName, + value: '' + }); + return; + } + + apiRef.current.stopCellEditMode({ id, field: fieldName }); + }; + + return ( + + setValue(e.target.value)} + onBlur={() => { + apiRef.current.setEditCellValue({ + id, + field: fieldName, + value: (isArea ? getClosestAreaUnit(value) : getClosestUnit(value)) || value + }); + handleCommit(); + }} + onKeyDown={e => { + if (e.key === 'Enter') { + apiRef.current.setEditCellValue({ + id, + field: fieldName, + value: (isArea ? getClosestAreaUnit(value) : getClosestUnit(value)) || value + }); + handleCommit(); + } + }} + error={error} + /> + + ); +}; + +const getClosestUnit = (input: string): string | null => { + const normalizedInput = input.trim().toLowerCase(); + + // Define threshold for acceptable "closeness" (tune this value) + const threshold = 2; + + let closestUnit: string | null = null; + let minDistance = Infinity; + + for (const option of unitSelectionOptions) { + const distance = levenshteinDistance(normalizedInput, option); + if (distance < minDistance && distance <= threshold) { + minDistance = distance; + closestUnit = option; + } + } + + // Return the closest match if within the acceptable threshold, otherwise return null + return closestUnit; +}; + +function levenshteinDistance(a: string, b: string): number { + const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0)); + + for (let i = 0; i <= a.length; i++) matrix[i][0] = i; + for (let j = 0; j <= b.length; j++) matrix[0][j] = j; + + for (let i = 1; i <= a.length; i++) { + for (let j = 1; j <= b.length; j++) { + const cost = a[i - 1] === b[j - 1] ? 0 : 1; + matrix[i][j] = Math.min( + matrix[i - 1][j] + 1, // Deletion + matrix[i][j - 1] + 1, // Insertion + matrix[i - 1][j - 1] + cost // Substitution + ); + } + } + return matrix[a.length][b.length]; +} + +function normalizeString(str: string): string { + return str.replace(/[\s-]+/g, '').toLowerCase(); +} + +const getClosestStatus = (input: string): string | null => { + const normalizedInput = normalizeString(input); + + // Define threshold for acceptable "closeness" (tune this value) + const threshold = 2; + + let closestStatus: string | null = null; + let minDistance = Infinity; + + for (const option of AttributeStatusOptions) { + const normalizedOption = normalizeString(option); + const distance = levenshteinDistance(normalizedInput, normalizedOption); + if (distance < minDistance && distance <= threshold) { + minDistance = distance; + closestStatus = option; // Return the original option, not the normalized one + } + } + + // Return the closest match if within the acceptable threshold, otherwise return null + return closestStatus; +}; + +const StyledInput = styled('input')({ + border: 'none', + minWidth: 0, + outline: 0, + padding: 0, + paddingTop: '1em', + flex: 1, + color: 'inherit', + backgroundColor: 'transparent', + fontFamily: 'inherit', + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + lineHeight: 'inherit', + textOverflow: 'ellipsis', + '&::placeholder': { + opacity: 0, + transition: '0.1s ease-out' + }, + '&:focus::placeholder': { + opacity: 1 + }, + '&:focus ~ label, &:not(:placeholder-shown) ~ label, &:-webkit-autofill ~ label': { + top: '0.5rem', + fontSize: '0.75rem' + }, + '&:focus ~ label': { + color: 'var(--Input-focusedHighlight)' + }, + '&:-webkit-autofill': { + alignSelf: 'stretch' + }, + '&:-webkit-autofill:not(* + &)': { + marginInlineStart: 'calc(-1 * var(--Input-paddingInline))', + paddingInlineStart: 'var(--Input-paddingInline)', + borderTopLeftRadius: 'calc(var(--Input-radius) - var(--variant-borderWidth, 0px))', + borderBottomLeftRadius: 'calc(var(--Input-radius) - var(--variant-borderWidth, 0px))' + } +}); + +const StyledLabel = styled('label')(({ theme }) => ({ + position: 'absolute', + lineHeight: 1, + top: 'calc((var(--Input-minHeight) - 1em) / 2)', + color: theme.vars.palette.text.tertiary, + fontWeight: theme.vars.fontWeight.md, + transition: 'all 150ms cubic-bezier(0.4, 0, 0.2, 1)' +})); + +const InnerInput = React.forwardRef< + HTMLInputElement, + React.JSX.IntrinsicElements['input'] & { + error?: boolean; + noInput?: boolean; + } +>(function InnerInput(props, ref) { + const { error, noInput, ...rest } = props; + const id = React.useId(); + + return ( + + + {noInput ? AttributeStatusOptions.join(', ') : error ? 'Invalid status' : 'Accepted!'} + + ); +}); + +const EditStatusCell = (params: GridRenderEditCellParams) => { + const apiRef = useGridApiContext(); + const { id, hasFocus } = params; + const [value, setValue] = React.useState(params.row['status']); + const [error, setError] = React.useState(false); + const ref = React.useRef(null); + + useEnhancedEffect(() => { + if (hasFocus && ref.current) { + const input = ref.current.querySelector(`input[value="${value}"]`); + input?.focus(); + } + }, [hasFocus, value]); + + React.useEffect(() => { + if (!(apiRef.current.getCellMode(id, 'status') === 'edit')) { + apiRef.current.startCellEditMode({ id, field: 'status' }); + } + }, [apiRef, id]); + + React.useEffect(() => { + setError(!getClosestStatus(value) && value !== ''); + }, [value]); + + const handleCommit = () => { + const correctedValue = getClosestStatus(value); + + console.log('handle commit: corrected value: ', correctedValue); + + apiRef.current.setEditCellValue({ + id, + field: 'status', + value: value + }); + + apiRef.current.stopCellEditMode({ id, field: 'status' }); + }; + + return ( + } + slots={{ input: InnerInput }} + slotProps={{ + input: { + placeholder: 'Enter status...', + type: 'text', + error, + noInput: value === '' + } + }} + sx={{ '--Input-minHeight': '56px', '--Input-radius': '6px' }} + onChange={e => setValue(e.target.value)} + onBlur={handleCommit} + onKeyDown={e => { + if (e.key === 'Enter' || e.key === 'Tab') { + console.log('on key down: enter || tab'); + handleCommit(); + } + }} + error={error} + /> + ); +}; + +export const AttributesFormGridColumns: GridColDef[] = [ + { + field: 'id', + headerName: '#', + headerClassName: 'header', + flex: 0.3, + headerAlign: 'right', + editable: false + }, + { + field: 'code', + headerName: 'Code', + headerClassName: 'header', + flex: 1, + editable: true + }, + { + field: 'description', + headerName: 'Description', + headerClassName: 'header', + flex: 1, + editable: true + }, + { + field: 'status', + headerName: 'Status', + headerClassName: 'header', + flex: 1, + editable: true + // This is temporarily being suspended -- it's a nice to have, not a need to have + // renderEditCell: params => + } +]; + +export const PersonnelFormGridColumns: GridColDef[] = [ + { + field: 'id', + headerName: '#', + headerClassName: 'header', + flex: 0.3, + align: 'right', + headerAlign: 'right', + editable: false + }, + { + field: 'firstname', + headerName: 'First Name', + headerClassName: 'header', + flex: 1, + align: 'left', + editable: true + }, + { + field: 'lastname', + headerName: 'Last Name', + headerClassName: 'header', + flex: 1, + align: 'left', + editable: true + }, + { + field: 'role', + headerName: 'Role', + headerClassName: 'header', + flex: 1, + align: 'left', + editable: true + }, + { + field: 'roledescription', + headerName: 'Role Description', + headerClassName: 'header', + flex: 1, + align: 'left', + editable: true + } +]; + +export const SpeciesFormGridColumns: GridColDef[] = [ + { + field: 'id', + headerName: '#', + headerClassName: 'header', + flex: 0.3, + align: 'right', + headerAlign: 'right', + editable: false + }, + { + field: 'spcode', + headerName: 'Species Code', + headerClassName: 'header', + flex: 1, + align: 'left', + editable: true + }, + { + field: 'family', + headerName: 'Family', + headerClassName: 'header', + flex: 1, + align: 'left', + editable: true + }, + { + field: 'genus', + headerName: 'Genus', + headerClassName: 'header', + flex: 1, + align: 'left', + editable: true + }, + { + field: 'species', + headerName: 'Species', + headerClassName: 'header', + flex: 1, + align: 'left', + editable: true + }, + { + field: 'subspecies', + headerName: 'Subspecies', + headerClassName: 'header', + flex: 1, + align: 'left', + editable: true + }, + { + field: 'idlevel', + headerName: 'ID Level', + headerClassName: 'header', + flex: 1, + align: 'left', + editable: true + }, + { + field: 'authority', + headerName: 'Authority', + headerClassName: 'header', + flex: 1, + align: 'left', + editable: true + }, + { + field: 'subspeciesauthority', + headerName: 'Subspecies Authority', + headerClassName: 'header', + flex: 1, + align: 'left', + editable: true + } +]; + +export const QuadratsFormGridColumns: GridColDef[] = [ + { + field: 'id', + headerName: '#', + headerClassName: 'header', + flex: 0.3, + align: 'right', + headerAlign: 'right', + editable: false + }, + { + field: 'quadrat', + headerName: 'Quadrat Name', + headerClassName: 'header', + flex: 1, + align: 'left', + editable: true + }, + { + field: 'startx', + headerName: 'StartX', + headerClassName: 'header', + flex: 1, + align: 'left', + type: 'number', + editable: true + }, + { + field: 'starty', + headerName: 'StartY', + headerClassName: 'header', + flex: 1, + align: 'left', + type: 'number', + editable: true + }, + { + field: 'coordinateunit', + headerName: 'Coordinate Units', + headerClassName: 'header', + align: 'left', + editable: true, + renderEditCell: params => + }, + { + field: 'dimx', + headerName: 'Dimension X', + headerClassName: 'header', + flex: 1, + align: 'left', + type: 'number', + editable: true + }, + { + field: 'dimy', + headerName: 'Dimension Y', + headerClassName: 'header', + flex: 1, + align: 'left', + type: 'number', + editable: true + }, + { + field: 'dimensionunit', + headerName: 'Dimension Units', + headerClassName: 'header', + flex: 0.3, + align: 'left', + editable: true, + renderEditCell: params => + }, + { + field: 'area', + headerName: 'Area', + headerClassName: 'header', + flex: 1, + align: 'left', + type: 'number', + editable: true + }, + { + field: 'areaunit', + headerName: 'Area Units', + headerClassName: 'header', + flex: 0.3, + align: 'left', + editable: true, + renderEditCell: params => + }, + { + field: 'quadratshape', + headerName: 'Quadrat Shape', + headerClassName: 'header', + flex: 1, + align: 'left', + editable: true + } +]; +/** + * [FormType.measurements]: [ + * { label: 'tag' }, + * { label: 'stemtag' }, + * { label: 'spcode' }, + * { label: 'quadrat' }, + * { label: 'lx' }, + * { label: 'ly' }, + * { label: 'coordinateunit' }, + * { label: 'dbh' }, + * { label: 'dbhunit' }, + * { label: 'hom' }, + * { label: 'homunit' }, + * { label: 'date' }, + * { label: 'codes' } + * ], + */ +export const MeasurementsFormGridColumns: GridColDef[] = [ + { + field: 'id', + headerName: '#', + headerClassName: 'header', + flex: 0.3, + align: 'right', + headerAlign: 'right', + editable: false + }, + { + field: 'tag', + headerName: 'Tree Tag', + headerClassName: 'header', + renderHeader: () => formatHeader('Tree', 'Tag'), + flex: 0.75, + align: 'center', + editable: true + }, + { + field: 'stemtag', + headerName: 'Stem Tag', + headerClassName: 'header', + renderHeader: () => formatHeader('Stem', 'Tag'), + flex: 0.75, + align: 'center', + editable: true + }, + { + field: 'spcode', + headerName: 'Species Code', + headerClassName: 'header', + renderHeader: () => formatHeader('Species', 'Code'), + flex: 0.75, + align: 'center', + editable: true + }, + { + field: 'quadrat', + headerName: 'Quadrat Name', + headerClassName: 'header', + renderHeader: () => formatHeader('Quadrat', 'Name'), + flex: 0.75, + align: 'center', + editable: true + }, + { + field: 'lx', + headerName: 'X', + headerClassName: 'header', + flex: 0.3, + align: 'center', + type: 'number', + editable: true + }, + { + field: 'ly', + headerName: 'Y', + headerClassName: 'header', + flex: 0.3, + align: 'center', + type: 'number', + editable: true + }, + { + field: 'coordinateunit', + headerName: '<= Units', + headerClassName: 'header', + // renderHeader: () => formatHeader('Coordinate', 'Units'), + flex: 0.5, + align: 'center', + editable: true, + renderEditCell: params => + }, + { + field: 'dbh', + headerName: 'DBH', + headerClassName: 'header', + flex: 0.75, + align: 'center', + type: 'number', + editable: true + }, + { + field: 'dbhunit', + headerName: '<= Units', + headerClassName: 'header', + // renderHeader: () => formatHeader('DBH', 'Units'), + flex: 0.5, + align: 'center', + editable: true, + renderEditCell: params => + }, + { + field: 'hom', + headerName: 'HOM', + headerClassName: 'header', + flex: 0.75, + align: 'center', + type: 'number', + editable: true + }, + { + field: 'homunit', + headerName: '<= Units', + headerClassName: 'header', + // renderHeader: () => formatHeader('HOM', 'Units'), + flex: 0.5, + align: 'center', + editable: true, + renderEditCell: params => + } +]; diff --git a/frontend/components/client/githubfeedbackmodal.tsx b/frontend/components/client/githubfeedbackmodal.tsx index 19d7dd6d..57d3751f 100644 --- a/frontend/components/client/githubfeedbackmodal.tsx +++ b/frontend/components/client/githubfeedbackmodal.tsx @@ -12,7 +12,6 @@ import { Divider, FormControl, FormLabel, - Grid, Input, LinearProgress, List, @@ -36,6 +35,7 @@ import { usePathname } from 'next/navigation'; import { useSession } from 'next-auth/react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; +import Grid from '@mui/material/Grid2'; // this has been shelved -- it's a little too complicated for a first iteration. // saving it for a later version. @@ -185,7 +185,7 @@ ${pathname} - + {currentSite ? ( @@ -197,7 +197,7 @@ ${pathname} No site selected. )} - + {currentPlot ? ( Selected Plot: {currentPlot.plotName} @@ -207,7 +207,7 @@ ${pathname} No plot selected. )} - + {currentCensus ? ( Selected Census: {currentCensus.plotCensusNumber} diff --git a/frontend/components/client/postvalidationrow.tsx b/frontend/components/client/postvalidationrow.tsx new file mode 100644 index 00000000..4cf11919 --- /dev/null +++ b/frontend/components/client/postvalidationrow.tsx @@ -0,0 +1,218 @@ +'use client'; +import React from 'react'; +import { Box, Collapse, TableCell, TableRow, Typography } from '@mui/material'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import { PostValidationQueriesRDS } from '@/config/sqlrdsdefinitions/validations'; +import { Checkbox, IconButton, Textarea, Tooltip } from '@mui/joy'; +import { Done } from '@mui/icons-material'; +import dynamic from 'next/dynamic'; +import moment from 'moment/moment'; +import { darken } from '@mui/system'; + +interface PostValidationRowProps { + postValidation: PostValidationQueriesRDS; + selectedResults: PostValidationQueriesRDS[]; + expanded: boolean; + isDarkMode: boolean; + expandedQuery: number | null; + replacements: { schema: string | undefined; currentPlotID: number | undefined; currentCensusID: number | undefined }; + handleExpandClick: (queryID: number) => void; + handleExpandResultsClick: (queryID: number) => void; + handleSelectResult: (postValidation: PostValidationQueriesRDS) => void; +} + +const Editor = dynamic(() => import('@monaco-editor/react'), { ssr: false }); + +const PostValidationRow: React.FC = ({ + expandedQuery, + replacements, + postValidation, + expanded, + isDarkMode, + handleExpandClick, + handleExpandResultsClick, + handleSelectResult, + selectedResults +}) => { + const formattedResults = JSON.stringify(JSON.parse(postValidation.lastRunResult ?? '{}'), null, 2); + + const successColor = !isDarkMode ? 'rgba(54, 163, 46, 0.3)' : darken('rgba(54,163,46,0.6)', 0.7); + const failureColor = !isDarkMode ? 'rgba(255, 0, 0, 0.3)' : darken('rgba(255,0,0,0.6)', 0.7); + + return ( + <> + + + handleExpandResultsClick(postValidation.queryID!)} + > + {expanded ? : } + + + handleSelectResult(postValidation)} style={{ cursor: 'pointer', padding: '0', textAlign: 'center' }}> + + } + label={''} + checked={selectedResults.includes(postValidation)} + slotProps={{ + root: ({ checked, focusVisible }) => ({ + sx: !checked + ? { + '& svg': { opacity: focusVisible ? 1 : 0 }, + '&:hover svg': { + opacity: 1 + } + } + : undefined + }) + }} + onChange={e => e.stopPropagation()} + /> + + + + {postValidation.queryName} + + + + {expandedQuery === postValidation.queryID ? ( + + String(replacements[p1 as keyof typeof replacements] ?? '') + )} + options={{ + readOnly: true, + minimap: { enabled: false }, + scrollBeyondLastLine: false, + wordWrap: 'off', + lineNumbers: 'off' + }} + theme={isDarkMode ? 'vs-dark' : 'light'} + /> + ) : ( +