diff --git a/CoVAR-app/src/app/(pages)/evaluate/components/fileUpload.tsx b/CoVAR-app/src/app/(pages)/evaluate/components/fileUpload.tsx index ac1e25c..c91679c 100644 --- a/CoVAR-app/src/app/(pages)/evaluate/components/fileUpload.tsx +++ b/CoVAR-app/src/app/(pages)/evaluate/components/fileUpload.tsx @@ -11,6 +11,7 @@ interface FileUploadProps { const FileUpload: React.FC = ({ onFileSubmit, client, organization }) => { const [selectedFile, setSelectedFile] = useState(null); + const [cooldown, setCooldown] = useState(false); const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files && event.target.files[0]; @@ -20,6 +21,8 @@ const FileUpload: React.FC = ({ onFileSubmit, client, organizat }; const handleSubmitFile = async () => { + setCooldown(true); + setTimeout(() => setCooldown(false), 5000); if (!selectedFile) { return; } @@ -38,7 +41,7 @@ const FileUpload: React.FC = ({ onFileSubmit, client, organizat fileContent: base64File, filename: selectedFile.name, }, { - headers: { + headers: { Authorization: `Bearer ${token}`, }, }); @@ -57,8 +60,21 @@ const FileUpload: React.FC = ({ onFileSubmit, client, organizat setSelectedFile(null); // Reset selected file }; + //If disabled true then return loading spinner + if (cooldown) { + return ( + + + Uploading... + + + ); + } + + return ( + {!selectedFile && ( <> @@ -66,7 +82,7 @@ const FileUpload: React.FC = ({ onFileSubmit, client, organizat = ({ onFileSubmit, client, organizat variant="contained" sx={{ ...uploadButtonStyles, marginTop: 2 }} onClick={handleSubmitFile} + > Submit File diff --git a/CoVAR-app/src/app/(pages)/evaluate/organization/[organization]/page.tsx b/CoVAR-app/src/app/(pages)/evaluate/organization/[organization]/page.tsx index 526b2f3..8408944 100644 --- a/CoVAR-app/src/app/(pages)/evaluate/organization/[organization]/page.tsx +++ b/CoVAR-app/src/app/(pages)/evaluate/organization/[organization]/page.tsx @@ -27,6 +27,7 @@ import DownloadIcon from '@mui/icons-material/Download'; import DeleteIcon from '@mui/icons-material/Delete'; import AddIcon from '@mui/icons-material/Add'; import RemoveIcon from '@mui/icons-material/Remove'; +import { Loader } from '@/styles/conflictStyle'; interface FileUpload { upload_id: number; @@ -55,56 +56,119 @@ const OrganizationEvaluation: React.FC = () => { const [snackbarOpen, setSnackbarOpen] = useState(false); const [reportNames, setReportNames] = useState([]); const [snackbarOpenInvalid, setSnackbarOpenInvalid] = useState(false); - + const [lastReportId, setLastReportId] = useState(0); + const [snackbarOpenMaxUpload, setSnackbarOpenMaxUpload] = useState(false); + const [snackbarOpenNoAccess, setSnackbarOpenNoAccess] = useState(false); + const [unauthorized, setUnauthorized] = useState(false); + const [loading, setLoading] = useState(true); // Track loading state useEffect(() => { const fetchInitialUploads = async () => { try { if (organizationName) { - const data = await fetchUploadsOrganization(organizationName); - setUploads(data); - const inReportIds = data.filter((upload: FileUpload) => upload.in_report).map((upload: FileUpload) => upload.upload_id); - setReportIds(inReportIds); + try { + const data = await fetchUploadsOrganization(organizationName); + setUploads(data); + + const inReportIds = data + .filter((upload: FileUpload) => upload.in_report) + .map((upload: FileUpload) => upload.upload_id); + setReportIds(inReportIds); + } catch (error: any) { + // Handle specific API errors + if (error.response) { + const status = error.response.status; + + if (status === 401) { + setSnackbarOpenNoAccess(true); // Unauthorized access + setUnauthorized(true); + console.error('Unauthorized: Please check your permissions.'); + } else if (status === 404) { + setSnackbarOpenNoAccess(true); // Client not found + setUnauthorized(true); + console.error('Client not found.'); + } else { + console.error('Error generating reports:', error); + } + } else { + console.error('Network or server error:', error); + } + } } } catch (error: any) { + if (error.response?.status === 403) { + console.error('Forbidden: Redirecting to login.'); redirectToLogin(); + } else { + console.error('Unexpected error:', error); } + } finally { + setLoading(false); } }; + fetchInitialUploads(); }, [organizationName, redirectToLogin]); useEffect(() => { + const fetchInitialReports = async () => { try { if (reportIds.length > 0) { - const fetchedReports = await fetchReports(reportIds); - setReports(fetchedReports); + try { + const fetchedReports = await fetchReports(reportIds); + setReports(fetchedReports); + } catch (error) { + setSnackbarOpenInvalid(true); + //make sure reportNames.length is not 0 + if (reportNames.length > 0) { + handleRemove(lastReportId, reportNames[reportNames.length - 1]); + } else { + handleRemove(lastReportId, ''); + } + console.error('Error generating reports:', error); + } } else { setReports([]); } } catch (error) { setSnackbarOpenInvalid(true); + console.error('Error generating reports:', error); } }; fetchInitialReports(); + }, [reportIds]); const handleFileSubmit = async () => { + + if (uploads.length >= 6) { + setSnackbarOpenMaxUpload(true); + return; + } + try { if (organizationName) { - const data = await fetchUploadsOrganization(organizationName); - setUploads(data); - const inReportIds = data.filter((upload: FileUpload) => upload.in_report).map((upload: FileUpload) => upload.upload_id); - setReportIds(inReportIds); + try { + const data = await fetchUploadsOrganization(organizationName); + setUploads(data); + const inReportIds = data.filter((upload: FileUpload) => upload.in_report).map((upload: FileUpload) => upload.upload_id); + setReportIds(inReportIds); + } + catch (error) { + setSnackbarOpenInvalid(true); + console.error('Error generating reports:', error); + } } } catch (error: any) { if (error.response?.status === 403) { redirectToLogin(); } } + + }; const handleRemove = async (upload_id: number, fileName: string) => { @@ -121,26 +185,52 @@ const OrganizationEvaluation: React.FC = () => { }; - + const handleToggle = async (upload_id: number, fileName: string) => { + // Create local copies of the state for updates + let updatedReportNames = [...reportNames]; + let updatedReportIds = [...reportIds]; + + try { if (reportIds.includes(upload_id)) { - await handleToggleReport(upload_id); - setReportNames(reportNames.filter(name => name !== fileName)); - setReportIds(reportIds.filter(id => id !== upload_id)); + // Attempt to remove the report + try { + await handleToggleReport(upload_id); + // Remove from local copy + updatedReportNames = updatedReportNames.filter(name => name !== fileName); + updatedReportIds = updatedReportIds.filter(id => id !== upload_id); + } catch (error) { + setSnackbarOpenInvalid(true); + console.error('Error updating report status:', error); + return; + } } else { + if (reportIds.length < 2) { - await handleToggleReport(upload_id); - setReportNames(reportNames.filter(name => name !== fileName)); - setReportIds([...reportIds, upload_id]); + setLastReportId(upload_id); + try { + await handleToggleReport(upload_id); + // Add to local copy + updatedReportNames.push(fileName); + updatedReportIds.push(upload_id); + } catch (error) { + setSnackbarOpenInvalid(true); + console.error('Error updating report status:', error); + return; + } } else { setSnackbarOpen(true); + return; // Exit early if limit reached } } + // Update the state only after successful API calls + setReportNames(updatedReportNames); + setReportIds(updatedReportIds); } catch (error) { setSnackbarOpenInvalid(true); - console.error('Error updating report status:', error); + console.error('Error in handleToggle:', error); } }; @@ -148,11 +238,64 @@ const OrganizationEvaluation: React.FC = () => { setSnackbarOpen(false); }; - const handleCloseSnackbarInvalid = () => { setSnackbarOpenInvalid(false); }; + const handleCloseSnackbarMaxUpload = () => { + setSnackbarOpenMaxUpload(false); + } + + const handleCloseSnackbarNoAccess = () => { + setSnackbarOpenNoAccess(false); + } + + if (loading) { + return ( + + + + ); + } + + if (unauthorized) { + return ( + + + Page does not exist + + + + ); + } + + return ( { message="Cannot add more than 2 reports" anchorOrigin={{ vertical: 'top', horizontal: 'center' }} /> + + + { open={snackbarOpenInvalid} autoHideDuration={1000} onClose={handleCloseSnackbarInvalid} - message="Invalid Report Format" + message="Invalid Report Format, File Removed." anchorOrigin={{ vertical: 'top', horizontal: 'center' }} /> @@ -188,7 +340,7 @@ const OrganizationEvaluation: React.FC = () => { Organisation: {organizationName} )} - + {/* Gap between upload files and uploaded files */} @@ -238,22 +390,22 @@ const OrganizationEvaluation: React.FC = () => { - + scrollbarWidth: 'thin', + scrollbarColor: 'gray transparent', + }}> diff --git a/CoVAR-app/src/app/(pages)/evaluate/user/[username]/page.tsx b/CoVAR-app/src/app/(pages)/evaluate/user/[username]/page.tsx index 8892d90..5b3bd46 100644 --- a/CoVAR-app/src/app/(pages)/evaluate/user/[username]/page.tsx +++ b/CoVAR-app/src/app/(pages)/evaluate/user/[username]/page.tsx @@ -6,12 +6,13 @@ import DeleteIcon from '@mui/icons-material/Delete'; import AddIcon from '@mui/icons-material/Add'; import RemoveIcon from '@mui/icons-material/Remove'; import { usePathname } from 'next/navigation'; -import { mainContentStyles, buttonStyles } from '../../../../../styles/evaluateStyle'; +import { mainContentStyles, buttonStyles, boxStyles } from '../../../../../styles/evaluateStyle'; import FileUpload from '../../components/fileUpload'; import { handleDownloadFile } from '../../../../../functions/requests'; import ReportPreview from '../../components/reportPreview'; import { fetchUploadsClient, fetchReports, handleRemoveFile, handleToggleReport } from '../../../../../functions/requests'; import { useRouter } from 'next/navigation'; +import { Loader } from '@/styles/conflictStyle'; interface FileUpload { upload_id: number; @@ -40,56 +41,122 @@ const UserEvaluation: React.FC = () => { const [snackbarOpen, setSnackbarOpen] = useState(false); const [reportNames, setReportNames] = useState([]); const [snackbarOpenInvalid, setSnackbarOpenInvalid] = useState(false); + const [lastReportId, setLastReportId] = useState(0); + const [snackbarOpenMaxUpload, setSnackbarOpenMaxUpload] = useState(false); + const [snackbarOpenNoAccess, setSnackbarOpenNoAccess] = useState(false); + const [unauthorized, setUnauthorized] = useState(false); + const [loading, setLoading] = useState(true); // Track loading state useEffect(() => { const fetchInitialUploads = async () => { try { - if (username) { - const data = await fetchUploadsClient(username); - setUploads(data); - const inReportIds = data.filter((upload: FileUpload) => upload.in_report).map((upload: FileUpload) => upload.upload_id); - setReportIds(inReportIds); + try { + const data = await fetchUploadsClient(username); + setUploads(data); + + const inReportIds = data + .filter((upload: FileUpload) => upload.in_report) + .map((upload: FileUpload) => upload.upload_id); + setReportIds(inReportIds); + } catch (error: any) { + // Handle specific API errors + if (error.response) { + const status = error.response.status; + + if (status === 401) { + setSnackbarOpenNoAccess(true); // Unauthorized access + setUnauthorized(true); + console.error('Unauthorized: Please check your permissions.'); + } else if (status === 404) { + setSnackbarOpenNoAccess(true); // Client not found + setUnauthorized(true); + console.error('Client not found.'); + } else { + console.error('Error generating reports:', error); + } + } else { + console.error('Network or server error:', error); + } + } } } catch (error: any) { + if (error.response?.status === 403) { + console.error('Forbidden: Redirecting to login.'); redirectToLogin(); + } else { + console.error('Unexpected error:', error); } + } finally { + setLoading(false); } }; + fetchInitialUploads(); }, [username, redirectToLogin]); + useEffect(() => { + const fetchInitialReports = async () => { try { if (reportIds.length > 0) { - const fetchedReports = await fetchReports(reportIds); - setReports(fetchedReports); + try { + const fetchedReports = await fetchReports(reportIds); + setReports(fetchedReports); + } catch (error) { + setSnackbarOpenInvalid(true); + //make sure reportNames.length is not 0 + if (reportNames.length > 0) { + handleRemove(lastReportId, reportNames[reportNames.length - 1]); + } else { + handleRemove(lastReportId, ''); + } + console.error('Error generating reports:', error); + } } else { setReports([]); } } catch (error) { setSnackbarOpenInvalid(true); + console.error('Error generating reports:', error); } }; fetchInitialReports(); + }, [reportIds]); + + const handleFileSubmit = async () => { + + if (uploads.length >= 6) { + setSnackbarOpenMaxUpload(true); + return; + } + try { if (username) { - const data = await fetchUploadsClient(username); - setUploads(data); - const inReportIds = data.filter((upload: FileUpload) => upload.in_report).map((upload: FileUpload) => upload.upload_id); - setReportIds(inReportIds); + try { + const data = await fetchUploadsClient(username); + setUploads(data); + const inReportIds = data.filter((upload: FileUpload) => upload.in_report).map((upload: FileUpload) => upload.upload_id); + setReportIds(inReportIds); + } + catch (error) { + setSnackbarOpenInvalid(true); + console.error('Error generating reports:', error); + } } } catch (error: any) { if (error.response?.status === 403) { redirectToLogin(); } } + + }; const handleRemove = async (upload_id: number, fileName: string) => { @@ -106,26 +173,52 @@ const UserEvaluation: React.FC = () => { }; const handleToggle = async (upload_id: number, fileName: string) => { + // Create local copies of the state for updates + let updatedReportNames = [...reportNames]; + let updatedReportIds = [...reportIds]; + + try { if (reportIds.includes(upload_id)) { - await handleToggleReport(upload_id); - setReportNames(reportNames.filter(name => name !== fileName)); - setReportIds(reportIds.filter(id => id !== upload_id)); + // Attempt to remove the report + try { + await handleToggleReport(upload_id); + // Remove from local copy + updatedReportNames = updatedReportNames.filter(name => name !== fileName); + updatedReportIds = updatedReportIds.filter(id => id !== upload_id); + } catch (error) { + setSnackbarOpenInvalid(true); + console.error('Error updating report status:', error); + return; + } } else { + if (reportIds.length < 2) { - await handleToggleReport(upload_id); - setReportNames([...reportNames, fileName]); - setReportIds([...reportIds, upload_id]); + setLastReportId(upload_id); + try { + await handleToggleReport(upload_id); + // Add to local copy + updatedReportNames.push(fileName); + updatedReportIds.push(upload_id); + } catch (error) { + setSnackbarOpenInvalid(true); + console.error('Error updating report status:', error); + return; + } } else { setSnackbarOpen(true); + return; // Exit early if limit reached } } + setReportNames(updatedReportNames); + setReportIds(updatedReportIds); } catch (error) { setSnackbarOpenInvalid(true); - console.error('Error updating report status:', error); + console.error('Error in handleToggle:', error); } }; + const handleCloseSnackbar = () => { setSnackbarOpen(false); }; @@ -134,6 +227,60 @@ const UserEvaluation: React.FC = () => { setSnackbarOpenInvalid(false); }; + const handleCloseSnackbarMaxUpload = () => { + setSnackbarOpenMaxUpload(false); + } + + const handleCloseSnackbarNoAccess = () => { + setSnackbarOpenNoAccess(false); + } + + + if (loading) { + return ( + + + + ); + } + + if (unauthorized) { + return ( + + + Page does not exist + + + + ); + } + return ( @@ -149,6 +296,26 @@ const UserEvaluation: React.FC = () => { anchorOrigin={{ vertical: 'top', horizontal: 'center' }} /> + + + + { open={snackbarOpenInvalid} autoHideDuration={1000} onClose={handleCloseSnackbarInvalid} - message="Invalid Report Format" + message="Invalid Report Format, File Removed." anchorOrigin={{ vertical: 'top', horizontal: 'center' }} /> @@ -222,21 +389,21 @@ const UserEvaluation: React.FC = () => { - diff --git a/server/lib/pipe.js b/server/lib/pipe.js index 8f97347..3b106e1 100644 --- a/server/lib/pipe.js +++ b/server/lib/pipe.js @@ -22,7 +22,24 @@ const handleFileUpload = async (req, res, vaId) => { const orgResult = await pgClient.query(orgQuery, [organizationName]); organization_id = orgResult.rows[0]?.organization_id; } - + + //Check if Va is assigned to client/ org + if (client_id) { + const assignmentQuery = 'SELECT * FROM assignment WHERE va = $1 AND client = $2'; + const assignmentResult = await pgClient.query(assignmentQuery, [vaId, client_id]); + if (assignmentResult.rows.length === 0) { + return res.status(401).send('Unauthorized'); + } + } + + if (organization_id) { + const assignmentQuery = 'SELECT * FROM assignment WHERE va = $1 AND organization = $2'; + const assignmentResult = await pgClient.query(assignmentQuery, [vaId, organization_id]); + if (assignmentResult.rows.length === 0) { + return res.status(401).send('Unauthorized'); + } + } + // Create a new large object const loCreateQuery = 'SELECT lo_creat(-1)'; diff --git a/server/routes/uploads.js b/server/routes/uploads.js index d88c5d2..a981da7 100644 --- a/server/routes/uploads.js +++ b/server/routes/uploads.js @@ -324,7 +324,7 @@ router.put('/uploads/inReport/:upload_id', authenticateToken, async (req, res) = router.get('/uploads/organization/:organizationName', authenticateToken, async (req, res) => { const token = req.headers['authorization'].split(' ')[1]; const decodedToken = verifyToken(token); - const id = decodedToken.user_id; + const vaId = decodedToken.user_id; const { organizationName } = req.params; try { @@ -340,28 +340,39 @@ router.get('/uploads/organization/:organizationName', authenticateToken, async ( const organizationId = orgResult.rows[0].organization_id; + // Check if the VA ID is assigned to the client in the assignment table + const assignmentResult = await pgClient.query( + 'SELECT * FROM assignment WHERE va = $1 AND organization = $2', + [vaId, organizationId] + ); + + if (assignmentResult.rows.length === 0) { + return res.status(401).send('Unauthorized'); + } + // Fetch uploads for the organization UUID const uploads = await pgClient.query( 'SELECT * FROM raw_uploads WHERE va = $1 AND organization = $2', - [id, organizationId] + [vaId, organizationId] ); - res.send(uploads.rows); + res.status(200).send(uploads.rows); } catch (err) { console.error(err.message); res.status(500).send('Server Error'); } }); -// Get all uploads for a specific client assigned to logged in VA + +// Get all uploads for a specific client assigned to logged-in VA router.get('/uploads/client/:clientName', authenticateToken, async (req, res) => { const token = req.headers['authorization'].split(' ')[1]; const decodedToken = verifyToken(token); - const id = decodedToken.user_id; + const vaId = decodedToken.user_id; const { clientName } = req.params; try { - // Get the UUID for the clientName + // Check if the client exists in the users table const clientResult = await pgClient.query( 'SELECT user_id FROM users WHERE username = $1', [clientName] @@ -373,13 +384,23 @@ router.get('/uploads/client/:clientName', authenticateToken, async (req, res) => const clientId = clientResult.rows[0].user_id; - // Fetch uploads for the client UUID + // Check if the VA ID is assigned to the client in the assignment table + const assignmentResult = await pgClient.query( + 'SELECT * FROM assignment WHERE va = $1 AND client = $2', + [vaId, clientId] + ); + + if (assignmentResult.rows.length === 0) { + return res.status(401).send('Unauthorized'); + } + + // Fetch uploads for the client assigned to the VA const uploads = await pgClient.query( 'SELECT * FROM raw_uploads WHERE va = $1 AND client = $2', - [id, clientId] + [vaId, clientId] ); - res.send(uploads.rows); + res.status(200).send(uploads.rows); } catch (err) { console.error(err.message); res.status(500).send('Server Error');