Fakultas Ilmu Komputer UI

Commit 7ad61e21 authored by Kefas Satrio Bangkit Solidedantyo's avatar Kefas Satrio Bangkit Solidedantyo
Browse files

progress diet nutritionist backend integration

parent 5b4c01da
......@@ -77,7 +77,9 @@ const ClientList: FC<Props> = ({
});
}}
onPressClientDietReport={() => {
navigation.navigate(clientDietReportRoute, {});
navigation.navigate(clientDietReportRoute, {
id: client.user.id,
});
}}
onPressClientChat={() => {
navigation.navigate(clientChatRoute, {});
......
import { TextFieldSchema } from 'types/form';
import {
likertScale5,
likertScale10,
averageConsumptionOptions,
} from './options';
export const dietReportTextFields: { [_: string]: TextFieldSchema[] } = {
dietReportPage1: [
{
label: 'Berat Badan (kg)',
placeholder: 'Masukan berat badan yang terakhir anda ukur',
name: 'weight',
required: true,
keyboardType: 'numeric',
},
{
label: 'Tinggi Badan (cm)',
placeholder: 'Masukan tinggi badan yang terakhir anda ukur',
name: 'height',
required: true,
keyboardType: 'numeric',
},
{
label: 'Lingkar Pinggang (cm)',
placeholder: 'Masukan lingkar pinggang yang terakhir anda ukur',
name: 'waist_size',
keyboardType: 'numeric',
},
],
dietReportPage2: [
{
label:
'Selama 1 minggu terakhir, berapa rata-rata total gelas air putih yang Anda minum?',
placeholder: 'Masukan dalam angka',
name: 'water_consumption',
keyboardType: 'numeric',
},
],
dietReportPage4: [
{
label:
'Dalam 1 minggu terakhir, Apa saja yang sudah bisa Anda pelajari dari program ini?',
placeholder: 'Masukan pelajarann-pelajaran yang anda dapatkan',
name: 'lesson_learned',
},
{
label:
'Silahkan sampaikan disini, jika Anda mempunyai kendala atau keluhan atau kesulitan dalam mengikuti program.',
placeholder:
'Jika ada masukan, saran, dan komplain terkait layanan sejauh ini, juga bisa disampaikan disini',
name: 'problem_faced_and_feedbacks',
},
],
};
export const dietReportSelectFields: { [_: string]: any[] } = {
dietReportPage2: [
{
label: 'Apakah sudah mulai terasa ada perubahan ukuran baju atau celana?',
placeholder: 'Skor 3: jika tidak ada perubahan sama sekali',
name: 'changes_felt',
lowestScoreDescription: '1: Belum terasa sama sekali',
highestScoreDescription: '5: Sudah sangat berubah',
choiceList: likertScale5,
},
{
label:
'Secara rata-rata, sebelum waktu makan selama 1 minggu terakhir ini, dimana level rasa lapar yang Anda rasakan?',
placeholder: 'Cek indikator di program book kamu ya',
name: 'hunger_level',
lowestScoreDescription: '1: Sangat kelaparan',
highestScoreDescription: '10: Sangat begah (kenyang berlebihan)',
choiceList: likertScale10,
},
{
label:
'Secara rata-rata, saat berhenti makan selama 1 minggu terakhir ini, dimana level rasa kenyang yang Anda rasakan?',
placeholder: 'Cek indikator di program book kamu ya',
name: 'fullness_level',
lowestScoreDescription: '1: Sangat kelaparan',
highestScoreDescription: '10: Sangat begah (kenyang berlebihan)',
choiceList: likertScale10,
},
{
label:
'Selama 1 minggu terakhir, secara rata-rata, berapa kali Anda makan berat atau makan utama dalam 1 hari?',
placeholder: 'Pilih salah satu',
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?',
placeholder: 'Pilih salah satu',
name: 'snacks',
choiceList: [
'1x/hari',
'2x/hari',
'3x/hari',
'Lebih dari 3x/hari',
'Hampir tidak ada',
],
},
],
dietReportPage3: [
{
label: 'Minuman manis (satuan: gelas)',
placeholder: 'Pilih salah satu',
name: 'sweet_beverages',
choiceList: averageConsumptionOptions,
},
{
label:
'Gula pasir, gula aren, sirup, selai, atau madu (satuan: sendok makan)',
placeholder: 'Pilih salah satu',
name: 'sugary_ingredients',
choiceList: averageConsumptionOptions,
},
{
label: 'Cemilan digoreng (satuan: potong)',
placeholder: 'Pilih salah satu',
name: 'fried_snacks',
choiceList: averageConsumptionOptions,
},
{
label:
'Makanan ringan asin atau gurih (seperti makanan ringan kemasan, ciki-cikian, keripik) (satuan: bungkus)',
placeholder: 'Pilih salah satu',
name: 'umami_snacks',
choiceList: averageConsumptionOptions,
},
{
label:
'Cemilan manis (seperti kue-kue manis, brownis, cake, biskuit, cokelat, wafer) (satuan: potong)',
placeholder: 'Pilih salah satu',
name: 'sweet_snacks',
choiceList: averageConsumptionOptions,
},
{
label: 'Porsi buah',
placeholder: 'Pilih salah satu',
name: 'fruits_portion',
choiceList: averageConsumptionOptions,
},
{
label: 'Porsi sayur',
placeholder: 'Pilih salah satu',
name: 'vegetables_portion',
choiceList: averageConsumptionOptions,
},
],
dietReportPage4: [
{
label:
'Selama 1 minggu terakhir, pilih semua jenis aktivitas atau olahraga yang sudah Anda lakukan',
placeholder: 'Dapat memilih lebih dari satu',
name: 'physical_activity',
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',
],
},
{
label:
'Selama 1 minggu (7 hari) terakhir, berapa total menit yang Anda habiskan untuk melakukan bergerak aktif dan olahraga di atas dalam seminggu?',
placeholder: 'Pilih salah satu',
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?',
placeholder: 'Pilih salah satu',
name: 'feeling_rating',
choiceList: [
'Bintang 1: Rasanya mau menyerah saja',
'Bintang 2: Capek, susah, bosen, males, repot, sibuk',
'Bintang 3: Biasa aja, meski ada kendala tapi semua bisa diatur',
'Bintang 4: Lancar terus, semangat cukup stabil, gak ada masalah',
'Bintang 5: Super seneng, semangat banget, worry-free lah',
],
},
],
};
export const validWeeklyReportCommentValues: { [_: string]: any } = {
weight: 'baik',
height: 'baik',
waist_size: 'baik',
changes_felt: 'baik',
hunger_level: 'baik',
fullness_level: 'baik',
heavy_meal: 'baik',
snacks: 'baik',
sweet_beverages: 'baik',
sugary_ingredients: 'baik',
fried_snacks: 'baik',
umami_snacks: 'baik',
sweet_snacks: 'baik',
fruits_portion: 'baik',
vegetables_portion: 'baik',
water_consumption: 'baik',
physical_activity: 'baik',
physical_activity_other: 'baik',
time_for_activity: 'baik',
feeling_rating: 'baik',
lesson_learned: 'baik',
problem_faced_and_feedbacks: 'baik',
};
import React from 'react';
import { render } from 'utils/testing';
import * as ROUTES from 'constants/routes';
import DietReportPage from '.';
import { dietReportPage1 } from '../../pages/DietReportPage1';
import { mockUserReportResponse } from '__mocks__/userReport';
import { dietReportPage } from '../../pages/types';
import { dietReportPage2 } from '../../pages/DietReportPage2';
import { dietReportPage3 } from '../../pages/DietReportPage3';
import { dietReportPage4 } from '../../pages/DietReportPage4';
describe('DietReportPage', () => {
it('diet report page 1 renders correctly', () => {
render(
<DietReportPage
pageName={dietReportPage.PAGE1}
content={dietReportPage1(mockUserReportResponse, jest.fn())}
getTextInputProps={jest.fn()}
/>,
ROUTES.clientDietReportNutritionist,
);
});
it('diet report page 2 renders correctly', () => {
render(
<DietReportPage
pageName={dietReportPage.PAGE2}
content={dietReportPage2(mockUserReportResponse, jest.fn())}
getTextInputProps={jest.fn()}
/>,
ROUTES.clientDietReportNutritionist,
);
});
it('diet report page 3 renders correctly', () => {
render(
<DietReportPage
pageName={dietReportPage.PAGE3}
content={dietReportPage3(mockUserReportResponse, jest.fn())}
getTextInputProps={jest.fn()}
/>,
ROUTES.clientDietReportNutritionist,
);
});
it('diet report page 4 renders correctly', () => {
render(
<DietReportPage
pageName={dietReportPage.PAGE4}
content={dietReportPage4(mockUserReportResponse, jest.fn())}
getTextInputProps={jest.fn()}
/>,
ROUTES.clientDietReportNutritionist,
);
});
});
......@@ -7,12 +7,12 @@ import { DietReportPageContent, dietReportPage } from '../../pages/types';
import QuestionAnswerCommentCard from '../QuestionAnswerCommentCard';
import { Text } from 'react-native-elements';
import { typographyStyles } from 'styles';
import { TextInput } from 'react-native-gesture-handler';
import { TextField } from 'components/form';
const DietReportPage: FC<{
content: DietReportPageContent;
pageName: string;
getTextInputProps?: any;
getTextInputProps: any;
}> = ({ content, pageName, getTextInputProps }) => {
return (
<ScrollView style={styles.container}>
......@@ -49,11 +49,15 @@ const DietReportPage: FC<{
})}
{pageName === dietReportPage.PAGE3 ? (
<View>
<Text style={[typographyStyles.bodySmall, styles.topMargin]}>
Komentar:
<Text
style={[
typographyStyles.bodySmall,
styles.topMargin,
styles.negativeBottomMargin,
]}>
Komentar: <Text style={styles.red}>*</Text>
</Text>
<TextInput
style={[typographyStyles.bodySmall, styles.input]}
<TextField
{...getTextInputProps('average_consumption')}
placeholder="Tuliskan komentar..."
multiline={true}
......
......@@ -8,12 +8,7 @@ export const styles = StyleSheet.create({
width: Dimensions.get('window').width - 40,
},
input: {
borderWidth: 1,
height: 90,
borderRadius: 4,
marginBottom: 20,
padding: 15,
marginTop: 5,
},
bottomMargin: {
marginBottom: 10,
......@@ -21,4 +16,10 @@ export const styles = StyleSheet.create({
topMargin: {
marginTop: 10,
},
negativeBottomMargin: {
marginBottom: -15,
},
red: {
color: 'red',
},
});
import { FC } from 'react';
import { View, Text, TextInput } from 'react-native';
import { View, Text } from 'react-native';
import React from 'react';
import { InfoCard } from 'components/core';
import { typographyStyles } from 'styles';
import { styles } from './styles';
import { dietReportPage } from '../../pages/types';
import { TextField } from 'components/form';
interface Props {
question: string;
......@@ -52,9 +53,6 @@ const QuestionAnswerCommentCard: FC<Props> = ({
{question}
</Text>
<InfoCard content={answer} />
{/* <Text style={[typographyStyles.bodyMedium, styles.label]}>
{answer}
</Text> */}
</View>
) : null}
......@@ -74,15 +72,16 @@ const QuestionAnswerCommentCard: FC<Props> = ({
{highestScoreDescription}
</Text>
) : null}
{/* <Text style={[typographyStyles.bodyMedium, styles.label]}>
{answer}
</Text> */}
<InfoCard content={answer} />
<Text style={[typographyStyles.bodySmall, styles.topMargin]}>
Komentar:
<Text
style={[
typographyStyles.bodySmall,
styles.topMargin,
styles.negativeBottomMargin,
]}>
Komentar: <Text style={styles.red}>*</Text>
</Text>
<TextInput
style={[typographyStyles.bodySmall, styles.input]}
<TextField
{...textInputProps}
placeholder="Tuliskan komentar..."
multiline={true}
......
......@@ -27,11 +27,12 @@ export const styles = StyleSheet.create({
marginTop: 5,
},
input: {
borderRadius: 4,
borderWidth: 1,
height: 90,
marginTop: 5,
marginBottom: 20,
padding: 15,
},
negativeBottomMargin: {
marginBottom: -15,
},
red: {
color: 'red',
},
});
import React from 'react';
import { render } from 'utils/testing';
import * as ROUTES from 'constants/routes';
import { render, waitFor } from 'utils/testing';
import DietReportForNutritionist from '.';
import { mockUserReportResponse } from '__mocks__/userReport';
import axios from 'axios';
import * as ROUTES from 'constants/routes';
jest.useFakeTimers();
jest.mock('axios');
const mockAxios = axios as jest.Mocked<typeof axios>;
describe('DietReportForNutritionist', () => {
it('renders correctly', () => {
render(<DietReportForNutritionist />, ROUTES.userReport);
const userReports = [mockUserReportResponse];
const retrieveUserReportApi = () =>
Promise.resolve({
status: 200,
data: userReports,
});
it('renders correctly', async () => {
mockAxios.request.mockImplementationOnce(retrieveUserReportApi);
render(<DietReportForNutritionist />, ROUTES.clientDietReportNutritionist, {
routeParams: { id: 1 },
});
await waitFor(() => expect(mockAxios.request).toBeCalled());
});
});
import React, { FC, useState } from 'react';
import { View, StyleSheet, Dimensions } from 'react-native';
import { View, StyleSheet } from 'react-native';
import { layoutStyles } from 'styles';
import Carousel from 'react-native-snap-carousel';
import { pages } from './pages';
import { CarouselPagination, BigButton } from 'components/core';
import { useForm } from 'hooks';
import { WizardContainer, Loader, EmptyDataPage, Toast } from 'components/core';
import { useForm, useApi } from 'hooks';
import { dietReportCommentInitialValues, fieldValidations } from './schema';
import { generateValidationSchema } from 'utils/form';
import { DietReportPage } from './components';
import { mockUserReportResponse } from '__mocks__/userReport';
import { useRoute, useNavigation } from '@react-navigation/native';
import {
retrieveUserReportById,
createNutritionistCommentApi,
} from 'services/progress';
import { NutritionistCommentRequest } from 'services/progress/models';
import * as ROUTES from 'constants/routes';
import {
dietReportTextFields,
dietReportSelectFields,
} from 'constants/weeklyReport';
interface ParamsDietReport {
id: number;
}
const DietReportForNutritionist: FC = () => {
const [activeSlide, setActiveSlide] = useState(0);
const userReport = mockUserReportResponse;
const navigation = useNavigation();
const route = useRoute();
const { id } = route.params as ParamsDietReport;
const { isLoading, data: userReports = [] } = useApi(() =>
retrieveUserReportById(id),
);
const { getTextInputProps, handleSubmit, isSubmitting } = useForm({
const [activeSlide, setActiveSlide] = useState(1);
const {
getTextInputProps,
handleSubmit,
isSubmitting,
isFormUntouched,
isFieldError,
} = useForm({
initialValues: dietReportCommentInitialValues,
validationSchema: generateValidationSchema(fieldValidations),
onSubmit: async (values) => {
console.log('masuk');
console.log(values);
const payload: NutritionistCommentRequest = {
weekly_report: userReports[userReports.length - 1].id,
...values,
};
const response = await createNutritionistCommentApi(payload);
if (response.success && response.data) {
Toast.show({
type: 'success',
text1: 'Sukses membuat komen',
text2: 'Komen anda terhadap laporan mingguan klien sudah tersimpan.',
});
navigation.navigate(ROUTES.clientListForNutritionist);
} else {
Toast.show({
type: 'error',
text1: 'Gagal membuat komen',
text2: 'Komen anda terhadap laporan mingguan klien gagal tersimpan.',
});
}
},
});
if (isLoading) {
return <Loader />;
}
if (!userReports.length) {
return <EmptyDataPage text="Klien belum mengisi laporan diet mingguan" />;
}
const userReport = userReports[userReports.length - 1];
const isCurrentPageError = (): boolean => {
if (activeSlide === 3) {
return isFieldError('average_consumption');
}
const fields = [
...(dietReportTextFields[`dietReportPage${activeSlide}`] || []),
...(dietReportSelectFields[`dietReportPage${activeSlide}`] || []),
];
return (
isFormUntouched() ||
fields.reduce(
(acc: boolean, item) => acc || isFieldError(item.name),
false,
)
);
};
return (
<View style={[layoutStyles, styles.reportContainer]}>
<View style={[styles.flexContainer]}>
<Carousel
data={pages.map((page, idx) => (
<WizardContainer
currentStep={activeSlide}
setCurrentStep={setActiveSlide}
onFinish={handleSubmit}
isLoading={isSubmitting}
isNextDisabled={isCurrentPageError()}
components={pages.map((page, idx) => (
<DietReportPage
key={idx}
content={page.pageContent(userReport, getTextInputProps)}
......@@ -35,19 +108,6 @@ const DietReportForNutritionist: FC = () => {
getTextInputProps={getTextInputProps}
/>
))}
renderItem={({ item }: any) => item}
sliderWidth={Dimensions.get('window').width}
itemWidth={Dimensions.get('window').width}
onSnapToItem={setActiveSlide}
containerCustomStyle={styles.flexContainer}