Fakultas Ilmu Komputer UI

Commit 7c265074 authored by Wulan Mantiri's avatar Wulan Mantiri
Browse files

Merge branch 'PBI-9-diet_questionnaire_ui_and_validation' into 'staging'

Implement diet questionnaire UI layout and form validation

See merge request !41
parents 012d6878 0be41e76
Pipeline #75934 passed with stages
in 84 minutes and 46 seconds
......@@ -10,10 +10,6 @@ import {
export interface iUserContext {
user: User;
isAuthenticated: boolean;
isUnpaidClient: boolean;
isPaidClient: boolean;
isNutritionist: boolean;
isAdmin: boolean;
isLoading: boolean;
isFirstLoading: boolean;
getUser: () => Promise<void>;
......
import React from 'react';
import { render } from '@testing-library/react-native';
import { render, waitFor } from '@testing-library/react-native';
import Provider from '.';
describe('Provider', () => {
it('renders correctly', async () => {
render(<Provider>children</Provider>);
const { queryByText } = render(<Provider>children</Provider>);
await waitFor(() => expect(queryByText(/nothing/i)).toBeFalsy());
});
});
......@@ -99,7 +99,7 @@ const Checkout: FC = () => {
<BigButton
title="bayar dengan midtrans"
onPress={pay}
disabled={cartId ? undefined : true}
disabled={!cartId}
/>
</Section>
</View>
......
import React from 'react';
import { render } from '@testing-library/react-native';
import ConsentForm from '.';
const mockedNavigate = jest.fn();
jest.mock('@react-navigation/native', () => {
return {
useNavigation: () => ({
navigate: mockedNavigate,
}),
};
});
describe('ConsentForm', () => {
it('renders correctly', () => {
render(<ConsentForm />);
});
});
import React, { FC } from 'react';
import { View, ScrollView } from 'react-native';
import { Text } from 'react-native-elements';
import { useNavigation } from '@react-navigation/native';
import { InfoCard, BigButton } from 'components/core';
import { RadioButtonGroup } from 'components/form';
import { Section } from 'components/layout';
import * as ROUTES from 'constants/routes';
import { useForm } from 'hooks';
import { layoutStyles, typographyStyles } from 'styles';
import { styles } from './styles';
import { consentQuestions, termsAndConditions, initialValues } from './schema';
const ConsentForm: FC = () => {
const navigation = useNavigation();
const {
getFormFieldProps,
handleSubmit,
isSubmitting,
values: formValues,
} = useForm({
initialValues,
onSubmit: async (values) => {
console.log(values);
navigation.navigate(ROUTES.extendedQuestionnaire);
},
});
const isValid =
formValues.agree_to_all_statements_consent &&
formValues.personal_data_consent;
return (
<ScrollView contentContainerStyle={layoutStyles}>
<View style={styles.tnc}>
<Text style={[typographyStyles.bodyMedium, styles.spacing]}>
Pelayanan gizi di Dietela, dilakukan dalam bentuk program perubahan
pola makan dan gaya hidup menjadi lebih sehat yang berlangsung mulai
dari 3 hari sampai dengan 6 bulan. Selama program berlangsung Anda
sebagai klien akan didampingi oleh seorang Nutrisionis atau Dietisien
yang didelegasikan oleh Dietela.
</Text>
<Text style={[typographyStyles.bodyMedium, styles.spacing]}>
Dalam program tersebut akan diberikan servis berupa, “pengecekan gizi”
secara langsung atau online, “konseling gizi” via pesan elektronik
atau telepon atau video call, “diet coaching” via pesan elektronik,
“monitoring rutin” melalui pesan elektronik, “kunjungan gizi ke
lokasi” klien (hanya untuk layanan tertentu), “follow-up” (hanya untuk
klien layanan berjangka 1 bulan atau lebih) melalui telepon atau video
call, dan “exit counseling” atau konseling akhir (hanya untuk klien
layanan berjangka 1 bulan atau lebih) yang dilakukan 1 bulan sekali
melalui telepon atau video call.
</Text>
<Text style={[typographyStyles.bodyMedium, styles.spacing]}>
Seluruh data dan informasi klien akan disimpan pada lokasi penyimpanan
yang aman dan tidak akan kami berikan kepada pihak manapun.
</Text>
<Text style={[typographyStyles.bodyMedium, styles.spacing]}>
Dengan menceklis kolom dibawah ini, maka klien setuju untuk:
</Text>
{termsAndConditions.map((condition, i) => (
<Section key={`condition${i}`}>
<InfoCard content={`${i + 1}. ${condition}`} />
</Section>
))}
</View>
{consentQuestions.map((props, i) => (
<Section key={`radiobuttongroup${i}`}>
<RadioButtonGroup
{...props}
{...getFormFieldProps(props.name)}
choices={props.choiceList.map((label, id) => ({
label,
value: id + 1,
}))}
/>
</Section>
))}
<Section>
<BigButton
title="lanjut"
onPress={handleSubmit}
loading={isSubmitting}
disabled={!isValid}
/>
</Section>
</ScrollView>
);
};
export default ConsentForm;
import { yesOrNo } from 'constants/options';
export const termsAndConditions = [
'Menyampaikan informasi tentang pola makan, aktivitas fisik, dan kondisi kesehatan dirinya dengan jujur',
'Memberikan data tambahan terkait diagnosis medis atau kondisi klinis yang diderita (jika ada)',
'Mengikuti alur program yang telah didesain oleh Dietela',
'Menjawab setiap pertanyaan diet coaching, monitoring rutin, dan follow-up gizi yang diajukan oleh Ahli Gizi atau Dietisien yang menangani klien',
'Mengirimkan foto makanan selama 3 x 24 jam setiap minggunya, yaitu 2 x 24 jam di hari kerja dan 1 x 24 jam di akhir pekan. Foto makanan ini penting untuk Nutrisionis atau Dietisien menganalisa pola makan dan keberhasilan program yang dilakukan oleh klien (Khusus bagi Klien One-Time Consultation dan Holistic Diet Coaching)',
'Foto seluruh badan tampak badan, samping dan belakang yang sangat berguna untuk proses assessment atau pemeriksaan fisik dan evaluasi oleh ahli gizi mengenai penerapan meal plan dan coaching terhadap komposisi tubuh',
'Mengirimkan foto pribadi yang akan digunakan untuk identifikasi pada dokumen meal plan klien',
];
export const initialValues = {
agree_to_all_statements_consent: 0,
personal_data_consent: 0,
};
export const consentQuestions = [
{
name: 'agree_to_all_statements_consent',
label: 'Saya menyetujui semua pernyataan diatas.',
required: true,
choiceList: yesOrNo,
},
{
name: 'personal_data_consent',
label:
'Saya menyetujui data saya boleh digunakan untuk keperluan pengembangan & penelitian ilmu gizi dan keseharan dengan data identitas saya yang dirahasiakan.',
required: true,
choiceList: yesOrNo,
},
];
import { StyleSheet } from 'react-native';
import { colors } from 'styles';
export const styles = StyleSheet.create({
spacing: {
marginBottom: 14,
},
tnc: {
marginBottom: 14,
paddingVertical: 14,
borderBottomColor: colors.border,
borderBottomWidth: 1,
},
});
import React from 'react';
import { render } from '@testing-library/react-native';
import Questionnaire1 from '.';
const mockedNavigate = jest.fn();
jest.mock('@react-navigation/native', () => {
return {
useNavigation: () => ({
navigate: mockedNavigate,
}),
};
});
describe('Questionnaire1', () => {
it('renders correctly', () => {
render(<Questionnaire1 />);
});
});
import React, { FC } from 'react';
import { View } from 'react-native';
import {
RadioButtonGroup,
TextField,
Picker,
Datepicker,
} from 'components/form';
import { initialValues, fieldValidations } from './schema';
import { textFields, selectFields } from 'constants/questionnaire';
import QuestionnaireWrapper, { styles } from '../QuestionnaireWrapper';
const Questionnaire1: FC = () => (
<QuestionnaireWrapper
Questionnaire={({ getTextInputProps, getFormFieldProps }) => (
<View>
<Datepicker
{...getFormFieldProps('date_of_birth')}
label="Tanggal Lahir"
required
/>
{textFields.identity.map((props, i) => (
<TextField
{...props}
{...getTextInputProps(props.name)}
required={props.required}
key={`textfield${i}`}
/>
))}
{selectFields.identity.map((props, i) => {
const FormField = props.picker ? Picker : RadioButtonGroup;
return (
<View key={`select${i}`} style={styles.spacing}>
<FormField
{...props}
choices={props.choiceList.map((label, id) => ({
label,
value: id + 1,
}))}
{...getFormFieldProps(props.name)}
required
/>
</View>
);
})}
</View>
)}
initialValues={initialValues}
fieldValidations={fieldValidations}
/>
);
export default Questionnaire1;
import { FieldValidation, FieldType } from 'utils/form';
import { textFields, selectFields } from 'constants/questionnaire';
export const initialValues = {
date_of_birth: new Date(),
city_and_area_of_residence: '',
handphone_no: '',
whatsapp_no: '',
waist_size: '',
profession: 0,
last_education: 0,
meal_preference: 0,
general_purpose: 0,
dietary_change: 0,
has_weigher: 0,
};
export const fieldValidations: FieldValidation[] = [
...textFields.identity.map((field) => ({
name: field.name,
required: field.required,
label: field.label,
type: FieldType.TEXT,
})),
...selectFields.identity.map((field) => ({
name: field.name,
required: true,
label: field.label,
type: FieldType.RADIO_BUTTON,
})),
];
import React from 'react';
import { render } from '@testing-library/react-native';
import Questionnaire2 from '.';
const mockedNavigate = jest.fn();
jest.mock('@react-navigation/native', () => {
return {
useNavigation: () => ({
navigate: mockedNavigate,
}),
};
});
describe('Questionnaire2', () => {
it('renders correctly', () => {
render(<Questionnaire2 />);
});
});
import React, { FC } from 'react';
import { View } from 'react-native';
import { RadioButtonGroup, TextField, Picker } from 'components/form';
import { initialValues, fieldValidations } from './schema';
import { textFields, selectFields } from 'constants/questionnaire';
import QuestionnaireWrapper, { styles } from '../QuestionnaireWrapper';
const Questionnaire2: FC = () => (
<QuestionnaireWrapper
Questionnaire={({ getTextInputProps, getFormFieldProps }) => (
<View>
{selectFields.eatingPattern.map((props, i) => {
const FormField = props.picker ? Picker : RadioButtonGroup;
return (
<View key={`select${i}`} style={styles.spacing}>
<FormField
{...props}
choices={props.choiceList.map((label, id) => ({
label,
value: id + 1,
}))}
{...getFormFieldProps(props.name)}
required
/>
</View>
);
})}
{textFields.eatingPattern.map((props, i) => (
<TextField
{...props}
{...getTextInputProps(props.name)}
required
key={`textfield${i}`}
/>
))}
</View>
)}
initialValues={initialValues}
fieldValidations={fieldValidations}
/>
);
export default Questionnaire2;
import { FieldValidation, FieldType } from 'utils/form';
import { textFields, selectFields } from 'constants/questionnaire';
export const initialValues = {
breakfast_frequency: 0,
breakfast_meal_type: 0,
sweet_tea_consumption_frequency: 0,
coffee_consumption_frequency: 0,
milk_consumption_frequency: 0,
other_drink_consumption_frequency: 0,
additional_sugar_in_a_day: 0,
liquid_consumption_frequency: 0,
meal_consumed_almost_every_day: '',
unliked_food: '',
preferred_food_taste: '',
expected_food_on_breakfast: '',
expected_food_on_lunch_dinner: '',
};
export const fieldValidations: FieldValidation[] = [
...textFields.eatingPattern.map((field) => ({
name: field.name,
required: true,
label: field.label,
errorMessage: field.errorMessage,
type: FieldType.TEXT,
})),
...selectFields.eatingPattern.map((field) => ({
name: field.name,
required: true,
label: field.label,
type: FieldType.RADIO_BUTTON,
})),
];
import React from 'react';
import { render } from '@testing-library/react-native';
import Questionnaire3 from '.';
const mockedNavigate = jest.fn();
jest.mock('@react-navigation/native', () => {
return {
useNavigation: () => ({
navigate: mockedNavigate,
}),
};
});
describe('Questionnaire3', () => {
it('renders correctly', () => {
render(<Questionnaire3 />);
});
});
import React, { FC } from 'react';
import { Text } from 'react-native-elements';
import { View } from 'react-native';
import { InfoCard } from 'components/core';
import { typographyStyles } from 'styles';
import { TextField } from 'components/form';
import { initialValues, answerFormat } from './schema';
import { textFields } from 'constants/questionnaire';
import QuestionnaireWrapper, { styles } from '../QuestionnaireWrapper';
const Questionnaire3: FC = () => (
<QuestionnaireWrapper
Questionnaire={({ getTextInputProps }) => (
<View>
<Text style={[typographyStyles.headingMedium, styles.spacing]}>
Tuliskan makanan dan minuman (selain air mineral) yang Anda konsumsi
kemarin sesuai jam yang ditanyakan.
</Text>
<Text style={[typographyStyles.bodyMedium, styles.spacing]}>
Sertakan jenis makanan, pengolahan makanan (digoreng, disemur,
ditumis, direbus), banyaknya jumlah yang dikonsumsi baik dengan
estimasi menggunakan takaran alat makan (1 centong, 1 gelas) atau
dengan estimasi jumlah persis (misalnya: 200 g, 500 ml, 3 buah).
</Text>
<Text style={[typographyStyles.overlineBig]}>
Contoh format jawaban:
</Text>
<InfoCard content={answerFormat} />
<View style={styles.spacing} />
{textFields.foodConsumption.map((props, i) => (
<TextField
{...props}
{...getTextInputProps(props.name)}
key={`textfield${i}`}
multiline
/>
))}
</View>
)}
initialValues={initialValues}
/>
);
export default Questionnaire3;
export const answerFormat = `Nasi goreng 1 centong
Ayam goreng 1 potong
Tempe orek kurang lebih 2 sendok makan
Sambal goreng kurang lebih 1 sendok makan
Roti isi coklat 1 potong
Jus alpukat 1 gelas`;
export const initialValues = {
breakfast_meal_explanation: '',
morning_snack_explanation: '',
lunch_meal_explanation: '',
evening_snack_explanation: '',
dinner_meal_explanation: '',
night_snack_explanation: '',
};
import React from 'react';
import { render } from '@testing-library/react-native';
import Questionnaire4 from '.';
const mockedNavigate = jest.fn();
jest.mock('@react-navigation/native', () => {
return {
useNavigation: () => ({
navigate: mockedNavigate,
}),
};
});
describe('Questionnaire4', () => {
it('renders correctly', () => {
render(<Questionnaire4 />);
});
});
import React, { FC } from 'react';
import { View } from 'react-native';
import { TextField, CheckboxGroup } from 'components/form';
import { textFields, selectFields } from 'constants/questionnaire';
import { initialValues } from './schema';
import QuestionnaireWrapper, { styles } from '../QuestionnaireWrapper';
const Questionnaire4: FC = () => (
<QuestionnaireWrapper
Questionnaire={({ getTextInputProps, getFormFieldProps }) => (
<View>
{selectFields.lifestyle.map((props, i) => {
let otherProps = {};
if (props.hasOtherChoice) {
const textFieldProps = getTextInputProps(`other_${props.name}`);
otherProps = {
hasOtherChoice: true,
otherValue: textFieldProps.value,
setOtherValue: textFieldProps.onChangeText,
};
}
return (
<View key={`checkbox${i}`} style={styles.spacing}>
<CheckboxGroup
{...props}
choices={props.choiceList.map((label, id) => ({
label,
value: props.hasOtherChoice ? label : id + 1,
}))}
{...getFormFieldProps(props.name)}