From b46ecd395aecd410a294f76de15ff062a7d54ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20La=20Scala?= Date: Wed, 6 Nov 2024 15:12:41 +0100 Subject: [PATCH] feat: add results viz for ratings work in progress --- src/modules/common/response/Response.tsx | 3 +- .../visualization/RatingsVisualization.tsx | 98 ++++++++++----- src/modules/context/RatingsContext.tsx | 119 +++++++++++++++++- src/modules/results/RatingsResults.tsx | 38 ++++++ src/modules/results/ResultsView.tsx | 24 +++- 5 files changed, 240 insertions(+), 42 deletions(-) create mode 100644 src/modules/results/RatingsResults.tsx diff --git a/src/modules/common/response/Response.tsx b/src/modules/common/response/Response.tsx index 3ae48571..2104c649 100644 --- a/src/modules/common/response/Response.tsx +++ b/src/modules/common/response/Response.tsx @@ -168,7 +168,6 @@ const Response: FC = ({ variant="outlined" sx={{ width: '100%', - // backgroundColor: highlight ? 'hsla(0, 100%, 90%, 0.3)' : 'white', }} data-cy={RESPONSE_CY} > @@ -217,7 +216,7 @@ const Response: FC = ({ {renderEvaluationComponent()} - {showRatings && } + {showRatings && } {typeof nbrOfVotes !== 'undefined' && } {showActions && ( <> diff --git a/src/modules/common/response/visualization/RatingsVisualization.tsx b/src/modules/common/response/visualization/RatingsVisualization.tsx index b3a88e12..b9c6a779 100644 --- a/src/modules/common/response/visualization/RatingsVisualization.tsx +++ b/src/modules/common/response/visualization/RatingsVisualization.tsx @@ -1,36 +1,68 @@ -import { useTranslation } from 'react-i18next'; - -import { TRANSLATIONS_NS } from '@/config/i18n'; - -const RatingsVisualization = (): JSX.Element => { - const { t } = useTranslation(TRANSLATIONS_NS); - // const { activity } = useSettings(); - // const { evaluationType } = activity; - // const { getRatingsForResponse } = useRatings(evaluationType); - - // const ratings = useMemo( - // () => getRatingsForResponse(response.id), - // [getRatingsForResponse, response], - // ); - - // if (evaluationType === EvaluationType.DimensionsOfGIRating) { - // return ( - // []} - // /> - // ); - // } - // if (evaluationType === EvaluationType.SFERARating) { - // return []} />; - // } - // if (evaluationType === EvaluationType.UsefulnessNoveltyRating) { - // return ( - // []} - // /> - // ); - // } - return <>{t('NO_VISUALIZATION')}; +import { FC, useEffect, useState } from 'react'; + +import CircularProgress from '@mui/material/CircularProgress'; +import Stack from '@mui/material/Stack'; + +import { RatingData } from '@/config/appDataTypes'; +import { useRatingsContext } from '@/modules/context/RatingsContext'; + +import CircularIndicator from './indicators/CircularIndicator'; + +interface RatingsVisualizationProps { + responseId: string; +} + +const RatingsVisualization: FC = ({ + responseId, +}): JSX.Element => { + const { + ratings: ratingsDef, + getRatingsStatsForResponse, + ratingsThresholds, + } = useRatingsContext(); + + const [ratings, setRatings] = useState( + undefined, + ); + + useEffect(() => { + getRatingsStatsForResponse(responseId).then((d) => setRatings(d)); + }, [getRatingsStatsForResponse, responseId]); + + if (typeof ratingsDef === 'undefined' || typeof ratings === 'undefined') { + // TODO: Make that look good. + return ; + } + + const nbrRatings = ratingsDef?.length ?? 0; + + return ( + + {ratingsDef.map((singleRatingDefinition, index) => { + const { name } = singleRatingDefinition; + if (ratings) { + const result = ratings[index]; + console.info(`For aspect ${name}, result is:`, result.value); + return ( + + ); + } + return ; + })} + + ); }; export default RatingsVisualization; diff --git a/src/modules/context/RatingsContext.tsx b/src/modules/context/RatingsContext.tsx index 89b31778..ea2deb32 100644 --- a/src/modules/context/RatingsContext.tsx +++ b/src/modules/context/RatingsContext.tsx @@ -1,13 +1,21 @@ import { FC, createContext, useCallback, useContext, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import { useLocalContext } from '@graasp/apps-query-client'; import { AppDataVisibility } from '@graasp/sdk'; import { AppDataTypes, RatingAppData, RatingData } from '@/config/appDataTypes'; +import { TRANSLATIONS_NS } from '@/config/i18n'; import { EvaluationParameters } from '@/interfaces/evaluation'; import { useAppDataContext } from './AppDataContext'; +type ThresholdType = { + value: number; + label: string; + color: 'success' | 'warning' | 'error' | 'primary'; +}; + type RatingsContextType = { ratingsName: EvaluationParameters['ratingsName']; ratings: EvaluationParameters['ratings']; @@ -15,6 +23,13 @@ type RatingsContextType = { myRatings: Array; rate: (rating: RatingData) => Promise; removeRatingsFor: (responseId: string) => void; + getAllRatingsForResponse: ( + responseId: string, + ) => Promise | undefined>; + getRatingsStatsForResponse: ( + responseId: string, + ) => Promise; + ratingsThresholds: ThresholdType[]; }; const defaultContextValue: RatingsContextType = { ratingsName: undefined, @@ -23,6 +38,9 @@ const defaultContextValue: RatingsContextType = { myRatings: [], rate: () => Promise.resolve(), removeRatingsFor: () => Promise.resolve(), + getAllRatingsForResponse: () => Promise.resolve(undefined), + getRatingsStatsForResponse: () => Promise.resolve(undefined), + ratingsThresholds: [], }; const RatingsContext = createContext(defaultContextValue); @@ -36,6 +54,7 @@ export const RatingsProvider: FC = ({ evaluationParameters, children, }) => { + const { t } = useTranslation(TRANSLATIONS_NS); const { accountId } = useLocalContext(); const { appData, postAppData, deleteAppData, patchAppData } = @@ -101,6 +120,91 @@ export const RatingsProvider: FC = ({ [deleteAppData, findRatingsFor], ); + const getAllRatingsForResponse = useCallback( + async (responseId: string): Promise => + allRatings?.filter(({ data }) => data.responseRef === responseId), + [allRatings], + ); + + const ratingsLevels = useMemo(() => { + const levels = new Map(ratings.map((r) => [r.name, r.levels])); + return levels; + }, [ratings]); + + const computeMeanRatings = ( + accumulatedRatings: RatingData['ratings'], + currentRating: RatingData['ratings'], + _index: number, + array: RatingData['ratings'][], + ): RatingData['ratings'] => { + const nbrRatings = array.length; + const newAccumulatedRatings: RatingData['ratings'] = currentRating.map( + (r, index) => { + let prevVal = 0; + if (accumulatedRatings.length > index) { + prevVal = accumulatedRatings[index].value; + } + const value = + r.value / ((ratingsLevels.get(r.name) ?? 1) * nbrRatings) + prevVal; + console.debug( + `Accumulating for aspect ${r.name}, current value = ${value} (prevVal=${prevVal})`, + ); + return { + ...r, + value, + }; + }, + ); + return newAccumulatedRatings; + }; + + const getRatingsStatsForResponse = useCallback( + async (responseId: string): Promise => { + const ratingsForResponse = await getAllRatingsForResponse(responseId); + if (typeof ratingsForResponse !== 'undefined') { + const extractedRatings = ratingsForResponse.map( + ({ data }) => data.ratings, + ); + + const initialVal = extractedRatings[0].map((r) => ({ + ...r, + value: 0, + })); + + const accumulatedRatings = extractedRatings.reduce( + computeMeanRatings, + initialVal, + ); + console.debug('Accumulated ratings:'); + console.debug(accumulatedRatings); + return accumulatedRatings; + } + return undefined; + }, + [computeMeanRatings, getAllRatingsForResponse], + ); + + const ratingsThresholds: ThresholdType[] = useMemo( + () => [ + { + value: 0, + color: 'error', + label: t('BAD'), + }, + { + value: 1 / 3, + color: 'warning', + label: t('OKAY'), + }, + { + value: 2 / 3, + color: 'success', + label: t('GOOD'), + }, + ], + [t], + ); + const contextValue = useMemo( () => ({ ratingsName, @@ -109,8 +213,21 @@ export const RatingsProvider: FC = ({ myRatings, rate, removeRatingsFor, + getAllRatingsForResponse, + getRatingsStatsForResponse, + ratingsThresholds, }), - [allRatings, myRatings, rate, ratings, ratingsName, removeRatingsFor], + [ + allRatings, + getAllRatingsForResponse, + getRatingsStatsForResponse, + myRatings, + rate, + ratings, + ratingsName, + ratingsThresholds, + removeRatingsFor, + ], ); return ( diff --git a/src/modules/results/RatingsResults.tsx b/src/modules/results/RatingsResults.tsx new file mode 100644 index 00000000..df73145b --- /dev/null +++ b/src/modules/results/RatingsResults.tsx @@ -0,0 +1,38 @@ +import { FC } from 'react'; + +import Grid from '@mui/material/Grid'; +import Stack from '@mui/material/Stack'; + +import Response from '@/modules/common/response/Response'; + +import ExportResponsesButton from '../common/ExportRepsonsesButton'; +import { useActivityContext } from '../context/ActivityContext'; + +type RatingsResultsProps = unknown; + +const RatingsResults: FC = () => { + const { allResponses } = useActivityContext(); + // const sortedResponses = useMemo( + // () => sortResponsesByNumberOfVote(allResponses, allVotes), + // [allResponses, allVotes], + // ); + return ( + + + {allResponses.map((response) => ( + + + + ))} + + + + ); +}; + +export default RatingsResults; diff --git a/src/modules/results/ResultsView.tsx b/src/modules/results/ResultsView.tsx index 3cb1f4b9..397a88a3 100644 --- a/src/modules/results/ResultsView.tsx +++ b/src/modules/results/ResultsView.tsx @@ -10,14 +10,22 @@ import Pausable from '../common/Pausable'; import { RatingsProvider } from '../context/RatingsContext'; import { VoteProvider } from '../context/VoteContext'; import NoEvaluationResults from './NoEvaluationResults'; +import RatingsResults from './RatingsResults'; import VoteResults from './VoteResults'; type ResultsViewProps = unknown; // eslint-disable-next-line @typescript-eslint/no-unused-vars const ResultsView = (props: ResultsViewProps): JSX.Element => { - const { currentStep } = useSteps(); + const { currentStep, previousStep } = useSteps(); const resultsType = currentStep?.resultsType ?? DEFAULT_EVALUATION_TYPE; + /** + * With this mechanism, bear in mind that the configuration of + * the previous step is used to determine the evaluation parameters. + * Therefore, one need to have the results step immediately after + * the evaluation step. + */ + const evaluationParameters = previousStep?.evaluationParameters; const renderResultsContext = (): JSX.Element | null => { switch (resultsType) { @@ -28,11 +36,15 @@ const ResultsView = (props: ResultsViewProps): JSX.Element => { ); case EvaluationType.Rate: - return ( - -

Nothing.

-
- ); + if (evaluationParameters) { + return ( + + + + ); + } + return ; + default: return ; }