diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css index 6e3d2d8..19ca8b4 100644 --- a/assets/css/dashboard.css +++ b/assets/css/dashboard.css @@ -343,4 +343,109 @@ table { #appVersion { font-size: 0.8rem; +} + +.smallerHeading { + font-size: 1.8rem; + font-weight: 700; +} + +.norcToolNote { + font-size: 1rem; + font-weight: 700; + margin-top: 0.8rem; + margin-bottom: 1rem; +} + +.norcToolTypeHeader { + font-size: 1.3rem; + font-weight: 700; +} + +.navTabsDataCorrectionsContainercontainer { + position: relative; + padding-bottom: 1px; +} + +.dataCorrectionsAlert { + margin-top: 1rem; + margin-bottom: 1rem; +} + +p.norcToolDropdownInfoText { + font-style: italic; + margin-bottom: 2rem; +} + +#dataCorrectionsTabsGroup { + width: max-content; + margin-bottom: 0; + padding: 0rem; +} + +#dataCorrectionsTabsGroup li { + text-decoration: none; + outline: none; + cursor: pointer; + display: inline-block; +} + +#dataCorrectionsTabsGroup li .dataCorrectionLink { + border: 1px solid #222; + color: #222; + margin-bottom: -1px; + padding: 0.5rem 1rem; + transition: all 0.8s ease; + font-weight: 500; +} + +#dataCorrectionsTabsGroup li .dataCorrectionLink:hover { + background-color: #B8DFF7; + color: #222; + box-shadow: 0 2px 4px rgba(13, 110, 253, 0.1); + text-decoration: none; +} + +#dataCorrectionsTabsGroup li .dataCorrectionLink.active { + background-color: #B8DFF7; + color: #222; + box-shadow: none; +} + +#dataCorrectionsTabsGroup li:first-child .dataCorrectionLink{ + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; +} + +#dataCorrectionsTabsGroup li:last-child .dataCorrectionLink { + border-top-right-radius: 10px; + border-bottom-right-radius: 10px; +} + +/* +Responsive data corrections nav link display +*/ +@media (max-width:636px) { + #dataCorrectionsTabsGroup { + display: flex; + flex-direction: column; + width: 100%; + } + + #dataCorrectionsTabsGroup li { + display: block; + padding: 0.4rem; + } + + #dataCorrectionsTabsGroup li .dataCorrectionLink { + display: block; + text-align: center; + margin-bottom: 0; + } + + #dataCorrectionsTabsGroup li:first-child .dataCorrectionLink, + #dataCorrectionsTabsGroup li:last-child .dataCorrectionLink, + #dataCorrectionsTabsGroup li .dataCorrectionLink { + border-radius: 10px; + } } \ No newline at end of file diff --git a/assets/images/kp-site/kpco-image-option-1.png b/assets/images/kp-site/kpco-image-option-1.png new file mode 100644 index 0000000..e71b985 Binary files /dev/null and b/assets/images/kp-site/kpco-image-option-1.png differ diff --git a/assets/images/kp-site/kpga-newsletter-photo.png b/assets/images/kp-site/kpga-newsletter-photo.png new file mode 100644 index 0000000..ee91d89 Binary files /dev/null and b/assets/images/kp-site/kpga-newsletter-photo.png differ diff --git a/assets/images/kp-site/kphi-newsletter-photo.png b/assets/images/kp-site/kphi-newsletter-photo.png new file mode 100644 index 0000000..6706991 Binary files /dev/null and b/assets/images/kp-site/kphi-newsletter-photo.png differ diff --git a/assets/images/kp-site/kpnw-newsletter-photo.png b/assets/images/kp-site/kpnw-newsletter-photo.png new file mode 100644 index 0000000..90ee103 Binary files /dev/null and b/assets/images/kp-site/kpnw-newsletter-photo.png differ diff --git a/index.js b/index.js index 56d2eb1..5640a17 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,10 @@ import { renderTable, filterdata, renderData, addEventFilterData, activeColumns, import { renderParticipantDetails } from './src/participantDetails.js'; import { renderParticipantSummary } from './src/participantSummary.js'; import { renderParticipantMessages } from './src/participantMessages.js'; -import { renderDataCorrectionsToolPage } from './src/dataCorrectionsTool.js'; +import { setupDataCorrectionsSelectionToolPage } from './src/dataCorrectionsTool/dataCorrectionsToolSelection.js'; +import { setupVerificationCorrectionsPage } from './src/dataCorrectionsTool/verificationCorrectionsTool.js'; +import { setupSurveyResetToolPage } from './src/dataCorrectionsTool/surveyResetTool.js'; +import { setupIncentiveEligibilityToolPage } from './src/dataCorrectionsTool/incentiveEligibilityTool.js'; import { renderSiteMessages } from './src/siteMessages.js'; import { renderParticipantWithdrawal } from './src/participantWithdrawal.js'; import { createNotificationSchema, editNotificationSchema } from './src/storeNotifications.js'; @@ -25,22 +28,30 @@ let saveFlag = false; let counter = 0; if ("serviceWorker" in navigator) { - navigator.serviceWorker.register("./serviceWorker.js").catch((error) => { - console.error("Service worker registration failed.", error); - return; - }); - - navigator.serviceWorker.ready.then(() => { - if (navigator.serviceWorker.controller) { - navigator.serviceWorker.controller.postMessage({ action: "getAppVersion" }); + navigator.serviceWorker + .register("./serviceWorker.js") + .then((registration) => { + registration.onupdatefound = () => { + const sw = registration.installing; + if (sw) { + sw.onstatechange = () => sw.state === "activated" && sw.postMessage({ action: "getAppVersion" }); } + }; + }) + .catch((err) => { + console.error("Service worker registration failed.", err); }); - navigator.serviceWorker.addEventListener("message", (event) => { - if (event.data.action === "sendAppVersion") { - document.getElementById("appVersion").textContent = event.data.payload; - } - }); + navigator.serviceWorker.ready.then(() => { + const sw = navigator.serviceWorker.controller; + sw && sw.postMessage({ action: "getAppVersion" }); + }); + + navigator.serviceWorker.addEventListener("message", (event) => { + if (event.data.action === "sendAppVersion") { + document.getElementById("appVersion").textContent = event.data.payload; + } + }); } const datadogConfig = { @@ -80,10 +91,10 @@ window.onload = async () => { } if (hasError) { - console.error("Local development requires firebaseConfig defined in src/local-dev/config.js."); + console.error("Local development requires firebaseConfig defined in /config/local-dev/config.js."); return; } - !firebase.apps.length ? firebase.initializeApp(localDevFirebaseConfig) : firebase.app(); + !firebase.apps.length ? firebase.initializeApp(localDevFirebaseConfig) : firebase.app(); } else { !firebase.apps.length ? firebase.initializeApp(devFirebaseConfig) : firebase.app(); @@ -114,6 +125,14 @@ window.onhashchange = () => { router(); }; +const dataCorrectionsToolRoutes = [ + '#dataCorrectionsToolSelection', + '#incentiveEligibilityTool', + '#surveyResetTool', + '#verificationCorrectionsTool', +]; + + const router = async () => { const hash = decodeURIComponent(window.location.hash); const route = hash || '#'; @@ -155,15 +174,33 @@ const router = async () => { renderParticipantMessages(participant); } } - else if (route === '#dataCorrectionsTool') { + else if (dataCorrectionsToolRoutes.includes(route)) { if (JSON.parse(localStorage.getItem("participant")) === null) { alert("No participant selected. Please select a participant from the participants dropdown or the participant lookup page"); } else { - let participant = JSON.parse(localStorage.getItem("participant")) - renderDataCorrectionsToolPage(participant); + let participant = JSON.parse(localStorage.getItem("participant")); + + switch(route) { + case '#dataCorrectionsToolSelection': + setupDataCorrectionsSelectionToolPage(participant) + break; + case '#verificationCorrectionsTool': + setupVerificationCorrectionsPage(participant) + break; + case '#surveyResetTool': + setupSurveyResetToolPage(participant) + break; + case '#incentiveEligibilityTool': + setupIncentiveEligibilityToolPage(participant) + break; + default: + window.location.hash = '#dataCorrectionsToolSelection'; + break; + } } } + else if (route === '#siteMessages') renderSiteMessages(); else if (route === '#participantWithdrawal' && isParent === 'true') { if (JSON.parse(localStorage.getItem("participant")) === null) { @@ -257,6 +294,7 @@ const renderDashboard = async () => { animation(true); const idToken = await getIdToken(); const isAuthorized = await authorize(idToken); + if (isAuthorized && isAuthorized.code === 200) { localStorage.setItem('isParent', isAuthorized.isParent) localStorage.setItem('coordinatingCenter', isAuthorized.coordinatingCenter) diff --git a/newsletter/fallnewsletter-2024-spanish.html b/newsletter/fallnewsletter-2024-spanish.html index 9a54ffe..7498a02 100644 --- a/newsletter/fallnewsletter-2024-spanish.html +++ b/newsletter/fallnewsletter-2024-spanish.html @@ -110,7 +110,7 @@ @@ -153,7 +153,7 @@ ${baselineExperienceSurvey(participant)} + + ${cancerScreeningHistorySurvey(participant)} + ${baselineBloodSample(participant)} @@ -139,7 +142,7 @@ const downloadCopyHandler = (participant) => { const lang = versionArray[3]; a.addEventListener('click', () => { try { - renderDownload(participant, humanReadableMDY(participant[fieldMapping.consentDate]), `./forms/Consent/${conceptToSiteMapping[participant[fieldMapping.healthcareProvider]]}_consent_${(version || defaultVersion)}${(lang ? '_'+lang : '')}.pdf`, + renderDownload(participant, formatUTCDate(participant[fieldMapping.consentDate]), `./forms/Consent/${conceptToSiteMapping[participant[fieldMapping.healthcareProvider]]}_consent_${(version || defaultVersion)}${(lang ? '_'+lang : '')}.pdf`, getHealthcareProviderCoordinates(conceptToSiteMapping[participant[fieldMapping.healthcareProvider]], 'consent', version || defaultVersion, lang || defaultLang)); } catch (error) { console.error(error); @@ -154,7 +157,7 @@ const downloadCopyHandler = (participant) => { const lang = versionArray[3]; b.addEventListener('click', () => { try { - renderDownload(participant, humanReadableMDY(participant[fieldMapping.hippaDate]), `./forms/HIPAA/${conceptToSiteMapping[participant[fieldMapping.healthcareProvider]]}_HIPAA_${(version || defaultVersion)}${(lang ? '_'+lang : '')}.pdf`, + renderDownload(participant, formatUTCDate(participant[fieldMapping.hippaDate]), `./forms/HIPAA/${conceptToSiteMapping[participant[fieldMapping.healthcareProvider]]}_HIPAA_${(version || defaultVersion)}${(lang ? '_'+lang : '')}.pdf`, getHealthcareProviderCoordinates(conceptToSiteMapping[participant[fieldMapping.healthcareProvider]], 'hipaa', version || defaultVersion, lang || defaultLang)); } catch (error) { console.error(error); @@ -168,7 +171,7 @@ const downloadCopyHandler = (participant) => { const version = versionArray[2] || 'V1.0'; const lang = versionArray[3]; c.addEventListener('click', () => { - renderDownload(participant, humanReadableMDY(participant[fieldMapping.dateHIPAARevoc]), `./forms/HIPAA Revocation/HIPAA_Revocation_${version}${(lang ? '_'+lang : '')}.pdf`, getRevocationCoordinates('HIPAA',version,lang || 'Eng'), 'hipaarevoc'); + renderDownload(participant, formatUTCDate(participant[fieldMapping.dateHIPAARevoc]), `./forms/HIPAA Revocation/HIPAA_Revocation_${version}${(lang ? '_'+lang : '')}.pdf`, getRevocationCoordinates('HIPAA',version,lang || 'Eng'), 'hipaarevoc'); }) } const d = document.getElementById('downloadCopyDataDestroy'); @@ -177,7 +180,7 @@ const downloadCopyHandler = (participant) => { const version = versionArray[2] || 'V1.0'; const lang = versionArray[3]; d.addEventListener('click', () => { - renderDownload(participant, humanReadableMDY(participant[fieldMapping.dateDataDestroy]), `./forms/Data Destruction/Data_Destruction_${version}${(lang ? '_'+lang : '')}.pdf`, getRevocationCoordinates('Data',version,lang || 'Eng'), 'datadestruction'); + renderDownload(participant, formatUTCDate(participant[fieldMapping.dateDataDestroy]), `./forms/Data Destruction/Data_Destruction_${version}${(lang ? '_'+lang : '')}.pdf`, getRevocationCoordinates('Data',version,lang || 'Eng'), 'datadestruction'); }) } @@ -308,7 +311,7 @@ const consentHandler = (participant) => { - + @@ -338,7 +341,7 @@ const hippaHandler = (participant) => { - + @@ -367,7 +370,7 @@ const hipaaRevocation = (participant) => { - + @@ -377,7 +380,7 @@ const hipaaRevocation = (participant) => { - + ` @@ -406,7 +409,7 @@ const dataDestroy = (participant) => { - + @@ -416,7 +419,7 @@ const dataDestroy = (participant) => { - + diff --git a/src/participantSummaryRow.js b/src/participantSummaryRow.js index 2e93a94..7df88ff 100644 --- a/src/participantSummaryRow.js +++ b/src/participantSummaryRow.js @@ -1,4 +1,4 @@ -import { humanReadableMDY } from './utils.js'; +import { formatUTCDate } from './utils.js'; import fieldMapping from './fieldToConceptIdMapping.js'; export const userProfile = (participant) => { @@ -8,7 +8,7 @@ export const userProfile = (participant) => { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Enrollment", "N/A", "User Profile", "N/A", "N/A", "N/A", "N", "N/A"); } else if (participant[fieldMapping.userProfileFlag] === fieldMapping.yes) { template += getTemplateRow("fa fa-check fa-2x", "color: green", "Enrollment", "N/A", "User Profile", "Submitted", - humanReadableMDY(participant[fieldMapping.userProfileDateTime]), "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.userProfileDateTime]), "N/A", "N", "N/A"); } else { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Enrollment", "N/A", "User Profile", "Not Submitted", "N/A", "N/A", "N/A", "N/A"); } @@ -23,19 +23,19 @@ export const verificationStatus = (participant) => { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Enrollment", "N/A", "Verification", "N/A", "N/A", "N/A", "N", "N/A"); } else if (participant[fieldMapping.verifiedFlag] === fieldMapping.verified) { template += getTemplateRow("fa fa-check fa-2x", "color: green", "Enrollment", "N/A", "Verification Status", "Verified", - humanReadableMDY(participant[fieldMapping.verficationDate]), "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.verficationDate]), "N/A", "N", "N/A"); } else if (participant[fieldMapping.verifiedFlag] === fieldMapping.cannotBeVerified) { template += getTemplateRow("fa fa-hashtag fa-2x", "color: orange", "Enrollment", "N/A", "Verification Status", "Can't be Verified", - humanReadableMDY(participant[fieldMapping.verficationDate]) || "N/A", "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.verficationDate]) || "N/A", "N/A", "N", "N/A"); } else if (participant[fieldMapping.verifiedFlag] === fieldMapping.notYetVerified) { template += getTemplateRow("fa fa-hashtag fa-2x", "color: orange", "Enrollment", "N/A", "Verification Status", "Not yet Verified", "N/A", "N/A", "N", "N/A"); } else if (participant[fieldMapping.verifiedFlag] === fieldMapping.duplicate) { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Enrollment", "N/A", "Verification Status", "Duplicate", - humanReadableMDY(participant[fieldMapping.verficationDate]) || "N/A", "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.verficationDate]) || "N/A", "N/A", "N", "N/A"); } else { template += getTemplateRow("fa fa-hashtag fa-2x", "color: orange", "Enrollment", "N/A", "Verification Status", "Outreach Timed Out", - humanReadableMDY(participant[fieldMapping.verficationDate]) || "N/A", "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.verficationDate]) || "N/A", "N/A", "N", "N/A"); } return template; @@ -173,10 +173,10 @@ export const baselineBOHSurvey = (participant) => { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Baseline", "Survey", "BOH", "N/A", "N/A", "N/A", "Y", "N/A"); } else if (participant[fieldMapping.bohStatusFlag1] === fieldMapping.submitted1) { template += getTemplateRow("fa fa-check fa-2x", "color: green", "Baseline", "Survey", "BOH", "Submitted", - humanReadableMDY(participant[fieldMapping.bohCompletedDate1]), "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.bohCompletedDate1]), "N/A", "N", "N/A"); } else if (participant[fieldMapping.bohStatusFlag1] === fieldMapping.started1) { template += getTemplateRow("fa fa-hashtag fa-2x", "color: orange", "Baseline", "Survey", "BOH", "Started", - humanReadableMDY(participant[fieldMapping.bohStartDate1]), "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.bohStartDate1]), "N/A", "N", "N/A"); } else if (participant[fieldMapping.bohStatusFlag1] === fieldMapping.notStarted1) { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Baseline", "Survey", "BOH", "Not Started", "N/A", "N/A", "N", "N/A"); } else { @@ -194,10 +194,10 @@ export const baselineMRESurvey = (participant) => { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Baseline", "Survey", "MRE", "N/A", "N/A", "N/A", "Y", "N/A"); } else if (participant[fieldMapping.mreStatusFlag1] === fieldMapping.submitted1) { template += getTemplateRow("fa fa-check fa-2x", "color: green", "Baseline", "Survey", "MRE", "Submitted", - humanReadableMDY(participant[fieldMapping.mreCompletedDate1]), "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.mreCompletedDate1]), "N/A", "N", "N/A"); } else if (participant[fieldMapping.mreStatusFlag1] === fieldMapping.started1) { template += getTemplateRow("fa fa-hashtag fa-2x", "color: orange", "Baseline", "Survey", "MRE", "Started", - humanReadableMDY(participant[fieldMapping.mreStartDate1]), "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.mreStartDate1]), "N/A", "N", "N/A"); } else if (participant[fieldMapping.mreStatusFlag1] === fieldMapping.notStarted1) { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Baseline", "Survey", "MRE", "Not Started", "N/A", "N/A", "N", "N/A"); } else { @@ -215,10 +215,10 @@ export const baselineSASSurvey = (participant) => { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Baseline", "Survey", "SAS", "N/A", "N/A", "N/A", "Y", "N/A"); } else if (participant[fieldMapping.sasStatusFlag1] === fieldMapping.submitted1) { template += getTemplateRow("fa fa-check fa-2x", "color: green", "Baseline", "Survey", "SAS", "Submitted", - humanReadableMDY(participant[fieldMapping.sasCompletedDate1]), "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.sasCompletedDate1]), "N/A", "N", "N/A"); } else if (participant[fieldMapping.sasStatusFlag1] === fieldMapping.started1) { template += getTemplateRow("fa fa-hashtag fa-2x", "color: orange", "Baseline", "Survey", "SAS", "Started", - humanReadableMDY(participant[fieldMapping.sasStartDate1]), "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.sasStartDate1]), "N/A", "N", "N/A"); } else if (participant[fieldMapping.sasStatusFlag1] === fieldMapping.notStarted1) { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Baseline", "Survey", "SAS", "Not Started", "N/A", "N/A", "N", "N/A"); } else { @@ -236,10 +236,10 @@ export const baselineLAWSurvey = (participant) => { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Baseline", "Survey", "LAW", "N/A", "N/A", "N/A", "Y", "N/A"); } else if (participant[fieldMapping.lawStausFlag1] === fieldMapping.submitted1) { template += getTemplateRow("fa fa-check fa-2x", "color: green", "Baseline", "Survey", "LAW", "Submitted", - humanReadableMDY(participant[fieldMapping.lawCompletedDate1]), "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.lawCompletedDate1]), "N/A", "N", "N/A"); } else if (participant[fieldMapping.lawStausFlag1] === fieldMapping.started1) { template += getTemplateRow("fa fa-hashtag fa-2x", "color: orange", "Baseline", "Survey", "LAW", "Started", - humanReadableMDY(participant[fieldMapping.lawStartDate1]), "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.lawStartDate1]), "N/A", "N", "N/A"); } else if (participant[fieldMapping.lawStausFlag1] === fieldMapping.notStarted1) { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Baseline", "Survey", "LAW", "Not Started", "N/A", "N/A", "N", "N/A"); } else { @@ -267,10 +267,10 @@ export const baselineCOVIDSurvey = (participant) => { if (participant[fieldMapping.covidFlag] === fieldMapping.submitted1) { template += getTemplateRow("fa fa-check fa-2x", "color: green", "Baseline", "Survey", "COVID", "Submitted", - humanReadableMDY(participant[fieldMapping.covidCompletedDate]), "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.covidCompletedDate]), "N/A", "N", "N/A"); } else if (participant[fieldMapping.covidFlag] === fieldMapping.started1) { template += getTemplateRow("fa fa-hashtag fa-2x", "color: orange", "Baseline", "Survey", "COVID", "Started", - humanReadableMDY(participant[fieldMapping.covidStartDate]), "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.covidStartDate]), "N/A", "N", "N/A"); } else if (participant[fieldMapping.covidFlag] === fieldMapping.notStarted1) { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Baseline", "Survey", "COVID", "Not Started", "N/A", "N/A", "N", "N/A"); } else { @@ -291,10 +291,10 @@ export const baselineBiospecSurvey = (participant) => { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Baseline", "Survey", "Blood/Urine/Mouthwash", "N/A", "N/A", "N/A", "N", "N/A"); } else if (participant[fieldMapping.combinedBoodUrineMouthwashSurvey] === fieldMapping.submitted1) { template += getTemplateRow("fa fa-check fa-2x", "color: green", "Baseline", "Survey", "Blood/Urine/Mouthwash", "Submitted", - humanReadableMDY(participant[fieldMapping.combinedBoodUrineMouthwashSurveyCompleteDate]), "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.combinedBoodUrineMouthwashSurveyCompleteDate]), "N/A", "N", "N/A"); } else if (participant[fieldMapping.combinedBoodUrineMouthwashSurvey] === fieldMapping.started1) { template += getTemplateRow("fa fa-hashtag fa-2x", "color: orange", "Baseline", "Survey", "Blood/Urine/Mouthwash", "Started", - humanReadableMDY(participant[fieldMapping.combinedBoodUrineMouthwashSurveyStartDate]), "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.combinedBoodUrineMouthwashSurveyStartDate]), "N/A", "N", "N/A"); } else { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Baseline", "Survey", "Blood/Urine/Mouthwash", "Not Started", "N/A", "N/A", "N", "N/A"); } @@ -312,10 +312,10 @@ export const baselineBloodUrineSurvey = (participant) => { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Baseline", "Survey", "Clinical Blood/Urine", "N/A", "N/A", "N/A", "N", "N/A"); } else if (participant[fieldMapping.bloodUrineSurveyFlag] === fieldMapping.submitted1) { template += getTemplateRow("fa fa-check fa-2x", "color: green", "Baseline", "Survey", "Clinical Blood/Urine", "Submitted", - humanReadableMDY(participant[fieldMapping.bloodUrineSurveyCompletedDate]), "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.bloodUrineSurveyCompletedDate]), "N/A", "N", "N/A"); } else if (participant[fieldMapping.bloodUrineSurveyFlag] === fieldMapping.started1) { template += getTemplateRow("fa fa-hashtag fa-2x", "color: orange", "Baseline", "Survey", "Clinical Blood/Urine", "Started", - humanReadableMDY(participant[fieldMapping.bloodUrineSurveyStartedDate]), "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.bloodUrineSurveyStartedDate]), "N/A", "N", "N/A"); } else { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Baseline", "Survey", "Clinical Blood/Urine", "Not Started", "N/A", "N/A", "N", "N/A"); } @@ -333,10 +333,10 @@ export const baselineMouthwashSurvey = (participantModule) => { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Baseline", "Survey", "Home Mouthwash", "N/A", "N/A", "N/A", "N", "N/A"); } else if (participantModule[fieldMapping.mouthwashSurveyFlag] === fieldMapping.submitted1) { template += getTemplateRow("fa fa-check fa-2x", "color: green", "Baseline", "Survey", "Home Mouthwash", "Submitted", - humanReadableMDY(participantModule[fieldMapping.mouthwashSurveyCompletedDate]), "N/A", "N", "N/A"); + formatUTCDate(participantModule[fieldMapping.mouthwashSurveyCompletedDate]), "N/A", "N", "N/A"); } else if (participantModule[fieldMapping.mouthwashSurveyFlag] === fieldMapping.started1) { template += getTemplateRow("fa fa-hashtag fa-2x", "color: orange", "Baseline", "Survey", "Home Mouthwash", "Started", - humanReadableMDY(participantModule[fieldMapping.mouthwashSurveyStartedDate]), "N/A", "N", "N/A"); + formatUTCDate(participantModule[fieldMapping.mouthwashSurveyStartedDate]), "N/A", "N", "N/A"); } else { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Baseline", "Survey", "Home Mouthwash", "Not Started", "N/A", "N/A", "N", "N/A"); } @@ -380,15 +380,27 @@ export const baselineExperienceSurvey = (participant) => { return getTemplateRow(icon, color, timeline, category, item, itemStatus, date, setting, refused, extra); }; +export const cancerScreeningHistorySurvey = (data) => { + const refusedAllFutureSurveys = data[fieldMapping.refusalOptions]?.[fieldMapping.refusedFutureSurveys]; + const refusedAllFutureActivities = data[fieldMapping.refusedAllFutureActivities]; + const refusedCancerScreeningHistorySurvey = data[fieldMapping.refusalOptions]?.[fieldMapping.refusedCancerScreeningHistorySurvey]; + const refused = refusedAllFutureSurveys === fieldMapping.yes || refusedAllFutureActivities === fieldMapping.yes || refusedCancerScreeningHistorySurvey === fieldMapping.yes ? "Y" : "N"; + let { icon, color, itemStatus, date } = getSurveyStatus(data, fieldMapping.cancerScreeningHistorySurveyStatus, fieldMapping.cancerScreeningHistorySurveyStartDate, fieldMapping.cancerScreeningHistorySurveyCompletedDate); + + if (!data[fieldMapping.cancerScreeningHistorySurveyStatus]) itemStatus = "Not Eligible"; + + return getTemplateRow(icon, color, "Follow-Up 9-mo", "Survey", "Cancer Screening History", itemStatus, date, "N/A", refused, "N/A"); +}; + export const baselineMenstrualSurvey = (participant) => { let template = ``; if (participant[fieldMapping.menstrualFlag] === fieldMapping.submitted1) { template += getTemplateRow("fa fa-check fa-2x", "color: green", "Baseline", "Survey", "Menstrual Cycle", "Submitted", - humanReadableMDY(participant[fieldMapping.menstrualDateTimeCompleted]), "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.menstrualDateTimeCompleted]), "N/A", "N", "N/A"); } else if (participant[fieldMapping.menstrualFlag] === fieldMapping.started1) { template += getTemplateRow("fa fa-hashtag fa-2x", "color: orange", "Baseline", "Survey", "Menstrual Cycle", "Started", - humanReadableMDY(participant[fieldMapping.menstrualDateTimeStart]), "N/A", "N", "N/A"); + formatUTCDate(participant[fieldMapping.menstrualDateTimeStart]), "N/A", "N", "N/A"); } else { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Baseline", "Survey", "Menstrual Cycle", "Not Started", "N/A", "N/A", "N", "N/A"); } @@ -404,7 +416,7 @@ export const baselineEMR = (participantModule) => { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Baseline", "EMR", "N/A", "Not Pushed", "N/A", "N/A", "N", "N/A"); } else if (baselineEMR[fieldMapping.baselineEMRflag] === fieldMapping.yes) { template += getTemplateRow("fa fa-check fa-2x", "color: green", "Baseline", "EMR", "N/A", "Pushed", - humanReadableMDY(baselineEMR[fieldMapping.baselineEMRpushDate]), "N/A", "N/A", "N/A"); + formatUTCDate(baselineEMR[fieldMapping.baselineEMRpushDate]), "N/A", "N/A", "N/A"); } else { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Baseline", "EMR", "N/A", "Not Pushed", "N/A", "N/A", "N/A", "N/A"); } @@ -422,8 +434,8 @@ export const baselinePayment = (participantModule) => { participantModule[fieldMapping.paymentRoundup][fieldMapping.baselinePayment][fieldMapping.eligiblePayment] === fieldMapping.yes ) { template += getTemplateRow("fa fa-check fa-2x", "color: green", "Baseline", "Payment", "N/A", "Eligible", - humanReadableMDY(participantModule[fieldMapping.paymentRoundup][fieldMapping.baselinePayment][fieldMapping.baselinePaymentDate]), - "N/A", "N/A", checkIncentiveIssued(participantModule) + formatUTCDate(participantModule[fieldMapping.paymentRoundup][fieldMapping.baselinePayment][fieldMapping.eligiblePaymentRoundTimestamp]), + "N/A", "N/A", checkIncentiveIssued(participantModule) ); } else { template += getTemplateRow("fa fa-times fa-2x", "color: red", "Baseline", "Payment", "N/A", "Not Eligible", "N/A", "N/A", "N/A", checkIncentiveIssued(participantModule) @@ -440,14 +452,14 @@ const getSurveyStatus = (participant, surveyFlag, startDate, completeDate) => { icon: "fa fa-check fa-2x", color: "color: green", itemStatus: "Submitted", - date: humanReadableMDY(participant[completeDate]), + date: formatUTCDate(participant[completeDate]), }; case fieldMapping.started1: return { icon: "fa fa-hashtag fa-2x", color: "color: orange", itemStatus: "Started", - date: humanReadableMDY(participant[startDate]), + date: formatUTCDate(participant[startDate]), }; case fieldMapping.notYetEligible1: return { @@ -478,9 +490,9 @@ const getSurveyStatus = (participant, surveyFlag, startDate, completeDate) => { const checkIncentiveIssued = (participantModule) => { return participantModule[fieldMapping.paymentRoundup] && (participantModule[fieldMapping.paymentRoundup][fieldMapping.biospecimenFollowUp][fieldMapping.paymentIssued] === (fieldMapping.yes)) ? - `Issued on ${humanReadableMDY(participantModule[fieldMapping.paymentRoundup][fieldMapping.biospecimenFollowUp][fieldMapping.datePaymentIssued])}`: + `Issued on ${formatUTCDate(participantModule[fieldMapping.paymentRoundup][fieldMapping.biospecimenFollowUp][fieldMapping.datePaymentIssued])}`: (participantModule[fieldMapping.paymentRoundup]?.[fieldMapping.biospecimenFollowUp]?.[fieldMapping.refusedBaselinePayment] === (fieldMapping.yes)) ? - `Declined on ${humanReadableMDY(participantModule[fieldMapping.paymentRoundup][fieldMapping.biospecimenFollowUp][fieldMapping.refusedBaselinePaymentDate])}`: + `Declined on ${formatUTCDate(participantModule[fieldMapping.paymentRoundup][fieldMapping.biospecimenFollowUp][fieldMapping.refusedBaselinePaymentDate])}`: `N/A` } @@ -517,11 +529,11 @@ const setSampleDateTime = (biospecimenRow, biospecimenFlag, researchDateTime, cl (biospecimenRow[fieldMapping.biospecimenCollectionDetail][fieldMapping.biospecimenFollowUp][biospecimenFlag]) === (fieldMapping.biospecimenResearch) ? ( - biospecimenSampleDateTime += humanReadableMDY(biospecimenRow[fieldMapping.biospecimenCollectionDetail][fieldMapping.biospecimenFollowUp][researchDateTime]) + biospecimenSampleDateTime += formatUTCDate(biospecimenRow[fieldMapping.biospecimenCollectionDetail][fieldMapping.biospecimenFollowUp][researchDateTime]) ) : (biospecimenRow[fieldMapping.biospecimenCollectionDetail][fieldMapping.biospecimenFollowUp][biospecimenFlag]) === (fieldMapping.biospecimenClinical) ? ( - biospecimenSampleDateTime += humanReadableMDY(biospecimenRow[fieldMapping.biospecimenCollectionDetail][fieldMapping.biospecimenFollowUp][clinicalDateTime]) + biospecimenSampleDateTime += formatUTCDate(biospecimenRow[fieldMapping.biospecimenCollectionDetail][fieldMapping.biospecimenFollowUp][clinicalDateTime]) ) : `` ) return biospecimenSampleDateTime; diff --git a/src/participantWithdrawal.js b/src/participantWithdrawal.js index db6bbe1..71740c3 100644 --- a/src/participantWithdrawal.js +++ b/src/participantWithdrawal.js @@ -87,6 +87,8 @@ const getParticipantSelectedRefusals = (participant) => { strArray.push("Refused all future QOL surveys (but willing to do other future surveys)"); if (refusalOptions[fieldMapping.refusedExperienceSurvey] === fieldMapping.yes) strArray.push("Refused 2024 Connect Experience Survey (but willing to do other future surveys)"); + if (refusalOptions[fieldMapping.refusedCancerScreeningHistorySurvey] === fieldMapping.yes) + strArray.push("Refused Cancer Screening History Survey (willing to do specimens)"); if (refusalOptions[fieldMapping.refusedAllFutureExperienceSurveys] === fieldMapping.yes) strArray.push("Refused all future Connect Experience surveys (but willing to do other future surveys)"); if (refusalOptions[fieldMapping.refusedFutureSurveys] === fieldMapping.yes) diff --git a/src/participantWithdrawalForm.js b/src/participantWithdrawalForm.js index cea796b..965e8b0 100644 --- a/src/participantWithdrawalForm.js +++ b/src/participantWithdrawalForm.js @@ -82,6 +82,12 @@ export const renderParticipantWithdrawalLandingPage = () => { 2024 Connect Experience Survey (but willing to do other future surveys)
+ + +
-
- + + + + + + +
+
+ +
+
+ + + + `; +} + +const setupContinueNavigationHandler = () => { + const continueBtn = document.querySelector('.continueButton'); + const selectButton = document.querySelector('.selectButton'); + if (!continueBtn || !selectButton) return; + + continueBtn.addEventListener('click', () => { + const selectedButtonType = selectButton.getAttribute('data-tool'); + if (!selectedButtonType) return; + + if (selectedButtonType === 'verificationCorrections') { + window.location.hash = '#verificationCorrectionsTool'; + } else if (selectedButtonType === 'surveyReset') { + window.location.hash = '#surveyResetTool'; + } else if (selectedButtonType === 'incentiveEligibility') { + window.location.hash = '#incentiveEligibilityTool'; + } + }); +} + +const setupDropdownSelectionHandler = () => { + // get dropdown menu options element + const dropdownMenu = document.getElementById('dropdownToolsMenu'); + const dropdownOptions = dropdownMenu.querySelectorAll('.dropdown-item'); + const selectButton = document.querySelector('.selectButton'); + const continueButton = document.querySelector('.continueButton'); + + if (!dropdownMenu || !dropdownOptions || !selectButton || !continueButton) return; + + for (let option of dropdownOptions) { + option.addEventListener('click', (e) => { + const selectedText = e.target.textContent.trim(); + const selectedToolType = e.target.getAttribute('data-tool'); + + if (selectedToolType) { + selectButton.textContent = selectedText; + continueButton.classList.remove('disabled'); + selectButton.setAttribute('data-tool', selectedToolType); + } else { + selectButton.textContent = 'Select'; + continueButton.classList.add('disabled'); + selectButton.setAttribute('data-tool', ''); + } + }); + } +}; \ No newline at end of file diff --git a/src/dataCorrectionsTool/incentiveEligibilityTool.js b/src/dataCorrectionsTool/incentiveEligibilityTool.js new file mode 100644 index 0000000..e4a65d1 --- /dev/null +++ b/src/dataCorrectionsTool/incentiveEligibilityTool.js @@ -0,0 +1,375 @@ +import fieldMapping from '../fieldToConceptIdMapping.js'; +import { dashboardNavBarLinks, removeActiveClass } from '../navigationBar.js'; +import { renderParticipantHeader } from '../participantHeader.js'; +import { handleBackToToolSelect, displayDataCorrectionsNavbar, setActiveDataCorrectionsTab } from './dataCorrectionsHelpers.js'; +import { showAnimation, hideAnimation, baseAPI, getIdToken, triggerNotificationBanner, formatUTCDate } from '../utils.js'; +import { findParticipant } from '../participantLookup.js'; + +let participantPaymentRound = null; +let isEligibleForIncentiveUpdate = null; +let selectedDateOfEligibility = null; // YYYY-MM-DD + +const conceptIdToPaymentRoundMapping = { + 266600170: 'baseline', +} + +export const setupIncentiveEligibilityToolPage = (participant) => { + if (participant !== undefined) { + const isParent = localStorage.getItem('isParent'); + document.getElementById('navBarLinks').innerHTML = dashboardNavBarLinks(isParent); + removeActiveClass('nav-link', 'active'); + document.getElementById('participantVerificationBtn').classList.add('active'); + mainContent.innerHTML = renderIncentiveEligibilityToolContent(participant); + handlePaymentRoundSelect(participant); + handleBackToToolSelect(); + clearPaymentRoundSelect(); + toggleSubmitButton(); + setupModalContent(); + setActiveDataCorrectionsTab(); + handleDatePickerSelection(); + } +}; + + +const renderIncentiveEligibilityToolContent = (participant) => { + return `
+
+ + ${renderParticipantHeader(participant)} + ${displayDataCorrectionsNavbar()} + +
+ +
+
+

Data Corrections Tool

+

+ Note: This tool should only be used to make corrections to participant data post-verification. All changes need to be approved by the CCC before being applied to the participant record via this tool. +

+
+
+ + +
+
+

Incentive Eligibility

+

+ To update the incentive eligibility for this participant, please choose the payment round and set the date of eligibility below. By clicking submit, the incentive eligibility status will be updated from 'not eligible' to 'eligible'. +

+ +
+

Payment Round:

+
+ + +
+
+
+
+ +
+
+

Current Incentive Eligibility Status:

+

Incentive Eligibility Status:

+

Date of Eligibility:

+

+
+
+
+ +
+
+
+
+ + +
+
+ +
+
+
+
+
+
+ + + + `; +}; + + +/** + * Handles the payment round selection dropdown conetent +*/ +const handlePaymentRoundSelect = (participant) => { + const paymentRoundElement = document.getElementById('dropdownPaymentMenu'); + const dropdownPaymentOptions = paymentRoundElement.children; + const incentiveStatusText = document.getElementById('incentiveStatusText'); + const isIncentiveEligibleNote = document.getElementById('isIncentiveEligibleNote'); + const selectButton = document.querySelector('.selectButton'); + const participantConnectId = participant?.["Connect_ID"]; + const query = `connectId=${participantConnectId}` + + if (!paymentRoundElement || + !dropdownPaymentOptions || + !incentiveStatusText || + !isIncentiveEligibleNote || + !selectButton) return; + + const { paymentRound, baselinePayment, eligiblePayment, norcPaymentEligibility, no } = fieldMapping; + + for (let option of dropdownPaymentOptions) { + option.addEventListener('click', async (e) => { // TODO: Add gaurd to prevent multiple event listeners from being added + participantPaymentRound = e.target.dataset.payment; + if (participantPaymentRound === fieldMapping['baseline'].toString()) { + selectButton.textContent = e.target.textContent; + try { + showAnimation(); + const response = await findParticipant(query); + hideAnimation(); + const participantData = response.data[0]; + localStorage.setItem('participant', JSON.stringify(participantData)); + + const isNORCPaymentEligible = participantData?.[paymentRound]?.[baselinePayment]?.[norcPaymentEligibility] === no; + const isIncentiveEligible = participantData?.[paymentRound]?.[baselinePayment]?.[eligiblePayment] === no; + const isEligibleForIncentiveUpdate = isNORCPaymentEligible && isIncentiveEligible; + + if (isEligibleForIncentiveUpdate) { + toggleSubmitButton(isEligibleForIncentiveUpdate); + displaySetDateOfEligibilityContent(); + setIncentiveEligibleInputDefaultValue(); + handleParticipantPaymentTextContent(participantData, isEligibleForIncentiveUpdate); + confirmIncentiveEligibilityUpdate(participant); + dateOfEligibilityInput.disabled = false; + } else { + toggleSubmitButton(); + handleParticipantPaymentTextContent(participantData, isEligibleForIncentiveUpdate); + } + setupModalContent(participantPaymentRound); + } catch (error) { + console.error(`Failed to fetch participant data for Connect ID ${participantConnectId}: `, error); + } + } else { + toggleSubmitButton(); + participantPaymentRound = null; + isEligibleForIncentiveUpdate = null; + selectButton.textContent = e.target.textContent; + removeSetDateOfEligibilityContent(); + isIncentiveEligibleNote.innerHTML = ``; + handleParticipantPaymentTextContent(participant, isEligibleForIncentiveUpdate); + } + }); + } +}; + +const handleParticipantPaymentTextContent = (participant, isEligibleForIncentiveUpdate) => { + const incentiveStatusText = document.getElementById('incentiveStatusText'); + const isIncentiveEligibleNote = document.getElementById('isIncentiveEligibleNote'); + const dateOfEligibilityText = document.getElementById('dateOfEligibilityText'); + if (!incentiveStatusText || !isIncentiveEligibleNote || !dateOfEligibilityText) return; + + const { paymentRound, baseline, eligiblePaymentRoundTimestamp } = fieldMapping; + + if (isEligibleForIncentiveUpdate) { + incentiveStatusText.textContent = 'Incentive Eligibility Status: Not Eligible'; + dateOfEligibilityText.textContent = 'Date of Eligibility: N/A'; + isIncentiveEligibleNote.innerHTML = ``; + + } else if (isEligibleForIncentiveUpdate === false) { + incentiveStatusText.textContent = 'Incentive Eligibility Status: Eligibile'; + dateOfEligibilityText.textContent = `Date of Eligibility: ${formatUTCDate(participant?.[paymentRound]?.[baseline]?.[eligiblePaymentRoundTimestamp])}`; // TODO: Add flexibility for other payment rounds + isIncentiveEligibleNote.innerHTML = ` This participant is already incentive eligible. The eligibility status cannot be updated.`; + + } else { + incentiveStatusText.textContent = 'Incentive Eligibility Status: '; + dateOfEligibilityText.textContent = 'Date of Eligibility:'; + isIncentiveEligibleNote.innerHTML = ``; + } +}; + +const setIncentiveEligibleInputDefaultValue = () => { + const dateOfEligibilityInput = document.getElementById('dateOfEligibilityInput'); + if (dateOfEligibilityInput) { + const currentDate = new Date().toLocaleDateString("en-CA", {timeZone:"America/New_York"}); // MM/DD/YYYY + dateOfEligibilityInput.value = currentDate; + dateOfEligibilityInput.max = currentDate; + } +}; + +const convertToISO8601 = (dateString) => { + const date = new Date(dateString); + return date.toISOString(); +}; + +/** + * Clears the payment round selection back to default and clears the text content + */ +const clearPaymentRoundSelect = () => { + const clearButton = document.getElementById('clearPaymentRoundButton'); + const isIncentiveEligibleNote = document.getElementById('isIncentiveEligibleNote'); + const selectButton = document.querySelector('.selectButton'); + if (!clearButton || !isIncentiveEligibleNote || !selectButton) return; + + clearButton.addEventListener('click', () => { + setParticipantPaymentRound(); + removeSetDateOfEligibilityContent(); + }); +}; + +const setParticipantPaymentRound = () => { + const clearButton = document.getElementById('clearPaymentRoundButton'); + const isIncentiveEligibleNote = document.getElementById('isIncentiveEligibleNote'); + const selectButton = document.querySelector('.selectButton'); + const incentiveStatusText = document.getElementById('incentiveStatusText'); + const dateOfEligibilityText = document.getElementById('dateOfEligibilityText'); + if (!clearButton || !isIncentiveEligibleNote || !selectButton || !incentiveStatusText || !dateOfEligibilityText) return; + + isIncentiveEligibleNote.textContent = ''; + dateOfEligibilityText.textContent = 'Date of Eligibility:'; + selectButton.textContent = ' Select '; + participantPaymentRound = null; + setIncentiveEligibleInputDefaultValue(); + incentiveStatusText.textContent = 'Incentive Eligibility Status: '; +}; + +const toggleSubmitButton = (isEligibleForIncentiveUpdate) => { + const submitButton = document.getElementById('submitButton'); + if (!submitButton) return; + if (isEligibleForIncentiveUpdate) { + submitButton.removeAttribute('disabled'); + } else { + submitButton.disabled = true; + } +}; + +const confirmIncentiveEligibilityUpdate = (participant) => { + const confirmButton = document.getElementById('confirmUpdateEligibility'); + const { paymentRound, baseline, eligiblePaymentRoundTimestamp } = fieldMapping; + + if (confirmButton && dateOfEligibilityInput) { + confirmButton.addEventListener('click', async (e) => { + const confirmUpdateEligibilityButton = document.getElementById('confirmUpdateEligibility'); + const selectedDateValue = selectedDateOfEligibility ? convertToISO8601(selectedDateOfEligibility) : convertToISO8601(dateOfEligibilityInput.value); + if (confirmUpdateEligibilityButton) { + try { + const updateResponse = await updateParticipantIncentiveEligibility(participant, participantPaymentRound, selectedDateValue); + const currentParticipantData = updateResponse.data; + + if (updateResponse.code === 200) { + triggerNotificationBanner("Participant incentive eligibility status updated successfully!", "success" ,14000); + localStorage.setItem('participant', JSON.stringify(currentParticipantData)); + document.getElementById('incentiveStatusText').textContent = 'Incentive Eligibility Status: Eligible'; + document.getElementById('isIncentiveEligibleNote').innerHTML = ` This participant is already incentive eligible. The eligibility status cannot be updated.`; + document.getElementById('dateOfEligibilityText').textContent = `Date of Eligibility: ${formatUTCDate(currentParticipantData?.[paymentRound]?.[baseline]?.[eligiblePaymentRoundTimestamp])}`; // TODO: Add flexibility for other payment rounds + removeSetDateOfEligibilityContent(); + toggleSubmitButton(); + } + } catch (error) { + console.error("Failed to update participant incentive eligibility: ", error); + triggerNotificationBanner(`${error.message}`, 'danger', 14000); + } + } + }); + } +} + +const setupModalContent = (participantPaymentRound) => { + const paymentRoundType = conceptIdToPaymentRoundMapping[participantPaymentRound]; + const modalBody = document.querySelector('.modal-body'); + if (!modalBody) return; + modalBody.textContent = `Are you sure you want to update the participant's incentive eligibility status for ${paymentRoundType}?`; +}; + +/** + * Handles user input choosing from the date picker and sets selectedDateOfEligibility value + * +*/ +const handleDatePickerSelection = () => { + const datePicker = document.getElementById("dateOfEligibilityInput"); + if (datePicker) { + datePicker.addEventListener("change", (e) => { + selectedDateOfEligibility = e.target.value; // YYYY-MM-DD + }) + } +}; + +const displaySetDateOfEligibilityContent = () => { + const setDateOfEligibilityContainer = document.getElementById('setDateOfEligibilityContainer'); + if (setDateOfEligibilityContainer) { + setDateOfEligibilityContainer.innerHTML = `

Set Date of Eligibility:

+ `; + } +}; + +const removeSetDateOfEligibilityContent = () => { + const setDateOfEligibilityContainer = document.getElementById('setDateOfEligibilityContainer'); + if (setDateOfEligibilityContainer) { + setDateOfEligibilityContainer.innerHTML = ''; + } +}; + +/** + * Update participant incentive eligibility ands returns updated data on success + * Async function to update participant incentive eligibility + * @param {Object} participant - Participant object + * @param {String} selectedPaymentRound - Selected payment round + * @param {String} selectedDateValue - Selected date of eligibility + * @returns {Promise<{ + * code: number, + * data: Object, + * message?: string + * }>} Response object - { code: 200, data: {100767870,...} } +*/ +const updateParticipantIncentiveEligibility = async (participant, selectedPaymentRound, selectedDateValue) => { + const connectId = participant['Connect_ID']; + + try { + const idToken = await getIdToken(); + const response = await fetch(`${baseAPI}/dashboard?api=updateParticipantIncentiveEligibility`, { + method: "POST", + headers: { + Authorization: "Bearer " + idToken, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + connectId: connectId, + currentPaymentRound: selectedPaymentRound, + dateOfEligibilityInput: selectedDateValue // ISO8601 date format + }), + }); + if (!response.ok) { + const error = (response.status + " Error" + ": " + (await response.json()).message); + throw new Error(error); + } + + const responseObj = await response.json(); + return responseObj; + } catch (error) { + console.error("Failed to update participant incentive eligibility: ", error); + throw error; + } +}; \ No newline at end of file diff --git a/src/dataCorrectionsTool/surveyResetTool.js b/src/dataCorrectionsTool/surveyResetTool.js new file mode 100644 index 0000000..2a35d4e --- /dev/null +++ b/src/dataCorrectionsTool/surveyResetTool.js @@ -0,0 +1,307 @@ +import fieldMapping from '../fieldToConceptIdMapping.js'; +import { dashboardNavBarLinks, removeActiveClass } from '../navigationBar.js'; +import { renderParticipantHeader } from '../participantHeader.js'; +import { findParticipant } from '../participantLookup.js'; +import { baseAPI, getIdToken, hideAnimation, showAnimation } from '../utils.js'; +import { handleBackToToolSelect, displayDataCorrectionsNavbar, setActiveDataCorrectionsTab } from './dataCorrectionsHelpers.js'; +import { triggerNotificationBanner } from '../utils.js'; + +let selectedSurvey = null; + +const statusMapping = { + "972455046": "Not Started", + "615768760": "Started", + "231311385": "Completed", +}; + +const surveyModalBody = { + [fieldMapping.ssnStatusFlag]: "Are you sure you want to reset the survey status for the SSN survey?", +}; + +export const setupSurveyResetToolPage = (participant) => { + if (participant !== undefined) { + const isParent = localStorage.getItem('isParent'); + document.getElementById('navBarLinks').innerHTML = dashboardNavBarLinks(isParent); + removeActiveClass('nav-link', 'active'); + document.getElementById('participantVerificationBtn').classList.add('active'); + mainContent.innerHTML = renderDataCorrectionsSelectionContent(participant); + handleSurveyTypeChange(participant); + handleBackToToolSelect(); + clearSurveySelection(); + submitSurveyStatusReset(); + disableSubmitButton(); + setActiveDataCorrectionsTab(); + } +}; + +const renderDataCorrectionsSelectionContent = (participant) => { + return ` +
+
+ ${renderParticipantHeader(participant)} + ${displayDataCorrectionsNavbar()} + + +
+
+
+

Data Corrections Tool

+

+ Note: This tool should only be used to make corrections to participant data post-verification. All changes need to be approved by the CCC before being applied to the participant record via this tool. +

+
+
+ +
+
+

Survey Status Reset Tool

+

+ Please select the survey to be reset below. A survey reset means that the survey data and answers will be permanently deleted, and the survey status flag will be reset to 'not started'. +

+
+

Update Survey:

+
+ + +
+
+

Current Survey Status:

+

Survey Name:

+

Survey Status:

+

+
+
+ +
+
+
+
+ + +
+
+ +
+
+
+
+
+
+ + + + `; +}; + + +const handleSurveyTypeChange = (participant) => { + const surveyDropdown = document.getElementById('dropdownSurveyMenu'); + const dropdownSurveyOptions = surveyDropdown?.children; + const selectButton = document.querySelector('.selectButton'); + if (!surveyDropdown || !dropdownSurveyOptions || !selectButton) return; + + const participantConnectId = participant['Connect_ID']; + const { ssnStatusFlag, notStarted } = fieldMapping; + let query; + + for (let option of dropdownSurveyOptions) { + option.addEventListener('click', async (e) => { // TODO: Add gaurd to prevent multiple event listeners from being added + selectedSurvey = Number(e.target.dataset.survey); + + if (selectedSurvey === ssnStatusFlag) { + selectButton.textContent = e.target.textContent; + try { + query = `connectId=${participantConnectId}` + showAnimation(); + const response = await findParticipant(query); + hideAnimation(); + const participantData = response.data[0]; + localStorage.setItem('participant', JSON.stringify(participantData)); + + if (participantData[ssnStatusFlag] === notStarted) { + displayAlreadyResetNote(); + disableSubmitButton(); + } else { + removeAlreadyResetNote(); + enableSubmitButton(); + } + updateSurveyStatusTextContent(participantData, selectedSurvey); + + } catch (error) { + console.error(`Failed to fetch participant data for Connect ID ${participantConnectId}: `, error); + } + } else { + selectButton.textContent = e.target.textContent; + selectedSurvey = null; + updateSurveyStatusTextContent(participant, selectedSurvey); + removeAlreadyResetNote(); + } + }); + } +}; + +const updateSurveyStatusTextContent = (participant, selectedSurvey, statusCode) => { + const surveyNameElement = document.getElementById('surveyNameText'); + const surveyStatusElement = document.getElementById('surveyStatusText'); + const { surveyStatus, ssnStatusFlag } = fieldMapping; + + const participantSurveyStatus = { + [ssnStatusFlag]: participant[ssnStatusFlag], + }; + + if (selectedSurvey === ssnStatusFlag) { + surveyNameElement.textContent = 'Survey Name: SSN Survey'; + surveyStatusElement.textContent = `Survey Status: ${statusMapping[participantSurveyStatus[ssnStatusFlag]] || ''} `; + + if (statusCode === 200) { + surveyStatusElement.textContent = `Survey Status: ${statusMapping[surveyStatus["notStarted"]]}`; + } + } else if (selectedSurvey === null) { + surveyNameElement.textContent = 'Survey Name: '; + surveyStatusElement.textContent = 'Survey Status: '; + disableSubmitButton(); + } +}; + +/** + * Clears the survey status selection back to default and clears the text content + */ +const clearSurveySelection = () => { + const clearButton = document.getElementById('clearSurveySelect'); + if (!clearButton) return; + + clearButton.addEventListener('click', () => { + const surveyNameElement = document.getElementById('surveyNameText'); + const surveyStatusElement = document.getElementById('surveyStatusText'); + const selectButton = document.querySelector('.selectButton'); + const submitButton = document.getElementById('submitButton'); + if (!surveyNameElement || !surveyStatusElement || !selectButton || !submitButton) return; + + surveyNameElement.textContent = 'Survey Name: '; + surveyStatusElement.textContent = 'Survey Status: '; + selectButton.textContent = 'Select'; + selectedSurvey = null; + disableSubmitButton(); + removeAlreadyResetNote(); + }); +}; + +const submitSurveyStatusReset = () => { + const submitButton = document.getElementById('submitButton'); + const confirmResetButton = document.getElementById('confirmResetButton'); + const surveyStatusElement = document.getElementById('surveyStatusText'); + if (!submitButton || !confirmResetButton || !surveyStatusElement) return; + + submitButton.addEventListener('click', async () => { + if (selectedSurvey === null) return; + setupModalContent(selectedSurvey); + }); + + if (confirmResetButton) { + confirmResetButton.addEventListener('click', async () => { + try { + const response = await resetParticipantSurvey(selectedSurvey); + + if (response.code === 200 || response.data) { + localStorage.setItem('participant', JSON.stringify(response.data)); + updateSurveyStatusTextContent(response.data, selectedSurvey, response.code); + displayAlreadyResetNote(); + triggerNotificationBanner("Survey has been successfully reset!", "success", 10000); + disableSubmitButton(); + } + } catch (error) { + console.error(`Failed to reset survey: ${error.message}`); + triggerNotificationBanner(`${error.message}`, "danger", 10000); + } + }); + } +}; + +const displayAlreadyResetNote = () => { + const isSurveyAlreadyResetNote = document.getElementById('isSurveyAlreadyResetNote'); + if (!isSurveyAlreadyResetNote) return; + isSurveyAlreadyResetNote.innerHTML = ` The survey is "Not Started". There is no survey data to be reset.`; +} + +const removeAlreadyResetNote = () => { + const isSurveyAlreadyResetNote = document.getElementById('isSurveyAlreadyResetNote'); + if (!isSurveyAlreadyResetNote) return; + isSurveyAlreadyResetNote.innerHTML = ``; +} + +const setupModalContent = (survey) => { + const modalBody = document.querySelector('.modal-body'); + if (!modalBody) return; + modalBody.textContent = surveyModalBody[survey]; +} + +const disableSubmitButton = () => { + const submitButton = document.getElementById('submitButton'); + if (!submitButton) return; + submitButton.disabled = true; +} + +const enableSubmitButton = () => { + const submitButton = document.getElementById('submitButton'); + if (!submitButton) return; + submitButton.disabled = false; +} + +/** + * Reset the participant survey status for the selected survey + * @param {number} selectedSurvey - the survey to reset, Ex. selectedSurvey = ssnStatusFlag (126331570) + * @param {object} participant - the participant object + * @returns {Promise} - the updated participant object + * +*/ +const resetParticipantSurvey = async (selectedSurvey) => { + const participant = JSON.parse(localStorage.getItem('participant')); + const connectId = participant['Connect_ID']; + + try { + const idToken = await getIdToken(); + const response = await fetch(`${baseAPI}/dashboard?api=resetParticipantSurvey`, { + method: "POST", + headers: { + Authorization: "Bearer " + idToken, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + connectId: connectId, + survey: selectedSurvey + }), + }); + if (!response.ok) { + const error = (response.status + " Error" + ": " + (await response.json()).message); + throw new Error(error); + } + + return await response.json(); + } catch (error) { + console.error("Failed to reset participant survey: ", error); + throw error; + } +}; \ No newline at end of file diff --git a/src/dataCorrectionsTool.js b/src/dataCorrectionsTool/verificationCorrectionsTool.js similarity index 90% rename from src/dataCorrectionsTool.js rename to src/dataCorrectionsTool/verificationCorrectionsTool.js index 6a2f323..d4411ba 100644 --- a/src/dataCorrectionsTool.js +++ b/src/dataCorrectionsTool/verificationCorrectionsTool.js @@ -1,36 +1,40 @@ -import fieldMapping from './fieldToConceptIdMapping.js'; -import { dashboardNavBarLinks, removeActiveClass } from './navigationBar.js'; -import { showAnimation, hideAnimation, baseAPI, getIdToken, getDataAttributes, triggerNotificationBanner } from './utils.js'; -import { renderParticipantHeader } from './participantHeader.js'; -import { keyToVerificationStatus, keyToDuplicateType, recruitmentType, updateRecruitmentType } from './idsToName.js'; -import { appState } from './stateManager.js'; -import { findParticipant } from './participantLookup.js'; +import fieldMapping from '../fieldToConceptIdMapping.js'; +import { dashboardNavBarLinks, removeActiveClass } from '../navigationBar.js'; +import { showAnimation, hideAnimation, baseAPI, getIdToken, getDataAttributes, triggerNotificationBanner } from '../utils.js'; +import { renderParticipantHeader } from '../participantHeader.js'; +import { keyToVerificationStatus, keyToDuplicateType, recruitmentType, updateRecruitmentType } from '../idsToName.js'; +import { appState } from '../stateManager.js'; +import { findParticipant } from '../participantLookup.js'; +import { handleBackToToolSelect, displayDataCorrectionsNavbar, setActiveDataCorrectionsTab } from './dataCorrectionsHelpers.js'; -export const renderDataCorrectionsToolPage = (participant) => { +export const setupVerificationCorrectionsPage = (participant) => { if (participant !== undefined) { const isParent = localStorage.getItem('isParent') document.getElementById('navBarLinks').innerHTML = dashboardNavBarLinks(isParent); removeActiveClass('nav-link', 'active'); document.getElementById('participantVerificationBtn').classList.add('active'); - mainContent.innerHTML = renderVerificationTool(participant); + mainContent.innerHTML = renderVerificationCorrections(participant); let selectedResponse = {}; dropdownTrigger('dropdownVerification', 'dropdownMenuButtonVerificationOptns', selectedResponse); dropdownTrigger('dropdownDuplicateType', 'dropdownMenuButtonDuplicateTypeOptns',selectedResponse); dropdownTrigger('dropdownUpdateRecruitType', 'dropdownMenuButtonUpdateRecruitTypeOptns', selectedResponse); viewOptionsSelected(participant); resetChanges(participant); + handleBackToToolSelect(); + setActiveDataCorrectionsTab(); } } -export const renderVerificationTool = (participant) => { +export const renderVerificationCorrections = (participant) => { let template = ``; template = `
${renderParticipantHeader(participant)} -
-
+ ${displayDataCorrectionsNavbar()} +
+

Data Corrections Tool

Note: This tool should only be used to make corrections to participant data post-verification. @@ -92,17 +96,21 @@ export const renderVerificationTool = (participant) => {
-
- - -
+
+
+ + +
+
+ +
+
- ` - + template += `
Agreement Consent Signed${participant[fieldMapping.consentDate] && humanReadableMDY(participant[fieldMapping.consentDate])}${participant[fieldMapping.consentDate] && formatUTCDate(participant[fieldMapping.consentDate])} ${participant[fieldMapping.consentVersion]} N/A Download LinkAgreement HIPAA Signed${participant[fieldMapping.hippaDate] && humanReadableMDY(participant[fieldMapping.hippaDate])}${participant[fieldMapping.hippaDate] && formatUTCDate(participant[fieldMapping.hippaDate])} ${participant[fieldMapping.hipaaVersion]} N/A Download LinkAgreement HIPAA Revoc Form Signed${(participant[fieldMapping.dateHIPAARevoc] !== undefined) ? humanReadableMDY(participant[fieldMapping.dateHIPAARevoc]) : `N/A`}${(participant[fieldMapping.dateHIPAARevoc] !== undefined) ? formatUTCDate(participant[fieldMapping.dateHIPAARevoc]) : `N/A`} ${(participant[fieldMapping.versionHIPAARevoc] !== undefined) ? participant[fieldMapping.versionHIPAARevoc] : `N/A`} N/A Download LinkAgreement HIPAA Revoc Form Not Signed${(participant[fieldMapping.dateHIPAARevoc] !== undefined) ? humanReadableMDY(participant[fieldMapping.dateHIPAARevoc]) : `N/A`}${(participant[fieldMapping.dateHIPAARevoc] !== undefined) ? formatUTCDate(participant[fieldMapping.dateHIPAARevoc]) : `N/A`} ${(participant[fieldMapping.versionHIPAARevoc] !== undefined) ? participant[fieldMapping.versionHIPAARevoc] : `N/A`} N/A Download LinkAgreement Data Destroy Form Signed${(participant[fieldMapping.dateDataDestroy] !== undefined) ? humanReadableMDY(participant[fieldMapping.dateDataDestroy]) : `N/A`}${(participant[fieldMapping.dateDataDestroy] !== undefined) ? formatUTCDate(participant[fieldMapping.dateDataDestroy]) : `N/A`} ${(participant[fieldMapping.versionDataDestroy] !== undefined) ? participant[fieldMapping.versionDataDestroy] : `N/A` } N/A Download LinkAgreement Data Destroy Form Not Signed${(participant[fieldMapping.dateDataDestroy] !== undefined) ? humanReadableMDY(participant[fieldMapping.dateDataDestroy]) : `N/A`}${(participant[fieldMapping.dateDataDestroy] !== undefined) ? formatUTCDate(participant[fieldMapping.dateDataDestroy]) : `N/A`} ${(participant[fieldMapping.versionDataDestroy] !== undefined) ? participant[fieldMapping.versionDataDestroy] : `N/A` } N/A Download Link