Fakultas Ilmu Komputer UI
Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
ppl-fasilkom-ui
2021
Kelas D
PT Gizi Sehat - Dietela
Dietela Mobile
Commits
7ad61e21
Commit
7ad61e21
authored
Jun 02, 2021
by
Kefas Satrio Bangkit Solidedantyo
Browse files
progress diet nutritionist backend integration
parent
5b4c01da
Changes
15
Show whitespace changes
Inline
Side-by-side
src/components/core/ClientList/index.tsx
View file @
7ad61e21
...
...
@@ -77,7 +77,9 @@ const ClientList: FC<Props> = ({
});
}
}
onPressClientDietReport
=
{
()
=>
{
navigation
.
navigate
(
clientDietReportRoute
,
{});
navigation
.
navigate
(
clientDietReportRoute
,
{
id
:
client
.
user
.
id
,
});
}
}
onPressClientChat
=
{
()
=>
{
navigation
.
navigate
(
clientChatRoute
,
{});
...
...
src/constants/userReport.ts
deleted
100644 → 0
View file @
5b4c01da
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
'
,
],
},
],
};
src/mocks/weeklyReport.ts
0 → 100644
View file @
7ad61e21
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
'
,
};
src/scenes/nutritionist/DietReportForNutritionist/components/DietReportPage/index.test.tsx
0 → 100644
View file @
7ad61e21
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
,
);
});
});
src/scenes/nutritionist/DietReportForNutritionist/components/DietReportPage/index.tsx
View file @
7ad61e21
...
...
@@ -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
{
Text
Input
}
from
'
react-native-gesture-handler
'
;
import
{
Text
Field
}
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
}
...
...
src/scenes/nutritionist/DietReportForNutritionist/components/DietReportPage/styles.ts
View file @
7ad61e21
...
...
@@ -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
'
,
},
});
src/scenes/nutritionist/DietReportForNutritionist/components/QuestionAnswerCommentCard/index.tsx
View file @
7ad61e21
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
}
...
...
src/scenes/nutritionist/DietReportForNutritionist/components/QuestionAnswerCommentCard/styles.ts
View file @
7ad61e21
...
...
@@ -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
'
,
},
});
src/scenes/nutritionist/DietReportForNutritionist/index.test.tsx
View file @
7ad61e21
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
());
});
});
src/scenes/nutritionist/DietReportForNutritionist/index.tsx
View file @
7ad61e21
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
}