Fakultas Ilmu Komputer UI

Commit 617addc6 authored by Wulan Mantiri's avatar Wulan Mantiri
Browse files

Integrate extended questionnaire API

parent 7711189a
import {
DietQuestionnaireResponse,
DietQuestionnaireRequest,
} from 'services/dietQuestionnaire/models';
const validFormValues: DietQuestionnaireRequest = {
agree_to_all_statements_consent: 1,
personal_data_consent: 1,
general_purpose: 1,
date_of_birth: '1995-12-30',
city_and_area_of_residence: 'yang jauh disana',
handphone_no: '081234567890',
whatsapp_no: '080987654321',
profession: 4,
last_education: 5,
meal_preference: 2,
waist_size: 60,
dietary_change: 2,
has_weigher: 1,
breakfast_frequency: 3,
breakfast_meal_type: 2,
sweet_tea_consumption_frequency: 9,
coffee_consumption_frequency: 9,
milk_consumption_frequency: 9,
other_drink_consumption_frequency: 9,
additional_sugar_in_a_day: 7,
liquid_consumption_frequency: 3,
meal_consumed_almost_every_day: 'gaada',
unliked_food: 'semua',
preferred_food_taste: 'gaada',
expected_food_on_breakfast: 'makanan mahal',
expected_food_on_lunch_dinner: 'makanan murah',
breakfast_meal_explanation: 'jadi, makan pagi saya tuh ga teratur hehe bye',
morning_snack_explanation: 'ga nyemil',
lunch_meal_explanation: 'yg penting enak',
evening_snack_explanation: 'gtw',
dinner_meal_explanation: 'makan enak',
night_snack_explanation: 'indomi',
food_alergies: 'kl banyak mo apa hah',
diet_drinks: 'hah',
meal_provider: [1, 2, 3],
cigarette_alcohol_condition: [],
multivitamin_tablet_suplement: 'gatau',
physical_activity: [1, 2],
other_physical_activity: 'another one',
diet_and_life_style_story: 'kepo bgt si',
disease: [17],
complaint: [1, 2, 3, 4, 5],
regular_drug_consumption: 'obatobatan',
other_disease: 'dis-ease',
motivation_using_dietela: 'biar kurus',
dietela_nutritionist_expectation: 'yg jago ya',
dietela_program_expectation: 'bikin sy kurus ya, kl gagal balikin duit lho',
};
export const mockDietQuestionnaire: DietQuestionnaireResponse = {
id: 7,
...validFormValues,
finished_steps: [],
user: 30,
};
......@@ -2,13 +2,15 @@ import * as ROUTES from 'constants/routes';
import {
publicNavigation,
unpaidClientNavigation,
onboardingClientNavigation,
clientNavigation,
} from 'constants/navigation';
import { User, UserRole } from 'services/auth/models';
import { UserRole, AuthUserResponse } from 'services/auth/models';
import { TransactionStatus } from 'services/payment/models';
export const getNavigation = (isAuthenticated: boolean, user: User) => {
export const getNavigation = (
isAuthenticated: boolean,
user: AuthUserResponse,
) => {
if (isAuthenticated) {
if (user.role === UserRole.CLIENT) {
if (user.transaction_status === TransactionStatus.UNPAID) {
......@@ -16,14 +18,11 @@ export const getNavigation = (isAuthenticated: boolean, user: User) => {
initialRoute: ROUTES.checkout,
navigation: unpaidClientNavigation,
};
} else if (!user.has_profile) {
return {
initialRoute: ROUTES.extendedQuestionnaire,
navigation: onboardingClientNavigation,
};
}
return {
initialRoute: ROUTES.clientProfile,
initialRoute: user.is_finished_onboarding
? ROUTES.clientProfile
: ROUTES.extendedQuestionnaire,
navigation: clientNavigation,
};
}
......
......@@ -2,7 +2,6 @@ import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import CheckboxGroup from '.';
import { checkbox } from 'constants/options';
describe('CheckboxGroup component', () => {
const props = {
......@@ -64,6 +63,7 @@ describe('CheckboxGroup component', () => {
value={value}
onChange={(v: typeof value) => (value = v)}
hasOtherChoice
otherChoiceValue={1}
/>,
);
......@@ -71,6 +71,6 @@ describe('CheckboxGroup component', () => {
expect(otherChoice).toBeTruthy();
fireEvent.press(otherChoice);
expect(value).toContain(checkbox.OTHER);
expect(value).toContain(1);
});
});
......@@ -6,7 +6,6 @@ import { typographyStyles } from 'styles';
import { styles } from './styles';
import { Props } from './types';
import FormLabel from '../FormLabel';
import { checkbox } from 'constants/options';
const CheckboxGroup: FC<Props> = ({
label,
......@@ -14,10 +13,11 @@ const CheckboxGroup: FC<Props> = ({
value,
onChange,
hasOtherChoice,
otherChoiceValue,
otherValue,
setOtherValue,
}) => {
const handlePress = (choiceValue: string | number) => {
const handlePress = (choiceValue?: string | number) => {
if (value.includes(choiceValue)) {
onChange(value.filter((item: number | string) => item !== choiceValue));
} else {
......@@ -53,8 +53,8 @@ const CheckboxGroup: FC<Props> = ({
/>
</View>
}
checked={value.includes(checkbox.OTHER)}
onPress={() => handlePress(checkbox.OTHER)}
checked={value.includes(otherChoiceValue)}
onPress={() => handlePress(otherChoiceValue)}
/>
) : null}
</View>
......
......@@ -6,6 +6,7 @@ export interface Props extends FormLabelProps {
value: any;
onChange: (_: any) => void;
hasOtherChoice?: boolean;
otherChoiceValue?: number | string;
otherValue?: string;
setOtherValue?: (_: string | number) => void;
}
......@@ -9,6 +9,7 @@ jest.mock('@react-navigation/native', () => {
return {
useNavigation: () => ({
navigate: mockedNavigate,
reset: mockedNavigate,
}),
};
});
......@@ -24,9 +25,9 @@ describe('StepByStepForm component', () => {
finishRedirectRoute: 'finishroute',
};
it('only has "Lanjut" button when form is never filled', () => {
it('only has "Isi" button when form is never filled', () => {
const { getByText, queryByText } = render(<StepByStepForm {...props} />);
const nextButton = getByText(/Lanjut/i);
const nextButton = getByText(/Isi/i);
expect(nextButton).toBeTruthy();
expect(queryByText(/Ubah/i)).toBeFalsy();
});
......@@ -38,9 +39,9 @@ describe('StepByStepForm component', () => {
expect(queryByText(/Ubah/i)).toBeTruthy();
});
it('redirects to designated route when button "Ubah" or "Lanjut" is pressed', () => {
it('redirects to designated route when button "Ubah" or "Isi" is pressed', () => {
const { getByText } = render(<StepByStepForm {...props} currentPage={1} />);
const nextButton = getByText(/Lanjut/i);
const nextButton = getByText(/Isi/i);
expect(nextButton).toBeTruthy();
fireEvent.press(nextButton);
......
......@@ -23,7 +23,7 @@ const StepByStepForm: FC<Props> = ({
if (i < currentPage) {
buttonLabel = 'Ubah';
} else if (i === currentPage) {
buttonLabel = 'Lanjut';
buttonLabel = 'Isi';
}
return buttonLabel ? (
<Button
......@@ -63,7 +63,12 @@ const StepByStepForm: FC<Props> = ({
<View style={styles.bottomContainer}>
<Button
title="Selesai"
onPress={() => navigation.navigate(finishRedirectRoute)}
onPress={() =>
navigation.reset({
index: 0,
routes: [{ name: finishRedirectRoute }],
})
}
buttonStyle={styles.finishButton}
titleStyle={styles.buttonTitle}
disabled={currentPage !== pages.length}
......
......@@ -27,9 +27,9 @@ import {
} from 'scenes';
import { FC } from 'react';
export interface NavRoute {
export interface NavRoute<T = any> {
name: string;
component: FC;
component: FC<T>;
header?: string;
}
......@@ -121,7 +121,12 @@ export const unpaidClientNavigation: NavRoute[] = [
...navigation,
];
export const onboardingClientNavigation: NavRoute[] = [
export const clientNavigation: NavRoute[] = [
{
name: ROUTES.clientProfile,
component: ClientProfile,
header: 'Profil Saya',
},
{
name: ROUTES.extendedQuestionnaire,
component: ExtendedQuestionnaire,
......@@ -158,15 +163,6 @@ export const onboardingClientNavigation: NavRoute[] = [
})),
];
export const clientNavigation: NavRoute[] = [
{
name: ROUTES.clientProfile,
component: ClientProfile,
header: 'Profil Saya',
},
...onboardingClientNavigation,
];
export const adminNavigation: NavRoute[] = [
{
name: ROUTES.clientProfile,
......@@ -177,7 +173,8 @@ export const adminNavigation: NavRoute[] = [
export const testNavigation: NavRoute[] = [
...navigation,
...onboardingClientNavigation,
...clientNavigation,
...nutritionistNavigation,
{
name: ROUTES.initial,
component: InitialPage,
......
......@@ -11,7 +11,3 @@ export const drinkFrequency = [
'2 gelas (500 ml) per hari',
'Lebih dari 2 gelas per hari',
];
export const checkbox = {
OTHER: 'OTHER',
};
......@@ -313,6 +313,7 @@ export const selectFields = {
label:
'Apakah Anda melakukan salah satu dari aktivitas fisik atau olahraga berikut?',
hasOtherChoice: true,
otherChoiceValue: 9,
choiceList: [
'Hampir tidak pernah olahraga',
'Jogging',
......
......@@ -8,16 +8,14 @@ const useApi = <T>(
): {
isLoading: boolean;
} & Response<T> => {
const [isLoading, setIsLoading] = useState(false);
const [response, setResponse] = useState({
isLoading: true,
success: false,
});
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
const apiResponse = await fetchApi();
setResponse(apiResponse);
if (!apiResponse.success) {
Toast.show({
type: 'error',
......@@ -25,16 +23,16 @@ const useApi = <T>(
text2: 'Terjadi kesalahan pada sisi kami.',
});
}
setIsLoading(false);
setResponse({
isLoading: false,
...apiResponse,
});
};
fetchData();
}, [fetchApi]);
return {
isLoading,
...response,
};
return response;
};
export default useApi;
......@@ -13,10 +13,10 @@ import {
linkUserDataApi,
} from 'services/auth';
import {
User,
RegistrationRequest,
LoginRequest,
LoginResponse,
AuthUserResponse,
} from 'services/auth/models';
import { setAuthHeader, resetAuthHeader } from 'services/api';
......@@ -29,7 +29,7 @@ export const initialUser = {
name: '',
role: null,
transaction_status: TransactionStatus.UNPAID,
has_profile: false,
is_finished_onboarding: false,
};
export const UserContext = createContext<iUserContext>({
......@@ -45,7 +45,7 @@ export const UserContext = createContext<iUserContext>({
});
export const useUserContext = (): iUserContext => {
const [user, setUser] = useState<User>(initialUser);
const [user, setUser] = useState<AuthUserResponse>(initialUser);
const [isLoading, setIsLoading] = useState(false);
const [isFirstLoading, setIsFirstLoading] = useState(false);
......@@ -64,11 +64,7 @@ export const useUserContext = (): iUserContext => {
setAuthHeader(token);
const response = await retrieveUserApi();
if (response.success && response.data) {
const { cart, ...userData } = response.data;
setUser({
...userData,
transaction_status: cart.transaction_status,
});
setUser(response.data);
} else {
await logout();
Toast.show({
......
......@@ -3,12 +3,12 @@ import {
LoginRequest,
LoginResponse,
RegistrationRequest,
User,
LinkUserDataResponse,
AuthUserResponse,
} from 'services/auth/models';
export interface iUserContext {
user: User;
user: AuthUserResponse;
isAuthenticated: boolean;
isLoading: boolean;
isFirstLoading: boolean;
......
import React from 'react';
import { render } from 'utils/testing';
import { render, waitFor } from 'utils/testing';
import * as ROUTES from 'constants/routes';
import axios from 'axios';
import ClientListNutritionist from '.';
jest.mock('axios');
const mockAxios = axios as jest.Mocked<typeof axios>;
describe('ClientListNutritionist', () => {
it('renders correctly', () => {
it('renders correctly', async () => {
mockAxios.request.mockImplementationOnce(() =>
Promise.resolve({
status: 200,
data: [],
}),
);
render(<ClientListNutritionist />, ROUTES.clientListNutritionist);
await waitFor(() => expect(mockAxios.request).toBeCalled());
});
});
......@@ -5,13 +5,18 @@ import { ClientCardNutritionist } from './components';
import { useNavigation } from '@react-navigation/native';
import * as ROUTES from 'constants/routes';
import { retrieveClientListApi } from 'services/profiles';
import { useApi } from 'hooks';
import { Loader } from 'components/core';
const ClientListNutritionist: FC = () => {
const navigation = useNavigation();
const clients = retrieveClientListApi();
const { isLoading, data: clients = [] } = useApi(retrieveClientListApi);
if (isLoading) {
return <Loader />;
}
return (
<ScrollView style={layoutStyles}>
<ScrollView contentContainerStyle={layoutStyles}>
{clients.map((client, idx) => (
<ClientCardNutritionist
key={idx}
......
import { StyleSheet } from 'react-native';
export const styles = StyleSheet.create({});
......@@ -116,7 +116,7 @@ const AllAccessQuestionnaire: FC = () => {
helperText={question.helperText}
choices={question.choiceList.map((choice, choiceId) => ({
label: choice,
value: choiceId + 1,
value: choiceId + (question.multiple ? 2 : 1),
}))}
{...getFormFieldProps(question.fieldName)}
/>
......
import { FieldValidation, FieldType } from 'utils/form';
import { FieldValidation, FieldType, filterArr } from 'utils/form';
import { TextFieldSchema, RadioButtonGroupSchema } from 'types/form';
import { DietProfileRequest } from 'services/dietelaQuiz/models';
......@@ -290,5 +290,7 @@ export const convertPayload = (
weight: parseInt(values.weight, 10),
special_condition: values.gender === 1 ? 1 : values.special_condition,
health_problem:
values.health_problem.length === 0 ? [1] : values.health_problem,
values.health_problem.length === 0
? [1]
: filterArr(values.health_problem, 1),
});
......@@ -2,19 +2,20 @@ import React from 'react';
import { render } from '@testing-library/react-native';
import ConsentForm from '.';
import { mockDietQuestionnaire } from '__mocks__/dietQuestionnaire';
const mockedNavigate = jest.fn();
jest.mock('@react-navigation/native', () => {
return {
useNavigation: () => ({
navigate: mockedNavigate,
reset: mockedNavigate,
}),
};
});
describe('ConsentForm', () => {
it('renders correctly', () => {
render(<ConsentForm />);
render(<ConsentForm route={{ params: mockDietQuestionnaire }} />);
});
});
import React, { FC } from 'react';
import { View, ScrollView } from 'react-native';
import { View } from 'react-native';
import { Text } from 'react-native-elements';
import { useNavigation } from '@react-navigation/native';
import { InfoCard, BigButton } from 'components/core';
import { InfoCard } 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 { typographyStyles } from 'styles';
import { styles } from './styles';
import { consentQuestions, termsAndConditions, initialValues } from './schema';
const ConsentForm: FC = () => {
const navigation = useNavigation();
import QuestionnaireWrapper from '../QuestionnaireWrapper';
import { NavProps } from '../QuestionnaireWrapper/types';
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}`} />
const ConsentForm: FC<NavProps> = (navProps) => (
<QuestionnaireWrapper
{...navProps}
Questionnaire={({ getFormFieldProps }) => (
<View>
<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”