Skip to content

Commit

Permalink
fix(aggregate-choice-prompt): collect custom data (V4-1348)
Browse files Browse the repository at this point in the history
- convert to custom prompt
- include into food-level in submission to be collected
- include in data-export
  • Loading branch information
lukashroch committed Nov 26, 2024
1 parent 85d1889 commit e45fbb0
Show file tree
Hide file tree
Showing 22 changed files with 74 additions and 57 deletions.
4 changes: 3 additions & 1 deletion apps/admin/src/components/prompts/custom/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import AggregateChoicePrompt from './aggregate-choice-prompt.vue';
import CheckboxListPrompt from './checkbox-list-prompt.vue';
import DatePickerPrompt from './date-picker-prompt.vue';
import InfoPrompt from './info-prompt.vue';
Expand All @@ -10,9 +11,10 @@ import TimePickerPrompt from './time-picker-prompt.vue';
import YesNoPrompt from './yes-no-prompt.vue';

export default {
AggregateChoicePrompt,
CheckboxListPrompt,
DatePickerPrompt,
InfoPrompt,
CheckboxListPrompt,
RadioListPrompt,
SelectPrompt,
SliderPrompt,
Expand Down
2 changes: 0 additions & 2 deletions apps/admin/src/components/prompts/standard/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import AddonFoodsPrompt from './addon-foods-prompt.vue';
import AggregateChoicePrompt from './aggregate-choice-prompt.vue';
import AssociatedFoodsPrompt from './associated-foods-prompt.vue';
import EditMealPrompt from './edit-meal-prompt.vue';
import ExternalSourcePrompt from './external-source-prompt.vue';
Expand All @@ -20,7 +19,6 @@ import SubmitPrompt from './submit-prompt.vue';

export default {
AddonFoodsPrompt,
AggregateChoicePrompt,
AssociatedFoodsPrompt,
GeneralAssociatedFoodsPrompt,
EditMealPrompt,
Expand Down
11 changes: 6 additions & 5 deletions apps/api/src/jobs/survey-schemes/survey-schemes-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,17 @@ export default class SurveySchemesSync extends BaseJob<'SurveySchemesSync'> {
const promptMap = this.getPromptMap();

const mergeCallback = (prompt: SinglePrompt) => {
const baseMerge = merge<SinglePrompt>(promptMap[prompt.component], prompt);
const { component, type, ...rest } = prompt;
const { actions, conditions, ...baseMerge } = merge<SinglePrompt>(promptMap[prompt.component], rest);
return {
...baseMerge,
actions: baseMerge.actions
actions: actions
? {
...baseMerge.actions,
items: baseMerge.actions.items.map(action => merge(defaultAction, action)),
...actions,
items: actions.items.map(action => merge(defaultAction, action)),
}
: undefined,
conditions: baseMerge.conditions.map(condition => merge<Condition>(getConditionDefaults(condition.object, condition.property.id), condition)),
conditions: conditions.map(condition => merge<Condition>(getConditionDefaults(condition.object, condition.property.id), condition)),
};
};

Expand Down
19 changes: 12 additions & 7 deletions apps/api/src/services/admin/data-export/data-export-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ function dataExportFields() {
/**
* Helper to filter custom Prompt to ExportField
*
* @param {Prompt} { type }
* @returns {boolean}
* @param {string} [component]
*/
const customPromptFilter = ({ type }: Prompt): boolean => type === 'custom';
const customPromptFilter = (component?: string) =>
(prompt: Prompt): boolean => prompt.type === 'custom' && (!component || prompt.component === component);

/**
* User fields
Expand Down Expand Up @@ -224,7 +224,7 @@ function dataExportFields() {
const submissionCustom = async (surveyScheme: SurveyScheme): Promise<ExportField[]> => {
const { preMeals, postMeals, submission } = surveyScheme.prompts;
return [...preMeals, ...postMeals, ...submission]
.filter(customPromptFilter)
.filter(customPromptFilter())
.map(customPromptMapper);
};

Expand Down Expand Up @@ -258,7 +258,7 @@ function dataExportFields() {
meals: { preFoods, postFoods },
} = surveyScheme.prompts;

return [...preFoods, ...postFoods].filter(customPromptFilter).map(customPromptMapper);
return [...preFoods, ...postFoods].filter(customPromptFilter()).map(customPromptMapper);
};

/**
Expand Down Expand Up @@ -343,8 +343,13 @@ function dataExportFields() {
*
* @returns {Promise<ExportField[]>}
*/
const foodCustom = async (surveyScheme: SurveyScheme): Promise<ExportField[]> =>
surveyScheme.prompts.meals.foods.filter(customPromptFilter).map(customPromptMapper);
const foodCustom = async (surveyScheme: SurveyScheme): Promise<ExportField[]> => {
const { preMeals, postMeals, meals: { foods } } = surveyScheme.prompts;
return [
...foods.filter(customPromptFilter()),
...[...preMeals, ...postMeals].filter(customPromptFilter('aggregate-choice-prompt')),
].map(customPromptMapper);
};

/**
* Food composition fields
Expand Down
5 changes: 4 additions & 1 deletion apps/api/src/services/survey/survey-submission.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,10 @@ function surveySubmissionService({
.filter(({ type }) => type === 'custom')
.map(({ id }) => id);

const foodCustomPrompts = foods.filter(({ type }) => type === 'custom').map(({ id }) => id);
const foodCustomPrompts = [
...foods.filter(({ type }) => type === 'custom').map(({ id }) => id),
...[...preMeals, ...postMeals].filter(({ component, type }) => type === 'custom' && component === 'aggregate-choice-prompt').map(({ id }) => id),
];

await db.system.transaction(async (transaction) => {
const { recallDate, startTime, endTime, userAgent } = state;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ import { computed, defineComponent } from 'vue';
import type { Prompts } from '@intake24/common/prompts';
import type { PromptSection } from '@intake24/common/surveys';
import type { FoodState } from '@intake24/common/types';
import { AggregateChoicePrompt } from '@intake24/survey/components/prompts/standard';
import { useSurvey } from '@intake24/survey/stores';
import { AggregateChoicePrompt, filterMealsForAggregateChoicePrompt } from '@intake24/survey/components/prompts/custom';
import { filterMealsForAggregateChoicePrompt } from '../../prompts/standard/aggregate-choice/aggregate-choice';
import { useSurvey } from '@intake24/survey/stores';
import { usePromptHandlerNoStore } from '../mixins';
export default defineComponent({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { MultiPrompt } from '@intake24/survey/components/prompts';
import { useCustomPromptHandler } from '../mixins';
defineOptions({
name: 'MultiCustomPromptHandler',
name: 'MultiPromptHandler',
});
const props = defineProps({
Expand Down
6 changes: 4 additions & 2 deletions apps/survey/src/components/handlers/custom/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import AggregateChoicePromptHandler from './AggregateChoicePromptHandler.vue';
import CustomPromptHandler from './CustomPromptHandler.vue';
import MultiCustomPromptHandler from './MultiCustomPromptHandler.vue';
import MultiPromptHandler from './MultiPromptHandler.vue';

export default {
AggregateChoicePromptHandler,
CustomPromptHandler,
MultiCustomPromptHandler,
MultiPromptHandler,
};
2 changes: 0 additions & 2 deletions apps/survey/src/components/handlers/standard/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import AddonFoodsPromptHandler from './AddonFoodsPromptHandler.vue';
import AggregateChoicePromptHandler from './AggregateChoicePromptHandler.vue';
import AssociatedFoodsPromptHandler from './AssociatedFoodsPromptHandler.vue';
import EditMealPromptHandler from './EditMealPromptHandler.vue';
import ExternalSourcePromptHandler from './ExternalSourcePromptHandler.vue';
Expand All @@ -20,7 +19,6 @@ import SubmitPromptHandler from './SubmitPromptHandler.vue';

export default {
AddonFoodsPromptHandler,
AggregateChoicePromptHandler,
AssociatedFoodsPromptHandler,
GeneralAssociatedFoodsPromptHandler,
EditMealPromptHandler,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { AggregateChoicePrompt } from '@intake24/common/prompts';
import type { Prompts } from '@intake24/common/prompts';
import type { MealState } from '@intake24/common/types';
import { evaluateCondition } from '@intake24/survey/dynamic-recall/prompt-manager';
import type { SurveyStore } from '@intake24/survey/stores';
import { flattenFoods } from '@intake24/survey/util/meal-food';

export function filterMealsForAggregateChoicePrompt(surveyStore: SurveyStore, prompt: AggregateChoicePrompt): MealState[] {
export function filterMealsForAggregateChoicePrompt(surveyStore: SurveyStore, prompt: Prompts['aggregate-choice-prompt']): MealState[] {
return surveyStore.data.meals.map(meal => ({
...meal,
foods: flattenFoods(meal.foods).filter((food) => {
Expand Down
2 changes: 2 additions & 0 deletions apps/survey/src/components/prompts/custom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import TextareaPrompt from './textarea-prompt.vue';
import TimePickerPrompt from './time-picker-prompt.vue';
import YesNoPrompt from './yes-no-prompt.vue';

export * from './aggregate-choice';

export default {
CheckboxListPrompt,
DatePickerPrompt,
Expand Down
1 change: 0 additions & 1 deletion apps/survey/src/components/prompts/standard/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export { default as AddonFoodsPrompt } from './AddonFoodsPrompt.vue';
export * from './aggregate-choice';
export { default as AssociatedFoodsPrompt } from './AssociatedFoodsPrompt.vue';
export { default as EditMealPrompt } from './EditMealPrompt.vue';
export { default as ExternalSourcePrompt } from './ExternalSourcePrompt.vue';
Expand Down
4 changes: 2 additions & 2 deletions apps/survey/src/components/recall/recall-mixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ export default defineComponent({

switch (prompt.type) {
case 'custom':
return prompt.component === 'multi-prompt'
? 'multi-custom-prompt-handler'
return ['multi-prompt', 'aggregate-choice-prompt'].includes(prompt.component)
? `${prompt.component}-handler`
: 'custom-prompt-handler';
case 'standard':
case 'portion-size':
Expand Down
4 changes: 2 additions & 2 deletions apps/survey/src/dynamic-recall/prompt-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ import {
recipeBuilderComplete,
standardPortionComplete,
} from '@intake24/common/util/portion-size-checks';
import type { PromptInstance } from '@intake24/survey/dynamic-recall/dynamic-recall';
import { filterMealsForAggregateChoicePrompt } from '@intake24/survey/components/prompts/custom';

import type { PromptInstance } from '@intake24/survey/dynamic-recall/dynamic-recall';
import {
addonFoodPromptCheck,
findMeal,
Expand All @@ -53,7 +54,6 @@ import {
surveyPortionSizeComplete,
surveySearchComplete,
} from '@intake24/survey/util';
import { filterMealsForAggregateChoicePrompt } from '../components/prompts/standard/aggregate-choice/aggregate-choice';
import { recallLog } from '../stores';

function foodEnergy(energy: number, food: FoodState): number {
Expand Down
25 changes: 19 additions & 6 deletions docs/admin/surveys/prompt-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,19 @@ Recipe builder prompt for foods with multiple ingredients, such as sandwiches, s

Prompts with customizable generic behavior. Custom prompts can be used multiple times per scheme as long as they are identified with scheme-unique [`Prompt ID`](/admin/surveys/prompt-editor#general).

### Aggregate choice prompt

Prompt to collect single option from a list of foods.

#### Options

- `options` - locale-specific list of options with properties:

- `label` - user-facing displayed label
- `value` - value stored in database

- `as only for specified foods` - conditions to limit the foods to which the prompt is applicable

### Checkbox list prompt

Multi-select list of options.
Expand All @@ -393,8 +406,8 @@ Multi-select list of options.

- `options` - locale-specific list of options with properties:

- `label` - (user-facing displayed value)
- `value` - (value stored in database) can be specified
- `label` - user-facing displayed label
- `value` - value stored in database
- `exclusive` - exclusive flag - if selected, other options are deselected

- `other` - `true` or `false` whether to show 'other' option, free-form text input
Expand Down Expand Up @@ -427,8 +440,8 @@ Prompt to collect single or multiple option(s) from a list of options using sele

- `options` - locale-specific list of options with properties:

- `label` (user-facing displayed value)
- `value` (value stored in database) can be specified
- `label` - user-facing displayed label
- `value` - value stored in database

### Slider prompt

Expand All @@ -454,8 +467,8 @@ Single-select list of options.

- `options` - locale-specific list of options with properties:

- `label` (user-facing displayed value)
- `value` (value stored in database) can be specified
- `label` - user-facing displayed label
- `value` - value stored in database

- `other` - `true` or `false` whether to show 'other' option, free-form text input

Expand Down
11 changes: 11 additions & 0 deletions packages/common/src/prompts/custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ import type { Prompts } from './prompts';
import { copy } from '@intake24/common/util';
import { basePrompt, promptValidation } from './base';

export const aggregateChoicePrompt: Prompts['aggregate-choice-prompt'] = copy({
...basePrompt,
component: 'aggregate-choice-prompt',
type: 'custom',
id: 'aggregate-choice-prompt',
name: 'Aggregate choice question prompt',
options: { en: [] },
foodFilter: undefined,
});

export const checkboxListPrompt: Prompts['checkbox-list-prompt'] = copy({
...basePrompt,
...promptValidation,
Expand Down Expand Up @@ -110,6 +120,7 @@ export const yesNoPrompt: Prompts['yes-no-prompt'] = copy({
});

export const customPrompts = [
aggregateChoicePrompt,
checkboxListPrompt,
datePickerPrompt,
infoPrompt,
Expand Down
11 changes: 3 additions & 8 deletions packages/common/src/prompts/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,10 @@ export const customComponentTypes = [
'yes-no-prompt',
'aggregate-choice-prompt',
] as const;

export type CustomComponentType = (typeof customComponentTypes)[number];

export const standardComponentTypes = [
'addon-foods-prompt',
'aggregate-choice-prompt',
'associated-foods-prompt',
'general-associated-foods-prompt',
'edit-meal-prompt',
Expand All @@ -51,7 +49,6 @@ export const standardComponentTypes = [
'split-food-prompt',
'submit-prompt',
] as const;

export type StandardComponentType = (typeof standardComponentTypes)[number];

export type PortionSizeComponentType =
Expand Down Expand Up @@ -198,15 +195,13 @@ export const timePicker = z.object({
});
export type TimePicker = z.infer<typeof timePicker>;

const aggregateChoicePrompt = baseStandardPrompt.extend({
// Custom
const aggregateChoicePrompt = baseCustomPrompt.extend({
component: z.literal('aggregate-choice-prompt'),
options: localeOptionList(),
foodFilter: condition.optional(),
});

export type AggregateChoicePrompt = z.infer<typeof aggregateChoicePrompt>;

// Custom
const checkboxListPrompt = baseCustomPrompt
.extend({
component: z.literal('checkbox-list-prompt'),
Expand Down Expand Up @@ -436,6 +431,7 @@ const submitPrompt = baseStandardPrompt.extend({

export const singlePrompt = z.discriminatedUnion('component', [
// Custom
aggregateChoicePrompt,
checkboxListPrompt,
datePickerPrompt,
infoPrompt,
Expand Down Expand Up @@ -480,7 +476,6 @@ export const singlePrompt = z.discriminatedUnion('component', [
sameAsBeforePrompt,
splitFoodPrompt,
submitPrompt,
aggregateChoicePrompt,
]);
export type SinglePrompt = z.infer<typeof singlePrompt>;

Expand Down
11 changes: 0 additions & 11 deletions packages/common/src/prompts/standard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,16 +194,6 @@ export const submitPrompt: Prompts['submit-prompt'] = copy({
},
});

export const aggregateChoicePrompt: Prompts['aggregate-choice-prompt'] = copy({
...basePrompt,
component: 'aggregate-choice-prompt',
type: 'standard',
id: 'aggregate-choice-prompt',
name: 'Aggregate food question prompt',
options: { en: [] },
foodFilter: undefined,
});

export const generalAssociatedFoodsPrompt: Prompts['general-associated-foods-prompt'] = copy({
...basePrompt,
component: 'general-associated-foods-prompt',
Expand Down Expand Up @@ -237,5 +227,4 @@ export const standardPrompts = [
sameAsBeforePrompt,
splitFoodPrompt,
submitPrompt,
aggregateChoicePrompt,
];
2 changes: 1 addition & 1 deletion packages/i18n/src/admin/en/survey-schemes.json
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,7 @@
"subtitle": "Ask to choose yes or no"
},
"aggregate-choice-prompt": {
"title": "Aggregate food question",
"title": "Aggregate choice question",
"subtitle": "Ask to choose an option for every reported food in a single screen"
},
"as-served-prompt": {
Expand Down

0 comments on commit e45fbb0

Please sign in to comment.