From 04ddabd720cfda7a4015870bfe09a8d6909561e4 Mon Sep 17 00:00:00 2001 From: Fabrizio Costa Medich <134924838+FabrizioCostaMedich@users.noreply.github.com> Date: Tue, 28 Jan 2025 14:15:30 +0100 Subject: [PATCH] On-time exams (#562) * feat(explanation-average): created modal for averaging explanation, Ref #145 * feat(course-screen): changed the averages and explanations, Ref #145 * feat(averages-info): add the test text,add description for the masterAdmissionAverageGrade, Ref #145 * fix(course-screen): fix to average error for master * fix(course-screen-modal): fix the layout of the bullet point,Ref #145 * fix(course-screen-modal): delete some not used export, and change the weight prop,Ref #145 * feat(services): webmail added (#481) * feat(webmail): add the webmail button and the hooks for the authentication,Ref #473 * fix(webmail): webmail improvements --------- Co-authored-by: FabrizioCostaMedich Co-authored-by: Emanuele Coricciati * fix(agenda): fix tablet agenda view (#480) * fix(agenda): fix agenda layout on tablet * fix(agenda): fix overlapping events --------- Co-authored-by: Emanuele Coricciati * feat(courses): add external links in course info tab (#485) * feat(link-course): create the section external links course in CourseInfoScreen.tsx * feat(course-screen-info): add text, and change some controls when the status is loading, Ref #301 * feat(course-info): changed the layout of the links and the text , Ref #301 * fix(courses): fix translations and link indexes * chore: update api spec --------- Co-authored-by: FabrizioCostaMedich Co-authored-by: Emanuele Coricciati Co-authored-by: Cristina Ferrian <54667563+Bri74@users.noreply.github.com> * build: bump version v.1.6.5 * ci: upgrade pipeline, unify rubies, upgrade to react-native 0.72 (#487) * ci: bump ios target sdk, enable macos github runners * ci: try to break everything * ci: try to break everything pt 2 * ci: try to unify ruby & co * ci: move caching before npm install, bump action versions * ci: upgrade ruby version * ci: more ruby fixes * ci: retry ruby fix * ci: enforce pod install * ci: react-native-permissions upgrade Podfile --------- Co-authored-by: Mobile AppleDev * fix(tickets): fix ticket reply sent multiple times, Ref #479 (#491) * fix(courses): fix link indexes (#490) * fix(ui): fix bottom modal on rotation (#489) * fix(webmail): fix unread count as string (#488) * fix(places): max zoom incompatible with tiles max level (#486) * fix: upgrade mapbox version to address ios xcprivacy issue (#492) Co-authored-by: Mobile AppleDev * refactor(ui): changed accept/reject grade confirm messages * fix(exams): new style exam screens (#500) * fix(exams): fix style on exams screens * fix(exams): do not show buttons when exam is not passed * fix(exams): fix checks on canBeRejected and canBeAccepted loot boxes * fix(exams): fix button position on ios and android * fix(exams): fix position of button by platform --------- Co-authored-by: Emanuele Coricciati * fix(exams): provide a feedback when an exam cannot be booked, Ref #495 (#499) * fix(exams): provide a feedback when an exam cannot be booked, Ref #495 * fix(exams): fix icon not bookable and places not available, Refs #495 * fix(teaching): update file name failing when already scheduled (#496) * build: bump version * fix(exams): sort exams by date in the TeachingScreen.tsx, Ref #504 (#505) * fix(login): enable login even when fcm is not enabled, but display warning (#515) fixes: #513 * fix(exams): fix exam booked count and available count (#511) * fix(courses): make search case-insensitive (#535) * fix(courses): prioritize sorting by date, if they are the same sort by name in increasing order (#536) * fix(tickets): fix issues in ticket format, update regex for links (#533) Co-authored-by: Fabrizio Costa Medich <134924838+FabrizioCostaMedich@users.noreply.github.com> * chore: upgrade to react native 0.76 (#557) * ci: upgrade tests * fix: fix android build for updated react native version * fix: migration to mapbox 0.75.3 * build: upgrade to rn 0.76 * fix: fix types * fix: update lock files * fix: fix places types * fix: fix lock files and format * fix(places): added control to check if floorId exist and set his state * fix(places): pass current floor when press a building * fix(places): fix invalid styles on layers * fix(places): add map loader * fix(places): fix camera animation and stateful menu, remove map modal --------- Co-authored-by: QcFe <10742159+QcFe@users.noreply.github.com> Co-authored-by: miky41195 Co-authored-by: FabrizioCostaMedich Co-authored-by: Cristina Ferrian <54667563+Bri74@users.noreply.github.com> * fix(profile): handle null smartcard and relocate fcm token * fix(transcript): added explanations for grades and show only one average * feat(explanation-average): created modal for averaging explanation, Ref #145 * feat(course-screen): changed the averages and explanations, Ref #145 * feat(averages-info): add the test text,add description for the masterAdmissionAverageGrade, Ref #145 * fix(course-screen): fix to average error for master * fix(course-screen-modal): fix the layout of the bullet point,Ref #145 * fix(course-screen-modal): delete some not used export, and change the weight prop,Ref #145 * fix(profile): handle null smartcard and relocate fcm token * fix(transcript): added explanations for grades and show only one average * feat(transcript): added on-time exam scores to transcript section * fix(transcript): changed text formula average * fix(transcript): changed some text * fix(transcript): change laude text * fix(transcript): change some text * fix(transcript): change some text * fix(exams): fix typo * fix(exams): fix typo lowercase --------- Co-authored-by: Emanuele Coricciati <73798800+emacoricciati@users.noreply.github.com> Co-authored-by: Emanuele Coricciati Co-authored-by: Cristina Ferrian <54667563+Bri74@users.noreply.github.com> Co-authored-by: Cristina Ferrian Co-authored-by: Federico Cucinella <10742159+QcFe@users.noreply.github.com> Co-authored-by: Mobile AppleDev Co-authored-by: Umberto Pepato Co-authored-by: Emanuele Coricciati Co-authored-by: miky41195 --- assets/translations/en.json | 34 +- assets/translations/it.json | 34 +- lib/ui/components/Metric.tsx | 4 +- lib/ui/components/ModalContent.tsx | 41 +- lib/ui/components/SectionHeader.tsx | 73 ++-- src/core/components/BottomModal.tsx | 31 +- .../teaching/components/TeachingNavigator.tsx | 12 + .../teaching/screens/TeachingScreen.tsx | 114 +++-- .../components/CareerScreenModal.tsx | 93 ++++ .../components/RecordedGradeListItem.tsx | 60 +++ .../transcript/screens/CareerScreen.tsx | 408 ++++++++++++------ .../transcript/screens/GradesScreen.tsx | 30 +- .../screens/RecordedGradeScreen.tsx | 206 +++++++++ src/utils/grades.ts | 8 + 14 files changed, 854 insertions(+), 294 deletions(-) create mode 100644 src/features/transcript/components/CareerScreenModal.tsx create mode 100644 src/features/transcript/components/RecordedGradeListItem.tsx create mode 100644 src/features/transcript/screens/RecordedGradeScreen.tsx diff --git a/assets/translations/en.json b/assets/translations/en.json index 133ce82d0..e1478d8e2 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 9c5b8b91a..6f4334235 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 07bb4e389..e4fe0b97f 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 713edf6c3..394a8aa02 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 d960ca2af..047298e58 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 73dc704cc..e8810c2b2 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 000000000..ce8ead44a --- /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 000000000..a50977787 --- /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 c439d39ce..cd1d4053f 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 ac0f04360..5f5d9eb7b 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 000000000..07bad9e93 --- /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 f6e10821f..ef906d5e5 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('/');