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],