diff --git a/src/api/src/routes/admin-participant-router.ts b/src/api/src/routes/admin-participant-router.ts index ff6b7d6..aa59ea9 100644 --- a/src/api/src/routes/admin-participant-router.ts +++ b/src/api/src/routes/admin-participant-router.ts @@ -105,7 +105,7 @@ adminParticipantRouter.delete("/:SID/stale", async (req: Request, res: Response) res.json({ data: {} }); }); -function makeToken(prefix: string) { +export function makeToken(prefix: string) { const chars = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890"; const randomArray = Array.from({ length: 64 }, (v, k) => chars[Math.floor(Math.random() * chars.length)]); diff --git a/src/api/src/routes/survey-router.ts b/src/api/src/routes/survey-router.ts index 77a3e55..c7edd86 100644 --- a/src/api/src/routes/survey-router.ts +++ b/src/api/src/routes/survey-router.ts @@ -3,6 +3,7 @@ import { ReturnValidationErrors } from "../middleware"; import { param } from "express-validator"; import * as knex from "knex"; import { DB_CONFIG, DB_SCHEMA } from "../config"; +import { makeToken } from "./admin-participant-router"; export const surveyRouter = express.Router(); @@ -100,6 +101,85 @@ surveyRouter.get( } ); +surveyRouter.get( + "/:token/manual", + [param("token").notEmpty()], + ReturnValidationErrors, + async (req: Request, res: Response) => { + const db = knex.knex(DB_CONFIG); + let { token } = req.params; + + token = token.substring(0, 64); + + let participant = await db("PARTICIPANT") + .withSchema(DB_SCHEMA) + .join("PARTICIPANT_DATA", "PARTICIPANT.TOKEN", "PARTICIPANT_DATA.TOKEN") + .where({ "PARTICIPANT.TOKEN": token }) + .whereNotNull("EMAIL") + .select("PARTICIPANT.*") + .first() + .then((r) => r) + .catch((err) => { + console.log("DATABASE CONNECTION ERROR", err); + res.status(500).send(err); + }); + + if (participant) { + let survey = await db("SURVEY").withSchema(DB_SCHEMA).where({ SID: participant.SID }).first(); + let choices = await db("JSON_DEF").withSchema(DB_SCHEMA).where({ SID: participant.SID }); + let questions = await db("QUESTION").withSchema(DB_SCHEMA).where({ SID: participant.SID }).orderBy("ORD"); + + for (let choice of choices) { + choice.choices = JSON.parse(choice.SELECTION_JSON); + } + + for (let q of questions) { + q.conditions = await db("Q_CONDITION_TBL").withSchema(DB_SCHEMA).where({ QID: q.QID }); + if (q.JSON_ID) { + q.choices = choices.find((c) => c.JSON_ID == q.JSON_ID)?.choices; + } + } + + return res.json({ data: { survey, questions } }); + } + + res.status(404).send(); + } +); + +surveyRouter.get( + "/:token/manual-entry", + [param("token").notEmpty()], + ReturnValidationErrors, + async (req: Request, res: Response) => { + const db = knex.knex(DB_CONFIG); + let { token } = req.params; + + let survey = await db("SURVEY").withSchema(DB_SCHEMA).where({ SID: token }).first(); + let choices = await db("JSON_DEF").withSchema(DB_SCHEMA).where({ SID: token }); + + for (let choice of choices) { + choice.choices = JSON.parse(choice.SELECTION_JSON); + } + + if (survey) { + let questions = await db("QUESTION").withSchema(DB_SCHEMA).where({ SID: token }).orderBy("ORD"); + + for (let q of questions) { + q.conditions = await db("Q_CONDITION_TBL").withSchema(DB_SCHEMA).where({ QID: q.QID }); + if (q.JSON_ID) { + q.choices = choices.find((c) => c.JSON_ID == q.JSON_ID)?.choices; + } + } + + const demographics = await db("SURVEY_DEMOGRAPHIC").withSchema(DB_SCHEMA).where({ SID: token }); + + return res.json({ data: { survey, questions, demographics } }); + } + res.status(404).send(); + } +); + surveyRouter.get( "/:token/preview", [param("token").notEmpty()], @@ -198,6 +278,73 @@ surveyRouter.post( } ); +surveyRouter.post( + "/:agentEmail/manual/:surveyId", + [param("surveyId").notEmpty()], + ReturnValidationErrors, + async (req: Request, res: Response) => { + const db = knex.knex(DB_CONFIG); + let { surveyId, agentEmail } = req.params; + let { questions, participant, demographics } = req.body; + + const token = makeToken("ME"); + + let existingAddresses = await db("PARTICIPANT_DATA") + .withSchema(DB_SCHEMA) + .innerJoin("PARTICIPANT", "PARTICIPANT_DATA.TOKEN", "PARTICIPANT.TOKEN") + .where("PARTICIPANT.SID", surveyId) + .whereNotNull("EMAIL") + .select("EMAIL"); + + let existingList = existingAddresses.map((e) => e.EMAIL); + if (existingList.includes(participant)) { + return res + .status(400) + .json({ messages: [{ variant: "error", text: "This email address has already been used for this survey." }] }); + } + + await db("PARTICIPANT").withSchema(DB_SCHEMA).insert({ TOKEN: token, SID: surveyId, CREATE_DATE: new Date() }); + await db("PARTICIPANT_DATA").withSchema(DB_SCHEMA).insert({ TOKEN: token, EMAIL: null, RESPONSE_DATE: new Date() }); + + for (let question of questions) { + let id = question.QID; + let answer = question.answer; + let answer_text = question.answer_text; + + let ans: any = { + TOKEN: token, + QID: id, + }; + + let nvalTest = parseFloat(`${ans.NVALUE}`); + + if (isFinite(nvalTest)) ans.NVALUE = answer; + else if (Array.isArray(answer)) ans.TVALUE = JSON.stringify(answer); + else ans.TVALUE = answer; + + if (answer_text && answer_text.length > 0) { + ans.TVALUE = answer_text; + } + + await db("RESPONSE_LINE").withSchema(DB_SCHEMA).insert(ans); + } + + if (demographics) { + for (let demo of demographics) { + await db("PARTICIPANT_DEMOGRAPHIC") + .withSchema(DB_SCHEMA) + .insert({ TOKEN: token, DEMOGRAPHIC: demo.DEMOGRAPHIC, TVALUE: demo.TVALUE }); + } + } + + await db("ONBEHALF_AUDIT") + .withSchema(DB_SCHEMA) + .insert({ TOKEN: token, ONBEHALF_USER: agentEmail, DATE_FILLED: new Date() }); + + return res.json({ data: {}, messages: [{ variant: "success" }] }); + } +); + surveyRouter.post( "/:token", [param("token").notEmpty()], diff --git a/src/web/src/modules/administration/modules/participants/views/ParticipantList.vue b/src/web/src/modules/administration/modules/participants/views/ParticipantList.vue index e836947..066a9bd 100644 --- a/src/web/src/modules/administration/modules/participants/views/ParticipantList.vue +++ b/src/web/src/modules/administration/modules/participants/views/ParticipantList.vue @@ -41,6 +41,7 @@ + Manual Entry Add Participants @@ -66,6 +67,12 @@ color="warning" size="x-small" @click="emailClick(item.TOKEN)"> + @@ -171,7 +178,7 @@ export default { { title: "Sent Date", key: "SENT_DATE" }, { title: "Resent Date", key: "RESENT_DATE" }, { title: "Response Date", key: "RESPONSE_DATE" }, - { title: "", key: "email", width: "60px" }, + { title: "", key: "email", width: "100px" }, ], search: "", parseMessage: "", @@ -324,6 +331,9 @@ export default { () => {} ); }, + async manualEntryClick(token: string) { + window.open(`/survey-manual/${token}`); + }, formatDate(input: any) { if (input) return moment(input).format("YYYY-MM-DD @ hh:mm a"); return ""; @@ -338,6 +348,10 @@ export default { closeEditor() { this.visible = false; }, + openManualEntry() { + console.log(this.batch.survey); + window.open(`/manual-entry/${this.batch.survey}`); + }, }, }; diff --git a/src/web/src/routes.ts b/src/web/src/routes.ts index db1f9c5..f037bec 100644 --- a/src/web/src/routes.ts +++ b/src/web/src/routes.ts @@ -29,6 +29,16 @@ const routes: Array = [ component: () => import("@/views/AuthenticatedSurvey.vue"), beforeEnter: requireLogin, }, + { + path: "/survey-manual/:token", + component: () => import("@/views/AuthenticatedManualSurvey.vue"), + beforeEnter: requireLogin, + }, + { + path: "/manual-entry/:token", + component: () => import("@/views/AuthenticatedFullManualSurvey.vue"), + beforeEnter: requireLogin, + }, { path: "/preview/:token", component: () => import("@/views/Preview.vue"), diff --git a/src/web/src/store/SurveyStore.ts b/src/web/src/store/SurveyStore.ts index d869071..7274422 100644 --- a/src/web/src/store/SurveyStore.ts +++ b/src/web/src/store/SurveyStore.ts @@ -39,6 +39,34 @@ export const useSurveyStore = defineStore("survey", { }); }, + async loadManualSurvey(id: any) { + this.isLoading = true; + let api = useApiStore(); + + await api + .call("get", `${SURVEY_URL}/${id}/manual`) + .then((resp) => { + this.setSurvey(resp.data); + }) + .finally(() => { + this.isLoading = false; + }); + }, + + async loadFullManualSurvey(id: any) { + this.isLoading = true; + let api = useApiStore(); + + await api + .call("get", `${SURVEY_URL}/${id}/manual-entry`) + .then((resp) => { + this.setSurvey(resp.data); + }) + .finally(() => { + this.isLoading = false; + }); + }, + async loadSurveyPreview(id: any) { this.isLoading = true; let api = useApiStore(); diff --git a/src/web/src/views/AuthenticatedFullManualSurvey.vue b/src/web/src/views/AuthenticatedFullManualSurvey.vue new file mode 100644 index 0000000..ee3aea6 --- /dev/null +++ b/src/web/src/views/AuthenticatedFullManualSurvey.vue @@ -0,0 +1,107 @@ + + + diff --git a/src/web/src/views/AuthenticatedManualSurvey.vue b/src/web/src/views/AuthenticatedManualSurvey.vue new file mode 100644 index 0000000..cf756b4 --- /dev/null +++ b/src/web/src/views/AuthenticatedManualSurvey.vue @@ -0,0 +1,81 @@ + + +