From 12baec0f9ca776ea5fe41e33422e2661874f959a Mon Sep 17 00:00:00 2001 From: Thomas Mauran <78204354+thomas-mauran@users.noreply.github.com> Date: Tue, 2 Jan 2024 19:20:54 +0100 Subject: [PATCH] feat(frontend): implement employer evaluation (#45) * feat: employer evaluation --------- Signed-off-by: Mauran --- front/assets/translations/en_US.json | 6 + front/assets/translations/fr_FR.json | 6 + .../employer/EmployerEvaluationForm.tsx | 112 +++++++++++++++++ .../components/evaluations/EvaluationItem.tsx | 5 +- .../messaging/MessageChannelItem.tsx | 25 ++-- .../messaging/MessageChannelList.tsx | 7 ++ .../profile/header/ProfileHeaderName.tsx | 4 +- front/src/components/utils/StarRating.tsx | 84 ------------- .../components/utils/StarRatingSelector.tsx | 116 ++++++++++++++++++ .../dtos/employer/EmployerEvaluationDto.ts | 16 +++ front/src/models/entities/employer.ts | 5 + .../src/models/entities/employerEvaluation.ts | 14 +++ .../pages/employer/EmployerEvaluationPage.tsx | 112 +++++++++++++++++ .../messaging/MessageChannelListPage.tsx | 9 ++ front/src/pages/messaging/MessagingNav.tsx | 17 ++- front/src/store/api/apiSlice.ts | 1 + front/src/store/api/employer.ts | 30 +++++ 17 files changed, 473 insertions(+), 96 deletions(-) create mode 100644 front/src/components/employer/EmployerEvaluationForm.tsx delete mode 100644 front/src/components/utils/StarRating.tsx create mode 100644 front/src/components/utils/StarRatingSelector.tsx create mode 100644 front/src/models/dtos/employer/EmployerEvaluationDto.ts create mode 100644 front/src/models/entities/employerEvaluation.ts create mode 100644 front/src/pages/employer/EmployerEvaluationPage.tsx create mode 100644 front/src/store/api/employer.ts diff --git a/front/assets/translations/en_US.json b/front/assets/translations/en_US.json index dfbc776..98b3651 100644 --- a/front/assets/translations/en_US.json +++ b/front/assets/translations/en_US.json @@ -7,6 +7,12 @@ "delete": "Delete", "confirm": "Confirm" }, + "employer": { + "evaluation": "Employer evaluation", + "giveAReview": "Give a review", + "review": "Describe your experience at this job", + "sendEvaluation": "Send my evaluation" + }, "jobOffer": { "info": { "jobOfferList": "Job offer list", diff --git a/front/assets/translations/fr_FR.json b/front/assets/translations/fr_FR.json index ec21fea..4d470a9 100644 --- a/front/assets/translations/fr_FR.json +++ b/front/assets/translations/fr_FR.json @@ -7,6 +7,12 @@ "delete": "Supprimer", "confirm": "Confirmer" }, + "employer": { + "evaluation": "Avis sur l'employeur", + "giveAReview": "Donnez un avis", + "review": "Decrivez votre expérience à ce job", + "sendEvaluation": "Envoyer mon avis" + }, "jobOffer": { "info": { "jobOfferList": "Liste des offres d'emploi", diff --git a/front/src/components/employer/EmployerEvaluationForm.tsx b/front/src/components/employer/EmployerEvaluationForm.tsx new file mode 100644 index 0000000..f6fa38c --- /dev/null +++ b/front/src/components/employer/EmployerEvaluationForm.tsx @@ -0,0 +1,112 @@ +import { FC, useCallback } from 'react'; +import { Image, StyleSheet, View } from 'react-native'; +import { Text, TextInput } from 'react-native-paper'; + +import StarRatingSelector from '@/components/utils/StarRatingSelector'; +import { Employer } from '@/models/entities/employer'; +import i18n from '@/utils/i18n'; + +/** + * The styles for the EmployerEvaluationForm component. + */ +const styles = StyleSheet.create({ + container: { + gap: 6, + }, + nameContainer: { + alignItems: 'center', + flexDirection: 'row', + gap: 8, + }, + profilePicture: { + borderRadius: 48, + height: 60, + width: 60, + }, + textInput: { + marginVertical: 8, + }, +}); + +/** + * The data for the employer evaluation form. + */ +export type EmployerEvaluationFormData = { + score: number; + review: string; +}; + +/** + * The props for the EmployerEvaluationForm component. + */ +type EmployerEvaluationFormProps = { + /** + * The employer to evaluate. + */ + employer: Employer; + + /** + * The data for the form. + */ + formData: EmployerEvaluationFormData; + + /** + * The function to call when the form data is updated. + */ + onFormDataUpdate: (data: EmployerEvaluationFormData) => void; +}; + +/** + * Displays an employer evaluation form. + * @constructor + */ +const EmployerEvaluationForm: FC = ({ + employer, + formData, + onFormDataUpdate, +}) => { + // Callbacks + const handleInputChange = useCallback( + ( + key: keyof EmployerEvaluationFormData, + value: EmployerEvaluationFormData[typeof key], + ) => { + onFormDataUpdate({ + ...formData, + [key]: value, + }); + }, + [formData, onFormDataUpdate], + ); + + return ( + + + + + {`${employer.firstName} ${employer.lastName}`} + + + {i18n.t('employer.giveAReview')} + handleInputChange('score', value)} + /> + handleInputChange('review', value)} + /> + + ); +}; + +export default EmployerEvaluationForm; diff --git a/front/src/components/evaluations/EvaluationItem.tsx b/front/src/components/evaluations/EvaluationItem.tsx index 1227204..5a5acc2 100644 --- a/front/src/components/evaluations/EvaluationItem.tsx +++ b/front/src/components/evaluations/EvaluationItem.tsx @@ -4,9 +4,10 @@ import { StyleSheet, View } from 'react-native'; import { Text } from 'react-native-paper'; import ProfilePicturePlaceholder from '@/components/utils/ProfilePicturePlaceholder'; -import StarRating from '@/components/utils/StarRating'; import { Evaluation } from '@/models/entities/evaluation'; +import StarRatingSelector from '../utils/StarRatingSelector'; + /** * The styles for the EvaluationItem component. */ @@ -54,7 +55,7 @@ const EvaluationItem: FC = ({ evaluation }) => { {`${evaluation.employerFirstName} ${evaluation.employerLastName}`} - + diff --git a/front/src/components/messaging/MessageChannelItem.tsx b/front/src/components/messaging/MessageChannelItem.tsx index f594476..52e26dd 100644 --- a/front/src/components/messaging/MessageChannelItem.tsx +++ b/front/src/components/messaging/MessageChannelItem.tsx @@ -2,6 +2,7 @@ import { FC } from 'react'; import { Image, StyleSheet, View } from 'react-native'; import { Text, TouchableRipple } from 'react-native-paper'; +import { Employer } from '@/models/entities/employer'; import { MessageChannel } from '@/models/entities/messageChannel'; /** @@ -33,10 +34,15 @@ const styles = StyleSheet.create({ */ type MessageChannelItemProps = { /** - * The function to call when an a message channel is pressed. + * The function to call when a message channel is pressed. */ onItemPress?: (messageChannel: MessageChannel) => void; + /** + * The function to call when the profile picture is pressed. + */ + onProfilePress?: (employerId: Employer) => void; + /** * The message channel to display. */ @@ -49,17 +55,22 @@ type MessageChannelItemProps = { */ const MessageChannelItem: FC = ({ onItemPress, + onProfilePress, messageChannel, }) => { return ( onItemPress?.(messageChannel)}> - + onProfilePress(messageChannel.employer)} + > + + void; + /** + * The function to call when the profile picture is pressed. + */ + onProfilePress?: (employer: Employer) => void; /** * The list of message channels to display. */ @@ -31,6 +36,7 @@ type MessageChannelListProps = { const MessageChannelList: FC = ({ onItemPress, + onProfilePress, messageChannels, }) => { return ( @@ -39,6 +45,7 @@ const MessageChannelList: FC = ({ diff --git a/front/src/components/profile/header/ProfileHeaderName.tsx b/front/src/components/profile/header/ProfileHeaderName.tsx index 6d2d757..a9be5bb 100644 --- a/front/src/components/profile/header/ProfileHeaderName.tsx +++ b/front/src/components/profile/header/ProfileHeaderName.tsx @@ -2,7 +2,7 @@ import { FC } from 'react'; import { Image, StyleSheet, View } from 'react-native'; import { Text } from 'react-native-paper'; -import StarRating from '@/components/utils/StarRating'; +import StarRatingSelector from '@/components/utils/StarRatingSelector'; /** * The styles for the ProfileHeaderName component. @@ -71,7 +71,7 @@ const ProfileHeaderName: FC = ({ {firstName} {lastName} - + ); diff --git a/front/src/components/utils/StarRating.tsx b/front/src/components/utils/StarRating.tsx deleted file mode 100644 index b4b5d57..0000000 --- a/front/src/components/utils/StarRating.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { FC, useMemo } from 'react'; -import { StyleSheet, View } from 'react-native'; -import { Text, useTheme } from 'react-native-paper'; -import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; - -/** - * The kind of a single star. - */ -enum StarKind { - FULL_STAR, - HALF_STAR, - EMPTY_STAR, -} - -/** - * The styles for the StarRating component. - */ -const styles = StyleSheet.create({ - container: { - alignItems: 'center', - flexDirection: 'row', - }, - ratingText: { - marginLeft: 8, - }, -}); - -/** - * The props for the StarRating component. - */ -type StarRatingProps = { - /** - * The rating to display as stars. - */ - rating: number; -}; - -const StarRating: FC = ({ rating }) => { - // Theme - const theme = useTheme(); - - const starArray = useMemo((): StarKind[] => { - const res: StarKind[] = []; - let remaining = Math.floor(Math.max(0, Math.min(5, rating)) * 2); - - while (remaining >= 2) { - res.push(StarKind.FULL_STAR); - remaining -= 2; - } - - if (remaining === 1) { - res.push(StarKind.HALF_STAR); - } - - while (res.length < 5) { - res.push(StarKind.EMPTY_STAR); - } - - return res; - }, [rating]); - - return ( - - {starArray.map((star, index) => ( - - ))} - - {rating} - - ); -}; - -export default StarRating; diff --git a/front/src/components/utils/StarRatingSelector.tsx b/front/src/components/utils/StarRatingSelector.tsx new file mode 100644 index 0000000..03b6fde --- /dev/null +++ b/front/src/components/utils/StarRatingSelector.tsx @@ -0,0 +1,116 @@ +import { FC, useMemo, useState } from 'react'; +import { StyleSheet, TouchableOpacity, View } from 'react-native'; +import { Text, useTheme } from 'react-native-paper'; +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; + +/** + * The kind of a single star. + */ +enum StarKind { + FULL_STAR, + HALF_STAR, + EMPTY_STAR, +} + +/** + * The styles for the StarRatingSelector component. + */ +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + flexDirection: 'row', + }, + ratingText: { + marginLeft: 8, + }, +}); + +/** + * The props for the StarRatingSelector component. + */ +type StarRatingSelectorProps = { + /** + * Whether the stars are editable. + */ + editable?: boolean; + /** + * The rating to display as stars. + */ + rating: number; + /** + * Callback function to handle star press. + */ + onStarPress?: (index: number) => void; +}; + +const StarRatingSelector: FC = ({ + editable = true, // Default to true if not provided + rating, + onStarPress, +}) => { + // Theme + const theme = useTheme(); + const [selectedStars, setSelectedStars] = useState(0); + + const starArray = useMemo((): StarKind[] => { + const res: StarKind[] = []; + let remaining = Math.floor(Math.max(0, Math.min(5, rating)) * 2); + + while (remaining >= 2) { + res.push(StarKind.FULL_STAR); + remaining -= 2; + } + + if (remaining === 1) { + res.push(StarKind.HALF_STAR); + } + + while (res.length < 5) { + res.push(StarKind.EMPTY_STAR); + } + + return res; + }, [rating]); + + const handleStarPress = (index: number) => { + if (editable) { + setSelectedStars(index + 1); + if (onStarPress) { + onStarPress(index + 1); + } + } + }; + + return ( + + {starArray.map((star, index) => ( + handleStarPress(index)} + disabled={!editable} + > + + + ))} + + {rating} + + ); +}; + +export default StarRatingSelector; diff --git a/front/src/models/dtos/employer/EmployerEvaluationDto.ts b/front/src/models/dtos/employer/EmployerEvaluationDto.ts new file mode 100644 index 0000000..e2c64a9 --- /dev/null +++ b/front/src/models/dtos/employer/EmployerEvaluationDto.ts @@ -0,0 +1,16 @@ +import { EmployerEvaluation } from '@/models/entities/employerEvaluation'; + +/** + * DTO for creating a new employer evaluation. + */ +export type EmployerEvaluationDto = { + /** + * The id of the employer. + */ + id: string; + + /** + * The employer evaluation. + */ + evaluation: EmployerEvaluation; +}; diff --git a/front/src/models/entities/employer.ts b/front/src/models/entities/employer.ts index ec05cae..176d040 100644 --- a/front/src/models/entities/employer.ts +++ b/front/src/models/entities/employer.ts @@ -2,6 +2,11 @@ * An employer. */ export type Employer = { + /** + * The id of the employer + */ + id: string; + /** * The first name of the employer. */ diff --git a/front/src/models/entities/employerEvaluation.ts b/front/src/models/entities/employerEvaluation.ts new file mode 100644 index 0000000..7b7e82f --- /dev/null +++ b/front/src/models/entities/employerEvaluation.ts @@ -0,0 +1,14 @@ +/** + * An employer evaluation. + */ +export type EmployerEvaluation = { + /** + * The score of an employer from a seasonal worker. + */ + score: number; + + /** + * The review of an employer from a seasonal worker. + */ + review: string; +}; diff --git a/front/src/pages/employer/EmployerEvaluationPage.tsx b/front/src/pages/employer/EmployerEvaluationPage.tsx new file mode 100644 index 0000000..4d9bd96 --- /dev/null +++ b/front/src/pages/employer/EmployerEvaluationPage.tsx @@ -0,0 +1,112 @@ +import { useFocusEffect } from '@react-navigation/native'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { FC, useCallback, useState } from 'react'; +import { StyleSheet, View } from 'react-native'; +import { Button } from 'react-native-paper'; + +import EmployerEvaluationForm, { + EmployerEvaluationFormData, +} from '@/components/employer/EmployerEvaluationForm'; +import { EmployerEvaluationDto } from '@/models/dtos/employer/EmployerEvaluationDto'; +import { + useGetEmployerQuery, + usePostEmployerEvaluationMutation, +} from '@/store/api/employer'; +import i18n from '@/utils/i18n'; + +import { MessagingStackParamList } from '../messaging/MessagingNav'; + +/** + * The styles for the EmployerEvaluationPage component. + */ +const styles = StyleSheet.create({ + contentContainer: { + flex: 1, + paddingBottom: 8, + paddingHorizontal: 16, + }, +}); + +/** + * The props for the EmployerEvaluationPage component. + */ +type EmployerEvaluationPageProps = NativeStackScreenProps< + MessagingStackParamList, + 'EmployerEvaluation' +>; + +/** + * The parameters for the Employer route. + */ +export type EmployerEvaluationPageParams = { + /** + * The ID of the message channel to view. + */ + id: string; +}; + +/** + * Displays the page for a single message channel. + * @constructor + */ +const EmployerEvaluationPage: FC = ({ + route, + navigation, +}) => { + const { id: employerId } = route.params; + + // API calls + const { data: employer, refetch: refetchEmployer } = + useGetEmployerQuery(employerId); + + const [postEmployerEvaluation] = usePostEmployerEvaluationMutation(); + + // State + const [formData, setFormData] = useState({ + score: 1, + review: '', + }); + + // Callbacks + const handleSubmitEvaluation = useCallback(() => { + const newEvaluation: EmployerEvaluationDto = { + id: employerId, + evaluation: { + score: formData.score, + review: formData.review, + }, + }; + + postEmployerEvaluation(newEvaluation) + .unwrap() + .then(() => { + navigation.goBack(); + }); + }, [employerId, formData, navigation, postEmployerEvaluation]); + + // Fetch data from the API when the page is focused + useFocusEffect( + useCallback(() => { + refetchEmployer(); + }, [refetchEmployer]), + ); + + if (employer === undefined) { + return null; + } + + return ( + + + + + ); +}; + +export default EmployerEvaluationPage; diff --git a/front/src/pages/messaging/MessageChannelListPage.tsx b/front/src/pages/messaging/MessageChannelListPage.tsx index 07ecbf9..f2a25ed 100644 --- a/front/src/pages/messaging/MessageChannelListPage.tsx +++ b/front/src/pages/messaging/MessageChannelListPage.tsx @@ -4,6 +4,7 @@ import { FC, useCallback } from 'react'; import { ScrollView, StyleSheet } from 'react-native'; import MessagingList from '@/components/messaging/MessageChannelList'; +import { Employer } from '@/models/entities/employer'; import { MessageChannel } from '@/models/entities/messageChannel'; import { useGetMessageChannelsQuery } from '@/store/api/messagingApiSlice'; @@ -48,6 +49,13 @@ const MessageChannelListPage: FC = ({ navigation }) => { [navigation], ); + const handleProfilePicturePress = useCallback( + (employer: Employer) => { + navigation.navigate('EmployerEvaluation', { id: employer.id }); + }, + [navigation], + ); + // Fetch data from the API when the page is focused useFocusEffect( useCallback(() => { @@ -66,6 +74,7 @@ const MessageChannelListPage: FC = ({ navigation }) => { > diff --git a/front/src/pages/messaging/MessagingNav.tsx b/front/src/pages/messaging/MessagingNav.tsx index e1c0890..18dc241 100644 --- a/front/src/pages/messaging/MessagingNav.tsx +++ b/front/src/pages/messaging/MessagingNav.tsx @@ -3,6 +3,9 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; import PaperNavigationBar from '@/components/utils/PaperNavigationBar'; import i18n from '@/utils/i18n'; +import EmployerEvaluationPage, { + EmployerEvaluationPageParams, +} from '../employer/EmployerEvaluationPage'; import MessageChannelPage, { MessageChannelPageParams, } from './MessageChannelPage'; @@ -15,6 +18,7 @@ export type MessagingStackParamList = { MessageChannelList: undefined; MessageChannel: MessageChannelPageParams; MessageTabBar: undefined; + EmployerEvaluation: EmployerEvaluationPageParams; }; const MessagingStack = createNativeStackNavigator(); @@ -35,7 +39,18 @@ const MessagingNav = () => { + ({}), tagTypes: [ 'Availabilities', + 'Employer', 'Evaluations', 'Experiences', 'JobCategories', diff --git a/front/src/store/api/employer.ts b/front/src/store/api/employer.ts new file mode 100644 index 0000000..e9202c7 --- /dev/null +++ b/front/src/store/api/employer.ts @@ -0,0 +1,30 @@ +import { EmployerEvaluationDto } from '@/models/dtos/employer/EmployerEvaluationDto'; +import { Employer } from '@/models/entities/employer'; +import { EmployerEvaluation } from '@/models/entities/employerEvaluation'; +import { apiSlice } from '@/store/api/apiSlice'; + +/** + * The extended API slice for employers. + */ +export const extendedApiSlice = apiSlice.injectEndpoints({ + endpoints: (builder) => ({ + getEmployer: builder.query({ + query: (id) => `employers/${id}/`, + providesTags: (_result, _error, id) => [{ type: 'Employer', id }], + }), + postEmployerEvaluation: builder.mutation< + EmployerEvaluation, + EmployerEvaluationDto + >({ + query: (body) => ({ + url: `employers/${body.id}/evaluations/`, + method: 'POST', + body: body.evaluation, + }), + invalidatesTags: [{ type: 'Employer', id: 'LIST' }], + }), + }), +}); + +export const { useGetEmployerQuery, usePostEmployerEvaluationMutation } = + extendedApiSlice;