From 2b77f01b5499c3452f339c95794a597ad59ae721 Mon Sep 17 00:00:00 2001 From: Luca Pezzolla Date: Tue, 16 Jan 2024 13:00:53 +0100 Subject: [PATCH] feat(transcript): add provisional grades to transcript Refs #415 --- assets/translations/it.json | 14 ++ lib/ui/components/VerticalDashedLine.tsx | 29 +++ package-lock.json | 4 +- src/core/queries/studentHooks.ts | 33 ++++ .../transcript/components/GradeStates.tsx | 136 ++++++++++++++ .../screens/ProvisionalGradeScreen.tsx | 166 ++++++++++++++---- 6 files changed, 344 insertions(+), 38 deletions(-) create mode 100644 lib/ui/components/VerticalDashedLine.tsx create mode 100644 src/features/transcript/components/GradeStates.tsx diff --git a/assets/translations/it.json b/assets/translations/it.json index 4066e869..a0b7148c 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -609,6 +609,20 @@ "title": "Profilo", "trainingOffer": "Offerta formativa" }, + "provisionalGradeScreen": { + "title": "Valutazione", + "contactProfessorCta": "Contatta il docente", + "rejectGradeCta": "Rifiuta la valutazione", + "acceptGradeCta": "Richiedi la registrazione immediata", + "acceptGradeFeedback": "Valutazione registrata, verrà visualizzata nel libretto", + "rejectGradeFeedback": "Valutazione rifiutata, verrà registrata nelle prossime ore", + "pendingState": "In fase di valutazione", + "pendingStateDescription": "La valutazione è in fase di valutazione da parte del docente", + "publishedState": "Pubblicato", + "publishedStateDescription": "La valutazione è stata pubblicata dal docente, ma è ancora modificabile - stato opzionale", + "confirmedState": "Consolidato", + "confirmedStateDescription": "La valutazione non è più modificabile - hai 24 ore per rifiutarla" + }, "sectionHeader": { "cta": "Vedi tutti", "ctaMoreSuffix": "(altri {{- count}})" diff --git a/lib/ui/components/VerticalDashedLine.tsx b/lib/ui/components/VerticalDashedLine.tsx new file mode 100644 index 00000000..3af85604 --- /dev/null +++ b/lib/ui/components/VerticalDashedLine.tsx @@ -0,0 +1,29 @@ +import * as Svg from 'react-native-svg'; +import { SvgProps } from 'react-native-svg/src/elements/Svg'; + +export const VerticalDashedLine = ({ + height, + width, + color, + style, + ...rest +}: SvgProps) => { + return ( + + + + ); +}; diff --git a/package-lock.json b/package-lock.json index c12d4c90..0a1054c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3829,8 +3829,8 @@ }, "node_modules/@polito/api-client": { "version": "1.0.0-ALPHA.54", - "resolved": "https://npm.pkg.github.com/download/@polito/api-client/1.0.0-ALPHA.54/bcc05d495431e945be303c676c9252b2a8700951", - "integrity": "sha512-Krvr8x9IArFFF8tdpECGShgryzRn9tdZHF0gudnZiTQxFxWRpF8qDavs+28nsViOit7AEtQOklAifKOr6eaB4Q==" + "resolved": "https://npm.pkg.github.com/download/@polito/api-client/1.0.0-ALPHA.54/fb0cc270393ee2e1624b45249dea52e18820f178", + "integrity": "sha512-jzWDAKwter8KxGQroPwTrLdxCPgOQWy0nHo37i0rAdb3Cx/P3T38uK1w/4QDtoATlJy/8jdziBntniL4aybGig==" }, "node_modules/@react-native-async-storage/async-storage": { "version": "1.19.8", diff --git a/src/core/queries/studentHooks.ts b/src/core/queries/studentHooks.ts index 8f4963ea..55367ffb 100644 --- a/src/core/queries/studentHooks.ts +++ b/src/core/queries/studentHooks.ts @@ -153,6 +153,39 @@ export const useGetProvisionalGrades = () => { // ?() => studentClient.getStudentProvisionalGrades().then(pluckData),); }; +export const useAcceptProvisionalGrade = () => { + const queryClient = useQueryClient(); + const studentClient = useStudentClient(); + + return useMutation( + (id: number) => + studentClient.acceptProvisionalGrade({ provisionalGradeId: id }), + { + onSuccess: () => + Promise.all([ + queryClient.invalidateQueries(PROVISIONAL_GRADES_QUERY_KEY), + queryClient.invalidateQueries(GRADES_QUERY_KEY), + ]), + }, + ); +}; + +export const useRejectProvisionalGrade = () => { + const queryClient = useQueryClient(); + const studentClient = useStudentClient(); + + return useMutation( + (id: number) => + studentClient.rejectProvisionalGrade({ provisionalGradeId: id }), + { + onSuccess: () => + Promise.all([ + queryClient.invalidateQueries(PROVISIONAL_GRADES_QUERY_KEY), + ]), + }, + ); +}; + const getDeadlineWeekQueryKey = (since: DateTime) => [ DEADLINES_QUERY_PREFIX, since, diff --git a/src/features/transcript/components/GradeStates.tsx b/src/features/transcript/components/GradeStates.tsx new file mode 100644 index 00000000..fa21ebdd --- /dev/null +++ b/src/features/transcript/components/GradeStates.tsx @@ -0,0 +1,136 @@ +import { useTranslation } from 'react-i18next'; +import { StyleSheet, View } from 'react-native'; + +import { Card } from '@lib/ui/components/Card'; +import { Col } from '@lib/ui/components/Col'; +import { Row } from '@lib/ui/components/Row'; +import { Text } from '@lib/ui/components/Text'; +import { VerticalDashedLine } from '@lib/ui/components/VerticalDashedLine'; +import { useStylesheet } from '@lib/ui/hooks/useStylesheet'; +import { Theme } from '@lib/ui/types/Theme'; +import { ProvisionalGradeStateEnum } from '@polito/api-client/models/ProvisionalGrade'; + +type RowProps = { + state: ProvisionalGradeStateEnum | 'pending'; + isActive?: boolean; +}; + +const GradeStateRow = ({ state, isActive = false }: RowProps) => { + const { t } = useTranslation(); + + const styles = useStylesheet(createStyles); + return ( + + + + + + + {t(`provisionalGradeScreen.${state}State`)} + + + {t(`provisionalGradeScreen.${state}StateDescription`)} + + + + ); +}; + +type Props = { + state: ProvisionalGradeStateEnum; +}; + +export const GradeStates = ({ state }: Props) => { + const styles = useStylesheet(createStyles); + + return ( + + + + + + + + + + ); +}; + +const createStyles = ({ + colors, + dark, + fontSizes, + fontWeights, + palettes, +}: Theme) => + StyleSheet.create({ + dashedLine: { + position: 'absolute', + top: 0, + left: 26, + borderColor: palettes.gray[dark ? 800 : 200], + }, + dot: { + width: 18, + height: 18, + borderRadius: 9, + borderWidth: 1, + }, + dotPublished: { + borderColor: palettes.warning[dark ? 400 : 600], + backgroundColor: palettes.warning[dark ? 700 : 300], + }, + dotConfirmed: { + borderColor: palettes.primary[dark ? 600 : 400], + backgroundColor: palettes.primary[dark ? 800 : 200], + }, + dotInactive: { + borderColor: palettes.gray[dark ? 600 : 400], + backgroundColor: palettes.gray[dark ? 900 : 100], + }, + stateTitle: { + fontSize: fontSizes.md, + lineHeight: fontSizes.md * 1.5, + fontWeight: fontWeights.medium, + }, + stateTitleInactive: { + color: palettes.text[dark ? 600 : 400], + }, + stateDescription: { + fontSize: fontSizes.sm, + lineHeight: fontSizes.sm * 1.5, + color: colors.secondaryText, + }, + stateDescriptionInactive: { + color: palettes.gray[dark ? 600 : 400], + }, + }); diff --git a/src/features/transcript/screens/ProvisionalGradeScreen.tsx b/src/features/transcript/screens/ProvisionalGradeScreen.tsx index 0c4a736b..d527e676 100644 --- a/src/features/transcript/screens/ProvisionalGradeScreen.tsx +++ b/src/features/transcript/screens/ProvisionalGradeScreen.tsx @@ -1,9 +1,11 @@ -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { SafeAreaView, ScrollView, StyleSheet, View } from 'react-native'; +import { SafeAreaView, ScrollView, StyleSheet } from 'react-native'; import { ActivityIndicator } from '@lib/ui/components/ActivityIndicator'; import { Col } from '@lib/ui/components/Col'; +import { CtaButton, CtaButtonSpacer } from '@lib/ui/components/CtaButton'; +import { CtaButtonContainer } from '@lib/ui/components/CtaButtonContainer'; import { RefreshControl } from '@lib/ui/components/RefreshControl'; import { Row } from '@lib/ui/components/Row'; import { ScreenTitle } from '@lib/ui/components/ScreenTitle'; @@ -13,61 +15,145 @@ import { Theme } from '@lib/ui/types/Theme'; import { ProvisionalGradeStateEnum } from '@polito/api-client/models/ProvisionalGrade'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; -import { useGetProvisionalGrades } from '../../../core/queries/studentHooks'; +import { BottomBarSpacer } from '../../../core/components/BottomBarSpacer'; +import { useFeedbackContext } from '../../../core/contexts/FeedbackContext'; +import { useOfflineDisabled } from '../../../core/hooks/useOfflineDisabled'; +import { + useAcceptProvisionalGrade, + useGetProvisionalGrades, + useRejectProvisionalGrade, +} from '../../../core/queries/studentHooks'; import { formatDate } from '../../../utils/dates'; import { TeachingStackParamList } from '../../teaching/components/TeachingNavigator'; +import { GradeStates } from '../components/GradeStates'; import { useGetRejectionTime } from '../hooks/useGetRejectionTime'; type Props = NativeStackScreenProps; -export const ProvisionalGradeScreen = ({ route }: Props) => { +export const ProvisionalGradeScreen = ({ navigation, route }: Props) => { const { t } = useTranslation(); const styles = useStylesheet(createStyles); + const { setFeedback } = useFeedbackContext(); + + const { id } = route.params; const gradesQuery = useGetProvisionalGrades(); const grade = useMemo( - () => gradesQuery.data?.find(g => g.id === route.params.id), - [gradesQuery.data], + () => gradesQuery.data?.find(g => g.id === id), + [gradesQuery.data, id], ); const rejectionTime = useGetRejectionTime({ rejectingExpiresAt: grade?.rejectingExpiresAt, }); + const acceptGradeQuery = useAcceptProvisionalGrade(); + const rejectGradeQuery = useRejectProvisionalGrade(); + + const isOffline = useOfflineDisabled(); + + const provideFeedback = useCallback( + (wasAccepted: boolean) => { + if (wasAccepted) { + setFeedback({ + text: t('provisionalGradeScreen.acceptGradeFeedback'), + isPersistent: false, + }); + } else { + setFeedback({ + text: t('provisionalGradeScreen.rejectGradeFeedback'), + isPersistent: false, + }); + } + + navigation.goBack(); + }, + [navigation, setFeedback, t], + ); + return ( - } - > - {grade === undefined ? ( - - ) : ( - - - - - {`${formatDate(grade.date)} - ${t( - 'common.creditsWithUnit', - { credits: grade.credits }, - )}`} - {grade.state === ProvisionalGradeStateEnum.Confirmed && - rejectionTime && ( - {rejectionTime} - )} - - - - {grade.grade} - - - - + <> + } + > + {grade === undefined ? ( + + ) : ( + + + + + {`${formatDate(grade.date)} - ${t( + 'common.creditsWithUnit', + { + credits: grade.credits, + }, + )}`} + {grade.state === ProvisionalGradeStateEnum.Confirmed && + rejectionTime && ( + {rejectionTime} + )} + + + {grade.grade} + + + + {grade?.state === ProvisionalGradeStateEnum.Confirmed && ( + + )} + + + )} + + + {grade?.state === ProvisionalGradeStateEnum.Published && ( + navigation.navigate('Person', { id: grade?.teacherId })} + /> )} - + {grade?.state === ProvisionalGradeStateEnum.Confirmed && ( + + Promise.resolve().then(() => provideFeedback(true))} + variant="outlined" + absolute={false} + loading={acceptGradeQuery.isLoading} + disabled={ + isOffline || + acceptGradeQuery.isLoading || + rejectGradeQuery.isLoading + } + containerStyle={{ paddingVertical: 0 }} + /> + Promise.resolve().then(() => provideFeedback(false))} + absolute={false} + loading={rejectGradeQuery.isLoading} + disabled={ + isOffline || + acceptGradeQuery.isLoading || + rejectGradeQuery.isLoading + } + containerStyle={{ paddingVertical: 0 }} + /> + + )} + ); }; -const createStyles = ({ dark, palettes, spacing }: Theme) => +const createStyles = ({ + dark, + fontSizes, + palettes, + spacing, + fontWeights, +}: Theme) => StyleSheet.create({ chartCard: { flex: 1, @@ -87,8 +173,16 @@ const createStyles = ({ dark, palettes, spacing }: Theme) => additionalMetric: { marginTop: spacing[4], }, + // eslint-disable-next-line react-native/no-color-literals grade: { - marginLeft: spacing[2], + width: 60, + height: 60, + backgroundColor: '#FFFFFF', + borderRadius: 12, + }, + gradeText: { + fontSize: fontSizes['2xl'], + fontWeight: fontWeights.semibold, }, rejectionTime: { color: dark ? palettes.danger[300] : palettes.danger[700],