Skip to content

Commit

Permalink
Manual Entry!
Browse files Browse the repository at this point in the history
  • Loading branch information
datajohnson committed Dec 15, 2024
1 parent 6e7a673 commit 4316fac
Show file tree
Hide file tree
Showing 7 changed files with 389 additions and 2 deletions.
2 changes: 1 addition & 1 deletion src/api/src/routes/admin-participant-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)]);

Expand Down
147 changes: 147 additions & 0 deletions src/api/src/routes/survey-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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()],
Expand Down Expand Up @@ -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()],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
</template>
<v-row>
<v-col>
<v-btn @click="openManualEntry" color="warning" variant="tonal" :disabled="!batch.survey">Manual Entry</v-btn>
<v-btn @click="openEditor" color="primary" variant="tonal" class="float-right" :disabled="!batch.survey"
>Add Participants</v-btn
>
Expand All @@ -66,6 +67,12 @@
color="warning"
size="x-small"
@click="emailClick(item.TOKEN)"></v-btn>
<v-btn
icon="mdi-format-list-checkbox"
variant="tonal"
color="warning"
size="x-small"
@click="manualEntryClick(item.TOKEN)"></v-btn>
</div>
</template>

Expand Down Expand Up @@ -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: "",
Expand Down Expand Up @@ -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 "";
Expand All @@ -338,6 +348,10 @@ export default {
closeEditor() {
this.visible = false;
},
openManualEntry() {
console.log(this.batch.survey);
window.open(`/manual-entry/${this.batch.survey}`);
},
},
};
</script>
10 changes: 10 additions & 0 deletions src/web/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ const routes: Array<RouteRecordRaw> = [
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"),
Expand Down
28 changes: 28 additions & 0 deletions src/web/src/store/SurveyStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
107 changes: 107 additions & 0 deletions src/web/src/views/AuthenticatedFullManualSurvey.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<template>
<div class="hello" v-if="survey && survey.survey">
<!-- SurveyHeader v-model="moveOn" /> -->
<h1>Survey Response Manual Entry</h1>
<v-card class="default">
<v-card-title>Participant Information</v-card-title>
<v-card-text>
<v-text-field label="Participant email" v-model="participantEmail" />

<div v-if="survey.demographics && survey.demographics.length > 0">
<h4 class="mb-3">Demographics</h4>

<div v-for="demographic of survey.demographics">
<v-text-field :label="demographic.DEMOGRAPHIC" v-model="demographic.TVALUE" />
</div>
</div>
</v-card-text>
</v-card>

<v-divider class="my-4" />

<!-- <div v-if="moveOn"> -->
<QuestionsRenderer v-model="allValid" />

<v-btn color="primary" :disabled="!allValid" @click="submitSurvey"> Submit </v-btn>

<span style="font-size: 0.9rem" class="pl-4 text-error" v-if="!allValid">
* Not all required questions have answers (look for the red asterisks next to the question)
</span>
<!-- </div> -->
<Notifications ref="notify"></Notifications>
</div>
</template>

<script>
import axios from "axios";
import { clone } from "lodash";
import { mapActions, mapState } from "pinia";
import { AuthHelper } from "@/plugins/auth";
import { useSurveyStore } from "@/store/SurveyStore";
import { SURVEY_URL } from "@/urls";
export default {
name: "Login",
data: () => ({
surveyId: "",
allValid: false,
participantEmail: "",
}),
computed: {
...mapState(useSurveyStore, ["survey"]),
allValidAndEmail() {
return this.allValid && this.participantEmail;
},
},
mounted() {
this.surveyId = this.$route.params.token;
this.loadFullManualSurvey(this.surveyId).catch((msg) => {
console.log("ERROR ON SURVEY GET: ", msg);
this.$router.push(`/survey/not-found`);
});
},
methods: {
...mapActions(useSurveyStore, ["loadFullManualSurvey"]),
submitSurvey() {
if (this.allValidAndEmail) {
let qs = [];
for (let sq of this.survey.questions) {
let q = clone(sq);
delete q.ASK;
delete q.RANGE;
delete q.SELECTION_JSON;
delete q.OPTIONAL;
delete q.ORD;
delete q.SID;
delete q.TYPE;
qs.push(q);
}
let agentEmail = AuthHelper.user.value?.email;
axios
.post(`${SURVEY_URL}/${agentEmail}/manual/${this.surveyId}`, {
questions: qs,
participant: this.participantEmail,
demographics: this.survey.demographics,
})
.then(() => {
this.$refs.notify.showSuccess("Survey submitted successfully");
this.participantEmail = "";
this.loadFullManualSurvey(this.surveyId);
})
.catch((msg) => {
this.$refs.notify.showError(msg.response.data);
console.log("ERROR", msg);
});
} else {
this.$refs.notify.showError("Please fill out all required fields including the Participant email");
}
},
},
};
</script>
Loading

0 comments on commit 4316fac

Please sign in to comment.