Fakultas Ilmu Komputer UI

Commit aaa77cc9 authored by Wulan Mantiri's avatar Wulan Mantiri
Browse files

Implement client weekly report layout and form validation

parent 5891d831
......@@ -43,6 +43,7 @@ describe('CheckboxGroup component', () => {
const { getByText } = render(
<CheckboxGroup
{...props}
errorMessage="some error"
value={value}
onChange={(v: typeof value) => (value = v)}
/>,
......
......@@ -9,6 +9,7 @@ import FormLabel from '../FormLabel';
const CheckboxGroup: FC<Props> = ({
label,
required,
choices,
value,
onChange,
......@@ -16,6 +17,7 @@ const CheckboxGroup: FC<Props> = ({
otherChoiceValue,
otherValue,
setOtherValue,
errorMessage,
}) => {
const handlePress = (choiceValue?: string | number) => {
if (value.includes(choiceValue)) {
......@@ -27,7 +29,7 @@ const CheckboxGroup: FC<Props> = ({
return (
<View>
<FormLabel label={label} />
<FormLabel label={label} required={required} />
<Text style={[typographyStyles.bodySmall, styles.helperText]}>
Dapat memilih lebih dari satu
</Text>
......@@ -57,6 +59,11 @@ const CheckboxGroup: FC<Props> = ({
onPress={() => handlePress(otherChoiceValue)}
/>
) : null}
{errorMessage ? (
<Text style={[typographyStyles.caption, styles.red]}>
{errorMessage}
</Text>
) : null}
</View>
);
};
......
......@@ -27,4 +27,8 @@ export const styles = StyleSheet.create({
marginHorizontal: 0,
paddingVertical: 0,
},
red: {
color: 'red',
marginTop: 4,
},
});
......@@ -9,4 +9,5 @@ export interface Props extends FormLabelProps {
otherChoiceValue?: number | string;
otherValue?: string;
setOtherValue?: (_: string | number) => void;
errorMessage?: string;
}
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import LikertScale from '.';
import RadioButton from '../RadioButton';
describe('LikertScale component', () => {
let value: any = null;
const props = {
choices: [
{
label: 'Choice 1',
value: 1,
},
{
label: 'Choice 2',
value: 2,
},
],
value: value,
onChange: (v: any) => (value = v),
};
it('only selects one choice at a time', () => {
const { UNSAFE_getAllByType } = render(
<LikertScale
{...props}
helperText="helper"
scaleDescription={{ lowest: 'hai', highest: 'hello' }}
/>,
);
const buttons = UNSAFE_getAllByType(RadioButton);
fireEvent.press(buttons[0]);
expect(value).toEqual(1);
fireEvent.press(buttons[1]);
expect(value).toEqual(2);
});
it('shows error message when there is error message props', () => {
const errorMessage = 'error';
const { getByText } = render(
<LikertScale {...props} errorMessage={errorMessage} />,
);
const errorText = getByText(errorMessage);
expect(errorText).toBeTruthy();
});
it('shows middle scale description', () => {
const { getByText } = render(
<LikertScale
{...props}
scaleDescription={{ lowest: 'hai', middle: 'hi', highest: 'hello' }}
/>,
);
const middleText = getByText(/hi/i);
expect(middleText).toBeTruthy();
});
});
import React, { FC } from 'react';
import { View } from 'react-native';
import { typographyStyles } from 'styles';
import { Text } from 'react-native-elements';
import { styles } from './styles';
import { Props } from './types';
import FormLabel from '../FormLabel';
import RadioButton from '../RadioButton';
const LikertScale: FC<Props> = ({
label,
required,
errorMessage,
helperText,
scaleDescription,
choices,
value,
onChange,
}) => (
<View>
<FormLabel label={label} required={required} />
{helperText ? (
<Text style={[typographyStyles.bodySmall, styles.helperText]}>
{helperText}
</Text>
) : null}
{scaleDescription ? (
<Text style={[typographyStyles.bodySmall, styles.helperText]}>
{choices[0].label}: {scaleDescription.lowest} {'\n'}
{scaleDescription.middle ? scaleDescription.middle + '\n' : ''}
{choices[choices.length - 1].label}: {scaleDescription.highest}
</Text>
) : null}
<View style={styles.textGroup}>
{choices.map((choice) => (
<View style={{ flex: 1 / choices.length }} key={`text${choice.value}`}>
<Text style={styles.radioText}>{choice.label}</Text>
</View>
))}
</View>
<View style={styles.buttonGroup}>
{choices.map((choice) => (
<View
style={{ flex: 1 / choices.length }}
key={`button${choice.value}`}>
<RadioButton
key={choice.value}
checked={value === choice.value}
onPress={() => onChange(choice.value)}
center
/>
</View>
))}
</View>
{errorMessage ? (
<Text style={[typographyStyles.caption, styles.red]}>{errorMessage}</Text>
) : null}
</View>
);
export default LikertScale;
import { StyleSheet } from 'react-native';
import { colors } from 'styles';
export const styles = StyleSheet.create({
helperText: {
color: colors.formLabel,
marginBottom: 6,
},
red: {
color: 'red',
marginTop: 4,
},
buttonGroup: {
flex: 1,
display: 'flex',
flexDirection: 'row',
},
textGroup: {
flex: 1,
display: 'flex',
flexDirection: 'row',
alignItems: 'flex-end',
marginTop: 6,
},
radioText: {
textAlign: 'center',
},
});
import { Choice } from '../MultipleChoice/types';
import { Props as FormLabelProps } from '../FormLabel/types';
export interface Props extends FormLabelProps {
errorMessage?: any;
helperText?: string;
choices: Choice[];
value: any;
onChange: (_: any) => void;
scaleDescription?: {
lowest: string;
middle?: string;
highest: string;
};
}
export { default as CheckboxGroup } from './CheckboxGroup';
export { default as Datepicker } from './Datepicker';
export { default as LikertScale } from './LikertScale';
export { default as MultipleCheckbox } from './MultipleCheckbox';
export { default as MultipleChoice } from './MultipleChoice';
export { default as Picker } from './Picker';
......
......@@ -32,6 +32,7 @@ import {
ProfileDietRecommendation,
ClientListAdmin,
LoginChoosePlan,
WeeklyReport,
} from 'scenes';
import { FC } from 'react';
......@@ -173,6 +174,11 @@ export const clientNavigation: NavRoute[] = [
component: ClientProfile,
header: 'Profil Saya',
},
{
name: ROUTES.clientWeeklyReport,
component: WeeklyReport,
header: 'Laporan Diet Saya',
},
...defaultClientNavigation,
];
......
......@@ -11,3 +11,37 @@ export const drinkFrequency = [
'2 gelas (500 ml) per hari',
'Lebih dari 2 gelas per hari',
];
export const physicalActivity = [
'Hampir tidak pernah olahraga',
'Jogging',
'Senam aerobic, zumba, yoga, dan sejenisnya',
'Sepak bola atau futsal',
'Renang',
'Basket',
'Bulu tangkis',
'Voli',
];
export const likertScale5 = ['1', '2', '3', '4', '5'];
export const likertScale10 = [
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'10',
];
export const averageConsumptionOptions = [
'0',
'1',
'2',
'3',
'4',
'Lebih\ndari 4',
];
import { yesOrNo, drinkFrequency } from 'constants/options';
import { yesOrNo, drinkFrequency, physicalActivity } from 'constants/options';
import { TextFieldSchema } from 'types/form';
export const pageHeaders: string[] = [
......@@ -319,16 +319,7 @@ export const selectFields: { [_: string]: any[] } = {
'Apakah Anda melakukan salah satu dari aktivitas fisik atau olahraga berikut?',
hasOtherChoice: true,
otherChoiceValue: 9,
choiceList: [
'Hampir tidak pernah olahraga',
'Jogging',
'Senam aerobic, zumba, yoga, dan sejenisnya',
'Sepak bola atau futsal',
'Renang',
'Basket',
'Bulu tangkis',
'Voli',
],
choiceList: physicalActivity,
},
],
healthCondition: [
......
......@@ -19,12 +19,13 @@ export const login = 'login';
export const loginChoosePlan = 'login-choose-plan';
export const nutritionistAdminLogin = 'nutritionist-admin-login';
const profile = 'profile';
export const clientProfile = `${profile}/client`;
export const payment = 'payment';
export const paymentResult = `${payment}/result`;
const client = 'client';
export const clientProfile = `${client}/profile`;
export const clientWeeklyReport = `${client}/report`;
const nutritionist = 'nutritionist';
export const clientListForNutritionist = `${nutritionist}/client-list`;
export const clientProfileNutritionist = `${nutritionist}/client-profile`;
......
import { TextFieldSchema } from 'types/form';
import {
averageConsumptionOptions,
likertScale10,
likertScale5,
physicalActivity,
} from './options';
export const dietReportTextFields: { [_: string]: TextFieldSchema[] } = {
dietReportPage1: [
{
label: 'Berat Badan (kg)',
placeholder: 'Masukkan yang terakhir diukur',
name: 'weight',
required: true,
keyboardType: 'numeric',
},
{
label: 'Tinggi Badan (cm)',
placeholder: 'Masukkan yang terakhir diukur',
name: 'height',
required: true,
keyboardType: 'numeric',
},
{
label: 'Lingkar Pinggang (cm)',
placeholder: 'Masukkan yang terakhir diukur',
name: 'waist_size',
required: true,
keyboardType: 'numeric',
},
],
dietReportPage2: [
{
label:
'Selama 1 minggu terakhir, berapa rata-rata total gelas air putih yang Anda minum?',
name: 'water_consumption',
required: true,
placeholder: 'ex: 8',
keyboardType: 'numeric',
errorMessage: 'Konsumsi minuman',
},
],
dietReportPage4: [
{
label:
'Dalam 1 minggu terakhir, apa saja yang sudah bisa Anda pelajari dari program ini?',
name: 'lesson_learned',
required: true,
multiline: true,
errorMessage: 'Pelajaran minggu ini',
},
{
label: `Silahkan sampaikan disini, jika Anda mempunyai:
(1) kendala, keluhan atau kesulitan dalam mengikuti program, atau
(2) masukan, saran, dan komplain terkait layanan sejauh ini.`,
name: 'problem_faced_and_feedbacks',
multiline: true,
},
],
};
export const dietReportSelectFields: { [_: string]: any[] } = {
dietReportPage2: [
{
label: 'Apakah sudah mulai terasa ada perubahan ukuran baju atau celana?',
name: 'changes_felt',
choiceList: likertScale5,
scaleDescription: {
lowest: 'Belum terasa sama sekali',
middle: '3: Tidak ada perubahan sama sekali',
highest: 'Sudah sangat berubah',
},
},
{
label:
'Secara rata-rata, sebelum waktu makan selama 1 minggu terakhir ini, dimana level rasa lapar yang Anda rasakan?',
helperText: 'Cek indikator di program book kamu ya',
name: 'hunger_level',
choiceList: likertScale10,
scaleDescription: {
lowest: 'Sangat kelaparan',
highest: 'Sangat begah (kenyang berlebihan)',
},
},
{
label:
'Secara rata-rata, saat berhenti makan selama 1 minggu terakhir ini, dimana level rasa kenyang yang Anda rasakan?',
helperText: 'Cek indikator di program book kamu ya',
name: 'fullness_level',
choiceList: likertScale10,
scaleDescription: {
lowest: 'Sangat kelaparan',
highest: 'Sangat begah (kenyang berlebihan)',
},
},
{
label:
'Selama 1 minggu terakhir, secara rata-rata, berapa kali Anda makan berat atau makan utama dalam 1 hari?',
name: 'heavy_meal',
choiceList: ['1x/hari', '2x/hari', '3x/hari', 'Lebih dari 3x/hari'],
},
{
label:
'Selama 1 minggu terakhir, secara rata-rata, berapa kali Anda makan cemilan dalam 1 hari?',
name: 'snacks',
choiceList: [
'1x/hari',
'2x/hari',
'3x/hari',
'Lebih dari 3x/hari',
'Hampir tidak ada',
],
},
],
dietReportPage3: [
{
label: 'Minuman manis',
helperText: 'dalam satuan gelas',
name: 'sweet_beverages',
choiceList: averageConsumptionOptions,
},
{
label: 'Gula pasir, gula aren, sirup, selai, atau madu',
helperText: 'dalam satuan sendok makan',
name: 'sugar',
choiceList: averageConsumptionOptions,
},
{
label: 'Cemilan digoreng',
helperText: 'dalam satuan potong',
name: 'fried_snacks',
choiceList: averageConsumptionOptions,
},
{
label:
'Makanan ringan asin atau gurih (seperti makanan ringan kemasan, ciki-cikian, keripik)',
helperText: 'dalam satuan bungkus',
name: 'umami_snacks',
choiceList: averageConsumptionOptions,
},
{
label:
'Cemilan manis (seperti kue-kue manis, brownis, cake, biskuit, cokelat, wafer)',
helperText: 'dalam satuan potong',
name: 'sweet_snacks',
choiceList: averageConsumptionOptions,
},
{
label: 'Porsi buah',
name: 'fruits_portion',
choiceList: averageConsumptionOptions,
},
{
label: 'Porsi sayur',
name: 'vegetables_portion',
choiceList: averageConsumptionOptions,
},
],
dietReportPage4: [
{
label:
'Selama 1 minggu terakhir, pilih semua jenis aktivitas atau olahraga yang sudah Anda lakukan',
name: 'physical_activity',
checkbox: true,
hasOtherChoice: true,
otherChoiceValue: 9,
choiceList: physicalActivity,
},
{
label:
'Selama 1 minggu terakhir, berapa total menit yang Anda habiskan untuk melakukan bergerak aktif dan olahraga diatas dalam seminggu?',
name: 'time_for_activity',
choiceList: [
'0 - 60 menit',
'60 - 100 menit',
'100 - 120 menit',
'120 - 150 menit',
'150 - 175 menit',
'175 - 200 menit',
'200 - 250 menit',
'Lebih dari 250 menit',
],
},
{
label: 'Sejauh ini, bagaimana perasaan Anda dalam mengikuti program?',
name: 'feeling_rating',
choiceList: [
'⭐️: Rasanya mau menyerah saja',
'⭐️⭐️: Capek, susah, bosen, males, repot, sibuk',
'⭐️⭐️⭐️: Biasa aja, meski ada kendala tapi semua bisa diatur',
'⭐️⭐️⭐️⭐️: Lancar terus, semangat cukup stabil, gak ada masalah',
'⭐️⭐️⭐️⭐️⭐️: Super seneng, semangat banget, worry-free lah',
],
},
],
};
......@@ -12,6 +12,7 @@ export { default as ExtendedQuestionnaire } from './questionnaire/ExtendedQuesti
export { default as ProfileDietRecommendation } from './questionnaire/ProfileDietRecommendation';
export { default as ReadOnlyDietProfile } from './questionnaire/ReadOnlyDietProfile';
export * from './questionnaire/ExtendedQuestionnaire/components';
export { default as WeeklyReport } from './questionnaire/WeeklyReport';
export { default as Checkout } from './cart/Checkout';
export { default as ChoosePlan } from './cart/ChoosePlan';
......
import React, { FC } from 'react';
import { ScrollView } from 'react-native';
import { TextField } from 'components/form';
import { dietReportTextFields } from 'constants/weeklyReport';
import { ReportProps } from '../../types';
import { layoutStyles } from 'styles';
const Report1: FC<ReportProps> = ({ getTextInputProps }) => (
<ScrollView contentContainerStyle={layoutStyles}>
{dietReportTextFields.dietReportPage1.map((props, i) => (
<TextField
{...props}
{...getTextInputProps(props.name)}
key={`report1-textfield${i}`}
/>
))}
</ScrollView>
);
export default Report1;
import React, { FC } from 'react';
import { ScrollView, View, StyleSheet } from 'react-native';
import { LikertScale, RadioButtonGroup, TextField } from 'components/form';
import {
dietReportSelectFields,
dietReportTextFields,