Skip to content

Commit

Permalink
feat(survey): redirect prompt
Browse files Browse the repository at this point in the history
  • Loading branch information
lukashroch committed May 18, 2023
1 parent 9f58831 commit bc13a7c
Show file tree
Hide file tree
Showing 19 changed files with 296 additions and 84 deletions.
14 changes: 10 additions & 4 deletions apps/admin/src/components/prompts/standard/redirect-prompt.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-select
<v-combobox
hide-details="auto"
:hint="$t('survey-schemes.prompts.redirect-prompt.identifier.hint')"
:items="identifiers"
:label="$t('survey-schemes.prompts.redirect-prompt.identifier._')"
outlined
persistent-hint
:value="identifier"
@change="update('identifier', $event)"
></v-select>
@change="updateIdentifier"
></v-combobox>
</v-col>
</v-row>
</v-card-text>
Expand Down Expand Up @@ -83,7 +85,7 @@ export default defineComponent({
data() {
return {
identifiers: ['userId', 'username', 'token', 'custom'].map((value) => ({
identifiers: ['userId', 'username', 'urlAuthToken'].map((value) => ({
text: this.$t(`survey-schemes.prompts.redirect-prompt.identifier.options.${value}`),
value,
})),
Expand All @@ -104,6 +106,10 @@ export default defineComponent({
const timerValue = parseInt(value, 10);
this.update('timer', Number.isNaN(timerValue) ? 0 : timerValue);
},
updateIdentifier(value: string | null | (typeof this.identifiers)[0]) {
this.update('identifier', !value || typeof value === 'string' ? value : value.value);
},
},
});
</script>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import type { SurveyState as SurveyStatus } from '@intake24/common/surveys';
import type { SurveyState } from '@intake24/common/types';
import type {
SurveyEntryResponse,
SurveyFollowUpResponse,
SurveyRequestHelpInput,
SurveyUserInfoResponse,
SurveyUserSessionResponse,
Expand Down Expand Up @@ -182,7 +181,7 @@ const surveyRespondentController = ({

const submission = async (
req: Request<{ slug: string }, any, { submission: SurveyState }, { tzOffset: number }>,
res: Response<SurveyFollowUpResponse>
res: Response<SurveyUserInfoResponse>
): Promise<void> => {
const {
headers: { 'user-agent': userAgent },
Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/services/survey/survey-submission.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
SurveyState,
WithKey,
} from '@intake24/common/types';
import type { SurveyFollowUpResponse } from '@intake24/common/types/http';
import type { SurveyUserInfoResponse } from '@intake24/common/types/http';
import type {
SurveySubmissionFieldCreationAttributes,
SurveySubmissionFoodCreationAttributes,
Expand Down Expand Up @@ -330,14 +330,14 @@ const surveySubmissionService = ({
* @param {User} user
* @param {SurveyState} state
* @param {number} tzOffset
* @returns {Promise<SurveyFollowUpResponse>}
* @returns {Promise<SurveyUserInfoResponse>}
*/
const submit = async (
slug: string,
user: User,
state: SurveyState,
tzOffset: number
): Promise<SurveyFollowUpResponse> => {
): Promise<SurveyUserInfoResponse> => {
const survey = await Survey.findOne({
where: { slug },
include: [{ association: 'surveyScheme', required: true }],
Expand Down
57 changes: 28 additions & 29 deletions apps/api/src/services/survey/survey.service.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { URL } from 'node:url';

import { addDays, addMinutes, startOfDay } from 'date-fns';
import ms from 'ms';

import type { IoC } from '@intake24/api/ioc';
import type { Prompts } from '@intake24/common/prompts';
import type { JobParams, SurveyState } from '@intake24/common/types';
import type {
CreateUserResponse,
SurveyFollowUpResponse,
SurveyUserInfoResponse,
} from '@intake24/common/types/http';
import type { FindOptions, SubmissionScope } from '@intake24/db';
import type { CreateUserResponse, SurveyUserInfoResponse } from '@intake24/common/types/http';
import type { FindOptions, Includeable, SubmissionScope } from '@intake24/db';
import { ApplicationError, ForbiddenError, NotFoundError } from '@intake24/api/http/errors';
import { jwt } from '@intake24/api/util';
import { randomString } from '@intake24/common/util';
Expand All @@ -31,8 +29,11 @@ export type RespondentWithPassword = {

const surveyService = ({
adminSurveyService,
logger: globalLogger,
scheduler,
}: Pick<IoC, 'adminSurveyService' | 'scheduler'>) => {
}: Pick<IoC, 'adminSurveyService' | 'logger' | 'scheduler'>) => {
const logger = globalLogger.child({ service: 'SurveyService' });

/**
* Generate random survey respondent
*
Expand Down Expand Up @@ -89,9 +90,6 @@ const surveyService = ({
if (!decoded || Object.prototype.toString.call(decoded) !== '[object Object]')
throw new ApplicationError('Malformed token payload');

if (!decoded || typeof decoded === 'string')
throw new ApplicationError('Malformed token payload');

const { user: username, redirect } = decoded;
if (!username || typeof username !== 'string')
throw new ApplicationError('Missing claim: user');
Expand Down Expand Up @@ -271,48 +269,49 @@ const surveyService = ({
);
if (!redirectPrompt) return null;

const user = await User.findByPk(userId, {
include: [
{ association: 'aliases', where: { surveyId }, required: false },
{ association: 'customFields', where: { name: 'redirect url' }, required: false },
],
});
const { identifier, url } = redirectPrompt as Prompts['redirect-prompt'];

const include: Includeable[] = [
{ association: 'aliases', where: { surveyId }, required: false },
];
if (identifier)
include.push({ association: 'customFields', where: { name: identifier }, required: false });

const user = await User.findByPk(userId, { include });

if (!user) throw new NotFoundError();
const { aliases = [], customFields = [] } = user;

const { identifier, url } = redirectPrompt as Prompts['redirect-prompt'];

let identifierValue: string | null;

switch (identifier) {
case 'userId':
identifierValue = user.id;
break;
case 'username':
identifierValue = aliases.length ? aliases[0].username : null;
break;
case 'token':
identifierValue = aliases.length ? aliases[0].urlAuthToken : null;
break;
case 'custom':
identifierValue = customFields.length ? customFields[0].value : null;
case 'urlAuthToken':
identifierValue = aliases.length ? aliases[0][identifier] : null;
break;
default:
identifierValue = null;
identifierValue = customFields.length ? customFields[0].value : null;
break;
}

if (!identifierValue) return null;
if (!identifierValue || !url) return null;

return url?.replace('{identifier}', identifierValue) ?? null;
try {
return new URL(url.replace('{identifier}', identifierValue)).href;
} catch (err) {
logger.error('Invalid follow-up URL', { err });
return null;
}
};

const followUp = async (
slug: string,
user: User,
tzOffset: number
): Promise<SurveyFollowUpResponse> => {
): Promise<SurveyUserInfoResponse> => {
const survey = await Survey.findOne({
where: { slug },
include: [{ association: 'surveyScheme', required: true }],
Expand Down
26 changes: 10 additions & 16 deletions apps/survey/src/components/handlers/standard/FinalPromptHandler.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
:is="prompt.component"
:key="prompt.id"
v-bind="{
canShowFeedback,
canRestart,
showFeedback,
prompt,
surveyId,
}"
Expand All @@ -17,13 +16,13 @@ import type { PropType } from 'vue';
import { defineComponent, ref } from 'vue';
import type { Prompts } from '@intake24/common/prompts';
import { FinalPrompt /*RedirectPrompt*/ } from '@intake24/survey/components/prompts/standard';
import { FinalPrompt } from '@intake24/survey/components/prompts/standard';
import { useSurvey } from '@intake24/survey/stores';
export default defineComponent({
name: 'FinalPromptHandler',
components: { FinalPrompt /*, RedirectPrompt */ },
components: { FinalPrompt },
props: {
prompt: {
Expand All @@ -34,28 +33,23 @@ export default defineComponent({
emits: ['action'],
setup() {
setup(props, { emit }) {
const { user } = useSurvey();
const canRestart = ref(
!user?.maximumDailySubmissionsReached && !user?.maximumTotalSubmissionsReached
);
const canShowFeedback = ref(user?.showFeedback);
const showFeedback = ref(user?.showFeedback);
return { canRestart, canShowFeedback };
const action = (type: string, id?: string) => {
emit('action', type, id);
};
return { action, showFeedback };
},
computed: {
surveyId(): string {
return this.$route.params.surveyId;
},
},
methods: {
action(type: string, id?: string) {
this.$emit('action', type, id);
},
},
});
</script>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<template>
<component
:is="prompt.component"
:key="prompt.id"
v-bind="{
followUpUrl,
showFeedback,
prompt,
surveyId,
}"
@action="action"
></component>
</template>

<script lang="ts">
import type { PropType } from 'vue';
import { defineComponent, ref } from 'vue';
import type { Prompts } from '@intake24/common/prompts';
import { RedirectPrompt } from '@intake24/survey/components/prompts/standard';
import { useSurvey } from '@intake24/survey/stores';
export default defineComponent({
name: 'FinalPromptHandler',
components: { RedirectPrompt },
props: {
prompt: {
type: Object as PropType<Prompts['redirect-prompt']>,
required: true,
},
},
emits: ['action'],
setup(props, { emit }) {
const { user } = useSurvey();
const showFeedback = ref(user?.showFeedback);
const followUpUrl = ref(user?.followUpUrl);
const action = (type: string, id?: string) => {
emit('action', type, id);
};
return { action, followUpUrl, showFeedback };
},
computed: {
surveyId(): string {
return this.$route.params.surveyId;
},
},
});
</script>

<style scoped></style>
2 changes: 2 additions & 0 deletions apps/survey/src/components/handlers/standard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import MealAddPromptHandler from './MealAddPromptHandler.vue';
import MealGapPromptHandler from './MealGapPromptHandler.vue';
import MealTimePromptHandler from './MealTimePromptHandler.vue';
import ReadyMealPromptHandler from './ReadyMealPromptHandler.vue';
import RedirectPromptHandler from './RedirectPromptHandler.vue';
import ReviewConfirmPromptHandler from './ReviewConfirmPromptHandler.vue';
import SameAsBeforePromptHandler from './SameAsBeforePromptHandler.vue';
import SplitFoodPromptHandler from './SplitFoodPromptHandler.vue';
Expand All @@ -20,6 +21,7 @@ export default {
MealGapPromptHandler,
MealTimePromptHandler,
ReadyMealPromptHandler,
RedirectPromptHandler,
ReviewConfirmPromptHandler,
SameAsBeforePromptHandler,
SplitFoodPromptHandler,
Expand Down
22 changes: 11 additions & 11 deletions apps/survey/src/components/prompts/standard/FinalPrompt.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@
class="px-4"
color="secondary"
large
outlined
:to="{ name: 'survey-home', params: { surveyId } }"
>
<v-icon left>$home</v-icon>
{{ $t('common.home') }}
</v-btn>
<v-btn
v-if="canShowFeedback"
v-if="showFeedback"
class="px-4"
color="secondary"
large
outlined
:to="{ name: 'feedback-home', params: { surveyId } }"
>
<v-icon left>$feedback</v-icon>
Expand All @@ -23,13 +25,15 @@
</template>
<template #nav-actions>
<v-btn :to="{ name: 'survey-home', params: { surveyId } }">
<span>{{ $t('common.home') }}</span>
<v-icon>$home</v-icon>
<span class="text-overline font-weight-medium">{{ $t('common.home') }}</span>
<v-icon class="pb-1">$home</v-icon>
</v-btn>
<v-divider vertical></v-divider>
<v-btn v-if="canShowFeedback" :to="{ name: 'feedback-home', params: { surveyId } }">
<span>{{ $t('recall.actions.nav.feedback') }}</span>
<v-icon>$feedback</v-icon>
<v-btn v-if="showFeedback" :to="{ name: 'feedback-home', params: { surveyId } }">
<span class="text-overline font-weight-medium">
{{ $t('recall.actions.nav.feedback') }}
</span>
<v-icon class="pb-1">$feedback</v-icon>
</v-btn>
</template>
</card-layout>
Expand All @@ -46,11 +50,7 @@ export default defineComponent({
mixins: [createBasePrompt<'final-prompt'>()],
props: {
canRestart: {
type: Boolean,
default: false,
},
canShowFeedback: {
showFeedback: {
type: Boolean,
default: false,
},
Expand Down

0 comments on commit bc13a7c

Please sign in to comment.