diff --git a/index.js b/index.js index 1fd96fb7..14cfa52a 100644 --- a/index.js +++ b/index.js @@ -22,8 +22,9 @@ import { packageReceiptScreen } from "./src/pages/receipts/packageReceipt.js"; import { csvFileReceiptScreen } from "./src/pages/receipts/csvFileReceipt.js"; import { kitReportsScreen } from "./src/pages/reports/kitReports.js"; import { collectionIdSearchScreen } from "./src/pages/reports/collectionIdSearch.js"; +import { bptlShipReportsScreen } from "./src/pages/reports/shippingReport.js"; import { checkOutReportTemplate } from "./src/pages/checkOutReport.js"; - +import { dailyReportTemplate } from "./src/pages/dailyReport.js"; let auth = ''; @@ -102,6 +103,8 @@ const manageRoutes = async () => { else if (route === "#collectionidsearch") collectionIdSearchScreen(auth, route); else if (route === "#reports") reportsQuery(auth, route); else if (route === "#checkoutreport") checkOutReportTemplate(auth, route); + else if (route === "#dailyreport") dailyReportTemplate(auth, route); + else if (route === "#bptlshipreports") bptlShipReportsScreen(auth, route); else if (route === "#manage_users") manageUsers(auth, route); else if (route === "#sign_out") signOut(); else window.location.hash = "#welcome"; diff --git a/src/events.js b/src/events.js index f861ce7a..10511b89 100644 --- a/src/events.js +++ b/src/events.js @@ -142,7 +142,7 @@ export const addEventsearchSpecimen = () => { const biospecimen = await searchSpecimen(masterSpecimenId); if (biospecimen.code !== 200 || Object.keys(biospecimen.data).length === 0) { hideAnimation(); - showNotifications({ title: 'Not found', body: 'Specimen not found!' }, true) + showNotifications({ title: 'Not found', body: 'Specimen not found!' }) return } const biospecimenData = biospecimen.data; @@ -150,14 +150,14 @@ export const addEventsearchSpecimen = () => { if(getWorkflow() === 'research') { if(biospecimenData[conceptIds.collection.collectionSetting] !== conceptIds.research) { hideAnimation(); - showNotifications({ title: 'Incorrect Dashboard', body: 'Clinical Collections cannot be viewed on Research Dashboard' }, true); + showNotifications({ title: 'Incorrect Dashboard', body: 'Clinical Collections cannot be viewed on Research Dashboard' }); return; } } else { if(biospecimenData[conceptIds.collection.collectionSetting] === conceptIds.research) { hideAnimation(); - showNotifications({ title: 'Incorrect Dashboard', body: 'Research Collections cannot be viewed on Clinical Dashboard' }, true); + showNotifications({ title: 'Incorrect Dashboard', body: 'Research Collections cannot be viewed on Clinical Dashboard' }); return; } } @@ -183,11 +183,11 @@ export const addEventAddSpecimenToBox = () => { const shippingLocationValue = document.getElementById('selectLocationList').value; if(shippingLocationValue === 'none') { - showNotifications({ title: 'Shipping Location Not Selected', body: 'Please select a shipping location from the dropdown.' }, true); + showNotifications({ title: 'Shipping Location Not Selected', body: 'Please select a shipping location from the dropdown.' }); return; } if (masterSpecimenId === '') { - showNotifications({ title: 'Empty Entry or Scan', body: 'Please enter or scan a specimen bag ID or Full Specimen ID.' }, true); + showNotifications({ title: 'Empty Entry or Scan', body: 'Please enter or scan a specimen bag ID or Full Specimen ID.' }); return; } @@ -197,7 +197,7 @@ export const addEventAddSpecimenToBox = () => { const isScannedIdInUnshippedBoxes = scannedIdInUnshippedBoxes['foundMatch']; if (foundScannedIdShipped){ - showNotifications({ title:'Item reported as already shipped', body: 'Please enter or scan another specimen bag ID or Full Specimen ID.'}, true); + showNotifications({ title:'Item reported as already shipped', body: 'Please enter or scan another specimen bag ID or Full Specimen ID.'}); return; } @@ -206,7 +206,7 @@ export const addEventAddSpecimenToBox = () => { const siteSpecificLocation = conceptIdToSiteSpecificLocation[scannedIdInUnshippedBoxes[conceptIds.shippingLocation]]; const siteSpecificLocationName = siteSpecificLocation || ''; const scannedInput = scannedIdInUnshippedBoxes['inputScanned']; - showNotifications({ title:`${scannedInput} has already been recorded`, body: `${scannedInput} is recorded as being in ${boxNum} in ${siteSpecificLocationName}`}, true); + showNotifications({ title:`${scannedInput} has already been recorded`, body: `${scannedInput} is recorded as being in ${boxNum} in ${siteSpecificLocationName}`}); return; } @@ -214,7 +214,7 @@ export const addEventAddSpecimenToBox = () => { const biospecimensList = specimenTablesResult.biospecimensList; if (biospecimensList.length === 0) { - showNotifications({ title: 'Item not found', body: `Item not reported as collected. Go to the Collection Dashboard to add specimen.` }, true); + showNotifications({ title: 'Item not found', body: `Item not reported as collected. Go to the Collection Dashboard to add specimen.` }); return; } else { document.getElementById('submitMasterSpecimenId').click(); @@ -237,7 +237,7 @@ const addEventSubmitSpecimenBuildModal = () => { const tableIndex = specimenTablesResult.tableIndex; if (biospecimensList.length == 0) { - showNotifications({ title: 'Not found', body: 'The specimen with entered search criteria was not found!' }, true); + showNotifications({ title: 'Not found', body: 'The specimen with entered search criteria was not found!' }); hideAnimation(); const delay = ms => new Promise(res => setTimeout(res, ms)); await delay(500); @@ -260,6 +260,7 @@ export const addEventAddSpecimensToListModalButton = (bagId, tableIndex, isOrpha let boxIdAndBagsObj = {}; for (let i = 0; i < boxList.length; i++) { const box = boxList[i]; + if (!box['bags']) box['bags'] = {}; boxIdAndBagsObj[box[conceptIds.shippingBoxId]] = box['bags']; locations[box[conceptIds.shippingBoxId]] = box[conceptIds.shippingLocation]; } @@ -276,7 +277,7 @@ export const addEventAddSpecimensToListModalButton = (bagId, tableIndex, isOrpha updateShippingStateAddBagToBox(currBoxId, bagId, boxToUpdate); await startShipping(appState.getState().userName, true, currBoxId); } else { - showNotifications({ title: 'Error', body: 'Error updating box' }, true); + showNotifications({ title: 'Error', body: 'Error updating box' }); } } }, { once: true }) @@ -571,7 +572,7 @@ const addEventNewUserForm = (userEmail) => { } else if (response.code === 400 && response.message === 'User with this email already exists') { hideAnimation(); - showNotifications({ title: 'User already exists!', body: `User with email: ${data.email} already exists` }, true); + showNotifications({ title: 'User already exists!', body: `User with email: ${data.email} already exists` }); } }) } @@ -917,7 +918,7 @@ const btnsClicked = async (connectId, formData) => { hideAnimation(); if (specimenData?.Connect_ID && parseInt(specimenData.Connect_ID) !== particpantData.Connect_ID) { - showNotifications({ title: 'Collection ID Duplication', body: 'Entered Collection ID is already associated with a different Connect ID.' }, true) + showNotifications({ title: 'Collection ID Duplication', body: 'Entered Collection ID is already associated with a different Connect ID.' }) return; } @@ -928,7 +929,7 @@ const btnsClicked = async (connectId, formData) => { const storeResponse = await storeSpecimen([formData]); if (storeResponse.code === 400) { hideAnimation(); - showNotifications({ title: 'Specimen already exists!', body: `Collection ID ${collectionID} is already associated with a different Connect ID` }, true); + showNotifications({ title: 'Specimen already exists!', body: `Collection ID ${collectionID} is already associated with a different Connect ID` }); return; } } @@ -1999,7 +2000,7 @@ export const addEventSaveAndContinueButton = async (boxIdAndBagsObj, userName, b const boxIdArray = Object.keys(boxIdAndBagsObj); const trackingNumConfirmEls = Array.from(document.getElementsByClassName("invalid")); if (trackingNumConfirmEls.length > 0) { - showNotifications({ title: 'Invalid Fields', body: 'Please add valid inputs to fields.' }, true); + showNotifications({ title: 'Invalid Fields', body: 'Please add valid inputs to fields.' }); return; } @@ -2007,7 +2008,7 @@ export const addEventSaveAndContinueButton = async (boxIdAndBagsObj, userName, b const trackingId = document.getElementById(boxId + "trackingId").value.toUpperCase(); const trackingIdConfirm = document.getElementById(boxId + "trackingIdConfirm").value.toUpperCase(); if (trackingId === '' || trackingIdConfirm === '') { - showNotifications({ title: 'Missing Fields', body: 'Please enter in shipment tracking numbers'}, true); + showNotifications({ title: 'Missing Fields', body: 'Please enter in shipment tracking numbers'}); return; } @@ -2089,6 +2090,15 @@ export const addEventSaveButton = async (boxIdAndBagsObj) => { }) } +const validateShipperEmail = (email) => { + const finalizeSignInputEle = document.getElementById('finalizeSignInput'); + if (finalizeSignInputEle.value.toUpperCase() !== email.toUpperCase()) { + showNotifications({ title: 'Error Shipping Box(es)', body: `Email mismatch. You entered: ${finalizeSignInputEle.value}, which does not match the email on record.` }); + return false; + } + return true; +} + /** * Handle 'Sign' button click * @param {object} boxIdAndTrackingObj eg: {Box1: {959708259: '123456789012', specimens:{'CXA001234 0008':{...}} }} @@ -2097,62 +2107,55 @@ export const addEventSaveButton = async (boxIdAndBagsObj) => { * @param {string} shipmentCourier name of shipment courier (eg: 'FedEx') */ export const addEventCompleteShippingButton = (boxIdAndTrackingObj, userName, boxWithTempMonitor, shipmentCourier) => { - document.getElementById('finalizeModalSign').addEventListener('click', async () => { - const finalizeSignInputEle = document.getElementById('finalizeSignInput'); - const [firstName, lastName] = userName.split(/\s+/); - const firstNameShipper = firstName ?? ""; - const lastNameShipper = lastName ?? ""; - const errorMessageEle = document.getElementById('finalizeModalError'); + const finalizeModalSignElement = document.getElementById('finalizeModalSign'); + const finalizeModalCancelElement = document.getElementById('finalizeModalCancel'); - if (finalizeSignInputEle.value.toUpperCase() !== userName.toUpperCase()) { - errorMessageEle.style.display = "block"; - return; - } - - // Block subsequent requests before the first one is completed - if (requestsBlocker.isBlocking()) return; - requestsBlocker.block(); + const finalizeAndShip = async () => { + if (!validateShipperEmail(userName)) return; const commonShippingData = { - 666553960: conceptIds[shipmentCourier], - 948887825: firstNameShipper, - 885486943: lastNameShipper, - boxWithTempMonitor, + [conceptIds.shipmentCourier]: conceptIds[shipmentCourier], + [conceptIds.shippedByFirstName]: userName, + boxWithTempMonitor, }; - let boxIdToTrackingNumberObj = {}; - for (const boxId in boxIdAndTrackingObj) { - boxIdToTrackingNumberObj[boxId] = boxIdAndTrackingObj[boxId][conceptIds.shippingTrackingNumber]; - } + const boxIdToTrackingNumberObj = Object.fromEntries( + Object.entries(boxIdAndTrackingObj).map( + ([boxId, value]) => [boxId, value[conceptIds.shippingTrackingNumber]] + ) + ); - const shipment = await ship(boxIdToTrackingNumberObj, commonShippingData); - - if (shipment.code === 200) { - boxWithTempMonitor && (await updateNewTempDate()); - document.getElementById('finalizeModalCancel').click(); - alert('This shipment is now finalized; no other changes can be made'); - localforage.removeItem('shipData'); - startShipping(userName); - } else { - errorMessageEle.style.display = 'block'; - errorMessageEle.textContent = - 'There was an error when saving the shipment data.'; + try { + showAnimation(); + const shipment = await ship(boxIdToTrackingNumberObj, commonShippingData); + hideAnimation(); - if (shipment.code === 500) { - errorMessageEle.textContent = - 'There was an error when saving the shipment data. Please sign again.'; - } + if (shipment.code === 200) { + boxWithTempMonitor && (await updateNewTempDate()); + finalizeModalCancelElement.click(); + localforage.removeItem('shipData'); + showNotifications({ title: 'Success Shipping Box(es)', body: 'Box(es) Shipped Successfully! No other changes can be made to the boxes that were just shipped.' }, 10000); + startShipping(userName); + } else { + showNotifications({ title: 'Error Shipping Box(es)', body: `There was an error shipping the box(es). Please try again: ${shipment.message}` }); + } + } catch (error) { + showNotifications({ title: 'Error Shipping Box(es)', body: `There was an error shipping the box(es). Please try again: ${error}` }); } + } - requestsBlocker.unblock(); - }); - - // Restore error message after closing the modal, in multiple clicks - document.getElementById('finalizeModalCancel').addEventListener('click', () => { + const finalizeModalCancelClickHandler = () => { const errorMessageEle = document.getElementById('finalizeModalError'); errorMessageEle.style.display = "none"; errorMessageEle.textContent = `*Please type in ${userName}`; - }); + }; + + // Remove event listeners, then add them (ensure no duplicates) + finalizeModalSignElement.removeEventListener('click', finalizeAndShip); + finalizeModalCancelElement.removeEventListener('click', finalizeModalCancelClickHandler); + + finalizeModalSignElement.addEventListener('click', finalizeAndShip); + finalizeModalCancelElement.addEventListener('click', finalizeModalCancelClickHandler); } export const populateFinalCheck = (boxIdAndTrackingObj) => { @@ -2271,9 +2274,9 @@ export const populateCourierBox = async () => { } -export const populateBoxTable = async (page, filter) => { +export const populateBoxTable = async (page, filter, source) => { showAnimation(); - let pageStuff = await getPage(page, 5, '656548982', filter) + let pageStuff = await getPage(page, 5, '656548982', filter, source) let currTable = document.getElementById('boxTable') currTable.innerHTML = '' let rowCount = currTable.rows.length; @@ -2302,43 +2305,29 @@ export const populateBoxTable = async (page, filter) => { for (let j = 0; j < keys.length; j++) { numTubes += currPage['bags'][keys[j]]['arrElements'].length; } - let shippedDate = '' - let receivedDate = '' - let packagedCondition = '' - - if (currPage.hasOwnProperty('656548982')) { - const shippedDateStr = currPage['656548982']; - shippedDate = retrieveDateFromIsoString(shippedDateStr) - } - - if(currPage.hasOwnProperty('926457119')) { - const receivedDateStr = currPage['926457119'] - receivedDate = retrieveDateFromIsoString(receivedDateStr) - } - - if(currPage.hasOwnProperty('238268405')) { - packagedCondition = currPage['238268405'] - } + const shippedDate = currPage['656548982'] ? retrieveDateFromIsoString(currPage['656548982']) : ''; + const receivedDate = currPage['926457119'] ? retrieveDateFromIsoString(currPage['926457119']) : ''; + const packagedCondition = currPage['238268405'] || ''; currRow.insertCell(0).innerHTML = currPage[conceptIds.shippingTrackingNumber] ?? ''; currRow.insertCell(1).innerHTML = shippedDate; currRow.insertCell(2).innerHTML = conceptIdToSiteSpecificLocation[currPage['560975149']]; currRow.insertCell(3).innerHTML = currPage['132929440']; currRow.insertCell(4).innerHTML = ''; - currRow.insertCell(5).innerHTML = currPage.hasOwnProperty('333524031') ? "Yes" : "No" + currRow.insertCell(5).innerHTML = currPage['333524031'] === 353358909 ? "Yes" : "No" currRow.insertCell(6).innerHTML = receivedDate; currRow.insertCell(7).innerHTML = convertConceptIdToPackageCondition(packagedCondition, packageConditonConversion); - currRow.insertCell(8).innerHTML = currPage.hasOwnProperty('870456401') ? currPage['870456401'] : '' ; - addEventViewManifestButton('reportsViewManifest' + i, currPage); + currRow.insertCell(8).innerHTML = currPage['870456401'] || '' ; + addEventViewManifestButton('reportsViewManifest' + i, currPage, source); } hideAnimation(); } -export const addEventViewManifestButton = (buttonId, currPage) => { +export const addEventViewManifestButton = (buttonId, currPage, source) => { let button = document.getElementById(buttonId); button.addEventListener('click', () => { - showReportsManifest(currPage); + showReportsManifest(currPage, source); }); } @@ -2432,7 +2421,7 @@ export const populateReportManifestTable = (currPage, searchSpecimenInstituteArr } } -export const addPaginationFunctionality = (lastPage, filter) => { +export const addPaginationFunctionality = (lastPage, filter, source) => { let paginationButtons = document.getElementById('paginationButtons'); paginationButtons.innterHTML = "" paginationButtons.innerHTML = ``; } diff --git a/src/pages/bptl.js b/src/pages/bptl.js index 7ece08dc..708c428a 100644 --- a/src/pages/bptl.js +++ b/src/pages/bptl.js @@ -44,9 +44,9 @@ const bptlScreenTemplate = (name, data, auth, route) => {

Home Collection

-
-
-
+
+
+

Supplies

@@ -64,6 +64,7 @@ const bptlScreenTemplate = (name, data, auth, route) => {

Reports

+
@@ -105,4 +106,8 @@ const redirectPageToLocation = () => { collectionIdSearchRedirection && collectionIdSearchRedirection.addEventListener("click", async () => { location.hash = "#collectionidsearch"; }); + const shippingReportRedirection = document.getElementById("bptlShipReports"); + shippingReportRedirection && shippingReportRedirection.addEventListener("click", async () => { + location.hash = "#bptlshipreports"; + }); }; \ No newline at end of file diff --git a/src/pages/dailyReport.js b/src/pages/dailyReport.js new file mode 100644 index 00000000..cc010c5c --- /dev/null +++ b/src/pages/dailyReport.js @@ -0,0 +1,164 @@ +import { userAuthorization, removeActiveClass, hideAnimation, showAnimation, getDailyParticipant, convertISODateTime, restrictNonBiospecimenUser, getDataAttributes, appState } from "./../shared.js" +import { homeNavBar, reportSideNavBar } from '../navbar.js'; +import fieldToConceptIdMapping from "../fieldToConceptIdMapping.js"; + + +export const dailyReportTemplate = (auth, route) => { + auth.onAuthStateChanged(async user => { + if(user){ + const response = await userAuthorization(route, user.displayName ? user.displayName : user.email); + appState.setState({siteAcronym: response.siteAcronym}); + if ( response.isBiospecimenUser === false ) { + restrictNonBiospecimenUser(); + return; + } + if(!response.role) return; + renderDailyReport(); + } + else { + document.getElementById('navbarNavAltMarkup').innerHTML = homeNavBar(); + window.location.hash = '#'; + } + }); +} + + +export const renderDailyReport = async () => { + let template = ` +
+
+
+

Reports

+ ${reportSideNavBar()} +
+
+
+ ${renderCollectionLocationList()} +
+ +
+
+
+
+
+
`; + document.getElementById('contentBody').innerHTML = template; + removeActiveClass('nav-link', 'active'); + const navBarBtn = document.getElementById('navBarDailyReport'); + navBarBtn.classList.add('active'); + initializeDailyReportTable(); +} + +const renderCollectionLocationList = () => { + let template = ``; + template += ` + + ` + return template; +} + +const initializeDailyReportTable = async () => { + showAnimation(); + const dailyReportsData = await getDailyParticipant().then(res => res.data); + appState.setState({dailyReportsData: dailyReportsData}); // store inital daily reports data + populateDailyReportTable(`Filter by Collection Location`, dailyReportsData); +} + +const populateDailyReportTable = (dropdownHeader, dailyReportsData) => { + const currTable = document.getElementById('populateDailyReportTable'); + currTable.innerHTML = ''; + + const headerRow = currTable.insertRow(); + headerRow.innerHTML = ` + Collection Location + Connect ID + Last Name + First Name + Check-In Date/Time + Collection ID + Collection Finalized + Check-Out Date/Time + `; + + for (const item of dailyReportsData) { + if (!item[fieldToConceptIdMapping.collection.selectedVisit]?.[fieldToConceptIdMapping.baseline.visitId]?.[fieldToConceptIdMapping.checkOutDateTime]) { + const newRow = currTable.insertRow(); + newRow.innerHTML = ` + + ${fieldToConceptIdMapping.collectionLocationMapping[item[fieldToConceptIdMapping.collectionLocation]]} + ${item['Connect_ID']} + ${item[fieldToConceptIdMapping.lastName]} + ${item[fieldToConceptIdMapping.firstName]} + ${convertISODateTime(item[fieldToConceptIdMapping.checkInDateTime])} + ${item[fieldToConceptIdMapping.collection.id]} + ${item[fieldToConceptIdMapping.collection.finalizedTime] !== undefined ? convertISODateTime(item[fieldToConceptIdMapping.collection.finalizedTime]) : ``} + ${item[fieldToConceptIdMapping.checkOutDateTime] !== undefined ? convertISODateTime(item[fieldToConceptIdMapping.checkOutDateTime]) : ``} + `; + } + } + hideAnimation(); + dropdownTrigger(dropdownHeader); +} + +const reInitalizeDailyReportTable = async (dropdownText, siteKey, dailyData) => { + showAnimation(); + let data = dailyData; + if (siteKey !== 'all') { + data = data.filter((dailyReportData) => dailyReportData[fieldToConceptIdMapping.collectionLocation] === fieldToConceptIdMapping.nameToKeyObj[siteKey]); + } + populateDailyReportTable(dropdownText, data) +} + + +const dropdownTrigger = (sitekeyName) => { + let a = document.getElementById('dropdownSites'); + let dropdownMenuButton = document.getElementById('dropdownMenuButtonSites'); + let tempSiteName = a.innerHTML = sitekeyName; + if (dropdownMenuButton) { + dropdownMenuButton.addEventListener('click', (e) => { + if (sitekeyName === `Filter by Collection Location` || sitekeyName === tempSiteName) { + a.innerHTML = e.target.textContent; + const sitekey = getDataAttributes(e.target) + const dailyReportsData = appState.getState().dailyReportsData; + reInitalizeDailyReportTable(e.target.textContent, sitekey, dailyReportsData); + } + }) + } +} \ No newline at end of file diff --git a/src/pages/receipts/activeReceiptsNavbar.js b/src/pages/receipts/activeReceiptsNavbar.js index 4cdedd25..1ade0e23 100644 --- a/src/pages/receipts/activeReceiptsNavbar.js +++ b/src/pages/receipts/activeReceiptsNavbar.js @@ -17,5 +17,5 @@ export const activeReceiptsNavbar = () => { csvFileReceiptNavItem.classList.add("active"); csvFileReceiptNavItem.style.backgroundColor = "#bbcffc85"; csvFileReceiptNavItem.style.borderRadius = "4px 4px 0 0"; - } else return; + }; }; \ No newline at end of file diff --git a/src/pages/receipts/csvFileReceipt.js b/src/pages/receipts/csvFileReceipt.js index 17713553..30e1c703 100644 --- a/src/pages/receipts/csvFileReceipt.js +++ b/src/pages/receipts/csvFileReceipt.js @@ -1,4 +1,4 @@ -import { showAnimation, hideAnimation, getIdToken, nameToKeyObj, keyToLocationObj, baseAPI, keyToNameObj, convertISODateTime, formatISODateTime, getAllBoxes, getSiteAcronym, conceptIdToSiteSpecificLocation } from "../../shared.js"; +import { showAnimation, hideAnimation, getIdToken, keyToNameAbbreviationObj, keyToLocationObj, baseAPI, keyToNameObj, convertISODateTime, formatISODateTime, getAllBoxes, conceptIdToSiteSpecificLocation, showNotifications } from "../../shared.js"; import fieldToConceptIdMapping from "../../fieldToConceptIdMapping.js"; import { receiptsNavbar } from "./receiptsNavbar.js"; import { nonUserNavBar } from "../../navbar.js"; @@ -103,7 +103,7 @@ const confirmFileSelection = () => { const radioVal = radio.value; document.getElementById('modalShowMoreData').querySelector('#closeModal').click(); // closes modal showAnimation(); - const response = await getAllBoxes(`bptl`); + const response = await getAllBoxes(`bptlPackagesInTransit`); hideAnimation(); const allBoxesShippedBySiteAndNotReceived = getRecentBoxesShippedBySiteNotReceived(response.data); let modifiedTransitResults = updateInTransitMapping(allBoxesShippedBySiteAndNotReceived); @@ -113,55 +113,63 @@ const confirmFileSelection = () => { } const csvFileButtonSubmit = () => { - document.getElementById("csvCreateFileButton").addEventListener("click", async (e)=> { - e.preventDefault(); - let dateFilter = document.getElementById("csvDateInput").value - dateFilter = dateFilter+'T00:00:00.000Z' - showAnimation(); - const results = await getBSIQueryData(dateFilter); - hideAnimation(); - document.getElementById("csvDateInput").value = ``; - let modifiedResults = modifyBSIQueryResults(results.data); - generateBSIqueryCSVData(modifiedResults); - }) + document.getElementById("csvCreateFileButton").addEventListener("click", async (e)=> { + e.preventDefault(); + const dateFilter = document.getElementById("csvDateInput").value + 'T00:00:00.000Z'; + showAnimation(); + + try { + const results = await getSpecimensByReceivedDate(dateFilter); + const modifiedResults = modifyBSIQueryResults(results.data); + generateBSIqueryCSVData(modifiedResults); + hideAnimation(); + } catch (e) { + hideAnimation(); + showNotifications({ title: 'Error', body: `Error fetching BSI Query Data -- ${e.message}` }); + } + }); } -const getCurrentDate = () => { - const currentDate = new Date().toLocaleDateString('en-CA'); - return currentDate; +const getSpecimensByReceivedDate = async (dateFilter) => { + try { + const idToken = await getIdToken(); + const response = await fetch(`${baseAPI}api=getSpecimensByReceivedDate&receivedTimestamp=${dateFilter}`, { + method: "GET", + headers: { + Authorization: "Bearer " + idToken, + }, + }); + + if (response.status !== 200) { + throw new Error(`Error fetching specimens by received date. ${response.status}`); + } + + return await response.json(); + } catch (e) { + console.error(e); + throw new Error(`Error fetching specimens by received date: ${e.message}`); + } } -const getBSIQueryData = async (filter) => { - const idToken = await getIdToken(); - const response = await fetch(`${baseAPI}api=queryBsiData&type=${filter}`, { - method: "GET", - headers: { - Authorization: "Bearer" + idToken, - }, - }); - - try { - if (response.status === 200) { - const bsiQueryData = await response.json(); - return bsiQueryData - } - } catch (e) { // if error return an empty array - console.log(e); - return []; - } -}; - +const getCurrentDate = () => { + return new Date().toLocaleDateString('en-CA'); +} const modifyBSIQueryResults = (results) => { - let filteredResults = results.filter(result => result.length !== 0 && (result[0][fieldToConceptIdMapping.collectionId] !== undefined && - result[0][fieldToConceptIdMapping.collectionId].split(' ')[1] !== '0008' && result[0][fieldToConceptIdMapping.collectionId].split(' ')[1] !== '0009') - && result[0][fieldToConceptIdMapping.discardFlag] !== fieldToConceptIdMapping.yes && result[0][fieldToConceptIdMapping.deviationNotFound] !== fieldToConceptIdMapping.yes) - filteredResults = filteredResults.flat() - filteredResults.forEach(filteredResult => { - let vialMappings = getVialTypesMappings(filteredResult) - updateResultMappings(filteredResult, vialMappings) - }) - return filteredResults + const csvDataArray = []; + results.forEach(result => { + const collectionType = result[fieldToConceptIdMapping.collectionType]; + const healthcareProvider = result[fieldToConceptIdMapping.healthcareProvider]; + const specimenKeysArray = Object.keys(result.specimens); + for (const specimenKey of specimenKeysArray) { + const [collectionId, tubeId] = result.specimens[specimenKey][fieldToConceptIdMapping.collectionId].split(' ') ?? ['', '']; + const vialMappings = getVialTypesMappings(tubeId, collectionType, healthcareProvider); + const csvRowsFromSpecimen = updateResultMappings(result, vialMappings, collectionId, tubeId); + csvDataArray.push(csvRowsFromSpecimen); + } + }); + + return csvDataArray; } /** @@ -214,149 +222,167 @@ const materialTypeMapping = (specimenId) => { return materialTypeObject[tubeId] ?? ''; } -/** - * Maps specimen id to material type based on last 4 digits, collection type & health care provider - * @param {object} filteredResult - Object containg essential information (health care provider, collection type, & more) - * @returns {array} Returns an array containing all the vial mapping -*/ - -const getVialTypesMappings = (filteredResult) => { - let vialMappingsHolder = [] - const tubeId = filteredResult[fieldToConceptIdMapping.collectionId].split(' ')[1] - const collectionType = filteredResult[fieldToConceptIdMapping.collectionType] - const healthCareProvider = filteredResult[fieldToConceptIdMapping.healthcareProvider] - - if (collectionType === fieldToConceptIdMapping.research && (tubeId === '0001' || tubeId === '0002' )) { - vialMappingsHolder.push('8.5 mL Serum separator tube', 'SST', 'Serum', '8.5') - } else if (collectionType === fieldToConceptIdMapping.research && tubeId === '0003') { - vialMappingsHolder.push('10 ml Vacutainer', 'Lithium Heparin', 'WHOLE BL', '10') - } else if (collectionType === fieldToConceptIdMapping.research && tubeId === '0004') { - vialMappingsHolder.push('10 ml Vacutainer', 'EDTA = K2', 'WHOLE BL', '10') - } else if (collectionType === fieldToConceptIdMapping.research && tubeId === '0005') { - vialMappingsHolder.push('6 ml Vacutainer', 'ACD', 'WHOLE BL', '6') - } else if (collectionType === fieldToConceptIdMapping.research && tubeId === '0006') { - vialMappingsHolder.push('10 ml Vacutainer', 'No Additive', 'Urine', '10') - } else if (collectionType === fieldToConceptIdMapping.research && tubeId === '0007') { - vialMappingsHolder.push('15ml Nalgene jar', 'Crest Alcohol Free', 'Saliva', '15') - } else if (collectionType === fieldToConceptIdMapping.clinical && (tubeId === '0001' || tubeId === '0002' || tubeId === '0011' || tubeId === '0012' || tubeId === '0021')) { - vialMappingsHolder.push('5 mL Serum separator tube', 'SST', 'Serum', '5') - } else if (collectionType === fieldToConceptIdMapping.clinical && (tubeId === '0003' || tubeId === '0013')) { - vialMappingsHolder.push('4 ml Vacutainer', 'Lithium Heparin', 'WHOLE BL', '4') - } else if (collectionType === fieldToConceptIdMapping.clinical && (tubeId === '0004' || tubeId === '0014' || tubeId === '0024')) { - vialMappingsHolder.push('4 ml Vacutainer', 'EDTA = K2', 'WHOLE BL', '4') - } else if (collectionType === fieldToConceptIdMapping.clinical && tubeId === '0005') { - vialMappingsHolder.push('6 ml Vacutainer', 'ACD', 'WHOLE BL', '6') - } else if (collectionType === fieldToConceptIdMapping.clinical && tubeId === '0006') { - vialMappingsHolder.push('6 ml Vacutainer', 'No Additive', 'Urine', '10') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["kpCO"] && (tubeId === '0001' || tubeId === '0002' || tubeId === '0011' - || tubeId === '0012' )) { - vialMappingsHolder.push('5 mL Serum separator tube', 'SST', 'Serum', '5') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["kpCO"] && (tubeId === '0003' || tubeId === '0013' )) { - vialMappingsHolder.push('4 ml Vacutainer', 'Lithium Heparin', 'WHOLE BL', '4') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["kpCO"] && (tubeId === '0004' || tubeId === '0014' )) { - vialMappingsHolder.push('4 ml Vacutainer', 'EDTA = K2', 'WHOLE BL', '4') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["kpCO"] && (tubeId === '0005')) { - vialMappingsHolder.push('6 ml Vacutainer', 'ACD', 'WHOLE BL', '6') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["kpCO"] && (tubeId === '0001')) { - vialMappingsHolder.push('6 ml Vacutainer', 'No Additive', 'Urine', '6') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["kpNW"] && (tubeId === '0001' || tubeId === '0002' - || tubeId === '0011' || tubeId === '0012' || tubeId === '0021' )) { - vialMappingsHolder.push('3.5 mL Serum separator tube', 'SST', 'Serum', '3.5') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["kpNW"] && (tubeId === '0013' || tubeId === '0003')) { - vialMappingsHolder.push('4 mL Serum separator tube', 'Lithium Heparin', 'WHOLE BL', '4') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["kpNW"] && (tubeId === '0014' || tubeId === '0004')) { - vialMappingsHolder.push('4 mL Serum separator tube', 'EDTA = K2', 'WHOLE BL', '4') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["kpNW"] && (tubeId === '0005')) { - vialMappingsHolder.push('6 ml Vacutainer', 'ACD', 'WHOLE BL', '6') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["kpNW"] && (tubeId === '0006')) { - vialMappingsHolder.push('10 ml Vacutainer', 'No Additive', 'Urine', '10') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["kpHI"] && (tubeId === '0001' || tubeId === '0002' - || tubeId === '0011' || tubeId === '0012')) { - vialMappingsHolder.push('5 ml Serum separator tube', 'SST', 'Serum', '5') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["kpHI"] && (tubeId === '0003' || tubeId === '0013')) { - vialMappingsHolder.push('4 mL Vacutainer', 'Lithium Heparin', 'WHOLE BL', '4') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["kpHI"] && (tubeId === '0004' || tubeId === '0014' - || tubeId === '0024')) { - vialMappingsHolder.push('3 mL Vacutainer', 'EDTA = K2', 'WHOLE BL', '3') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["kpHI"] && (tubeId === '0005')) { - vialMappingsHolder.push('6 ml Vacutainer', 'ACD', 'WHOLE BL', '6') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["kpHI"] && (tubeId === '0006')) { - vialMappingsHolder.push('15 ml Nalgene jar', 'No Additive', 'Urine', '10') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["kpGA"] && (tubeId === '0001' || tubeId === '0002' - || tubeId === '0011' || tubeId === '0012' )) { - vialMappingsHolder.push('5 ml Serum separator tube', 'SST', 'Serum', '5') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["kpGA"] && (tubeId === '0003' || tubeId === '0013')) { - vialMappingsHolder.push('4.5 mL Vacutainer', 'Lithium Heparin', 'WHOLE BL', '4.5') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["kpGA"] && (tubeId === '0004' || tubeId === '0014')) { - vialMappingsHolder.push('4 mL Vacutainer', 'EDTA = K2', 'WHOLE BL', '4') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["kpGA"] && (tubeId === '0005')) { - vialMappingsHolder.push('6 ml Vacutainer', 'ACD', 'WHOLE BL', '6') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["kpGA"] && (tubeId === '0006')) { - vialMappingsHolder.push('15 ml Nalgene jar', 'No Additive', 'Urine', '10') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["hfHealth"] && (tubeId === '0001' || tubeId === '0002')) { - vialMappingsHolder.push('10 ml Serum separator tube', 'SST', 'Serum', '10') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["hfHealth"] && (tubeId === '0003')) { - vialMappingsHolder.push('10 ml Vacutainer', 'Lithium Heparin', 'Whole Blood', '10') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["hfHealth"] && (tubeId === '0004')) { - vialMappingsHolder.push('10 ml Vacutainer', 'EDTA', 'Whole Blood', '10') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["hfHealth"] && (tubeId === '0005')) { - vialMappingsHolder.push('6 ml Vacutainer', 'ACD', 'Whole Blood', '6') - } else if (collectionType === fieldToConceptIdMapping.clinical && healthCareProvider === nameToKeyObj["hfHealth"] && (tubeId === '0006')) { - vialMappingsHolder.push('10 ml Vacutainer', 'No Additive', 'Urine', '6') - } else { - vialMappingsHolder.push('', '', '', '') - } +const vialMapping = { + research: { + default: { + '0001': ['10 mL Serum separator tube', 'SST', 'Serum', '10'], + '0002': ['10 mL Serum separator tube', 'SST', 'Serum', '10'], + '0003': ['10 ml Vacutainer', 'Lithium Heparin', 'WHOLE BL', '10'], + '0004': ['10 ml Vacutainer', 'EDTA', 'WHOLE BL', '10'], + '0005': ['6 ml Vacutainer', 'ACD', 'WHOLE BL', '6'], + '0006': ['10 ml Vacutainer', 'No Additive', 'Urine', '10'], + '0007': ['15 ml Nalgene jar', 'Crest Alcohol Free', 'Saliva', '10'], + }, + }, + clinical: { + hfHealth: { + '0001': ['10 mL Serum separator tube', 'SST', 'Serum', '10'], + '0002': ['10 mL Serum separator tube', 'SST', 'Serum', '10'], + '0003': ['10 ml Vacutainer', 'Lithium Heparin', 'WHOLE BL', '10'], + '0004': ['10 ml Vacutainer', 'EDTA', 'WHOLE BL', '10'], + '0005': ['6 ml Vacutainer', 'ACD', 'WHOLE BL', '6'], + '0006': ['10 ml Vacutainer', 'No Additive', 'Urine', '10'], + }, + kpCO: { + '0001': ['5 mL Serum separator tube', 'SST', 'Serum', '5'], + '0002': ['5 mL Serum separator tube', 'SST', 'Serum', '5'], + '0011': ['5 mL Serum separator tube', 'SST', 'Serum', '5'], + '0012': ['5 mL Serum separator tube', 'SST', 'Serum', '5'], + '0003': ['4 ml Vacutainer', 'Lithium Heparin', 'WHOLE BL', '4'], + '0013': ['4 ml Vacutainer', 'Lithium Heparin', 'WHOLE BL', '4'], + '0004': ['4 ml Vacutainer', 'EDTA = K2', 'WHOLE BL', '4'], + '0014': ['4 ml Vacutainer', 'EDTA = K2', 'WHOLE BL', '4'], + '0005': ['6 ml Vacutainer', 'ACD', 'WHOLE BL', '6'], + '0006': ['6 ml Vacutainer', 'No Additive', 'Urine', '6'], + }, + kpGA: { + '0001': ['5 mL Serum separator tube', 'SST', 'Serum', '5'], + '0002': ['5 mL Serum separator tube', 'SST', 'Serum', '5'], + '0011': ['5 mL Serum separator tube', 'SST', 'Serum', '5'], + '0012': ['5 mL Serum separator tube', 'SST', 'Serum', '5'], + '0003': ['4.5 ml Vacutainer', 'Lithium Heparin', 'WHOLE BL', '4.5'], + '0013': ['4.5 ml Vacutainer', 'Lithium Heparin', 'WHOLE BL', '4.5'], + '0004': ['4 ml Vacutainer', 'EDTA = K2', 'WHOLE BL', '4'], + '0014': ['4 ml Vacutainer', 'EDTA = K2', 'WHOLE BL', '4'], + '0005': ['6 ml Vacutainer', 'ACD', 'WHOLE BL', '6'], + '0006': ['15ml Nalgene jar', 'No Additive', 'Urine', '10'], + }, + kpHI: { + '0001': ['5 mL Serum separator tube', 'SST', 'Serum', '5'], + '0002': ['5 mL Serum separator tube', 'SST', 'Serum', '5'], + '0011': ['5 mL Serum separator tube', 'SST', 'Serum', '5'], + '0012': ['5 mL Serum separator tube', 'SST', 'Serum', '5'], + '0003': ['4 ml Vacutainer', 'Lithium Heparin', 'WHOLE BL', '4'], + '0013': ['4 ml Vacutainer', 'Lithium Heparin', 'WHOLE BL', '4'], + '0004': ['3 ml Vacutainer', 'EDTA = K2', 'WHOLE BL', '3'], + '0014': ['3 ml Vacutainer', 'EDTA = K2', 'WHOLE BL', '3'], + '0024': ['3 ml Vacutainer', 'EDTA = K2', 'WHOLE BL', '3'], + '0005': ['10 ml Vacutainer', 'ACD', 'WHOLE BL', '10'], + '0006': ['15ml Nalgene jar', 'No Additive', 'Urine', '10'], + }, + kpNW: { + '0001': ['3.5 mL Serum separator tube', 'SST', 'Serum', '3.5'], + '0002': ['3.5 mL Serum separator tube', 'SST', 'Serum', '3.5'], + '0011': ['3.5 mL Serum separator tube', 'SST', 'Serum', '3.5'], + '0012': ['3.5 mL Serum separator tube', 'SST', 'Serum', '3.5'], + '0021': ['3.5 mL Serum separator tube', 'SST', 'Serum', '3.5'], + '0003': ['4 ml Vacutainer', 'Lithium Heparin', 'WHOLE BL', '4'], + '0013': ['4 ml Vacutainer', 'Lithium Heparin', 'WHOLE BL', '4'], + '0004': ['4 ml Vacutainer', 'EDTA = K2', 'WHOLE BL', '4'], + '0014': ['4 ml Vacutainer', 'EDTA = K2', 'WHOLE BL', '4'], + '0005': ['6 ml Vacutainer', 'ACD', 'WHOLE BL', '6'], + '0006': ['10 ml Vacutainer', 'No Additive', 'Urine', '10'], + }, + default: { + '0001': ['5 mL Serum separator tube', 'SST', 'Serum', '5'], + '0002': ['5 mL Serum separator tube', 'SST', 'Serum', '5'], + '0011': ['5 mL Serum separator tube', 'SST', 'Serum', '5'], + '0012': ['5 mL Serum separator tube', 'SST', 'Serum', '5'], + '0021': ['5 mL Serum separator tube', 'SST', 'Serum', '5'], + '0003': ['4 ml Vacutainer', 'Lithium Heparin', 'WHOLE BL', '4'], + '0013': ['4 ml Vacutainer', 'Lithium Heparin', 'WHOLE BL', '4'], + '0004': ['4 ml Vacutainer', 'EDTA = K2', 'WHOLE BL', '4'], + '0014': ['4 ml Vacutainer', 'EDTA = K2', 'WHOLE BL', '4'], + '0024': ['4 ml Vacutainer', 'EDTA = K2', 'WHOLE BL', '4'], + '0005': ['6 ml Vacutainer', 'ACD', 'WHOLE BL', '6'], + '0006': ['10 ml Vacutainer', 'No Additive', 'Urine', '10'], + '0007': ['15ml Nalgene jar', 'Crest Alcohol Free', 'Saliva', '10'], + }, + } +}; - return vialMappingsHolder -} +const getVialTypesMappings = (tubeId, collectionType, healthcareProvider) => { + if (!collectionType || !tubeId) { + return ['', '', '', '']; + } + + const collectionTypeString = collectionType === fieldToConceptIdMapping.research ? 'research' : 'clinical'; + const healthCareProviderString = healthcareProvider ? keyToNameAbbreviationObj[healthcareProvider] : 'default'; + + if (collectionTypeString === 'research') { + return vialMapping[collectionTypeString].default[tubeId]; + } else { + return vialMapping[collectionTypeString][healthCareProviderString]?.[tubeId] || vialMapping[collectionTypeString].default[tubeId]; + } +}; -const updateResultMappings = (filteredResult, vialMappings) => { - filteredResult['Study ID'] = 'Connect Study'; - filteredResult['Sample Collection Center'] = (filteredResult[fieldToConceptIdMapping.collectionType] === fieldToConceptIdMapping.clinical) - ? keyToNameObj[filteredResult[fieldToConceptIdMapping.healthcareProvider]] : keyToLocationObj[filteredResult[fieldToConceptIdMapping.collectionLocation]]; - filteredResult['Sample ID'] = filteredResult[fieldToConceptIdMapping.collectionId]?.split(' ')[0] || ''; - filteredResult['Sequence'] = filteredResult[fieldToConceptIdMapping.collectionId]?.split(' ')[1] || ''; - filteredResult['BSI ID'] = filteredResult[fieldToConceptIdMapping.collectionId] || ''; - filteredResult['Subject ID'] = filteredResult['Connect_ID']; - filteredResult['Date Received'] = formatISODateTime(filteredResult[fieldToConceptIdMapping.dateReceived]) || ''; - filteredResult['Date Drawn'] = (filteredResult[fieldToConceptIdMapping.collectionType] === fieldToConceptIdMapping.clinical) - ? convertISODateTime(filteredResult[fieldToConceptIdMapping.clinicalDateTimeDrawn]) || '' : convertISODateTime(filteredResult[fieldToConceptIdMapping.dateWithdrawn]) || ''; - filteredResult['Vial Type'] = vialMappings[0]; - filteredResult['Additive/Preservative'] = vialMappings[1]; - filteredResult['Material Type'] = vialMappings[2]; - filteredResult['Volume'] = vialMappings[3]; - filteredResult['Volume Estimate'] = 'Assumed'; - filteredResult['Voume Unit'] = 'ml (cc)'; - filteredResult['Vial Warnings'] = ''; - filteredResult['Hermolyzed'] = ''; - filteredResult['Label Status'] = 'Barcoded'; - filteredResult['Visit'] = 'BL'; - - // Delete unwanted properties - delete filteredResult[fieldToConceptIdMapping.healthcareProvider]; - delete filteredResult[fieldToConceptIdMapping.collectionLocation]; - delete filteredResult['Connect_ID']; - delete filteredResult[fieldToConceptIdMapping.collectionId]; - delete filteredResult[fieldToConceptIdMapping.dateWithdrawn]; - delete filteredResult[fieldToConceptIdMapping.clinicalDateTimeDrawn]; - delete filteredResult[fieldToConceptIdMapping.dateReceived]; - delete filteredResult[fieldToConceptIdMapping.collectionType]; - delete filteredResult[fieldToConceptIdMapping.discardFlag]; - delete filteredResult[fieldToConceptIdMapping.deviationNotFound]; +const updateResultMappings = (filteredResult, vialMappings, collectionId, tubeId) => { + const collectionTypeValue = filteredResult[fieldToConceptIdMapping.collectionType]; + const clinicalDateTime = filteredResult[fieldToConceptIdMapping.clinicalDateTimeDrawn]; + const withdrawalDateTime = filteredResult[fieldToConceptIdMapping.dateWithdrawn]; + + return { + 'Study ID': 'Connect Study', + 'Sample Collection Center': (collectionTypeValue === fieldToConceptIdMapping.clinical) + ? keyToNameObj[filteredResult[fieldToConceptIdMapping.healthcareProvider]] + : keyToLocationObj[filteredResult[fieldToConceptIdMapping.collectionLocation]], + 'Sample ID': collectionId, + 'Sequence': tubeId, + 'BSI ID': `${collectionId} ${tubeId}`, + 'Subject ID': filteredResult['Connect_ID'], + 'Date Received': filteredResult[fieldToConceptIdMapping.dateReceived] ? formatISODateTime(filteredResult[fieldToConceptIdMapping.dateReceived]) : '', + 'Date Drawn': collectionTypeValue === fieldToConceptIdMapping.clinical + ? (clinicalDateTime ? convertISODateTime(clinicalDateTime) : '') + : (withdrawalDateTime ? convertISODateTime(withdrawalDateTime) : ''), + 'Vial Type': vialMappings[0], + 'Additive/Preservative': vialMappings[1], + 'Material Type': vialMappings[2], + 'Volume': vialMappings[3], + 'Volume Estimate': 'Assumed', + 'Volume Unit': 'ml (cc)', + 'Vial Warnings': '', + 'Hemolyzed': '', + 'Label Status': 'Barcoded', + 'Visit': 'BL' + } } const generateBSIqueryCSVData = (items) => { - let csv = ``; - csv += `Study ID, Sample Collection Center, Sample ID, Sequence, BSI ID, Subject ID, Date Received, Date Drawn, Vial Type, Additive/Preservative, Material Type, Volume, Volume Estimate, Volume Unit, Vial Warnings, Hemolyzed, Label Status, Visit\r\n` - downloadCSVfile(items, csv, 'BSI-data-export') + const csv = 'Study ID, Sample Collection Center, Sample ID, Sequence, BSI ID, Subject ID, Date Received, Date Drawn, Vial Type, Additive/Preservative, Material Type, Volume, Volume Estimate, Volume Unit, Vial Warnings, Hemolyzed, Label Status, Visit\r\n'; + downloadBSIQueryCSVFile(items, csv, 'BSI-data-export'); } const generateInTransitCSVData = (items) => { - let csv = ``; - csv += `Ship Date, Tracking Number, Shipped from Site, Shipped from Location, Shipped Date & Time, Expected Number of Samples, Temperature Monitor, Box Number, Specimen Bag ID Type, Full Specimen IDs, Material Type\r\n` - downloadCSVfile(items, csv, 'In-Transit-CSV-data-export') + const csv = `Ship Date, Tracking Number, Shipped from Site, Shipped from Location, Shipped Date & Time, Expected Number of Samples, Temperature Monitor, Box Number, Specimen Bag ID Type, Full Specimen IDs, Material Type\r\n`; + downloadCSVfile(items, csv, 'In-Transit-CSV-data-export'); +} + +// If value contains a comma, quote or newline, enclose it in double quotes and replace inner double quotes +const downloadBSIQueryCSVFile = (items, csv, title) => { + const csvData = items.map(row => + Object.values(row).map(value => + typeof value === 'string' && (value.includes(',') || value.includes('"') || value.includes('\n') || value.includes('\r')) + ? `"${value.replace(/"/g, '""')}"` + : value + ).join(',') + ).join('\r\n'); + + csv += csvData; + + generateFileToDownload(csv, title, 'csv'); } +//TODO: refactor this to the downloadBSIQueryCSVFile format, then remove downloadBSIQueryCSVFile() const downloadCSVfile = (items, csv, title) => { for (let row = 0; row < (items.length); row++) { let keysAmount = Object.keys(items[row]).length diff --git a/src/pages/receipts/packagesInTransit.js b/src/pages/receipts/packagesInTransit.js index b14b2d5d..7d25124b 100644 --- a/src/pages/receipts/packagesInTransit.js +++ b/src/pages/receipts/packagesInTransit.js @@ -1,4 +1,4 @@ -import { showAnimation, hideAnimation, getAllBoxes, conceptIdToSiteSpecificLocation, searchSpecimenByRequestedSite, appState } from "../../shared.js"; +import { showAnimation, hideAnimation, getAllBoxes, conceptIdToSiteSpecificLocation, searchSpecimenByRequestedSiteAndBoxId, appState } from "../../shared.js"; import fieldToConceptIdMapping from "../../fieldToConceptIdMapping.js"; import { receiptsNavbar } from "./receiptsNavbar.js"; import { nonUserNavBar, unAuthorizedUser } from "../../navbar.js"; @@ -15,7 +15,7 @@ export const packagesInTransitScreen = async (auth, route) => { const packagesInTransitTemplate = async (username, auth, route) => { showAnimation(); - const response = await getAllBoxes(`bptl`); + const response = await getAllBoxes(`bptlPackagesInTransit`); hideAnimation(); const allBoxesShippedBySiteAndNotReceived = getRecentBoxesShippedBySiteNotReceived(response.data); @@ -94,15 +94,7 @@ const packagesInTransitTemplate = async (username, auth, route) => { export const getRecentBoxesShippedBySiteNotReceived = (boxes) => { // boxes are from searchBoxes endpoint if (boxes.length === 0) return []; - - const filteredBoxesBySubmitShipmentTimeAndNotReceived = boxes.filter(boxObj => { - const hasShippingShipDate = boxObj[fieldToConceptIdMapping.shippingShipDate]; - const hasNoSiteShipmentDateReceivedKey = !boxObj[fieldToConceptIdMapping.siteShipmentDateReceived]; - const hasNotReceivedSiteShipment = boxObj[fieldToConceptIdMapping.siteShipmentReceived] === fieldToConceptIdMapping.no; - return hasShippingShipDate && (hasNoSiteShipmentDateReceivedKey || hasNotReceivedSiteShipment); - }); - - return filteredBoxesBySubmitShipmentTimeAndNotReceived.sort((a,b) => { + return boxes.sort((a,b) => { const shipDateA = a[fieldToConceptIdMapping.shippingShipDate]; const shipDateB = b[fieldToConceptIdMapping.shippingShipDate]; return (shipDateA < shipDateB) ? 1 : -1; @@ -196,8 +188,8 @@ const manifestButton = (allBoxesShippedBySiteAndNotReceived, bagIdArr, manifestM } = packagesInTransitModalData manifestModalBodyEl.innerHTML = modalBody; - showAnimation() - const searchSpecimenByRequestedSiteResponse = await searchSpecimenByRequestedSite(loginSite); + showAnimation(); + const searchSpecimenByRequestedSiteResponse = await searchSpecimenByRequestedSiteAndBoxId(loginSite, boxNumber); const searchSpecimenInstituteArray = searchSpecimenByRequestedSiteResponse?.data ?? []; modalBody = diff --git a/src/pages/reports/activeReportsNavbar.js b/src/pages/reports/activeReportsNavbar.js index 562030f0..5cef7ef2 100644 --- a/src/pages/reports/activeReportsNavbar.js +++ b/src/pages/reports/activeReportsNavbar.js @@ -14,5 +14,11 @@ export const activeReportsNavbar = () => { reportsNavItem.classList.add("active"); reportsNavItem.style.backgroundColor = "#bbcffc85"; reportsNavItem.style.borderRadius = "4px 4px 0 0"; - } else return; + } + else if (location.hash === "#bptlshipreports") { + const reportsNavItem = document.getElementById("bptlShippingReportsNavItem"); + reportsNavItem.classList.add("active"); + reportsNavItem.style.backgroundColor = "#bbcffc85"; + reportsNavItem.style.borderRadius = "4px 4px 0 0"; + } }; diff --git a/src/pages/reports/collectionIdSearch.js b/src/pages/reports/collectionIdSearch.js index 6e632d28..4d3b7918 100644 --- a/src/pages/reports/collectionIdSearch.js +++ b/src/pages/reports/collectionIdSearch.js @@ -62,7 +62,7 @@ const searchSpecimenEvent = () => { const biospecimen = await searchSpecimen(masterSpecimenId, true); if (biospecimen.code !== 200 || Object.keys(biospecimen.data).length === 0) { hideAnimation(); - showNotifications({ title: 'Not found', body: 'Specimen not found!' }, true) + showNotifications({ title: 'Not found', body: 'Specimen not found!' }) return } const biospecimenData = biospecimen.data; @@ -75,7 +75,7 @@ const searchSpecimenEvent = () => { finalizeTemplate(data, biospecimenData, true); } catch { - showNotifications({ title: 'Not found', body: 'Participant not found!' }, true) + showNotifications({ title: 'Not found', body: 'Participant not found!' }) } }) } \ No newline at end of file diff --git a/src/pages/reports/reportsNavbar.js b/src/pages/reports/reportsNavbar.js index cfdcd756..23bb63d1 100644 --- a/src/pages/reports/reportsNavbar.js +++ b/src/pages/reports/reportsNavbar.js @@ -6,10 +6,13 @@ export const reportsNavbar = () => { Home + `; diff --git a/src/pages/reports/shippingReport.js b/src/pages/reports/shippingReport.js new file mode 100644 index 00000000..7828f341 --- /dev/null +++ b/src/pages/reports/shippingReport.js @@ -0,0 +1,29 @@ +import { reportsNavbar } from "./reportsNavbar.js"; +import { nonUserNavBar } from "../../navbar.js"; +import { activeReportsNavbar } from "./activeReportsNavbar.js"; +import { appState } from '../../shared.js'; +import { startReport } from "../reportsQuery.js"; + +export const bptlShipReportsScreen = async (auth, route) => { + const user = auth.currentUser; + if (!user) return; + const username = user.displayName || user.email; + appState.setState({ username }); + bptlShipReportsScreenTemplate(username); +}; + +export const bptlShipReportsScreenTemplate = async (username) => { + const template = ` ${reportsNavbar()} +
+

Shipping Report Screen

+
+ ${startReport('bptlShippingReport')} +
+
+ `; + + document.getElementById("contentBody").innerHTML = template; + document.getElementById("navbarNavAltMarkup").innerHTML = nonUserNavBar(username); + activeReportsNavbar(); +}; + diff --git a/src/pages/reportsQuery.js b/src/pages/reportsQuery.js index 5f5e7569..98f37077 100644 --- a/src/pages/reportsQuery.js +++ b/src/pages/reportsQuery.js @@ -1,6 +1,7 @@ import { userAuthorization, removeActiveClass, restrictNonBiospecimenUser, hideAnimation, showAnimation, getNumPages, conceptIdToSiteSpecificLocation, searchSpecimenInstitute } from "./../shared.js"; import { populateBoxTable, populateReportManifestHeader, populateReportManifestTable, addPaginationFunctionality, addEventFilter } from "./../events.js"; import { homeNavBar, reportSideNavBar } from '../navbar.js'; +import { reportsNavbar } from "./reports/reportsNavbar.js"; import conceptIds from '../fieldToConceptIdMapping.js'; export const reportsQuery = (auth, route) => { @@ -20,17 +21,21 @@ export const reportsQuery = (auth, route) => { }); }; -export const startReport = async () => { +export const startReport = async (source) => { showAnimation(); - if (document.getElementById('navBarParticipantCheckIn')) document.getElementById('navBarParticipantCheckIn').classList.add('disabled'); - + if (document.getElementById('navBarParticipantCheckIn') && source !== `bptlShippingReport`) document.getElementById('navBarParticipantCheckIn').classList.add('disabled') let template = ` -
-
-
-

Reports

- ${reportSideNavBar()} -
+ ${ source !== `bptlShippingReport` ? ` +
+
+
+

Reports

+ ${reportSideNavBar()} +
` : + `${reportsNavbar()} +
+

Shipping Report Screen

` + }
@@ -42,6 +47,7 @@ export const startReport = async () => { to +
@@ -65,13 +71,14 @@ export const startReport = async () => { let numPages = await getNumPages(5, {}); document.getElementById('contentBody').innerHTML = template; removeActiveClass('navbar-btn', 'active'); - addEventFilter(); - populateBoxTable(0, {}); - addPaginationFunctionality(numPages, {}); + addEventFilter(source); + populateBoxTable(0, {}, source); + addPaginationFunctionality(numPages, {}, source); hideAnimation(); + clearEventFilter(source); }; -export const showReportsManifest = async (currPage) => { +export const showReportsManifest = async (currPage, source) => { showAnimation(); const searchSpecimenInstituteResponse = await searchSpecimenInstitute(); const searchSpecimenInstituteArray = searchSpecimenInstituteResponse?.data ?? []; @@ -116,8 +123,16 @@ export const showReportsManifest = async (currPage) => { window.print(); }); document.getElementById('returnToReports').addEventListener('click', e => { - startReport(); + startReport(source); }); hideAnimation(); }; + +const clearEventFilter = (source) => { + + let clearFilterButton = document.getElementById('clearFilter'); + clearFilterButton.addEventListener('click', async () => { + startReport(source); + }); +} \ No newline at end of file diff --git a/src/pages/shipping.js b/src/pages/shipping.js index 5426a134..6213b700 100644 --- a/src/pages/shipping.js +++ b/src/pages/shipping.js @@ -1,4 +1,4 @@ -import { addBox, appState, conceptIdToSiteSpecificLocation, displayContactInformation, getAllBoxes, getBoxes, getLocationsInstitute, hideAnimation, locationConceptIDToLocationMap, +import { addBoxAndUpdateSiteDetails, appState, conceptIdToSiteSpecificLocation, displayContactInformation, getAllBoxes, getBoxes, getLocationsInstitute, getSiteMostRecentBoxId, hideAnimation, locationConceptIDToLocationMap, removeActiveClass, removeBag, removeMissingSpecimen, showAnimation, showNotifications, siteSpecificLocation, siteSpecificLocationToConceptId, sortBiospecimensList, translateNumToType, userAuthorization } from "../shared.js" import { addDeviationTypeCommentsContent, addEventAddSpecimenToBox, addEventBackToSearch, addEventBoxSelectListChanged, addEventCheckValidTrackInputs, @@ -314,87 +314,119 @@ const handleRemoveBagButton = (currDeleteButton, currTubes, currBoxId) => { }); } -// Calculate the highest existing boxId and ++ for the new box. +// Get the highest existing boxId and ++ for the new box. // Create the box and add it to firestore. On success, add it to the relevant state objects (allBoxesList and boxesByLocationList, and detailedProviderBoxes). -export const addNewBox = async () => { - const siteLocation = document.getElementById('selectLocationList').value; - const siteLocationConversion = siteSpecificLocationToConceptId[siteLocation]; - const siteCode = siteSpecificLocation[siteLocation]["siteCode"]; +// Important: 0 is a valid box number: only check for null and undefined boxId values, not falsy values. +// Whether to create a new box is location specific (not site specific). Check whether location's most recent box is empty or populated. +// Box numbering is based on site (not location), always increment highest site box number. +// Create the new box and update the modal if the location's largest box is not empty or if the location has no current boxes. +export const addNewBox = async () => { + try { + const siteLocation = document.getElementById('selectLocationList').value; + const siteLocationConversion = siteSpecificLocationToConceptId[siteLocation]; + const siteCode = siteSpecificLocation[siteLocation]["siteCode"]; + const boxList = appState.getState().allBoxesList; + + const boxIdResponse = await getSiteMostRecentBoxId(); + let docId = boxIdResponse.data.docId; + let largestBoxNum = boxIdResponse.data.mostRecentBoxId; - const boxList = appState.getState().allBoxesList; + if (!docId) { + console.error('Error getting site details doc id'); + return false; + } - const { largestBoxIndex, largestBoxIndexAtLocation } = findLargestBoxData(boxList, siteLocation); - const shouldUpdateBoxModal = largestBoxIndexAtLocation !== -1 && Object.keys(boxList[largestBoxIndexAtLocation]['bags']).length !== 0; + if (largestBoxNum == null) { + largestBoxNum = await getLargestBoxNumFromAllBoxes(); + } + + const largestLocationBoxNum = largestBoxNum === -1 ? -1 : getLargestLocationBoxId(boxList, siteLocationConversion); + const largestLocationBoxIndex = boxList.findIndex(box => box[conceptIds.shippingBoxId] === 'Box' + largestLocationBoxNum.toString()); + const shouldCreateNewBox = Object.keys(boxList[largestLocationBoxIndex]?.['bags'] ?? {}).length !== 0 || largestLocationBoxIndex === -1; + + if (shouldCreateNewBox) { + const boxToAdd = await createNewBox(boxList, siteLocationConversion, siteCode, largestBoxNum, docId); + if (!boxToAdd) { + showNotifications({ title: 'ERROR ADDING BOX - PLEASE REFRESH YOUR BROWSER', body: 'Error: This box already exists. A member of your team may have recently created this box. Please refresh your browser and try again.' }); + return false; + } - let largestBoxId; - if (shouldUpdateBoxModal) largestBoxId = boxList[largestBoxIndex][conceptIds.shippingBoxId]; - else largestBoxId = largestBoxIndex !== -1 ? boxList[largestBoxIndex][conceptIds.shippingBoxId] : 'Box0'; - - if (largestBoxIndexAtLocation == -1 || shouldUpdateBoxModal) { - const boxToAdd = await createNewBox(boxList, siteLocationConversion, siteCode, largestBoxId, shouldUpdateBoxModal); - if (!boxToAdd) { - showNotifications({ title: 'ERROR ADDING BOX - PLEASE REFRESH YOUR BROWSER', body: 'Error: This box already exists. A member of your team may have recently created this box. Please refresh your browser and try again.' }); + document.getElementById('shippingModalChooseBox').setAttribute('data-new-box', boxToAdd[conceptIds.shippingBoxId]); + updateShippingStateCreateBox(boxToAdd); + return true; + } else { return false; } - - updateShippingStateCreateBox(boxToAdd); - return true; - } else { + } catch (e) { + console.error('Error adding box', e); + showNotifications({ + title: 'ERROR ADDING BOX', + body: 'An unexpected error occurred. Please try again later.' + }); return false; } } -const createNewBox = async (boxList, pageLocationConversion, siteCode, largestBoxId, updateBoxModal) => { - const newBoxNum = parseInt(largestBoxId.substring(3)) + 1; - const newBoxId = 'Box' + newBoxNum.toString(); +// Create the new box and add it to firestore. If the box already exists, wait one second, increment the boxId, and try again up to 3 times. +const createNewBox = async (boxList, pageLocationConversion, siteCode, largestBoxNum, docId) => { + let attempts = 0; + let maxAttempts = 3; + const boxToAdd = { - 'bags': {}, - [conceptIds.shippingBoxId]: newBoxId, [conceptIds.shippingLocation]: pageLocationConversion, [conceptIds.siteCode]: siteCode, [conceptIds.submitShipmentFlag]: conceptIds.no, - [conceptIds.siteShipmentReceived]: conceptIds.no + [conceptIds.siteShipmentReceived]: conceptIds.no, + ['siteDetailsDocRef']: docId, }; - try { - const addBoxResponse = await addBox(boxToAdd); - if (addBoxResponse.message !== 'Success!') { + while (attempts < maxAttempts) { + largestBoxNum++; + boxToAdd[conceptIds.shippingBoxId] = 'Box' + largestBoxNum.toString(); + + try { + const addBoxResponse = await addBoxAndUpdateSiteDetails(boxToAdd); + if (addBoxResponse.code === 200) { + boxList.push(boxToAdd); + return boxToAdd; + } else if (addBoxResponse.code === 409) { + attempts++; + await delayRetryAttempt(1000); + continue; + } else { + return null; + } + } catch (e) { + console.error('Error adding box', e); return null; } - } catch (e) { - console.error('Error adding box', e); - return null; } - - boxList.push(boxToAdd); - if (updateBoxModal) document.getElementById('shippingModalChooseBox').setAttribute('data-new-box', newBoxId); - return boxToAdd; + console.error('409 - Conflict! 3 Failed attempts creating new box.'); + return null; } -// Find the largest box among all boxes and the largest box at the specified location. -const findLargestBoxData = (boxList, siteLocation) => { - let largestBoxId = 0; - let largestBoxIndex = -1; - let largestLocationBoxId = 0; - let largestBoxIndexAtLocation = -1; +// Delay retry attempt for 1 second when box creation fails. +const delayRetryAttempt = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - for (let i = 0; i < boxList.length; i++) { - const currBoxNum = parseInt(boxList[i][conceptIds.shippingBoxId].substring(3)); - const currLocation = conceptIdToSiteSpecificLocation[boxList[i][conceptIds.shippingLocation]]; - - if (currBoxNum > largestBoxId) { - largestBoxId = currBoxNum; - largestBoxIndex = i; - } +// Find the largest box among all boxes. This is a fallback for new sites that have no boxes. +// It should only execute once per new site, and only if the siteDetails collection's 'mostRecentBoxId' field is null. +// -1 fallback value is handled in the parent function. +const getLargestBoxNumFromAllBoxes = async () => { + const getAllBoxesResponse = await getAllBoxes(); + const boxList = getAllBoxesResponse.data; - if (currLocation == siteLocation && currBoxNum > largestLocationBoxId) { - largestLocationBoxId = currBoxNum; - largestBoxIndexAtLocation = i; - } - } + return boxList.reduce((largestBoxNum, currentBox) => { + const currentBoxNum = parseInt(currentBox[conceptIds.shippingBoxId].substring(3)); + return Math.max(largestBoxNum, currentBoxNum); + }, -1); +} - return { largestBoxIndex, largestBoxIndexAtLocation }; +// Find the largest shipping box id for the location +// Return the highest numeric boxId or -1 if none exist +const getLargestLocationBoxId = (boxesList, siteLocationId) => { + const boxIdsForLocation = boxesList.filter(box => box[conceptIds.shippingLocation] === siteLocationId).map(box => parseInt(box[conceptIds.shippingBoxId].substring(3))); + return boxIdsForLocation.length > 0 ? Math.max(...boxIdsForLocation) : -1; } export const generateBoxManifest = (currBox) => { @@ -530,7 +562,7 @@ export const createShippingModalBody = (biospecimensList, masterBiospecimenId, i populateModalSelect(boxIdAndBagsObj); if (isBagEmpty) { - showNotifications({ title: 'Not found', body: 'The participant with entered search criteria not found!' }, true); + showNotifications({ title: 'Not found', body: 'The participant with entered search criteria not found!' }); document.getElementById('shippingCloseButton').click(); hideAnimation(); } @@ -768,7 +800,7 @@ export const generateShippingManifest = async (boxIdArray, userName, isTempMonit e.stopPropagation(); const tempBoxElement = document.getElementById('tempBox'); if (isTempMonitorIncluded && tempBoxElement.value === '') { - showNotifications({title: 'Missing field!', body: 'Please enter the box where the temperature monitor is being stored.'}, true); + showNotifications({title: 'Missing field!', body: 'Please enter the box where the temperature monitor is being stored.'}); return; } @@ -1104,7 +1136,7 @@ const renderSpecimenVerificationModal = () => {

Select Box or Create New Box

-