diff --git a/assets/translations/en.json b/assets/translations/en.json index 133ce82d..e1478d8e 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -643,6 +643,15 @@ "rejectGradeFeedback": "Evaluation rejected, it will be recorded in the next few hours", "title": "Evaluation" }, + "recordedGradeScreen": { + "additionalPoint": "Additional Scores", + "recordedGradeTitle": "Recorded grade", + "staff": "Staff", + "teacher": "Holder", + "textModal": "Having taken the exam by the first useful session, you are awarded an additional score of 0.5 out of a maximum of 4.\nYou can consult your total on-time points on career", + "titleOnTimePoint": "On-time Exam points", + "ctaButtonModal": "Access your career" + }, "sectionHeader": { "cta": "See all", "ctaMoreSuffix": "({{- count}} more)" @@ -740,13 +749,28 @@ "attendedCreditsLabel": "Attended credits", "averages": "Averages", "averagesAndGrades": "Averages and grades", - "estimatedFinalGrade": "Final grade", - "estimatedFinalGradePurged": "Estimated final grade", - "finalAverageLabel": "Final average", - "masterAdmissionAverage": "Master admission average", + "averageLabel": "Average grade for admission to the Final Examination", + "finalAverageLabelDescription":{ + "description": "It is the average grade, weighted according to the credits relating to each exam with a grade*, adjusted for the {{-excludedCreditsNumber}} worst credits", + "formula": "sum (grade * credits) / sum of credits,\nexcluding the {{-excludedCreditsNumber}} worst credits" + }, + "laude": "*Honours does not count toward the calculation", + "masterAdmissionAverage": "Master admission average grade", + "masterAdmissionAverageLabelDescription":{ + "description":"It is the average grade, weighted according to the credits relating to each exam with grade*, adjusted for the worst n credits\n(To find out more about n, refer to the Student Guide)", + "formula":"sum (grade * credits) / sum of credits\n(excluding the n worst credits)" + }, + "NB": "NB", + "NBDescription": "These definitions are indicative. To find out more, refer to the Student Guide.\nOnly graded exams are considered in all average grade above.", + "onTimeScores": "On-time Exams points", + "pointTitle": "Scores", + "pointModal": "The total of the Exam on-time points earned. This value can never exceed the maximum allowed by the degree program.\nTo find out more, refer to the Student Guide.", "thisYear": "This academic year", "title": "Career", - "weightedAverageLabel": "Weighted average", + "weightedAverageLabelDescription":{ + "description":"This is the average, weighted according to the credits relating to each exam with a grade*", + "formula": "sum (grade * credits) / sum of credits" + }, "yourCareer": "Your career" }, "videoControls": { diff --git a/assets/translations/it.json b/assets/translations/it.json index 9c5b8b91..6f433423 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -643,6 +643,15 @@ "rejectGradeFeedback": "Valutazione rifiutata, verrà registrata nelle prossime ore", "title": "Valutazione" }, + "recordedGradeScreen": { + "additionalPoint": "Punteggi aggiuntivi", + "recordedGradeTitle": "Valutazione Registrata", + "staff": "Docente", + "teacher": "Titolare", + "textModal": "Avendo sostenuto l’esame entro la prima sessione utile, ti viene riconosciuto un punteggio aggiuntivo di 0.5 su un massimo di 4.\nPuoi consultate il totale dei tuoi punteggi on-time su carriera", + "titleOnTimePoint": "Punti Esame on-time", + "ctaButtonModal": "Accedi alla tua cariera" + }, "sectionHeader": { "cta": "Vedi tutti", "ctaMoreSuffix": "(altri {{- count}})" @@ -740,13 +749,28 @@ "attendedCreditsLabel": "Crediti frequentati", "averages": "Medie", "averagesAndGrades": "Medie e voti", - "estimatedFinalGrade": "Voto di laurea", - "estimatedFinalGradePurged": "Voto di laurea depurato", - "finalAverageLabel": "Media depurata", - "masterAdmissionAverage": "Media di ammissione al secondo livello", + "averageLabel": "Media di ammissione all’esame di laurea", + "finalAverageLabelDescription":{ + "description": "E’ la media, pesata in funzione dei crediti relativi ad ogni esame con voto*, depurata dei {{-excludedCreditsNumber}} peggiori crediti", + "formula": "sum (voto * crediti) / sum crediti,\nescludendo i peggiori {{-excludedCreditsNumber}} crediti" + }, + "laude": "*Le lodi non concorrono al calcolo", + "masterAdmissionAverage": "Media di ammissione al II liv", + "masterAdmissionAverageLabelDescription":{ + "description":"È la media, pesata in funzione dei crediti relativi ad ogni esame con voto*, depurata degli n peggiori crediti\n(Per conoscere n, consulta la Guida Studenti)", + "formula":"sum (voto * crediti) / sum crediti\n(escludendo gli n peggiori crediti)" + }, + "NB": "NB", + "NBDescription": "Queste definizioni sono indicative. Per conoscere i dettagli, consulta la Guida Studenti del tuo corso.\nIn tutte le medie sopra riportate sono considerati esclusivamente gli esami con voto.", + "onTimeScores": "Punti Esami on-time", + "pointTitle": "Punteggi", + "pointModal": "Il totale dei punti on-time acquisiti. Il valore non può mai superare il massimo previsto dal corso di laurea.\nPer saperne di più, consulta la Guida Studenti.", "thisYear": "Questo anno accademico", "title": "Carriera", - "weightedAverageLabel": "Media ponderata", + "weightedAverageLabelDescription":{ + "description":"E’ la media, pesata in funzione dei crediti relativi ad ogni esame con voto*", + "formula": "sum (voto * crediti) / sum crediti" + }, "yourCareer": "La tua carriera" }, "videoControls": { diff --git a/lib/ui/components/Metric.tsx b/lib/ui/components/Metric.tsx index 07bb4e38..e4fe0b97 100644 --- a/lib/ui/components/Metric.tsx +++ b/lib/ui/components/Metric.tsx @@ -5,7 +5,7 @@ import { CardProps } from './Card'; import { Text, Props as TextProps } from './Text'; type Props = ViewProps & { - title: string; + title?: string; value: string | number | JSX.Element; color?: string; valueStyle?: TextProps['style']; @@ -19,7 +19,7 @@ export const Metric = ({ title, value, color, ...rest }: CardProps & Props) => { return ( - {title} + {title && {title}} {['string', 'number'].includes(typeof value) ? ( ) => { const styles = useStylesheet(createStyles); - return ( ({ - container: { - backgroundColor: colors.surface, - borderTopRightRadius: shapes.md, - borderTopLeftRadius: shapes.md, - }, - header: { - borderTopRightRadius: shapes.md, - borderTopLeftRadius: shapes.md, - paddingVertical: spacing[1], - }, - headerLeft: { padding: spacing[3] }, - modalTitle: { - fontSize: fontSizes.md, - fontWeight: fontWeights.semibold, - color: colors.prose, - }, -}); +}: Theme) => + StyleSheet.create({ + container: { + backgroundColor: colors.surface, + borderTopRightRadius: shapes.md, + borderTopLeftRadius: shapes.md, + maxHeight: '100%', + }, + header: { + borderTopRightRadius: shapes.md, + borderTopLeftRadius: shapes.md, + paddingVertical: spacing[1], + }, + headerLeft: { padding: spacing[3] }, + modalTitle: { + fontSize: fontSizes.md, + fontWeight: fontWeights.semibold, + color: colors.prose, + }, + }); diff --git a/lib/ui/components/SectionHeader.tsx b/lib/ui/components/SectionHeader.tsx index 713edf6c..394a8aa0 100644 --- a/lib/ui/components/SectionHeader.tsx +++ b/lib/ui/components/SectionHeader.tsx @@ -5,9 +5,12 @@ import { TextProps, TextStyle, TouchableOpacity, + TouchableOpacityProps, View, } from 'react-native'; +import { Props as FAProps } from '@fortawesome/react-native-fontawesome'; +import { IconButton } from '@lib/ui/components/IconButton'; import { Separator } from '@lib/ui/components/Separator'; import { Link, useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; @@ -23,12 +26,16 @@ interface Props { subtitle?: string; subtitleStyle?: StyleProp; ellipsizeTitle?: boolean; - linkTo?: To; linkToMoreCount?: number; - trailingItem?: JSX.Element; separator?: boolean; accessible?: boolean; accessibilityLabel?: string | undefined; + linkTo?: To; + trailingItem?: JSX.Element; + trailingIcon?: Pick & + TouchableOpacityProps & { + iconStyle?: FAProps['style']; + }; } /** @@ -46,6 +53,7 @@ export const SectionHeader = ({ linkToMoreCount, separator = true, trailingItem, + trailingIcon, }: Props) => { const styles = useStylesheet(createStyles); const { t } = useTranslation(); @@ -59,18 +67,25 @@ export const SectionHeader = ({ const Header = () => { return ( - + {separator && } - - {title} - + + + + {title} + + {trailingIcon && ( + + )} + + {subtitle && ( )} - {trailingItem - ? trailingItem - : linkTo && ( - - - {t('sectionHeader.cta')} - {(linkToMoreCount ?? 0) > 0 && - ' ' + - t('sectionHeader.ctaMoreSuffix', { - count: linkToMoreCount, - })} - - - )} + {trailingItem && trailingItem} + + {linkTo && ( + + + {t('sectionHeader.cta')} + {(linkToMoreCount ?? 0) > 0 && + ' ' + + t('sectionHeader.ctaMoreSuffix', { + count: linkToMoreCount, + })} + + + )} ); }; @@ -152,4 +167,10 @@ const createStyles = ({ spacing, colors }: Theme) => titleContainer: { flex: 1, }, + innerTitleContainer: { + alignItems: 'center', + flexDirection: 'row', + padding: 0, + margin: 0, + }, }); diff --git a/src/core/components/BottomModal.tsx b/src/core/components/BottomModal.tsx index d960ca2a..047298e5 100644 --- a/src/core/components/BottomModal.tsx +++ b/src/core/components/BottomModal.tsx @@ -1,7 +1,9 @@ -import { PropsWithChildren } from 'react'; -import { View, useWindowDimensions } from 'react-native'; +import { PropsWithChildren, useRef } from 'react'; +import { ScrollView, View } from 'react-native'; import Modal from 'react-native-modal'; +import { SCREEN_HEIGHT, SCREEN_WIDTH } from '@gorhom/bottom-sheet'; + export type BottomModalProps = PropsWithChildren<{ visible: boolean; onClose?: () => void; @@ -18,7 +20,17 @@ export const BottomModal = ({ dismissable && onClose?.(); }; - const { width, height } = useWindowDimensions(); + const scrollViewRef = useRef(null); + + const handleScrollTo = (position: { + x?: number; + y?: number; + animated?: boolean; + }) => { + if (scrollViewRef.current) { + scrollViewRef.current.scrollTo(position); + } + }; return ( {children} diff --git a/src/features/teaching/components/TeachingNavigator.tsx b/src/features/teaching/components/TeachingNavigator.tsx index 73dc704c..e8810c2b 100644 --- a/src/features/teaching/components/TeachingNavigator.tsx +++ b/src/features/teaching/components/TeachingNavigator.tsx @@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next'; import { Platform } from 'react-native'; import { useTheme } from '@lib/ui/hooks/useTheme'; +import { ExamGrade } from '@polito/api-client'; import { NavigatorScreenParams } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; @@ -23,6 +24,7 @@ import { PlacesStackParamList } from '../../places/components/PlacesNavigator'; import { CpdSurveysScreen } from '../../surveys/screens/CpdSurveysScreen'; import { TranscriptTopTabsNavigator } from '../../transcript/navigation/TranscriptTopTabsNavigator'; import { ProvisionalGradeScreen } from '../../transcript/screens/ProvisionalGradeScreen'; +import { RecordedGradeScreen } from '../../transcript/screens/RecordedGradeScreen'; import { ExamQuestionScreen } from '../screens/ExamQuestionScreen'; import { ExamRequestScreen } from '../screens/ExamRequestScreen'; import { ExamRescheduleScreen } from '../screens/ExamRescheduleScreen'; @@ -42,6 +44,7 @@ export type TeachingStackParamList = CourseSharedScreensParamList & MessagesModal: undefined; Transcript: undefined; ProvisionalGrade: { id: number }; + RecordedGrade: { grade: ExamGrade }; OnboardingModal: undefined; PlacesTeachingStack: NavigatorScreenParams; CpdSurveys: { categoryId: string; typeId: string; typeName: string }; @@ -147,6 +150,15 @@ export const TeachingNavigator = () => { headerBackTitleVisible: false, }} /> + { onPress={() => navigation.navigate('Transcript')} underlayColor={colors.touchableHighlight} > - - + + - {studentQuery.data?.estimatedFinalGradePurged ? ( - - ) : ( - + + + - )} + - )} @@ -345,4 +324,7 @@ const createStyles = ({ spacing }: Theme) => gap: spacing[1], alignItems: 'center', }, + graph: { + paddingHorizontal: spacing[4], + }, }); diff --git a/src/features/transcript/components/CareerScreenModal.tsx b/src/features/transcript/components/CareerScreenModal.tsx new file mode 100644 index 00000000..ce8ead44 --- /dev/null +++ b/src/features/transcript/components/CareerScreenModal.tsx @@ -0,0 +1,93 @@ +import { ScrollView, StyleSheet, View } from 'react-native'; + +import { ModalContent } from '@lib/ui/components/ModalContent'; +import { Text } from '@lib/ui/components/Text'; +import { useStylesheet } from '@lib/ui/hooks/useStylesheet'; +import { Theme } from '@lib/ui/types/Theme'; + +export const CareerScreenModal = ({ + title, + itemList, + onDismiss, +}: { + title: string; + itemList: { + title?: string; + content: { description: string; formula?: string }; + dot: boolean; + }[]; + onDismiss: () => void; +}) => { + const styles = useStylesheet(createStyles); + + return ( + + + {itemList.map((item, index) => { + if (item.content.description) { + return ( + + {item.dot && {`\u2022`} } + + + {item.title && ( + {`${item.title}: `} + )} + + {item.content.description} + + + {item.content.formula && ( + + {item.content.formula} + + )} + + + ); + } + })} + + + ); +}; + +const createStyles = ({ dark, fontSizes, colors, spacing }: Theme) => + StyleSheet.create({ + container: { + backgroundColor: colors.surface, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: dark ? colors.surfaceDark : colors.background, + paddingVertical: spacing[2], + }, + headerTitle: { + marginLeft: 'auto', + marginRight: 'auto', + fontSize: fontSizes.lg, + textAlign: 'center', + }, + content: { + padding: spacing[7], + gap: spacing[4], + }, + listItem: { + flexDirection: 'row', + alignItems: 'flex-start', + padding: spacing['1'], + }, + listItemTitle: { + fontWeight: '600', + }, + text: { + flexDirection: 'column', + }, + formula: { + marginTop: spacing[1], + }, + }); diff --git a/src/features/transcript/components/RecordedGradeListItem.tsx b/src/features/transcript/components/RecordedGradeListItem.tsx new file mode 100644 index 00000000..a5097778 --- /dev/null +++ b/src/features/transcript/components/RecordedGradeListItem.tsx @@ -0,0 +1,60 @@ +import { useTranslation } from 'react-i18next'; +import { StyleSheet } from 'react-native'; + +import { DisclosureIndicator } from '@lib/ui/components/DisclosureIndicator'; +import { ListItem } from '@lib/ui/components/ListItem'; +import { Row } from '@lib/ui/components/Row'; +import { Text } from '@lib/ui/components/Text'; +import { useStylesheet } from '@lib/ui/hooks/useStylesheet'; +import { Theme } from '@lib/ui/types/Theme'; +import { ExamGrade } from '@polito/api-client'; + +import { formatDate } from '../../../utils/dates'; +import { formatGrade } from '../../../utils/grades'; + +type RecordedGradeProps = { + grade: ExamGrade; +}; + +export const RecordedGradeListItem = ({ grade }: RecordedGradeProps) => { + const { t } = useTranslation(); + const styles = useStylesheet(createStyles); + + return ( + + + {t(formatGrade(grade.grade))} + + + + } + linkTo={{ + screen: 'RecordedGrade', + params: { + grade: { + ...grade, + date: grade.date.toISOString(), + }, + }, + }} + /> + ); +}; + +const createStyles = ({ spacing }: Theme) => + StyleSheet.create({ + grade: { + marginLeft: spacing[2], + }, + }); diff --git a/src/features/transcript/screens/CareerScreen.tsx b/src/features/transcript/screens/CareerScreen.tsx index c439d39c..cd1d4053 100644 --- a/src/features/transcript/screens/CareerScreen.tsx +++ b/src/features/transcript/screens/CareerScreen.tsx @@ -1,28 +1,38 @@ import { useTranslation } from 'react-i18next'; import { SafeAreaView, ScrollView, StyleSheet, View } from 'react-native'; +import { faQuestionCircle } from '@fortawesome/free-regular-svg-icons'; import { Card } from '@lib/ui/components/Card'; -import { Grid } from '@lib/ui/components/Grid'; +import { Col } from '@lib/ui/components/Col'; import { Metric } from '@lib/ui/components/Metric'; import { RefreshControl } from '@lib/ui/components/RefreshControl'; +import { Row } from '@lib/ui/components/Row'; import { Section } from '@lib/ui/components/Section'; import { SectionHeader } from '@lib/ui/components/SectionHeader'; +import { Text } from '@lib/ui/components/Text'; import { useStylesheet } from '@lib/ui/hooks/useStylesheet'; import { useTheme } from '@lib/ui/hooks/useTheme'; import { Theme } from '@lib/ui/types/Theme'; import { BottomBarSpacer } from '../../../core/components/BottomBarSpacer'; +import { BottomModal } from '../../../core/components/BottomModal'; +import { useBottomModal } from '../../../core/hooks/useBottomModal'; import { useGetGrades, useGetStudent, } from '../../../core/queries/studentHooks'; import { GlobalStyles } from '../../../core/styles/GlobalStyles'; -import { formatFinalGrade } from '../../../utils/grades'; +import { + formatExamOnTime, + formatFinalGrade, + formatThirtiethsGrade, +} from '../../../utils/grades'; import { ProgressChart } from '../../teaching/components/ProgressChart'; +import { CareerScreenModal } from '../components/CareerScreenModal'; export const CareerScreen = () => { const { t } = useTranslation(); - const { palettes } = useTheme(); + const { palettes, colors, dark } = useTheme(); const styles = useStylesheet(createStyles); const studentQuery = useGetStudent(); const gradesQuery = useGetGrades(); @@ -33,154 +43,282 @@ export const CareerScreen = () => { totalAttendedCredits, totalAcquiredCredits, totalCredits, + mastersAdmissionAverageGrade, + excludedCreditsNumber, + usePurgedAverageFinalGrade, + totalOnTimeExamPoints, + maxOnTimeExamPoints, } = studentQuery.data ?? {}; - return ( - - } - > - -
- - - - - - - - -
+ const { + open: showBottomModal, + modal: bottomModal, + close: closeBottomModal, + } = useBottomModal(); -
- - - - - - - { + showBottomModal( + - -
+ : { + title: t('transcriptMetricsScreen.averageLabel'), + content: { + description: t( + 'transcriptMetricsScreen.weightedAverageLabelDescription.description', + ), + formula: t( + 'transcriptMetricsScreen.weightedAverageLabelDescription.formula', + ), + }, + dot: true, + }, + mastersAdmissionAverageGrade + ? { + title: t('transcriptMetricsScreen.masterAdmissionAverage'), + content: { + description: t( + 'transcriptMetricsScreen.masterAdmissionAverageLabelDescription.description', + ), + formula: t( + 'transcriptMetricsScreen.masterAdmissionAverageLabelDescription.formula', + ), + }, + dot: true, + } + : { title: '', content: { description: '' }, dot: false }, + { + content: { + description: t('transcriptMetricsScreen.laude'), + }, + dot: false, + }, + { + title: t('transcriptMetricsScreen.NB'), + content: { + description: t('transcriptMetricsScreen.NBDescription'), + }, + dot: false, + }, + ]} + onDismiss={closeBottomModal} + />, + ); + }; -
- - - - + const onPressPointEvent = () => { + showBottomModal( + , + ); + }; - + + + } + > + +
+ + + + + + + + +
- {studentQuery.data?.averageGradePurged && ( +
+ + + - )} - - {studentQuery.data?.estimatedFinalGradePurged && ( - )} - + + + +
- {studentQuery.data?.mastersAdmissionAverageGrade && ( - + + + + + + {t('transcriptMetricsScreen.averageLabel')} + + + + + + + {studentQuery.data?.mastersAdmissionAverageGrade && ( + <> + + + {t('transcriptMetricsScreen.masterAdmissionAverage')} + + + + + + + )} + + +
+ {totalOnTimeExamPoints && maxOnTimeExamPoints && ( +
+ - )} - -
- -
-
+ + + + + )} + + +
+ ); }; -const createStyles = ({ spacing }: Theme) => +const createStyles = ({ spacing, fontSizes, fontWeights }: Theme) => StyleSheet.create({ container: { paddingVertical: spacing[5], @@ -206,4 +344,8 @@ const createStyles = ({ spacing }: Theme) => grade: { marginLeft: spacing[2], }, + title: { + fontSize: fontSizes.md, + fontWeight: fontWeights.normal, + }, }); diff --git a/src/features/transcript/screens/GradesScreen.tsx b/src/features/transcript/screens/GradesScreen.tsx index ac0f0436..5f5d9eb7 100644 --- a/src/features/transcript/screens/GradesScreen.tsx +++ b/src/features/transcript/screens/GradesScreen.tsx @@ -1,12 +1,10 @@ import { useTranslation } from 'react-i18next'; import { SafeAreaView, ScrollView, StyleSheet } from 'react-native'; -import { ListItem } from '@lib/ui/components/ListItem'; import { OverviewList } from '@lib/ui/components/OverviewList'; import { RefreshControl } from '@lib/ui/components/RefreshControl'; import { Section } from '@lib/ui/components/Section'; import { SectionHeader } from '@lib/ui/components/SectionHeader'; -import { Text } from '@lib/ui/components/Text'; import { useStylesheet } from '@lib/ui/hooks/useStylesheet'; import { Theme } from '@lib/ui/types/Theme'; @@ -17,9 +15,8 @@ import { useGetGrades, useGetProvisionalGrades, } from '../../../core/queries/studentHooks'; -import { formatDate } from '../../../utils/dates'; -import { formatGrade } from '../../../utils/grades'; import { ProvisionalGradeListItem } from '../components/ProvisionalGradeListItem'; +import { RecordedGradeListItem } from '../components/RecordedGradeListItem'; export const GradesScreen = () => { const { t } = useTranslation(); @@ -80,30 +77,7 @@ export const GradesScreen = () => { } > {gradesQuery.data?.map((grade, index) => ( - - {t(formatGrade(grade.grade))} -
- } - /> + ))} diff --git a/src/features/transcript/screens/RecordedGradeScreen.tsx b/src/features/transcript/screens/RecordedGradeScreen.tsx new file mode 100644 index 00000000..07bad9e9 --- /dev/null +++ b/src/features/transcript/screens/RecordedGradeScreen.tsx @@ -0,0 +1,206 @@ +import { useTranslation } from 'react-i18next'; +import { SafeAreaView, ScrollView, StyleSheet, View } from 'react-native'; + +import { faQuestionCircle } from '@fortawesome/free-regular-svg-icons'; +import { faFlagCheckered } from '@fortawesome/free-solid-svg-icons'; +import { ActivityIndicator } from '@lib/ui/components/ActivityIndicator'; +import { Col } from '@lib/ui/components/Col'; +import { CtaButton } from '@lib/ui/components/CtaButton'; +import { Icon } from '@lib/ui/components/Icon'; +import { ListItem } from '@lib/ui/components/ListItem'; +import { ModalContent } from '@lib/ui/components/ModalContent'; +import { OverviewList } from '@lib/ui/components/OverviewList'; +import { PersonListItem } from '@lib/ui/components/PersonListItem'; +import { RefreshControl } from '@lib/ui/components/RefreshControl'; +import { Row } from '@lib/ui/components/Row'; +import { ScreenTitle } from '@lib/ui/components/ScreenTitle'; +import { Section } from '@lib/ui/components/Section'; +import { SectionHeader } from '@lib/ui/components/SectionHeader'; +import { Text } from '@lib/ui/components/Text'; +import { useStylesheet } from '@lib/ui/hooks/useStylesheet'; +import { useTheme } from '@lib/ui/hooks/useTheme'; +import { Theme } from '@lib/ui/types/Theme'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; + +import { BottomBarSpacer } from '../../../core/components/BottomBarSpacer'; +import { BottomModal } from '../../../core/components/BottomModal'; +import { useBottomModal } from '../../../core/hooks/useBottomModal'; +import { useGetPerson } from '../../../core/queries/peopleHooks'; +import { formatDate } from '../../../utils/dates'; +import { TeachingStackParamList } from '../../teaching/components/TeachingNavigator'; + +type Props = NativeStackScreenProps; +export const RecordedGradeScreen = ({ navigation, route }: Props) => { + const { t } = useTranslation(); + const { grade } = route.params; + const { fontSizes, colors } = useTheme(); + + const teacherIds = grade.teacherId !== null ? grade.teacherId : undefined; + const staffQueries = useGetPerson(teacherIds); + + const styles = useStylesheet(createStyles); + const isNumber = (value: string): boolean => !isNaN(Number(value)); + + const { + open: showBottomModal, + modal: bottomModal, + close: closeBottomModal, + } = useBottomModal(); + + const onPressEvent = () => { + showBottomModal( + + + {t('recordedGradeScreen.textModal')} + navigation.navigate('TranscriptCareer')} + absolute={false} + containerStyle={{ paddingHorizontal: 0 }} + /> + + , + ); + }; + + return ( + <> + } + > + {!grade ? ( + + ) : ( + + + + + + {`${formatDate(new Date(grade.date))} - ${t( + 'common.creditsWithUnit', + { + credits: grade.credits, + }, + )}`} + + + + + {isNumber(grade.grade) + ? grade.grade + : grade.grade.charAt(0).toUpperCase() + + grade.grade.slice(1).toLowerCase()} + + + +
+ {!!grade.onTimeExamPoints && ( + <> + + + + } + title={t('recordedGradeScreen.titleOnTimePoint')} + trailingItem={ + + {'+' + grade.onTimeExamPoints} + + } + /> + + + )} +
+ +
+ {staffQueries.data && ( + <> + + + + + + )} +
+
+ )} + +
+ + + ); +}; + +const createStyles = ({ + colors, + fontSizes, + spacing, + fontWeights, + palettes, + dark, +}: Theme) => + StyleSheet.create({ + grade: { + minWidth: 60, + height: 60, + backgroundColor: colors.surface, + borderRadius: 12, + padding: spacing[2], + }, + gradeText: { + fontSize: fontSizes['2xl'], + fontWeight: fontWeights.semibold, + }, + longGradeText: { + fontSize: fontSizes.lg, + fontWeight: fontWeights.semibold, + }, + onTimeItem: { + fontWeight: 'bold', + fontSize: fontSizes.xl, + color: palettes.success[dark ? 400 : 700], + }, + textModal: { + padding: spacing[7], + gap: spacing[2], + }, + }); diff --git a/src/utils/grades.ts b/src/utils/grades.ts index f6e10821..ef906d5e 100644 --- a/src/utils/grades.ts +++ b/src/utils/grades.ts @@ -8,3 +8,11 @@ export const formatGrade = (grade: string) => export const formatFinalGrade = (grade?: number | null) => [grade ?? '--', 110].join('/'); + +export const formatExamOnTime = ( + totalOnTimeExamPoints: number | null, + maxOnTimeExamPoints: number, +) => (totalOnTimeExamPoints ?? '0') + '/' + maxOnTimeExamPoints; + +export const formatThirtiethsGrade = (grade?: number | null) => + [grade ?? '--', 30].join('/');