Skip to content

Commit

Permalink
Multiline Data Entry & Post Validation System (#179)
Browse files Browse the repository at this point in the history
* Broken utils functions have been corrected. Personnel upload has been confirmed in accordance with new schema structure.

* feature upgrades. multiline form system implemented and set up to act as bulk update forms. API endpoint set up to connect the form system to connect to the existing SQLLoad app system, needs testing, though

* Restructure and implementation of the multiline form interaction are continuing. Saving changes to correct core quadrat schema structure failure.

* requisite repairs to the quadrats processing system to make sure that the multiline handler was correctly interacting with the system. correcting modal close functions to also refresh datagrid after closing.

* scrubbing old datagrid instances that aren't used anymore

* continuing postvalidation construction. saving changes here to test a new display approach so that I can revert if need be.

* first-iteration postvalidation UI is completed. need to apply final tuning and then will push to dev site.

* postvalidation query testing is complete. integrated into sidebar successfully -- places check to ensure that > 0 coremeasurements before allowing access. Download/Run system checked.
  • Loading branch information
siddheshraze authored Oct 18, 2024
1 parent b38965f commit 5cd20db
Show file tree
Hide file tree
Showing 52 changed files with 2,777 additions and 1,846 deletions.
3 changes: 3 additions & 0 deletions frontend/app/(hub)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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<UnifiedChangelogRDS[]>(Array(5));
const [isLoading, setIsLoading] = useState(false);

Expand Down
292 changes: 223 additions & 69 deletions frontend/app/(hub)/measurementshub/postvalidation/page.tsx
Original file line number Diff line number Diff line change
@@ -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<PostValidations[]>([]);
const [validationResults, setValidationResults] = useState<Record<number, PostValidationResults | null>>({});
const [loadingQueries, setLoadingQueries] = useState<boolean>(false);
const [postValidations, setPostValidations] = useState<PostValidationQueriesRDS[]>([]);
const [expandedQuery, setExpandedQuery] = useState<number | null>(null);
const [expandedResults, setExpandedResults] = useState<number | null>(null);
const [selectedResults, setSelectedResults] = useState<PostValidationQueriesRDS[]>([]);
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(`<pre>${printContent}</pre>`);
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<HTMLInputElement>) => {
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 (
<Box sx={{ flex: 1, display: 'flex', width: '100%' }}>
{loadingQueries ? (
<LinearProgress />
) : postValidations.length > 0 ? (
<Box>
{postValidations.map(postValidation => (
<Box key={postValidation.queryID}>
<div>{postValidation.queryName}</div>
{validationResults[postValidation.queryID] ? <LinearProgress determinate value={100} /> : <LinearProgress />}
</Box>
))}
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', width: '100%' }}>
<Box sx={{ justifyContent: 'space-between', alignContent: 'space-between', flex: 1, display: 'flex', flexDirection: 'row' }}>
<Typography>These statistics can be used to analyze entered data. Please select and run, download, or print statistics as needed.</Typography>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<Button variant={'soft'} onClick={() => saveResultsToFile()} sx={{ marginX: 1 }}>
Download Statistics
</Button>
<Button variant={'soft'} onClick={() => printResults()} sx={{ marginX: 1 }}>
Print Statistics
</Button>
<Button
variant={'soft'}
onClick={async () => {
if (selectedResults.length === 0) {
alert('Please select at least one statistic to run.');
return;
}
setLoading(true, 'Running validations...');
for (const postValidation of selectedResults) {
await fetchValidationResults(postValidation);
setSelectedResults([]);
}
await loadPostValidations();
setLoading(false);
}}
>
Run Statistics
</Button>
</Box>
</Box>

{postValidations.length > 0 ? (
<Box sx={{ width: '100%' }}>
<TableContainer component={Paper}>
<Table
stickyHeader
sx={{
tableLayout: 'fixed',
width: '100%'
}}
>
<TableHead>
<TableRow>
<TableCell
sx={{
width: '50px',
textAlign: 'center',
padding: '0'
}}
/>
<TableCell
sx={{
flex: 0.5,
display: 'flex',
alignSelf: 'center',
alignItems: 'center',
justifyContent: 'center',
padding: '0'
}}
>
<Checkbox
uncheckedIcon={<Done />}
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)}
/>
</TableCell>
<TableCell>Query Name</TableCell>
<TableCell
sx={{
width: '45%'
}}
>
Query Definition
</TableCell>
<TableCell>Description</TableCell>
<TableCell>Last Run At</TableCell>
<TableCell>Last Run Result</TableCell>
</TableRow>
</TableHead>

<TableBody>
{enabledPostValidations.map(postValidation => (
<PostValidationRow
key={postValidation.queryID}
postValidation={postValidation}
selectedResults={selectedResults}
expanded={expandedResults !== null && expandedResults === postValidation.queryID}
isDarkMode={isDarkMode}
expandedQuery={expandedQuery}
replacements={replacements}
handleExpandClick={handleExpandClick}
handleExpandResultsClick={handleExpandResultsClick}
handleSelectResult={handleSelectResult}
/>
))}

{disabledPostValidations.map(postValidation => (
<PostValidationRow
key={postValidation.queryID}
postValidation={postValidation}
selectedResults={selectedResults}
expanded={expandedResults !== null && expandedResults === postValidation.queryID}
isDarkMode={isDarkMode}
expandedQuery={expandedQuery}
replacements={replacements}
handleExpandClick={handleExpandClick}
handleExpandResultsClick={handleExpandResultsClick}
handleSelectResult={handleSelectResult}
/>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
) : (
<div>No validations available.</div>
<Box>No validations available.</Box>
)}
</Box>
);
Expand Down
54 changes: 54 additions & 0 deletions frontend/app/api/bulkcrud/[dataType]/[[...slugs]]/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
Loading

0 comments on commit 5cd20db

Please sign in to comment.